From 053177177a09c1c3dfde144399ed20ddf19ee6c8 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Wed, 19 Nov 2025 23:59:34 +0900 Subject: [PATCH 01/44] [feature] Chatting API implementation --- build.gradle | 1 + .../domain/chatList/dto/ChatListDto.java | 2 +- .../repository/ChatListRepository.java | 5 +- .../chatList/service/ChatListService.java | 2 +- .../controller/ChatMessageController.java | 46 ++++++++++++ .../domain/message/dto/MessageDto.java | 26 +++++-- .../message/service/ChatMessageService.java | 61 ++++++++++++++++ .../domain/room/entity/AnonymousRoom.java | 7 ++ .../global/config/WebSocketConfig.java | 38 ++++++++++ .../bravest/global/handler/StompHandler.java | 72 +++++++++++++++++++ .../global/security/jwt/JwtTokenProvider.java | 21 ++++++ 11 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java create mode 100644 src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java create mode 100644 src/main/java/opensource/bravest/global/config/WebSocketConfig.java create mode 100644 src/main/java/opensource/bravest/global/handler/StompHandler.java diff --git a/build.gradle b/build.gradle index fa3659a..c801fec 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'me.paulschwarz:spring-dotenv:4.0.0' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java index d4ae3a2..3ad8a10 100644 --- a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java +++ b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java @@ -49,7 +49,7 @@ public static ChatListResponse fromEntity(ChatList chatList) { .id(chatList.getId()) .roomId(chatList.getRoomId()) .content(chatList.getContent()) - .registeredBy(chatList.getRegisteredBy()) + .registeredBy(chatList.getRegisteredBy().getId()) .createdAt(chatList.getCreatedAt()) .build(); } diff --git a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java index 9d85170..45db599 100644 --- a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java +++ b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java @@ -2,11 +2,12 @@ import opensource.bravest.domain.chatList.entity.ChatList; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; public interface ChatListRepository extends JpaRepository { - // 특정 roomId에 해당하는 모든 아이디어 목록을 조회하는 메서드 추가 - List findAllByRoomIdOrderByCreatedAtDesc(Long roomId); + @Query("SELECT c FROM ChatList c WHERE c.room.id = :roomId ORDER BY c.createdAt DESC") + List findAllByRoomId(Long roomId); } \ No newline at end of file diff --git a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java index a9969b7..f44e908 100644 --- a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java +++ b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java @@ -52,7 +52,7 @@ public ChatListResponse createChatList(ChatListCreateRequest request) { } public List getChatListsByRoomId(Long roomId) { - List chatLists = chatListRepository.findAllByRoomIdOrderByCreatedAtDesc(roomId); + List chatLists = chatListRepository.findAllByRoomId(roomId); return chatLists.stream() .map(ChatListResponse::fromEntity) .collect(Collectors.toList()); diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java new file mode 100644 index 0000000..41e8e32 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -0,0 +1,46 @@ +package opensource.bravest.domain.message.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import opensource.bravest.domain.message.service.ChatMessageService; +import opensource.bravest.global.apiPayload.ApiResponse; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import java.security.Principal; + +import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; +import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; +import static opensource.bravest.domain.message.dto.MessageDto.ChatReadRequest; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ChatMessageController { + private final ChatMessageService chatMessageService; + private final SimpMessagingTemplate messagingTemplate; + + @MessageMapping("/chat.send") + public void sendMessage(MessageRequest request, Principal principal) { + Long id = Long.parseLong(principal.getName()); + MessageResponse response = chatMessageService.send(request, id); + + // 특정 채팅방 구독자들에게 메시지 전송 + messagingTemplate.convertAndSend("/sub/chat-room/" + request.getChatRoomId(), ApiResponse.onSuccess(response)); + } + + @MessageMapping("/chat/read") + public void readMessage(ChatReadRequest request, Long memberId) { + chatMessageService.readMessages(request.getChatRoomId(), memberId); + } + + /** + * 채팅 테스트용 페이지 + */ + @GetMapping("/chat-test") + public String chatTestPage() { + return "chat-test"; + } +} diff --git a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java index 6e87694..1660bed 100644 --- a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java +++ b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java @@ -1,6 +1,8 @@ package opensource.bravest.domain.message.dto; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.message.entity.ChatMessage; import java.time.LocalDateTime; @@ -12,15 +14,31 @@ public static class SendMessageRequest { } @Getter + @RequiredArgsConstructor public static class MessageResponse { private final String senderName; // 익명 닉네임 private final String content; private final LocalDateTime createdAt; - public MessageResponse(String senderName, String content, LocalDateTime createdAt) { - this.senderName = senderName; - this.content = content; - this.createdAt = createdAt; + public static MessageResponse from(ChatMessage chatMessage) { + return new MessageResponse( + chatMessage.getSender().getAnonymousName(), + chatMessage.getContent(), + chatMessage.getCreatedAt() + ); } } + + @Getter + @RequiredArgsConstructor + public static class MessageRequest { + private final Long chatRoomId; + private final String content; + } + + @Getter + @RequiredArgsConstructor + public static class ChatReadRequest { + private final Long chatRoomId; + } } diff --git a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java new file mode 100644 index 0000000..b0db7ef --- /dev/null +++ b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java @@ -0,0 +1,61 @@ +package opensource.bravest.domain.message.service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; + +import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; +import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._CHATROOM_NOT_FOUND; +import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._USER_NOT_FOUND; + +import opensource.bravest.domain.message.entity.ChatMessage; +import opensource.bravest.domain.message.repository.ChatMessageRepository; +import opensource.bravest.domain.profile.entity.AnonymousProfile; +import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; +import opensource.bravest.domain.room.entity.AnonymousRoom; +import opensource.bravest.domain.room.repository.AnonymousRoomRepository; +import opensource.bravest.global.exception.CustomException; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +@Transactional +@RequiredArgsConstructor +public class ChatMessageService { + + private final AnonymousProfileRepository memberRepository; + private final AnonymousRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + + // 메시지 전송 + public MessageResponse send(MessageRequest request, Long id) { + AnonymousProfile sender = memberRepository.findById(id) + .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); + + AnonymousRoom chatRoom = chatRoomRepository.findById(request.getChatRoomId()) + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + + ChatMessage chatMessage = ChatMessage.builder() + .room(chatRoom) + .sender(sender) + .content(request.getContent()) + .build(); + + chatMessageRepository.save(chatMessage); + + return MessageResponse.from(chatMessage); + } + + @Transactional + public void readMessages(Long chatRoomId, Long memberId) { + AnonymousRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + +// if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && !Objects.equals(chatRoom.getMember2().getId(), +// memberId)) { +// throw new BaseException(ChatExceptionType.CHAT_ROOM_ACCESS_DENIED); +// } +// messageReceiptRepository.bulkUpdateStatusToRead(chatRoomId, memberId); + } +} \ No newline at end of file diff --git a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java index b6d6e04..1f9e134 100644 --- a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java +++ b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java @@ -1,8 +1,11 @@ package opensource.bravest.domain.room.entity; import jakarta.persistence.*; import lombok.*; +import opensource.bravest.domain.profile.entity.AnonymousProfile; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -23,5 +26,9 @@ public class AnonymousRoom { @Column(nullable = false, length = 100) private String title; + @OneToMany(mappedBy = "room") + private List profiles = new ArrayList<>(); + + private LocalDateTime createdAt; } \ No newline at end of file diff --git a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java new file mode 100644 index 0000000..0517500 --- /dev/null +++ b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java @@ -0,0 +1,38 @@ +package opensource.bravest.global.config; + +import lombok.RequiredArgsConstructor; +import opensource.bravest.global.handler.StompHandler; +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; + +@Configuration +@RequiredArgsConstructor +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompHandler stompHandler; + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws-connect") + .setAllowedOriginPatterns("*") + .withSockJS(); + registry.addEndpoint("/ws-connect") + .setAllowedOriginPatterns("*"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/sub"); + registry.setApplicationDestinationPrefixes("/pub"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } +} diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java new file mode 100644 index 0000000..0792242 --- /dev/null +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -0,0 +1,72 @@ +package opensource.bravest.global.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; +import opensource.bravest.global.security.jwt.JwtTokenProvider; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.security.Principal; + +@Slf4j +@Component +@RequiredArgsConstructor +public class StompHandler implements ChannelInterceptor { + private final JwtTokenProvider jwtProvider; + private final AnonymousProfileRepository anonymousProfileRepository; + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor == null) { + return message; + } + + StompCommand command = accessor.getCommand(); + if (StompCommand.CONNECT.equals(command)) { + String token = accessor.getFirstNativeHeader("Authorization"); + + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); + } + + // 토큰 검증 후 인증 객체 생성 + if (token != null && jwtProvider.validateToken(token)) { + Long id = jwtProvider.getIdFromToken(token); + + anonymousProfileRepository.findById(id).ifPresent(member -> { + Authentication authentication = new UsernamePasswordAuthenticationToken( + id, null + ); + // SecurityContext에도 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + // STOMP 세션 사용자로도 설정 -> @MessageMapping Principal로 전달됨 + accessor.setUser(authentication); + log.info("STOMP 연결 인증 성공 및 Principal 설정: {}", id); + }); + } else { + log.warn("STOMP CONNECT 토큰 검증 실패 또는 토큰 누락"); + } + } else if (StompCommand.SEND.equals(command) || StompCommand.SUBSCRIBE.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + } + } + } + + return message; + } +} diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java index ff762a4..ad8fc06 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java @@ -56,6 +56,27 @@ public String createRefreshToken(String subject) { .compact(); } + public Long getIdFromToken(String token) { + return Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) + .build() + .parseClaimsJws(token) + .getBody() + .get("id", Long.class); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) + .build() + .parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + public Claims parseClaims(String token) { return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); } From 3a118478d955ea4fdda39d06203768efb11b546e Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 01:00:31 +0900 Subject: [PATCH 02/44] [bugfix] mapping name rule fix --- .../domain/message/controller/ChatMessageController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java index 41e8e32..0312d6e 100644 --- a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -22,7 +22,7 @@ public class ChatMessageController { private final ChatMessageService chatMessageService; private final SimpMessagingTemplate messagingTemplate; - @MessageMapping("/chat.send") + @MessageMapping("/chat/send") public void sendMessage(MessageRequest request, Principal principal) { Long id = Long.parseLong(principal.getName()); MessageResponse response = chatMessageService.send(request, id); From b9a5d1631f9c165cdbef56205d92d36ff6806cb6 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Thu, 20 Nov 2025 01:17:41 +0900 Subject: [PATCH 03/44] [hotix] Change ResponseEntity to ApiResponse --- .../controller/ChatListController.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java index 7748ff5..11c5a3d 100644 --- a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java +++ b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java @@ -5,6 +5,7 @@ import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListCreateRequest; import opensource.bravest.domain.chatList.service.ChatListService; +import opensource.bravest.global.apiPayload.ApiResponse; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -28,33 +29,33 @@ public class ChatListController { private final ChatListService chatListService; @PostMapping - public ResponseEntity createChatList(@Valid @RequestBody ChatListCreateRequest request) { + public ApiResponse createChatList(@Valid @RequestBody ChatListCreateRequest request) { ChatListResponse response = chatListService.createChatList(request); - return new ResponseEntity<>(response, HttpStatus.CREATED); // 201 Created + return ApiResponse.onSuccess(response); } @GetMapping("/room/{roomId}") - public ResponseEntity> getChatListsByRoomId(@PathVariable Long roomId) { + public ApiResponse> getChatListsByRoomId(@PathVariable Long roomId) { List response = chatListService.getChatListsByRoomId(roomId); - return ResponseEntity.ok(response); // 200 OK + return ApiResponse.onSuccess(response); } @GetMapping("/{id}") - public ResponseEntity getChatListById(@PathVariable Long id) { + public ApiResponse getChatListById(@PathVariable Long id) { ChatListResponse response = chatListService.getChatListById(id); - return ResponseEntity.ok(response); // 200 OK + return ApiResponse.onSuccess(response); } @PutMapping("/{id}") - public ResponseEntity updateChatList(@PathVariable Long id, + public ApiResponse updateChatList(@PathVariable Long id, @Valid @RequestBody ChatListUpdateRequest request) { ChatListResponse response = chatListService.updateChatList(id, request); - return ResponseEntity.ok(response); // 200 OK + return ApiResponse.onSuccess(response); } @DeleteMapping("/{id}") - public ResponseEntity deleteChatList(@PathVariable Long id) { + public ApiResponse deleteChatList(@PathVariable Long id) { chatListService.deleteChatList(id); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); // 204 No Content + return ApiResponse.onSuccess(null); } } \ No newline at end of file From ccb79320f3775ef98101ba258320bf838b563677 Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 01:58:44 +0900 Subject: [PATCH 04/44] [bugfix] AnonymousProfileResponse dev --- .../profile/dto/AnonymousProfileResponse.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java diff --git a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java new file mode 100644 index 0000000..e7d8c8c --- /dev/null +++ b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java @@ -0,0 +1,22 @@ +package opensource.bravest.domain.profile.dto; + +import lombok.Builder; +import lombok.Getter; +import opensource.bravest.domain.profile.entity.AnonymousProfile; + +@Getter +@Builder +public class AnonymousProfileResponse { + private Long id; + private Long roomId; + private String nickname; + // 필요한 필드만 + + public static AnonymousProfileResponse from(AnonymousProfile profile) { + return AnonymousProfileResponse.builder() + .id(profile.getId()) + .roomId(profile.getRoom().getId()) + .nickname(profile.getAnonymousName()) + .build(); + } +} From 109f45d66eb9ce7022870a729b18a9a7be64e0bc Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 01:59:22 +0900 Subject: [PATCH 05/44] [bugfix] AnonymousProfileService dev --- .../service/AnonymousProfileService.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java diff --git a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java new file mode 100644 index 0000000..74326d5 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java @@ -0,0 +1,49 @@ +package opensource.bravest.domain.profile.service; + +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.profile.dto.CreateAnonymousProfileRequest; +import opensource.bravest.domain.profile.entity.AnonymousProfile; +import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; +import opensource.bravest.domain.room.entity.AnonymousRoom; +import opensource.bravest.domain.room.repository.AnonymousRoomRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AnonymousProfileService { + + private final AnonymousProfileRepository anonymousProfileRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + + @Transactional + public AnonymousProfile createAnonymousProfile(Long roomId, CreateAnonymousProfileRequest request) { + AnonymousRoom room = anonymousRoomRepository.findById(roomId) + .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); + + // 중복 프로필 체크 + Optional existingProfile = anonymousProfileRepository.findByRoomAndRealUserId(room, request.getRealUserId()); + if (existingProfile.isPresent()) { + throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); + } + + AnonymousProfile newProfile = AnonymousProfile.builder() + .room(room) + .realUserId(request.getRealUserId()) + .anonymousName(request.getAnonymousName()) + .build(); + + return anonymousProfileRepository.save(newProfile); + } + + @Transactional + public void deleteAnonymousProfile(Long profileId) { + if (!anonymousProfileRepository.existsById(profileId)) { + throw new RuntimeException("없는 사용자임. 너~ 누구야!"); + } + anonymousProfileRepository.deleteById(profileId); + } +} From 0aea50f12f7a85e79ae1dc20b1ce52609ba93cc2 Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 01:59:47 +0900 Subject: [PATCH 06/44] [feature] AnonymousProfileRequest dev --- .../profile/dto/CreateAnonymousProfileRequest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java diff --git a/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java new file mode 100644 index 0000000..4b06c9e --- /dev/null +++ b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java @@ -0,0 +1,11 @@ +package opensource.bravest.domain.profile.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CreateAnonymousProfileRequest { + private Long realUserId; + private String anonymousName; +} From 1cb7abd9af732910d7f6eefbcb04b2d8fc44093f Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:00:03 +0900 Subject: [PATCH 07/44] [feature] AnonymousProfileController dev --- .../AnonymousProfileController.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java diff --git a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java new file mode 100644 index 0000000..26a5d5d --- /dev/null +++ b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java @@ -0,0 +1,39 @@ +package opensource.bravest.domain.profile.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.profile.dto.AnonymousProfileResponse; +import opensource.bravest.domain.profile.dto.CreateAnonymousProfileRequest; +import opensource.bravest.domain.profile.entity.AnonymousProfile; +import opensource.bravest.domain.profile.service.AnonymousProfileService; +import opensource.bravest.global.apiPayload.ApiResponse; +import opensource.bravest.global.apiPayload.code.status.SuccessStatus; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/anonymous-profiles") +public class AnonymousProfileController { + + private final AnonymousProfileService anonymousProfileService; + + + @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") + @PostMapping("/rooms/{roomId}") + public ApiResponse createAnonymousProfile( + @PathVariable Long roomId, + @RequestBody CreateAnonymousProfileRequest request + ) { + AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); + AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); + } + + @DeleteMapping("/{profileId}") + @Operation(summary = "익명 프로필 삭제", description = "ID로 특정 익명 프로필을 삭제합니다.") + public ApiResponse deleteAnonymousProfile(@PathVariable Long profileId) { + anonymousProfileService.deleteAnonymousProfile(profileId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } +} From 5dbb11e628b5d8b82aca8d3d7168a70fa48939dc Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:00:16 +0900 Subject: [PATCH 08/44] [feature] Room dev --- .../room/controller/RoomController.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/room/controller/RoomController.java diff --git a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java new file mode 100644 index 0000000..b331461 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java @@ -0,0 +1,80 @@ +package opensource.bravest.domain.room.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.room.dto.RoomDto; +import opensource.bravest.domain.room.entity.AnonymousRoom; +import opensource.bravest.domain.room.service.RoomService; +import opensource.bravest.global.apiPayload.ApiResponse; +import opensource.bravest.global.apiPayload.code.status.SuccessStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/rooms") +public class RoomController { + + private final RoomService roomService; + + @PostMapping + @Operation(summary = "채팅방 생성", description = "새로운 채팅방을 생성합니다.") + public ApiResponse createRoom(@RequestBody RoomDto.CreateRoomRequest request) { + AnonymousRoom room = roomService.createRoom(request); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } + + @GetMapping("/{roomId}") + @Operation(summary = "채팅방 조회", description = "ID로 특정 채팅방의 정보를 조회합니다.") + public ApiResponse getRoom(@PathVariable Long roomId) { + AnonymousRoom room = roomService.getRoom(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } + + @PutMapping("/{roomId}") + @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") + public ApiResponse updateRoom(@PathVariable Long roomId, @RequestBody RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = roomService.updateRoom(roomId, request); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } + + @DeleteMapping("/{roomId}") + @Operation(summary = "채팅방 삭제", description = "ID로 특정 채팅방을 삭제합니다.") + public ApiResponse deleteRoom(@PathVariable Long roomId) { + roomService.deleteRoom(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @GetMapping("/{roomId}/invite-code") + @Operation(summary = "초대 코드 조회", description = "ID로 특정 채팅방의 초대 코드를 조회합니다.") + public ApiResponse getInviteCode(@PathVariable Long roomId) { + String inviteCode = roomService.getInviteCode(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), inviteCode); + } + + @PostMapping("/join") + @Operation(summary = "초대 코드로 채팅방 참여", description = "초대 코드를 사용하여 특정 채팅방에 참여합니다.") + public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { + AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } +} From 2ddd47e967913aba0bc3626d29f8908d2179f26f Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:00:24 +0900 Subject: [PATCH 09/44] [feature] Room dev --- .../domain/room/service/RoomService.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/opensource/bravest/domain/room/service/RoomService.java diff --git a/src/main/java/opensource/bravest/domain/room/service/RoomService.java b/src/main/java/opensource/bravest/domain/room/service/RoomService.java new file mode 100644 index 0000000..2ffb4a5 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/room/service/RoomService.java @@ -0,0 +1,68 @@ +package opensource.bravest.domain.room.service; + +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.room.dto.RoomDto; +import opensource.bravest.domain.room.entity.AnonymousRoom; +import opensource.bravest.domain.room.repository.AnonymousRoomRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomService { + + private final AnonymousRoomRepository anonymousRoomRepository; + + @Transactional + public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { + String roomCode = generateUniqueRoomCode(); + AnonymousRoom room = AnonymousRoom.builder() + .title(request.getTitle()) + .roomCode(roomCode) + .createdAt(LocalDateTime.now()) + .build(); + return anonymousRoomRepository.save(room); + } + + public AnonymousRoom getRoom(Long roomId) { + return anonymousRoomRepository.findById(roomId) + .orElseThrow(() -> new RuntimeException("Room not found")); + } + + @Transactional + public AnonymousRoom updateRoom(Long roomId, RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = getRoom(roomId); + room.updateTitle(request.getTitle()); + return room; + } + + @Transactional + public void deleteRoom(Long roomId) { + if (!anonymousRoomRepository.existsById(roomId)) { + throw new RuntimeException("Room not found"); + } + anonymousRoomRepository.deleteById(roomId); + } + + public String getInviteCode(Long roomId) { + AnonymousRoom room = getRoom(roomId); + return room.getRoomCode(); + } + + public AnonymousRoom joinRoom(String roomCode) { + return anonymousRoomRepository.findByRoomCode(roomCode) + .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); + } + + private String generateUniqueRoomCode() { + String roomCode; + do { + roomCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); + } while (anonymousRoomRepository.existsByRoomCode(roomCode)); + return roomCode; + } +} From adcd2b9493f5650d886ee743f64070d95cef0e5f Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:00:48 +0900 Subject: [PATCH 10/44] [feature] add SecurityConfig dev --- .../java/opensource/bravest/global/config/SecurityConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index 90466a3..6596360 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -40,6 +40,8 @@ public class SecurityConfig { "/oauth2/**", "/login/**", "/login/oauth2/**", "/api/test/auth/**", + "/rooms/**", + "/anonymous-profiles/**" }; // 정적 리소스 From e075b1f83e2ab81ed5e6e6358a249b6a4ccbca1f Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:01:07 +0900 Subject: [PATCH 11/44] [feature] Room dev --- .../bravest/domain/room/dto/RoomDto.java | 41 +++++++++++-------- .../domain/room/entity/AnonymousRoom.java | 7 +++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java index 996eb29..e1175e2 100644 --- a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java +++ b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java @@ -1,35 +1,40 @@ package opensource.bravest.domain.room.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; public class RoomDto { @Getter + @NoArgsConstructor public static class CreateRoomRequest { private String title; } @Getter - public static class CreateRoomResponse { - private final String roomCode; - private final String title; - - public CreateRoomResponse(String roomCode, String title) { - this.roomCode = roomCode; - this.title = title; - } + @NoArgsConstructor + public static class UpdateRoomRequest { + private String title; } @Getter - public static class JoinRoomResponse { - private final String roomCode; - private final String title; - private final String anonymousName; + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RoomResponse { + private Long id; + private String roomCode; + private String title; + private LocalDateTime createdAt; + } - public JoinRoomResponse(String roomCode, String title, String anonymousName) { - this.roomCode = roomCode; - this.title = title; - this.anonymousName = anonymousName; - } + @Getter + @NoArgsConstructor + public static class JoinRoomRequest { + private String roomCode; } -} \ No newline at end of file +} diff --git a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java index 1f9e134..aae4aa7 100644 --- a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java +++ b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java @@ -26,9 +26,14 @@ public class AnonymousRoom { @Column(nullable = false, length = 100) private String title; - @OneToMany(mappedBy = "room") + @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default private List profiles = new ArrayList<>(); private LocalDateTime createdAt; + + public void updateTitle(String title) { + this.title = title; + } } \ No newline at end of file From ec7370c0cc8901b2b3096ed27f07c5e96e847895 Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:01:29 +0900 Subject: [PATCH 12/44] [feature] Setting different dev --- .../apiPayload/code/status/SuccessStatus.java | 1 + .../global/security/jwt/JwtTokenProvider.java | 41 +++++++++++++------ src/main/resources/application.yaml | 4 ++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java index 2fbfca6..2400189 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java @@ -10,6 +10,7 @@ @AllArgsConstructor public enum SuccessStatus implements BaseCode { _OK(HttpStatus.OK, "COMMON2000", "성공입니다."), + _CREATED(HttpStatus.CREATED, "COMMON201", "생성되었습니다."), ; private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java index ad8fc06..e2e3b36 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Date; import java.util.Map; @@ -29,9 +30,19 @@ public class JwtTokenProvider { @PostConstruct void init() { + if (secret == null || secret.isBlank()) { + throw new IllegalStateException("jwt.secret is not configured. Check your application.yml / env."); + } + byte[] keyBytes; - // secret이 Base64가 아니더라도 자동 처리 가능하게 - try { keyBytes = Decoders.BASE64.decode(secret); } catch (Exception e) { keyBytes = secret.getBytes(); } + try { + // secret이 Base64면 여기서 정상 디코딩 + keyBytes = Decoders.BASE64.decode(secret); + } catch (IllegalArgumentException e) { + // Base64 아니면 그냥 문자열 바이트로 사용 + keyBytes = secret.getBytes(StandardCharsets.UTF_8); + } + this.key = Keys.hmacShaKeyFor(keyBytes); } @@ -57,20 +68,21 @@ public String createRefreshToken(String subject) { } public Long getIdFromToken(String token) { - return Jwts.parser() - .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) - .build() - .parseClaimsJws(token) - .getBody() - .get("id", Long.class); + Claims claims = Jwts.parser() + .verifyWith(key) // init()에서 만든 key 재사용 + .build() + .parseSignedClaims(token) + .getPayload(); + + return claims.get("id", Long.class); } public boolean validateToken(String token) { try { Jwts.parser() - .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) - .build() - .parseClaimsJws(token); + .verifyWith(key) + .build() + .parseSignedClaims(token); return true; } catch (Exception e) { return false; @@ -78,7 +90,10 @@ public boolean validateToken(String token) { } public Claims parseClaims(String token) { - return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); } } - diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 087fee5..9a13741 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -20,3 +20,7 @@ server: encoding: charset: UTF-8 force: true +jwt: + secret: ${JWT_SECRET} + access-token-validity-seconds: ${JWT_ACCESS_TOKEN_VALIDITY_SECONDS} + refresh-token-validity-seconds: ${JWT_REFRESH_TOKEN_VALIDITY_SECONDS} \ No newline at end of file From 829483c86d7865354eee800dbce1eb8fc6b0ef35 Mon Sep 17 00:00:00 2001 From: 2heunxun Date: Thu, 20 Nov 2025 02:27:16 +0900 Subject: [PATCH 13/44] [feature] vote system --- .../vote/controller/VoteController.java | 62 +++++++++ .../bravest/domain/vote/dto/VoteDto.java | 43 ++++++ .../bravest/domain/vote/entity/UserVote.java | 29 ++++ .../bravest/domain/vote/entity/Vote.java | 40 ++++++ .../domain/vote/entity/VoteOption.java | 31 +++++ .../vote/repository/UserVoteRepository.java | 12 ++ .../vote/repository/VoteOptionRepository.java | 7 + .../vote/repository/VoteRepository.java | 7 + .../domain/vote/service/VoteService.java | 127 ++++++++++++++++++ .../bravest/global/config/SecurityConfig.java | 3 +- 10 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/main/java/opensource/bravest/domain/vote/controller/VoteController.java create mode 100644 src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java create mode 100644 src/main/java/opensource/bravest/domain/vote/entity/UserVote.java create mode 100644 src/main/java/opensource/bravest/domain/vote/entity/Vote.java create mode 100644 src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java create mode 100644 src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java create mode 100644 src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java create mode 100644 src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java create mode 100644 src/main/java/opensource/bravest/domain/vote/service/VoteService.java diff --git a/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java new file mode 100644 index 0000000..bf40633 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java @@ -0,0 +1,62 @@ +package opensource.bravest.domain.vote.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.vote.dto.VoteDto; +import opensource.bravest.domain.vote.entity.Vote; +import opensource.bravest.domain.vote.service.VoteService; +import opensource.bravest.global.apiPayload.ApiResponse; +import opensource.bravest.global.apiPayload.code.status.SuccessStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/votes") +public class VoteController { + + private final VoteService voteService; + + @PostMapping + @Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다.") + public ApiResponse createVote(@RequestBody VoteDto.CreateVoteRequest request) { + Vote vote = voteService.createVote(request); + // The response DTO needs to be built manually + VoteDto.VoteResponse responseDto = voteService.getVoteResult(vote.getId()); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), responseDto); + } + + @GetMapping("/{voteId}") + @Operation(summary = "투표 조회", description = "ID로 특정 투표의 정보를 조회합니다.") + public ApiResponse getVote(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @PostMapping("/{voteId}/cast") + @Operation(summary = "투표 참여", description = "특정 투표 항목에 투표합니다.") + public ApiResponse castVote(@PathVariable Long voteId, @RequestBody VoteDto.CastVoteRequest request) { + voteService.castVote(voteId, request); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @PostMapping("/{voteId}/end") + @Operation(summary = "투표 종료", description = "특정 투표를 종료합니다.") + public ApiResponse endVote(@PathVariable Long voteId) { + voteService.endVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @GetMapping("/{voteId}/result") + @Operation(summary = "투표 결과 조회", description = "종료된 투표의 결과를 조회합니다.") + public ApiResponse getVoteResult(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @DeleteMapping("/{voteId}") + @Operation(summary = "투표 삭제", description = "ID로 특정 투표를 삭제합니다.") + public ApiResponse deleteVote(@PathVariable Long voteId) { + voteService.deleteVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } +} diff --git a/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java new file mode 100644 index 0000000..77ac32d --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java @@ -0,0 +1,43 @@ +package opensource.bravest.domain.vote.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +public class VoteDto { + + @Getter + @NoArgsConstructor + public static class CreateVoteRequest { + private Long roomId; + private List messages; + } + + @Getter + @NoArgsConstructor + public static class CastVoteRequest { + private Long voteOptionId; + private Long anonymousProfileId; + } + + @Getter + @Builder + public static class VoteResponse { + private Long id; + private String title; + private boolean isActive; + private LocalDateTime createdAt; + private List options; + } + + @Getter + @Builder + public static class VoteOptionResponse { + private Long id; + private String messageContent; + private int voteCount; + } +} diff --git a/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java new file mode 100644 index 0000000..2b71b82 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java @@ -0,0 +1,29 @@ +package opensource.bravest.domain.vote.entity; + +import jakarta.persistence.*; +import lombok.*; +import opensource.bravest.domain.profile.entity.AnonymousProfile; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserVote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_option_id", nullable = false) + private VoteOption voteOption; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "anonymous_profile_id", nullable = false) + private AnonymousProfile voter; +} diff --git a/src/main/java/opensource/bravest/domain/vote/entity/Vote.java b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java new file mode 100644 index 0000000..03ca1d5 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java @@ -0,0 +1,40 @@ +package opensource.bravest.domain.vote.entity; + +import jakarta.persistence.*; +import lombok.*; +import opensource.bravest.domain.room.entity.AnonymousRoom; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Vote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; + + @Column(nullable = false, length = 100) + private String title; + + @Builder.Default + @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true) + private List options = new ArrayList<>(); + + private boolean isActive; + + private LocalDateTime createdAt; + + public void endVote() { + this.isActive = false; + } +} diff --git a/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java new file mode 100644 index 0000000..2ed3825 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java @@ -0,0 +1,31 @@ +package opensource.bravest.domain.vote.entity; + +import jakarta.persistence.*; +import lombok.*; +import opensource.bravest.domain.message.entity.ChatMessage; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class VoteOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; + + @Column(name = "message_content", nullable = false) + private String messageContent; + + @Column(nullable = false) + private int voteCount; + + public void incrementVoteCount() { + this.voteCount++; + } +} diff --git a/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java new file mode 100644 index 0000000..d9e3579 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java @@ -0,0 +1,12 @@ +package opensource.bravest.domain.vote.repository; + +import opensource.bravest.domain.profile.entity.AnonymousProfile; +import opensource.bravest.domain.vote.entity.UserVote; +import opensource.bravest.domain.vote.entity.Vote; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserVoteRepository extends JpaRepository { + Optional findByVoteAndVoter(Vote vote, AnonymousProfile voter); +} diff --git a/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java new file mode 100644 index 0000000..5b97137 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java @@ -0,0 +1,7 @@ +package opensource.bravest.domain.vote.repository; + +import opensource.bravest.domain.vote.entity.VoteOption; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VoteOptionRepository extends JpaRepository { +} diff --git a/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java new file mode 100644 index 0000000..e7b9d91 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java @@ -0,0 +1,7 @@ +package opensource.bravest.domain.vote.repository; + +import opensource.bravest.domain.vote.entity.Vote; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VoteRepository extends JpaRepository { +} diff --git a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java new file mode 100644 index 0000000..8d39887 --- /dev/null +++ b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java @@ -0,0 +1,127 @@ +package opensource.bravest.domain.vote.service; + +import lombok.RequiredArgsConstructor; +import opensource.bravest.domain.message.entity.ChatMessage; +import opensource.bravest.domain.message.repository.ChatMessageRepository; +import opensource.bravest.domain.profile.entity.AnonymousProfile; +import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; +import opensource.bravest.domain.room.entity.AnonymousRoom; +import opensource.bravest.domain.room.repository.AnonymousRoomRepository; +import opensource.bravest.domain.vote.dto.VoteDto; +import opensource.bravest.domain.vote.entity.UserVote; +import opensource.bravest.domain.vote.entity.Vote; +import opensource.bravest.domain.vote.entity.VoteOption; +import opensource.bravest.domain.vote.repository.UserVoteRepository; +import opensource.bravest.domain.vote.repository.VoteRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class VoteService { + + private final VoteRepository voteRepository; + private final UserVoteRepository userVoteRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousProfileRepository anonymousProfileRepository; + + @Transactional + public Vote createVote(VoteDto.CreateVoteRequest request) { + AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) + .orElseThrow(() -> new RuntimeException("Room not found")); + + Vote vote = Vote.builder() + .room(room) + .title(room.getTitle()) + .isActive(true) + .createdAt(LocalDateTime.now()) + .build(); + + List options = request.getMessages().stream() + .map(message -> VoteOption.builder() + .vote(vote) + .messageContent(message) + .voteCount(0) + .build()) + .collect(Collectors.toList()); + + vote.getOptions().addAll(options); + + return voteRepository.save(vote); + } + + @Transactional + public void castVote(Long voteId, VoteDto.CastVoteRequest request) { + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new RuntimeException("Vote not found")); + if (!vote.isActive()) { + throw new RuntimeException("Vote is not active"); + } + + AnonymousProfile voter = anonymousProfileRepository.findById(request.getAnonymousProfileId()) + .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); + + if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { + throw new RuntimeException("User has already voted"); + } + + VoteOption voteOption = vote.getOptions().stream() + .filter(option -> option.getId().equals(request.getVoteOptionId())) + .findFirst() + .orElseThrow(() -> new RuntimeException("VoteOption not found")); + + voteOption.incrementVoteCount(); + + UserVote userVote = UserVote.builder() + .vote(vote) + .voteOption(voteOption) + .voter(voter) + .build(); + userVoteRepository.save(userVote); + } + + @Transactional + public void endVote(Long voteId) { + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new RuntimeException("Vote not found")); + vote.endVote(); + } + + public VoteDto.VoteResponse getVoteResult(Long voteId) { + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new RuntimeException("Vote not found")); + + return buildVoteResponse(vote); + } + + @Transactional + public void deleteVote(Long voteId) { + if (!voteRepository.existsById(voteId)) { + throw new RuntimeException("Vote not found"); + } + voteRepository.deleteById(voteId); + } + + private VoteDto.VoteResponse buildVoteResponse(Vote vote) { + List optionResponses = vote.getOptions().stream() + .map(option -> VoteDto.VoteOptionResponse.builder() + .id(option.getId()) + .messageContent(option.getMessageContent()) + .voteCount(option.getVoteCount()) + .build()) + .collect(Collectors.toList()); + + return VoteDto.VoteResponse.builder() + .id(vote.getId()) + .title(vote.getTitle()) + .isActive(vote.isActive()) + .createdAt(vote.getCreatedAt()) + .options(optionResponses) + .build(); + } +} diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index 6596360..8de3f21 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -41,7 +41,8 @@ public class SecurityConfig { "/login/**", "/login/oauth2/**", "/api/test/auth/**", "/rooms/**", - "/anonymous-profiles/**" + "/anonymous-profiles/**", + "/votes/**" }; // 정적 리소스 From afc4dfd5cab2bdbd7a5c0b4ae485e2b669c52ad8 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 24 Nov 2025 21:51:50 +0900 Subject: [PATCH 14/44] [hotix] Add Websocket, Chatlist endpoint in SecurityConfig --- .../bravest/domain/chatList/service/ChatListService.java | 3 --- .../java/opensource/bravest/global/config/SecurityConfig.java | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java index f44e908..6050c7e 100644 --- a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java +++ b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java @@ -32,9 +32,6 @@ public class ChatListService { @Transactional public ChatListResponse createChatList(ChatListCreateRequest request) { - - Long id = 1L; - AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index 8de3f21..eb5b4be 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -41,8 +41,10 @@ public class SecurityConfig { "/login/**", "/login/oauth2/**", "/api/test/auth/**", "/rooms/**", + "/chatlists/**", "/anonymous-profiles/**", - "/votes/**" + "/votes/**", + "/ws-connect/**", "/chat-test", "/pub/**", "/sub/**" }; // 정적 리소스 From 4c6074f6c79765c27c07f000edbd4be429ccd291 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 24 Nov 2025 22:05:25 +0900 Subject: [PATCH 15/44] [hotix] Add Websocket, Chatlist endpoint in SecurityConfig --- .../bravest/domain/chatList/controller/ChatListController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java index 11c5a3d..3f6bdc7 100644 --- a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java +++ b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java @@ -22,7 +22,7 @@ import java.util.List; @RestController -@RequestMapping("/api/v1/chatlists") +@RequestMapping("/chatlists") @RequiredArgsConstructor public class ChatListController { From 5065b2650b4c33e36ab158d3bb7468cfd41c684d Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 24 Nov 2025 22:23:57 +0900 Subject: [PATCH 16/44] [hotix] delete all contraints --- .../opensource/bravest/domain/chatList/dto/ChatListDto.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java index 3ad8a10..282cfb4 100644 --- a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java +++ b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java @@ -15,13 +15,10 @@ public class ChatListDto { @Setter public static class ChatListCreateRequest { - @NotNull(message = "채팅방 ID는 필수입니다.") private Long roomId; - @NotBlank(message = "아이디어 내용은 필수입니다.") private String content; - @NotBlank(message = "등록자는 필수입니다.") private Long registeredBy; } @@ -31,7 +28,6 @@ public static class ChatListCreateRequest { public static class ChatListUpdateRequest { // 아이디어 내용 수정만 가정 - @NotBlank(message = "수정할 내용은 필수입니다.") private String content; } From e0ea52017ab07f8ca07f6d3f04d4f9f058c1460f Mon Sep 17 00:00:00 2001 From: semi-yu Date: Wed, 26 Nov 2025 17:17:40 +0900 Subject: [PATCH 17/44] chore: Add backend dockerfile and deployment doc Added `.dockerignore` Added `Dockerfile.backend` which describes how to build and dockerize the Bravest backend WAS Added BACKED_DEPLOYMENT.md document to explain the purpose of the Dockerfile Removed compose.yaml to reduce further confusion --- .dockerignore | 6 +++++ BACKEND_DEPLOYMENT.md | 61 +++++++++++++++++++++++++++++++++++++++++++ Dockerfile.backend | 31 ++++++++++++++++++++++ compose.yaml | 10 ------- 4 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 .dockerignore create mode 100644 BACKEND_DEPLOYMENT.md create mode 100644 Dockerfile.backend delete mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f27098d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gradle +build +.idea +*.iml +.env \ No newline at end of file diff --git a/BACKEND_DEPLOYMENT.md b/BACKEND_DEPLOYMENT.md new file mode 100644 index 0000000..48ab1c8 --- /dev/null +++ b/BACKEND_DEPLOYMENT.md @@ -0,0 +1,61 @@ +## 개요 +당 문서는 Bravest 백엔드를 컨테이너 이미지로 패키징하는 `Dockerfile.backend`에 대해 설명합니다. + +작성자 @semi-yu + +## 상세 + +### 사전 지식 + +자바 애플리케이션은 배포 전에 먼저 바이트코드(JAR)로 컴파일되어야 합니다. 이 과정에서 의존성 해소 등을 위해 Gradle이 관여하지만, 이 프로젝트에서는 `Dockerfile.backend`가 `./gradlew bootJar`를 실행하도록 설정되어 있으므로 별도로 신경 쓸 필요는 없습니다. + +다만, 이 컴파일(빌드) 과정을 전용 Dockerfile로 따로 분리하면 설정이 중복되거나 빌드 파이프라인이 복잡해질 수 있습니다. 따라서 하나의 Dockerfile에서 **빌드 스테이지와 실행 스테이지를 모두 정의하는 멀티 스테이지 빌드**를 사용합니다. 이를 통해 어플리케이션 JAR 파일을 생성한 뒤, 실행에 필요한 JDK(내부에 JRE 포함)만 포함하는 컨테이너 이미지를 바로 만들 수 있으며, 결과 이미지 크기 또한 줄일 수 있습니다. + +### 기타 통제 사항 +`Dockerfile.backend`는 여러분이 요청하신 대로 21 버전의 JDK를 사용하도록 정의하였습니다. 배포판은 Temurin을 사용합니다. +### 실행 + +한편, 멀티 스테이지 빌드는 **이미지 빌드 시점의 구조**에만 영향을 줄 뿐, 이후 컨테이너를 실행하는 과정 자체에는 특별한 차이를 만들지 않습니다. 아래 절차를 따르면 됩니다. + +#### 이미지를 빌드하기 위해서… + +- 작업 디렉토리를 `Dockerfile.backend`가 존재하는 프로젝트 루트로 이동한 뒤, 다음 명령어를 실행합니다: + +```bash +docker build \ + -f Dockerfile.backend \ + -t bravest-backend \ + . +``` +위 명령어는 `Dockerfile.backend`를 참조하여 백엔드 애플리케이션 이미지를 빌드합니다. + +#### 컨테이너 인스턴스를 실행하기 위해서... + +- `.env` 파일을 `Dockerfile.backend`가 존재하는 경로에 두고, 다음 명령어를 실행합니다: +```bash +docker run -d \ + --name bravest-backend \ + -p 8080:8080 \ + --env-file .env \ + bravest-backend +``` +- 위 명령어는 빌드된 bravest-backend 이미지를 기반으로 컨테이너 인스턴스를 실행합니다. +- 컨테이너 인스턴스가 외부 DB에 접속하기 위해 필요한 접속 정보는 `.env` 파일에 정의되어 있어야 하며, `--env-file .env` 옵션을 통해 컨테이너에 주입됩니다. + - 이를 통해 중요한 정보를 배포 환경의 파일 시스템에 저장하지 않고 환경 변수로 주입할 수 있습니다. + - 해당 파일을 얻으려면 팀원에게 연락해주세요. +#### 현재는... +- 스프링 부트로 작성된 백엔드의 Dockerfile만 정의되어 있습니다. +- 가까운 시일 내에, 전체 서비스를 정의하는 `docker-compose.yml`를 정의하겠습니다. +### 요약 + +#### 빌드 및 실행 +```bash +# 빌드 +docker build -f Dockerfile.backend -t bravest-backend . + +# 실행 +docker run -d --name bravest-backend -p 8080:8080 --env-file .env bravest-backend + +``` +- 사전 조건 + - 프로젝트 루트에 `.env` 파일이 있어야 합니다. 파일이 없다면 팀원에게 요청하세요. \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..e635dcb --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,31 @@ +# 빌드와 실행을 동시에 수행합니다. + +# 빌드 단계 +FROM eclipse-temurin:21-jdk-jammy AS build +WORKDIR /workspace + +COPY gradlew . +COPY gradle ./gradle + +COPY build.gradle settings.gradle ./ + +RUN chmod +x gradlew + +RUN ./gradlew --no-daemon dependencies || true + +COPY . . + +RUN ./gradlew --no-daemon bootJar + +# 실행 단계 +FROM eclipse-temurin:21-jdk-jammy + +WORKDIR /app + +COPY --from=build /workspace/build/libs/*.jar app.jar + +ENV JAVA_OPTS="-Dspring.profiles.active=prod" + +EXPOSE 8080 + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 4d2047e..0000000 --- a/compose.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - mysql: - image: 'mysql:latest' - environment: - - 'MYSQL_DATABASE=mydatabase' - - 'MYSQL_PASSWORD=secret' - - 'MYSQL_ROOT_PASSWORD=verysecret' - - 'MYSQL_USER=myuser' - ports: - - '3306' From 967f19c8d7c6a2c0682a4d08a7e813bfaabb048d Mon Sep 17 00:00:00 2001 From: semi-yu Date: Thu, 27 Nov 2025 13:43:35 +0900 Subject: [PATCH 18/44] chore: Add backend-manual-build.yml Add an action manually triggered on github, checks if `gradlew clean build` succeeds --- .github/workflows/backend-manual-build.yml | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/backend-manual-build.yml diff --git a/.github/workflows/backend-manual-build.yml b/.github/workflows/backend-manual-build.yml new file mode 100644 index 0000000..04303e6 --- /dev/null +++ b/.github/workflows/backend-manual-build.yml @@ -0,0 +1,34 @@ +name: Backend Manual Build + +on: + workflow_dispatch: + +jobs: + backend-manual-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: ./gradlew clean build From 5f4de3e4514230e5cce9cd789f4d18406f21fdf3 Mon Sep 17 00:00:00 2001 From: semi-yu <97109907+semi-yu@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:00:55 +0900 Subject: [PATCH 19/44] ci: wire backend manual build to github secrets --- .github/workflows/backend-manual-build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/backend-manual-build.yml b/.github/workflows/backend-manual-build.yml index 04303e6..7b3ed5d 100644 --- a/.github/workflows/backend-manual-build.yml +++ b/.github/workflows/backend-manual-build.yml @@ -7,6 +7,14 @@ jobs: backend-manual-build: runs-on: ubuntu-latest + env: + DB_URL: ${{ secrets.DB_URL }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + JWT_ACCESS_TOKEN_VALIDITY_SECONDS: ${{ secrets.JWT_ACCESS_TOKEN_VALIDITY_SECONDS }} + JWT_REFRESH_TOKEN_VALIDITY_SECONDS: ${{ secrets.JWT_REFRESH_TOKEN_VALIDITY_SECONDS }} + steps: - name: Checkout repository uses: actions/checkout@v4 From b04fdf06291955c9e245b0141ade690195d5d024 Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sat, 29 Nov 2025 11:56:57 +0900 Subject: [PATCH 20/44] fix: Change websocket configure To provide proper fallback, left only one registry `addEndpoint()` Changed subscription and publish endpoint --- .../opensource/bravest/global/config/WebSocketConfig.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java index 0517500..87931a0 100644 --- a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java +++ b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java @@ -21,14 +21,12 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-connect") .setAllowedOriginPatterns("*") .withSockJS(); - registry.addEndpoint("/ws-connect") - .setAllowedOriginPatterns("*"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/sub"); - registry.setApplicationDestinationPrefixes("/pub"); + registry.enableSimpleBroker("/subs"); + registry.setApplicationDestinationPrefixes("/pubs"); } @Override From 4301c63d2467604f73b9f6d258a45617364ce626 Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sat, 29 Nov 2025 12:02:51 +0900 Subject: [PATCH 21/44] fix: Add STOMP annotation Spring STOMP send publish or subscribe message to /pubs/** and /subs/** Unnecessary methods on product environment removed --- .../controller/ChatMessageController.java | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java index 0312d6e..7c7bee1 100644 --- a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -5,15 +5,14 @@ import opensource.bravest.domain.message.service.ChatMessageService; import opensource.bravest.global.apiPayload.ApiResponse; import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import java.security.Principal; import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; -import static opensource.bravest.domain.message.dto.MessageDto.ChatReadRequest; @Slf4j @Controller @@ -22,25 +21,16 @@ public class ChatMessageController { private final ChatMessageService chatMessageService; private final SimpMessagingTemplate messagingTemplate; - @MessageMapping("/chat/send") - public void sendMessage(MessageRequest request, Principal principal) { + @MessageMapping("/send") + @SendTo("/subs/chat-rooms") + public void receiveMessage(MessageRequest request, Principal principal) { Long id = Long.parseLong(principal.getName()); MessageResponse response = chatMessageService.send(request, id); // 특정 채팅방 구독자들에게 메시지 전송 - messagingTemplate.convertAndSend("/sub/chat-room/" + request.getChatRoomId(), ApiResponse.onSuccess(response)); - } - - @MessageMapping("/chat/read") - public void readMessage(ChatReadRequest request, Long memberId) { - chatMessageService.readMessages(request.getChatRoomId(), memberId); - } - - /** - * 채팅 테스트용 페이지 - */ - @GetMapping("/chat-test") - public String chatTestPage() { - return "chat-test"; + messagingTemplate.convertAndSend( + "/subs/chat-rooms/" + request.getChatRoomId(), + ApiResponse.onSuccess(response) + ); } } From 83a1a0e7e328322018846db3f5d39ffce12929be Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sat, 29 Nov 2025 21:06:57 +0900 Subject: [PATCH 22/44] fix: Use anonymousId as STOMP principal - Anonymous user is now identified with anonymousId, instead of JWT - StompHandler JWT validation logic replaced with anonymousId validation - Get anonymousId from native header in CONNECT frame - Generate Principal --- .../bravest/global/handler/StompHandler.java | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index 0792242..803784d 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -27,36 +27,34 @@ public class StompHandler implements ChannelInterceptor { @Override public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + StompHeaderAccessor accessor = + MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor == null) { return message; } StompCommand command = accessor.getCommand(); if (StompCommand.CONNECT.equals(command)) { - String token = accessor.getFirstNativeHeader("Authorization"); - - if (token != null && token.startsWith("Bearer ")) { - token = token.substring(7); + String anonymousId = accessor.getFirstNativeHeader("anonymousId"); + if (anonymousId == null || anonymousId.isBlank()) { + log.warn("STOMP CONNECT: anonymousId 누락"); + throw new IllegalArgumentException("anonymousId header is required"); } - // 토큰 검증 후 인증 객체 생성 - if (token != null && jwtProvider.validateToken(token)) { - Long id = jwtProvider.getIdFromToken(token); - - anonymousProfileRepository.findById(id).ifPresent(member -> { - Authentication authentication = new UsernamePasswordAuthenticationToken( - id, null - ); - // SecurityContext에도 저장 - SecurityContextHolder.getContext().setAuthentication(authentication); - // STOMP 세션 사용자로도 설정 -> @MessageMapping Principal로 전달됨 - accessor.setUser(authentication); - log.info("STOMP 연결 인증 성공 및 Principal 설정: {}", id); - }); - } else { - log.warn("STOMP CONNECT 토큰 검증 실패 또는 토큰 누락"); - } + anonymousProfileRepository.findById(Long.valueOf(anonymousId)) + .ifPresentOrElse(member -> { + Authentication auth = new UsernamePasswordAuthenticationToken( + anonymousId, + null, + java.util.List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) + ); + SecurityContextHolder.getContext().setAuthentication(auth); + accessor.setUser(auth); // → @MessageMapping의 Principal로 전달 + log.info("STOMP CONNECT: anonymousId={} Principal 설정 완료", anonymousId);}, () -> { + log.warn("STOMP CONNECT: 존재하지 않는 anonymousId={}", anonymousId); + throw new IllegalArgumentException("Invalid anonymousId"); + }); } else if (StompCommand.SEND.equals(command) || StompCommand.SUBSCRIBE.equals(command)) { Principal user = accessor.getUser(); if (user == null) { @@ -66,7 +64,6 @@ public Message preSend(Message message, MessageChannel channel) { } } } - return message; } } From 2ecdd9e17c7523d4514b77044cdc7dbdd6c61afe Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sat, 29 Nov 2025 23:21:49 +0900 Subject: [PATCH 23/44] fix: Prevent duplicate STOMP subscriptions - Add user-based subscription tracking to `StompHandler` --- .../bravest/global/handler/StompHandler.java | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index 803784d..41992a7 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -17,6 +17,9 @@ import org.springframework.stereotype.Component; import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Slf4j @Component @@ -25,6 +28,8 @@ public class StompHandler implements ChannelInterceptor { private final JwtTokenProvider jwtProvider; private final AnonymousProfileRepository anonymousProfileRepository; + private final Map> userDestinations = new ConcurrentHashMap<>(); + @Override public Message preSend(Message message, MessageChannel channel) { StompHeaderAccessor accessor = @@ -55,7 +60,68 @@ public Message preSend(Message message, MessageChannel channel) { log.warn("STOMP CONNECT: 존재하지 않는 anonymousId={}", anonymousId); throw new IllegalArgumentException("Invalid anonymousId"); }); - } else if (StompCommand.SEND.equals(command) || StompCommand.SUBSCRIBE.equals(command)) { + } + + if (StompCommand.SUBSCRIBE.equals(command)) { + String destination = accessor.getDestination(); + Principal user = accessor.getUser(); + + // CONNECT에서 setUser를 해줬다면 여기서 null이면 안 되는 게 정상이나, + // 혹시 모르니 한 번 더 SecurityContext에서 복구 시도 + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + user = auth; + } + } + + if (user != null && destination != null) { + String userKey = user.getName(); // == anonymousId + + Set dests = + userDestinations.computeIfAbsent( + userKey, + k -> ConcurrentHashMap.newKeySet() + ); + + if (!dests.add(destination)) { + log.warn("중복 SUBSCRIBE 감지: anonymousId={}, destination={}", + userKey, destination); + // 이 SUBSCRIBE 프레임 자체를 무시 + return null; + } + } + } + + // 3) SEND에도 Principal이 비어 있으면 SecurityContext에서 복구 + if (StompCommand.SEND.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + } + } + } + + // (선택) DISCONNECT 시 userDestinations에서 정리 + if (StompCommand.DISCONNECT.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + user = auth; + } + } + if (user != null) { + String key = user.getName(); + userDestinations.remove(key); + log.info("DISCONNECT: anonymousId={} 구독 정보 제거", key); + } + } + + if (StompCommand.SEND.equals(command) || StompCommand.SUBSCRIBE.equals(command)) { Principal user = accessor.getUser(); if (user == null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); From 56ef3228348bc8452d9cd473d04c7c37ebb847e5 Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sat, 29 Nov 2025 23:28:26 +0900 Subject: [PATCH 24/44] ci: Add MySQL service to docker-compose - Add `db` service using official mysql:8.0 image --- docker-compose.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..1b88df9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,30 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.backend + env_file: + - .env + container_name: bravest-backend + ports: + - "8080:8080" + networks: + - bravest-net + depends_on: + - db + + db: + image: mysql:8.0 + env_file: + - .env + container_name: bravest-db + ports: + - "3306:3306" + networks: + - bravest-net + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + +networks: + bravest-net: + driver: bridge From 625dee11b6a5f6176e84889497f7fac3ef26c464 Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sun, 30 Nov 2025 18:28:09 +0900 Subject: [PATCH 25/44] ci: Add keystore service - Add dependency `spring-boot-starter-data-redis` in `build.gradle` to use redis managing functionality - Define service `keystore` in `docker-compose.yaml` - Add healthcheck property in service `db` to ensure TCP listening in `docker-compose.yaml` - Add `ValkeyConfig.java` to manage redis connection with Backend server - Define data property to Valkey --- build.gradle | 3 ++- docker-compose.yaml | 19 ++++++++++++++++++- .../bravest/global/config/ValkeyConfig.java | 16 ++++++++++++++++ src/main/resources/application.yaml | 6 ++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/main/java/opensource/bravest/global/config/ValkeyConfig.java diff --git a/build.gradle b/build.gradle index c801fec..10b912a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/docker-compose.yaml b/docker-compose.yaml index 1b88df9..6369852 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,7 +11,10 @@ services: networks: - bravest-net depends_on: - - db + db: + condition: service_healthy + keystore: + condition: service_started db: image: mysql:8.0 @@ -24,6 +27,20 @@ services: - bravest-net environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpw" ] + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s + + keystore: + image: valkey/valkey:9.0.0 + container_name: bravest-keystore + ports: + - "6379:6379" + networks: + - bravest-net networks: bravest-net: diff --git a/src/main/java/opensource/bravest/global/config/ValkeyConfig.java b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java new file mode 100644 index 0000000..f13aea9 --- /dev/null +++ b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java @@ -0,0 +1,16 @@ +package opensource.bravest.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +@Configuration +public class ValkeyConfig { + + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9a13741..eeec720 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,6 +14,12 @@ spring: show_sql: true dialect: org.hibernate.dialect.MySQLDialect + data: + redis: + host: bravest-keystore + port: 6379 + database: 0 + server: port: 8080 servlet: From 067bfb9a086120bd6f8f91da771ffbe18c34bb40 Mon Sep 17 00:00:00 2001 From: semi-yu Date: Sun, 30 Nov 2025 18:37:47 +0900 Subject: [PATCH 26/44] feat: Change Dedup logic to use keystore - Deduplicate subscription originally depends on in-memory data structure which harms observability. - Improved logic stores and checks in `Valkey` key-storage if one subscribe. --- .../bravest/global/handler/StompHandler.java | 96 +++++++++++-------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index 41992a7..c3ab889 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; -import opensource.bravest.global.security.jwt.JwtTokenProvider; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; @@ -17,18 +17,19 @@ import org.springframework.stereotype.Component; import java.security.Principal; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.List; @Slf4j @Component @RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { - private final JwtTokenProvider jwtProvider; + private final AnonymousProfileRepository anonymousProfileRepository; + private final StringRedisTemplate redisTemplate; - private final Map> userDestinations = new ConcurrentHashMap<>(); + private static final String USER_SUB_KEY_PREFIX = "ws:subs:user:"; // + anonymousId + private static final String METRIC_TOTAL_SUB = "ws:metrics:sub:total"; + private static final String METRIC_DUP_SUB = "ws:metrics:sub:duplicate"; @Override public Message preSend(Message message, MessageChannel channel) { @@ -40,10 +41,12 @@ public Message preSend(Message message, MessageChannel channel) { } StompCommand command = accessor.getCommand(); + + // 1) CONNECT: anonymousId를 Principal로 설정 if (StompCommand.CONNECT.equals(command)) { String anonymousId = accessor.getFirstNativeHeader("anonymousId"); if (anonymousId == null || anonymousId.isBlank()) { - log.warn("STOMP CONNECT: anonymousId 누락"); + log.warn("STOMP CONNECT: anonymousId missing"); throw new IllegalArgumentException("anonymousId header is required"); } @@ -52,22 +55,20 @@ public Message preSend(Message message, MessageChannel channel) { Authentication auth = new UsernamePasswordAuthenticationToken( anonymousId, null, - java.util.List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) + List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) ); SecurityContextHolder.getContext().setAuthentication(auth); - accessor.setUser(auth); // → @MessageMapping의 Principal로 전달 - log.info("STOMP CONNECT: anonymousId={} Principal 설정 완료", anonymousId);}, () -> { - log.warn("STOMP CONNECT: 존재하지 않는 anonymousId={}", anonymousId); + accessor.setUser(auth); + log.info("STOMP CONNECT: anonymousId={} principal set", anonymousId); + }, () -> { + log.warn("STOMP CONNECT: invalid anonymousId={}", anonymousId); throw new IllegalArgumentException("Invalid anonymousId"); }); } + // 2) SUBSCRIBE: Redis를 사용해 anonymousId 기준 중복 구독 방지 + 메트릭 기록 if (StompCommand.SUBSCRIBE.equals(command)) { - String destination = accessor.getDestination(); Principal user = accessor.getUser(); - - // CONNECT에서 setUser를 해줬다면 여기서 null이면 안 되는 게 정상이나, - // 혹시 모르니 한 번 더 SecurityContext에서 복구 시도 if (user == null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { @@ -76,25 +77,42 @@ public Message preSend(Message message, MessageChannel channel) { } } + String destination = accessor.getDestination(); + if (user != null && destination != null) { - String userKey = user.getName(); // == anonymousId + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; - Set dests = - userDestinations.computeIfAbsent( - userKey, - k -> ConcurrentHashMap.newKeySet() - ); + log.info("[SUBSCRIBE] handling: anonymousId={}, destination={}, key={}", + anonymousId, destination, key); + + try { + Long total = redisTemplate.opsForValue().increment(METRIC_TOTAL_SUB); + Long added = redisTemplate.opsForSet().add(key, destination); + redisTemplate.expire(key, java.time.Duration.ofHours(1)); + + log.info("[SUBSCRIBE] redis result: total={}, added={}", total, added); + + if (added != null && added == 0L) { + Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); + log.warn("[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", + anonymousId, destination, dup); + return null; + } + + log.info("[SUBSCRIBE] stored in Redis: key={}, member={}", key, destination); - if (!dests.add(destination)) { - log.warn("중복 SUBSCRIBE 감지: anonymousId={}, destination={}", - userKey, destination); - // 이 SUBSCRIBE 프레임 자체를 무시 - return null; + } catch (Exception e) { + log.error("Redis error while handling SUBSCRIBE", e); } + } else { + log.warn("[SUBSCRIBE] skipped: user or destination is null (user={}, dest={})", + user, destination); } } - // 3) SEND에도 Principal이 비어 있으면 SecurityContext에서 복구 + + // 3) SEND: Principal 비어 있으면 SecurityContext에서 복구 if (StompCommand.SEND.equals(command)) { Principal user = accessor.getUser(); if (user == null) { @@ -105,7 +123,8 @@ public Message preSend(Message message, MessageChannel channel) { } } - // (선택) DISCONNECT 시 userDestinations에서 정리 + // 4) DISCONNECT: 유저별 구독 키를 정리할지 여부 (옵션) + // - 전체 방 전체 유저 수가 크지 않다면 TTL만으로도 충분. if (StompCommand.DISCONNECT.equals(command)) { Principal user = accessor.getUser(); if (user == null) { @@ -115,21 +134,18 @@ public Message preSend(Message message, MessageChannel channel) { } } if (user != null) { - String key = user.getName(); - userDestinations.remove(key); - log.info("DISCONNECT: anonymousId={} 구독 정보 제거", key); - } - } - - if (StompCommand.SEND.equals(command) || StompCommand.SUBSCRIBE.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - accessor.setUser(auth); + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; + try { + // 완전히 정리하고 싶으면 delete + redisTemplate.delete(key); + log.info("DISCONNECT: cleared subscriptions for anonymousId={}", anonymousId); + } catch (Exception e) { + log.error("Redis error while handling DISCONNECT", e); } } } + return message; } } From f743447dc860e6a49fcbd596e0bbc5a40173685f Mon Sep 17 00:00:00 2001 From: Seunghun Yu <168928296+2heunxun@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:52:02 +0900 Subject: [PATCH 27/44] docs: explain CodeQL languages in Korean --- .github/workflows/codeql.yml | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e23c759 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: '0 8 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + # 필요한 언어를 여기에 추가하면 CodeQL이 해당 모듈까지 스캔합니다. + # 예) 'java', 'kotlin', 'python' 등을 쉼표로 구분해 넣으세요. + language: ['java'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + # 바로 위 매트릭스에 정의된 언어를 그대로 사용합니다. 별도의 환경변수 설정은 필요 없습니다. + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 From e9ab579441f273c5274108c277efa612f144436d Mon Sep 17 00:00:00 2001 From: Seunghun Yu <168928296+2heunxun@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:55:23 +0900 Subject: [PATCH 28/44] Add 'develop' branch to CodeQL workflow changing for push main & develop --- .github/workflows/codeql.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e23c759..c92e12d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL" on: push: - branches: ["main"] + branches: ["main", "develop"] pull_request: - branches: ["main"] + branches: ["main", "develop"] schedule: - cron: '0 8 * * 0' @@ -19,8 +19,6 @@ jobs: strategy: fail-fast: false matrix: - # 필요한 언어를 여기에 추가하면 CodeQL이 해당 모듈까지 스캔합니다. - # 예) 'java', 'kotlin', 'python' 등을 쉼표로 구분해 넣으세요. language: ['java'] steps: @@ -37,7 +35,6 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: - # 바로 위 매트릭스에 정의된 언어를 그대로 사용합니다. 별도의 환경변수 설정은 필요 없습니다. languages: ${{ matrix.language }} - name: Autobuild From f715049698ddb43cc63c225ee17f685a43a927a9 Mon Sep 17 00:00:00 2001 From: Seunghun Yu <168928296+2heunxun@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:13:33 +0900 Subject: [PATCH 29/44] ci/cd - dependency security check ci/cd - dependency security check --- .github/workflows/dependency-review.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..015f665 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,23 @@ +name: Dependency Review + +on: + pull_request: + branches: ["main", "develop"] + +permissions: + contents: read + +jobs: + dependency-review: + name: Audit dependencies + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Review dependencies + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: critical From a81858887403d408db329bde67c97148957347b8 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 01:45:19 +0900 Subject: [PATCH 30/44] [ci/cd] Adding workflow file assign PR assignees --- .github/workflows/auto-assign.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/auto-assign.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..02a1f2d --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,25 @@ +name: Auto Assignees and PR Reviewers + +on: + issues: + types: [ opened ] + pull_request: + types: [ opened, ready_for_review ] + +jobs: + auto_assign: + runs-on: ubuntu-latest + steps: + - name: Assign Issue assignees + if: ${{ github.event_name == 'issues' }} + uses: actions-ecosystem/action-add-assignees@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + assignees: ${{ github.actor }} + + - name: Assign PR reviewers + if: ${{ github.event_name == 'pull_request' }} + uses: hkusu/review-assign-action@v1 + with: + assignees: ${{ github.actor }} + reviewers: 2heunxun, JangYeongHu, semi-yu \ No newline at end of file From b4f476065380f2b85132aaed680c5d6b78c88833 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 02:19:05 +0900 Subject: [PATCH 31/44] [ci/cd] Adding workflow file test docker-compose.yaml --- .github/workflows/dockercompose-test.yml | 117 +++++++++++++++++++++++ docker-compose.yaml | 1 + 2 files changed, 118 insertions(+) create mode 100644 .github/workflows/dockercompose-test.yml diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml new file mode 100644 index 0000000..b3dd5fa --- /dev/null +++ b/.github/workflows/dockercompose-test.yml @@ -0,0 +1,117 @@ +name: Docker Compose Test + +on: + pull_request: + branches: [ main, develop ] + paths: + - 'docker-compose.yml' + - 'Dockerfile.backend' + - 'src/**' + push: + branches: [ main, develop ] + +jobs: + docker-compose-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew bootJar + + - name: Create .env file from secrets + run: | + cat > .env << EOF + # Database Configuration + DB_PASSWORD=${{ secrets.DB_PASSWORD }} + DB_USERNAME=${{ secrets.DB_USERNAME }} + DB_URL=${{ secrets.DB_URL }} + + # JWT Configuration + JWT_SECRET=${{ secrets.JWT_SECRET }} + JWT_ACCESS_TOKEN_VALIDITY_SECONDS=${{ secrets.JWT_ACCESS_TOKEN_VALIDITY_SECONDS }} + JWT_REFRESH_TOKEN_VALIDITY_SECONDS=${{ secrets.JWT_REFRESH_TOKEN_VALIDITY_SECONDS }} + EOF + + - name: Start Docker Compose + run: docker-compose up -d + + - name: Check running containers + run: docker-compose ps + + - name: Wait for services to be ready + run: | + echo "⏳ Waiting for MySQL to be ready..." + timeout 30 bash -c 'until docker-compose exec -T db mysqladmin ping -h localhost -uroot -p${{ secrets.DB_PASSWORD }} --silent 2>/dev/null; do sleep 2; done' + echo "✅ MySQL is ready!" + + echo "⏳ Waiting for Valkey to be ready..." + timeout 30 bash -c 'until docker-compose exec -T keystore valkey-cli ping 2>/dev/null | grep -q PONG; do sleep 2; done' + echo "✅ Valkey is ready!" + + echo "⏳ Waiting for application to be ready..." + timeout 90 bash -c 'until curl -f http://localhost:8080/actuator/health 2>/dev/null; do sleep 3; done' + echo "✅ Application is ready!" + + - name: Health check + run: | + response=$(curl -s http://localhost:8080/actuator/health) + echo "Health check response: $response" + if echo "$response" | grep -q '"status":"UP"'; then + echo "✅ Health check passed!" + else + echo "❌ Health check failed!" + exit 1 + fi + + - name: Test MySQL connection + run: | + echo "Testing MySQL connection..." + docker-compose exec -T db mysql -u${{ secrets.DB_USERNAME }} -p${{ secrets.DB_PASSWORD }} -e "SELECT 1;" && \ + echo "✅ MySQL connection successful!" || \ + (echo "❌ MySQL connection failed!" && exit 1) + + - name: Test Valkey connection + run: | + echo "Testing Valkey connection..." + result=$(docker-compose exec -T keystore valkey-cli ping) + if [ "$result" = "PONG" ]; then + echo "✅ Valkey connection successful!" + else + echo "❌ Valkey connection failed!" + exit 1 + fi + + - name: Show container logs on failure + if: failure() + run: | + echo "=== Application Logs ===" + docker-compose logs app + echo "=== Database Logs ===" + docker-compose logs db + echo "=== Valkey Logs ===" + docker-compose logs keystore + + - name: Stop Docker Compose + if: always() + run: docker-compose down -v + + - name: Test Summary + if: success() + run: | + echo "🎉 All Docker Compose tests passed!" + echo "✅ MySQL connection verified" + echo "✅ Valkey connection verified" + echo "✅ Application health check passed" + echo "✅ All services running correctly" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 6369852..96e6389 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,6 +26,7 @@ services: networks: - bravest-net environment: + MYSQL_DATABASE: bravest MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} healthcheck: test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpw" ] From 42fd670f5d55502e24eb41a5a01ad4fe111b294b Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 02:23:04 +0900 Subject: [PATCH 32/44] [ci/cd] Add dispatch --- .github/workflows/dockercompose-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml index b3dd5fa..d5abb4b 100644 --- a/.github/workflows/dockercompose-test.yml +++ b/.github/workflows/dockercompose-test.yml @@ -9,6 +9,7 @@ on: - 'src/**' push: branches: [ main, develop ] + workflow_dispatch: jobs: docker-compose-test: From 991cbe4f233b3495997bb924235d5600d373d9c3 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 02:32:38 +0900 Subject: [PATCH 33/44] [ci/cd] fix dockercompose-test.yml syntax error --- .github/workflows/dockercompose-test.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml index d5abb4b..326e157 100644 --- a/.github/workflows/dockercompose-test.yml +++ b/.github/workflows/dockercompose-test.yml @@ -9,7 +9,7 @@ on: - 'src/**' push: branches: [ main, develop ] - workflow_dispatch: + workflow_dispatch: jobs: docker-compose-test: @@ -46,19 +46,19 @@ jobs: EOF - name: Start Docker Compose - run: docker-compose up -d + run: docker compose up -d - name: Check running containers - run: docker-compose ps + run: docker compose ps - name: Wait for services to be ready run: | echo "⏳ Waiting for MySQL to be ready..." - timeout 30 bash -c 'until docker-compose exec -T db mysqladmin ping -h localhost -uroot -p${{ secrets.DB_PASSWORD }} --silent 2>/dev/null; do sleep 2; done' + timeout 30 bash -c 'until docker compose exec -T db mysqladmin ping -h localhost -uroot -p${{ secrets.DB_PASSWORD }} --silent 2>/dev/null; do sleep 2; done' echo "✅ MySQL is ready!" echo "⏳ Waiting for Valkey to be ready..." - timeout 30 bash -c 'until docker-compose exec -T keystore valkey-cli ping 2>/dev/null | grep -q PONG; do sleep 2; done' + timeout 30 bash -c 'until docker compose exec -T keystore valkey-cli ping 2>/dev/null | grep -q PONG; do sleep 2; done' echo "✅ Valkey is ready!" echo "⏳ Waiting for application to be ready..." @@ -79,14 +79,14 @@ jobs: - name: Test MySQL connection run: | echo "Testing MySQL connection..." - docker-compose exec -T db mysql -u${{ secrets.DB_USERNAME }} -p${{ secrets.DB_PASSWORD }} -e "SELECT 1;" && \ + docker compose exec -T db mysql -u${{ secrets.DB_USERNAME }} -p${{ secrets.DB_PASSWORD }} -e "SELECT 1;" && \ echo "✅ MySQL connection successful!" || \ (echo "❌ MySQL connection failed!" && exit 1) - name: Test Valkey connection run: | echo "Testing Valkey connection..." - result=$(docker-compose exec -T keystore valkey-cli ping) + result=$(docker compose exec -T keystore valkey-cli ping) if [ "$result" = "PONG" ]; then echo "✅ Valkey connection successful!" else @@ -98,15 +98,15 @@ jobs: if: failure() run: | echo "=== Application Logs ===" - docker-compose logs app + docker compose logs app echo "=== Database Logs ===" - docker-compose logs db + docker compose logs db echo "=== Valkey Logs ===" - docker-compose logs keystore + docker compose logs keystore - name: Stop Docker Compose if: always() - run: docker-compose down -v + run: docker compose down -v - name: Test Summary if: success() From 2006251ff618ae88ce113cec973e4ad4e27a84c8 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 02:48:34 +0900 Subject: [PATCH 34/44] [ci/cd] fix dockercompose-test.yml syntax error --- .github/workflows/dockercompose-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml index 326e157..e4b51e4 100644 --- a/.github/workflows/dockercompose-test.yml +++ b/.github/workflows/dockercompose-test.yml @@ -77,9 +77,12 @@ jobs: fi - name: Test MySQL connection + env: + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} run: | echo "Testing MySQL connection..." - docker compose exec -T db mysql -u${{ secrets.DB_USERNAME }} -p${{ secrets.DB_PASSWORD }} -e "SELECT 1;" && \ + docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" -e "SELECT 1;" && \ echo "✅ MySQL connection successful!" || \ (echo "❌ MySQL connection failed!" && exit 1) From 388b1656a251c58cf0b28571a2dd3e04a438bdc8 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 03:27:06 +0900 Subject: [PATCH 35/44] [ci/cd] fix dockercompose-test.yml syntax error --- .github/workflows/dockercompose-test.yml | 34 +++++++++++++++--------- docker-compose.yaml | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml index e4b51e4..655c063 100644 --- a/.github/workflows/dockercompose-test.yml +++ b/.github/workflows/dockercompose-test.yml @@ -2,13 +2,20 @@ name: Docker Compose Test on: pull_request: - branches: [ main, develop ] + push: + branches: + - main + - develop + - "ci/**" paths: - 'docker-compose.yml' - 'Dockerfile.backend' - 'src/**' push: - branches: [ main, develop ] + branches: + - main + - develop + - "ci/**" workflow_dispatch: jobs: @@ -53,17 +60,18 @@ jobs: - name: Wait for services to be ready run: | - echo "⏳ Waiting for MySQL to be ready..." - timeout 30 bash -c 'until docker compose exec -T db mysqladmin ping -h localhost -uroot -p${{ secrets.DB_PASSWORD }} --silent 2>/dev/null; do sleep 2; done' - echo "✅ MySQL is ready!" - - echo "⏳ Waiting for Valkey to be ready..." - timeout 30 bash -c 'until docker compose exec -T keystore valkey-cli ping 2>/dev/null | grep -q PONG; do sleep 2; done' - echo "✅ Valkey is ready!" - - echo "⏳ Waiting for application to be ready..." - timeout 90 bash -c 'until curl -f http://localhost:8080/actuator/health 2>/dev/null; do sleep 3; done' - echo "✅ Application is ready!" + echo "⏳ Waiting for MySQL to be ready..." + timeout 30 bash -c 'until docker compose exec -T db mysqladmin ping \ + -h localhost -uroot -p"${{ secrets.DB_PASSWORD }}" --silent 2>/dev/null; do sleep 2; done' + echo "✅ MySQL is ready!" + + echo "⏳ Waiting for Valkey to be ready..." + timeout 30 bash -c 'until docker compose exec -T keystore valkey-cli ping 2>/dev/null | grep -q PONG; do sleep 2; done' + echo "✅ Valkey is ready!" + + echo "⏳ Waiting for application to be ready..." + timeout 90 bash -c 'until curl -f http://localhost:8080/actuator/health 2>/dev/null; do sleep 3; done' + echo "✅ Application is ready!" - name: Health check run: | diff --git a/docker-compose.yaml b/docker-compose.yaml index 96e6389..5d4c6f4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,7 +26,7 @@ services: networks: - bravest-net environment: - MYSQL_DATABASE: bravest + MYSQL_DATABASE: db MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} healthcheck: test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpw" ] From e49d4ed4bf20186ddae49a429dc5bf44348fdd7e Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 03:34:08 +0900 Subject: [PATCH 36/44] [ci/cd] add spring actuator --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 10b912a..da942d8 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'com.mysql:mysql-connector-j' // Swagger (springdoc) implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14' From 835b0a4439f78f7f72385cd6157127a88f84a7bf Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 03:37:53 +0900 Subject: [PATCH 37/44] [ci/cd] delete checking ci/** branches --- .github/workflows/dockercompose-test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dockercompose-test.yml b/.github/workflows/dockercompose-test.yml index 655c063..b3c5d53 100644 --- a/.github/workflows/dockercompose-test.yml +++ b/.github/workflows/dockercompose-test.yml @@ -6,7 +6,6 @@ on: branches: - main - develop - - "ci/**" paths: - 'docker-compose.yml' - 'Dockerfile.backend' @@ -15,7 +14,6 @@ on: branches: - main - develop - - "ci/**" workflow_dispatch: jobs: From 56035081fd6792e927c68a408d107ac9db0ae106 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 03:46:48 +0900 Subject: [PATCH 38/44] [ci/cd] add code style checker workflow --- .github/workflows/auto-assign.yml | 25 ---------------- .github/workflows/code-stype-test.yml | 43 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 25 deletions(-) delete mode 100644 .github/workflows/auto-assign.yml create mode 100644 .github/workflows/code-stype-test.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml deleted file mode 100644 index 02a1f2d..0000000 --- a/.github/workflows/auto-assign.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Auto Assignees and PR Reviewers - -on: - issues: - types: [ opened ] - pull_request: - types: [ opened, ready_for_review ] - -jobs: - auto_assign: - runs-on: ubuntu-latest - steps: - - name: Assign Issue assignees - if: ${{ github.event_name == 'issues' }} - uses: actions-ecosystem/action-add-assignees@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - assignees: ${{ github.actor }} - - - name: Assign PR reviewers - if: ${{ github.event_name == 'pull_request' }} - uses: hkusu/review-assign-action@v1 - with: - assignees: ${{ github.actor }} - reviewers: 2heunxun, JangYeongHu, semi-yu \ No newline at end of file diff --git a/.github/workflows/code-stype-test.yml b/.github/workflows/code-stype-test.yml new file mode 100644 index 0000000..b6551b7 --- /dev/null +++ b/.github/workflows/code-stype-test.yml @@ -0,0 +1,43 @@ +name: Code Style Check + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + code-style-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Grant permission for Gradle + run: chmod +x gradlew + + - name: Build without tests + run: ./gradlew assemble --no-daemon + + - name: Run Spotless Check + run: ./gradlew spotlessCheck --no-daemon + + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest --no-daemon + + - name: Run Gradle Check + run: ./gradlew check --no-daemon + + - name: Summary + if: success() + run: echo "🎉 Code style checks passed successfully!" + + - name: Fail Summary + if: failure() + run: echo "❌ Code style violations found. Please fix them before merging." From 54adda439f4f6daf4e17927bc64a2e3d97bbc378 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 03:51:22 +0900 Subject: [PATCH 39/44] [ci/cd] add plugin --- build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build.gradle b/build.gradle index 10b912a..62c7ad2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.7' id 'io.spring.dependency-management' version '1.1.7' + id 'com.diffplug.spotless' version '6.25.0' } group = 'opensource' @@ -64,3 +65,14 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +subprojects { + apply plugin: 'com.diffplug.spotless' + + spotless { + java { + googleJavaFormat() + target 'src/**/*.java' + } + } +} \ No newline at end of file From fabc98eb431df004b2c24835a3107c0678ed2e15 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 04:17:42 +0900 Subject: [PATCH 40/44] [ci/cd] refactor all of file --- build.gradle | 29 ++- config/checkstyle/google_checks.xml | 10 + .../bravest/BravestApplication.java | 7 +- .../controller/ChatListController.java | 78 +++--- .../domain/chatList/dto/ChatListDto.java | 84 +++---- .../domain/chatList/entity/ChatList.java | 65 +++-- .../repository/ChatListRepository.java | 9 +- .../chatList/service/ChatListService.java | 86 ++++--- .../controller/ChatMessageController.java | 35 ++- .../domain/message/dto/MessageDto.java | 66 +++-- .../domain/message/entity/ChatMessage.java | 31 ++- .../repository/ChatMessageRepository.java | 8 +- .../message/service/ChatMessageService.java | 63 +++-- .../AnonymousProfileController.java | 34 ++- .../profile/dto/AnonymousProfileResponse.java | 23 +- .../dto/CreateAnonymousProfileRequest.java | 4 +- .../profile/entity/AnonymousProfile.java | 28 +-- .../AnonymousProfileRepository.java | 9 +- .../service/AnonymousProfileService.java | 60 ++--- .../room/controller/RoomController.java | 128 +++++----- .../bravest/domain/room/dto/RoomDto.java | 53 ++-- .../domain/room/entity/AnonymousRoom.java | 41 ++- .../repository/AnonymousRoomRepository.java | 9 +- .../domain/room/service/RoomService.java | 92 +++---- .../vote/controller/VoteController.java | 92 +++---- .../bravest/domain/vote/dto/VoteDto.java | 61 +++-- .../bravest/domain/vote/entity/UserVote.java | 26 +- .../bravest/domain/vote/entity/Vote.java | 37 ++- .../domain/vote/entity/VoteOption.java | 27 +- .../vote/repository/UserVoteRepository.java | 5 +- .../vote/repository/VoteOptionRepository.java | 3 +- .../vote/repository/VoteRepository.java | 3 +- .../domain/vote/service/VoteService.java | 185 +++++++------- .../global/apiPayload/ApiResponse.java | 20 +- .../global/apiPayload/code/BaseCode.java | 3 +- .../global/apiPayload/code/BaseErrorCode.java | 1 + .../apiPayload/code/status/ErrorStatus.java | 8 +- .../apiPayload/code/status/SuccessStatus.java | 7 +- .../bravest/global/config/OpenApiConfig.java | 47 ++-- .../bravest/global/config/SecurityConfig.java | 111 +++++---- .../bravest/global/config/ValkeyConfig.java | 9 +- .../global/config/WebSocketConfig.java | 30 ++- .../global/exception/CustomException.java | 17 +- .../exception/GlobalExceptionHandler.java | 19 +- .../bravest/global/handler/StompHandler.java | 235 +++++++++--------- .../security/jwt/JwtAuthenticationFilter.java | 89 ++++--- .../global/security/jwt/JwtTokenProvider.java | 144 +++++------ .../bravest/BravestApplicationTests.java | 6 +- 48 files changed, 1116 insertions(+), 1121 deletions(-) create mode 100644 config/checkstyle/google_checks.xml diff --git a/build.gradle b/build.gradle index 62c7ad2..ed442ea 100644 --- a/build.gradle +++ b/build.gradle @@ -66,13 +66,26 @@ tasks.named('test') { useJUnitPlatform() } -subprojects { - apply plugin: 'com.diffplug.spotless' - - spotless { - java { - googleJavaFormat() - target 'src/**/*.java' - } +apply plugin: 'checkstyle' + +checkstyle { + toolVersion = '10.12.0' + configFile = file('config/checkstyle/google_checks.xml') +} + +tasks.withType(Checkstyle) { + reports { + xml.required.set(true) + html.required.set(true) + } +} + +// Spotless 적용 +apply plugin: 'com.diffplug.spotless' + +spotless { + java { + googleJavaFormat() + target 'src/**/*.java' } } \ No newline at end of file diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml new file mode 100644 index 0000000..6eff7fe --- /dev/null +++ b/config/checkstyle/google_checks.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/java/opensource/bravest/BravestApplication.java b/src/main/java/opensource/bravest/BravestApplication.java index 58c22c0..144da2e 100644 --- a/src/main/java/opensource/bravest/BravestApplication.java +++ b/src/main/java/opensource/bravest/BravestApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class BravestApplication { - public static void main(String[] args) { - SpringApplication.run(BravestApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(BravestApplication.class, args); + } } diff --git a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java index 3f6bdc7..38810d3 100644 --- a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java +++ b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java @@ -1,9 +1,12 @@ package opensource.bravest.domain.chatList.controller; +import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListCreateRequest; import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListResponse; import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListUpdateRequest; -import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListCreateRequest; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; import opensource.bravest.domain.chatList.service.ChatListService; import opensource.bravest.global.apiPayload.ApiResponse; import org.springframework.web.bind.annotation.DeleteMapping; @@ -15,47 +18,42 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import jakarta.validation.Valid; -import java.util.List; - @RestController @RequestMapping("/chatlists") @RequiredArgsConstructor public class ChatListController { - private final ChatListService chatListService; - - @PostMapping - public ApiResponse createChatList(@Valid @RequestBody ChatListCreateRequest request) { - ChatListResponse response = chatListService.createChatList(request); - return ApiResponse.onSuccess(response); - } - - @GetMapping("/room/{roomId}") - public ApiResponse> getChatListsByRoomId(@PathVariable Long roomId) { - List response = chatListService.getChatListsByRoomId(roomId); - return ApiResponse.onSuccess(response); - } - - @GetMapping("/{id}") - public ApiResponse getChatListById(@PathVariable Long id) { - ChatListResponse response = chatListService.getChatListById(id); - return ApiResponse.onSuccess(response); - } - - @PutMapping("/{id}") - public ApiResponse updateChatList(@PathVariable Long id, - @Valid @RequestBody ChatListUpdateRequest request) { - ChatListResponse response = chatListService.updateChatList(id, request); - return ApiResponse.onSuccess(response); - } - - @DeleteMapping("/{id}") - public ApiResponse deleteChatList(@PathVariable Long id) { - chatListService.deleteChatList(id); - return ApiResponse.onSuccess(null); - } -} \ No newline at end of file + private final ChatListService chatListService; + + @PostMapping + public ApiResponse createChatList( + @Valid @RequestBody ChatListCreateRequest request) { + ChatListResponse response = chatListService.createChatList(request); + return ApiResponse.onSuccess(response); + } + + @GetMapping("/room/{roomId}") + public ApiResponse> getChatListsByRoomId(@PathVariable Long roomId) { + List response = chatListService.getChatListsByRoomId(roomId); + return ApiResponse.onSuccess(response); + } + + @GetMapping("/{id}") + public ApiResponse getChatListById(@PathVariable Long id) { + ChatListResponse response = chatListService.getChatListById(id); + return ApiResponse.onSuccess(response); + } + + @PutMapping("/{id}") + public ApiResponse updateChatList( + @PathVariable Long id, @Valid @RequestBody ChatListUpdateRequest request) { + ChatListResponse response = chatListService.updateChatList(id, request); + return ApiResponse.onSuccess(response); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteChatList(@PathVariable Long id) { + chatListService.deleteChatList(id); + return ApiResponse.onSuccess(null); + } +} diff --git a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java index 282cfb4..77baf7d 100644 --- a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java +++ b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java @@ -1,53 +1,51 @@ package opensource.bravest.domain.chatList.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.Setter; import opensource.bravest.domain.chatList.entity.ChatList; -import java.time.LocalDateTime; public class ChatListDto { - // 1. 아이디어 생성 요청 DTO (Create Request) - @Getter - @Setter - public static class ChatListCreateRequest { - - private Long roomId; - - private String content; - - private Long registeredBy; - } - - // 2. 아이디어 수정 요청 DTO (Update Request) - @Getter - @Setter - public static class ChatListUpdateRequest { - - // 아이디어 내용 수정만 가정 - private String content; - } - - @Getter - @Builder - public static class ChatListResponse { - private Long id; - private Long roomId; - private String content; - private Long registeredBy; - private LocalDateTime createdAt; - - public static ChatListResponse fromEntity(ChatList chatList) { - return ChatListResponse.builder() - .id(chatList.getId()) - .roomId(chatList.getRoomId()) - .content(chatList.getContent()) - .registeredBy(chatList.getRegisteredBy().getId()) - .createdAt(chatList.getCreatedAt()) - .build(); - } + // 1. 아이디어 생성 요청 DTO (Create Request) + @Getter + @Setter + public static class ChatListCreateRequest { + + private Long roomId; + + private String content; + + private Long registeredBy; + } + + // 2. 아이디어 수정 요청 DTO (Update Request) + @Getter + @Setter + public static class ChatListUpdateRequest { + + // 아이디어 내용 수정만 가정 + private String content; + } + + @Getter + @Builder + public static class ChatListResponse { + private Long id; + private Long roomId; + private String content; + private Long registeredBy; + private LocalDateTime createdAt; + + public static ChatListResponse fromEntity(ChatList chatList) { + return ChatListResponse.builder() + .id(chatList.getId()) + .roomId(chatList.getRoomId()) + .content(chatList.getContent()) + .registeredBy(chatList.getRegisteredBy().getId()) + .createdAt(chatList.getCreatedAt()) + .build(); } -} \ No newline at end of file + } +} diff --git a/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java b/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java index a9014d1..63cb45a 100644 --- a/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java +++ b/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java @@ -8,12 +8,10 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; -import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -21,49 +19,46 @@ import opensource.bravest.domain.room.entity.AnonymousRoom; import org.hibernate.annotations.CreationTimestamp; -import java.time.LocalDateTime; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "chat_list") public class ChatList { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - @NotNull - @Column(length = 255) - private String content; + @NotNull + @Column(length = 255) + private String content; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - private AnonymousProfile registeredBy; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private AnonymousProfile registeredBy; - @CreationTimestamp - private LocalDateTime createdAt; + @CreationTimestamp private LocalDateTime createdAt; - @Builder - public ChatList(AnonymousRoom room, String content, AnonymousProfile registeredBy) { - this.room = room; - this.content = content; - this.registeredBy = registeredBy; - } + @Builder + public ChatList(AnonymousRoom room, String content, AnonymousProfile registeredBy) { + this.room = room; + this.content = content; + this.registeredBy = registeredBy; + } - public void updateContent(String content) { - this.content = content; - } + public void updateContent(String content) { + this.content = content; + } - public Long getRoomId() { - return this.room.getId(); - } + public Long getRoomId() { + return this.room.getId(); + } - public Long getProfileId() { - return this.registeredBy.getId(); - } -} \ No newline at end of file + public Long getProfileId() { + return this.registeredBy.getId(); + } +} diff --git a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java index 45db599..b90efd1 100644 --- a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java +++ b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java @@ -1,13 +1,12 @@ package opensource.bravest.domain.chatList.repository; +import java.util.List; import opensource.bravest.domain.chatList.entity.ChatList; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.List; - public interface ChatListRepository extends JpaRepository { - @Query("SELECT c FROM ChatList c WHERE c.room.id = :roomId ORDER BY c.createdAt DESC") - List findAllByRoomId(Long roomId); -} \ No newline at end of file + @Query("SELECT c FROM ChatList c WHERE c.room.id = :roomId ORDER BY c.createdAt DESC") + List findAllByRoomId(Long roomId); +} diff --git a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java index 6050c7e..e375a13 100644 --- a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java +++ b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java @@ -1,12 +1,14 @@ package opensource.bravest.domain.chatList.service; +import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListCreateRequest; import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListResponse; import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListUpdateRequest; -import static opensource.bravest.domain.chatList.dto.ChatListDto.ChatListCreateRequest; import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._CHATLIST_NOT_FOUND; import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._CHATROOM_NOT_FOUND; import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._USER_NOT_FOUND; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import opensource.bravest.domain.chatList.entity.ChatList; import opensource.bravest.domain.chatList.repository.ChatListRepository; @@ -18,64 +20,60 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ChatListService { - private final ChatListRepository chatListRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - private final AnonymousProfileRepository anonymousProfileRepository; + private final ChatListRepository chatListRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousProfileRepository anonymousProfileRepository; - @Transactional - public ChatListResponse createChatList(ChatListCreateRequest request) { - AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) + @Transactional + public ChatListResponse createChatList(ChatListCreateRequest request) { + AnonymousRoom room = + anonymousRoomRepository + .findById(request.getRoomId()) .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); - AnonymousProfile profile = anonymousProfileRepository.findById(request.getRegisteredBy()) + AnonymousProfile profile = + anonymousProfileRepository + .findById(request.getRegisteredBy()) .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); - ChatList chatList = ChatList.builder() - .room(room) - .registeredBy(profile) - .content(request.getContent()) - .build(); + ChatList chatList = + ChatList.builder().room(room).registeredBy(profile).content(request.getContent()).build(); - ChatList savedList = chatListRepository.save(chatList); - return ChatListResponse.fromEntity(savedList); - } + ChatList savedList = chatListRepository.save(chatList); + return ChatListResponse.fromEntity(savedList); + } - public List getChatListsByRoomId(Long roomId) { - List chatLists = chatListRepository.findAllByRoomId(roomId); - return chatLists.stream() - .map(ChatListResponse::fromEntity) - .collect(Collectors.toList()); - } + public List getChatListsByRoomId(Long roomId) { + List chatLists = chatListRepository.findAllByRoomId(roomId); + return chatLists.stream().map(ChatListResponse::fromEntity).collect(Collectors.toList()); + } - public ChatListResponse getChatListById(Long id) { - ChatList chatList = chatListRepository.findById(id) - .orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); - return ChatListResponse.fromEntity(chatList); - } + public ChatListResponse getChatListById(Long id) { + ChatList chatList = + chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); + return ChatListResponse.fromEntity(chatList); + } - @Transactional - public ChatListResponse updateChatList(Long id, ChatListUpdateRequest request) { - ChatList chatList = chatListRepository.findById(id) - .orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); + @Transactional + public ChatListResponse updateChatList(Long id, ChatListUpdateRequest request) { + ChatList chatList = + chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); - chatList.updateContent(request.getContent()); + chatList.updateContent(request.getContent()); - return ChatListResponse.fromEntity(chatList); - } + return ChatListResponse.fromEntity(chatList); + } - @Transactional - public void deleteChatList(Long id) { - if (!chatListRepository.existsById(id)) { - throw new CustomException(_CHATLIST_NOT_FOUND); - } - chatListRepository.deleteById(id); + @Transactional + public void deleteChatList(Long id) { + if (!chatListRepository.existsById(id)) { + throw new CustomException(_CHATLIST_NOT_FOUND); } -} \ No newline at end of file + chatListRepository.deleteById(id); + } +} diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java index 7c7bee1..609e347 100644 --- a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -1,36 +1,33 @@ package opensource.bravest.domain.message.controller; +import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; +import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; + +import java.security.Principal; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import opensource.bravest.domain.message.service.ChatMessageService; import opensource.bravest.global.apiPayload.ApiResponse; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; -import org.springframework.stereotype.Controller; import org.springframework.messaging.simp.SimpMessagingTemplate; - -import java.security.Principal; - -import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; -import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; +import org.springframework.stereotype.Controller; @Slf4j @Controller @RequiredArgsConstructor public class ChatMessageController { - private final ChatMessageService chatMessageService; - private final SimpMessagingTemplate messagingTemplate; + private final ChatMessageService chatMessageService; + private final SimpMessagingTemplate messagingTemplate; - @MessageMapping("/send") - @SendTo("/subs/chat-rooms") - public void receiveMessage(MessageRequest request, Principal principal) { - Long id = Long.parseLong(principal.getName()); - MessageResponse response = chatMessageService.send(request, id); + @MessageMapping("/send") + @SendTo("/subs/chat-rooms") + public void receiveMessage(MessageRequest request, Principal principal) { + Long id = Long.parseLong(principal.getName()); + MessageResponse response = chatMessageService.send(request, id); - // 특정 채팅방 구독자들에게 메시지 전송 - messagingTemplate.convertAndSend( - "/subs/chat-rooms/" + request.getChatRoomId(), - ApiResponse.onSuccess(response) - ); - } + // 특정 채팅방 구독자들에게 메시지 전송 + messagingTemplate.convertAndSend( + "/subs/chat-rooms/" + request.getChatRoomId(), ApiResponse.onSuccess(response)); + } } diff --git a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java index 1660bed..72b32c5 100644 --- a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java +++ b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java @@ -1,44 +1,42 @@ package opensource.bravest.domain.message.dto; +import java.time.LocalDateTime; import lombok.Getter; import lombok.RequiredArgsConstructor; import opensource.bravest.domain.message.entity.ChatMessage; -import java.time.LocalDateTime; - public class MessageDto { - @Getter - public static class SendMessageRequest { - private String content; - } - - @Getter - @RequiredArgsConstructor - public static class MessageResponse { - private final String senderName; // 익명 닉네임 - private final String content; - private final LocalDateTime createdAt; - - public static MessageResponse from(ChatMessage chatMessage) { - return new MessageResponse( - chatMessage.getSender().getAnonymousName(), - chatMessage.getContent(), - chatMessage.getCreatedAt() - ); - } - } - - @Getter - @RequiredArgsConstructor - public static class MessageRequest { - private final Long chatRoomId; - private final String content; - } - - @Getter - @RequiredArgsConstructor - public static class ChatReadRequest { - private final Long chatRoomId; + @Getter + public static class SendMessageRequest { + private String content; + } + + @Getter + @RequiredArgsConstructor + public static class MessageResponse { + private final String senderName; // 익명 닉네임 + private final String content; + private final LocalDateTime createdAt; + + public static MessageResponse from(ChatMessage chatMessage) { + return new MessageResponse( + chatMessage.getSender().getAnonymousName(), + chatMessage.getContent(), + chatMessage.getCreatedAt()); } + } + + @Getter + @RequiredArgsConstructor + public static class MessageRequest { + private final Long chatRoomId; + private final String content; + } + + @Getter + @RequiredArgsConstructor + public static class ChatReadRequest { + private final Long chatRoomId; + } } diff --git a/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java b/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java index e70ba58..2ab7d59 100644 --- a/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java +++ b/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java @@ -1,12 +1,11 @@ package opensource.bravest.domain.message.entity; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.*; import opensource.bravest.domain.profile.entity.AnonymousProfile; import opensource.bravest.domain.room.entity.AnonymousRoom; -import java.time.LocalDateTime; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -14,22 +13,22 @@ @Builder public class ChatMessage { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 어느 방의 메시지인지 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + // 어느 방의 메시지인지 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - // 누가 보냈는지 (익명 프로필 기준) - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "anonymous_profile_id", nullable = false) - private AnonymousProfile sender; + // 누가 보냈는지 (익명 프로필 기준) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "anonymous_profile_id", nullable = false) + private AnonymousProfile sender; - @Column(nullable = false, length = 1000) - private String content; + @Column(nullable = false, length = 1000) + private String content; - private LocalDateTime createdAt; + private LocalDateTime createdAt; } diff --git a/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java b/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java index 2866c8b..eaae205 100644 --- a/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java +++ b/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java @@ -1,14 +1,12 @@ package opensource.bravest.domain.message.repository; - +import java.util.List; import opensource.bravest.domain.message.entity.ChatMessage; import opensource.bravest.domain.room.entity.AnonymousRoom; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface ChatMessageRepository extends JpaRepository { - // 방 기준으로 최근 메시지 목록 - List findByRoomOrderByCreatedAtAsc(AnonymousRoom room); + // 방 기준으로 최근 메시지 목록 + List findByRoomOrderByCreatedAtAsc(AnonymousRoom room); } diff --git a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java index b0db7ef..57808a6 100644 --- a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java +++ b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java @@ -1,13 +1,12 @@ package opensource.bravest.domain.message.service; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; - import static opensource.bravest.domain.message.dto.MessageDto.MessageRequest; +import static opensource.bravest.domain.message.dto.MessageDto.MessageResponse; import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._CHATROOM_NOT_FOUND; import static opensource.bravest.global.apiPayload.code.status.ErrorStatus._USER_NOT_FOUND; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; import opensource.bravest.domain.message.entity.ChatMessage; import opensource.bravest.domain.message.repository.ChatMessageRepository; import opensource.bravest.domain.profile.entity.AnonymousProfile; @@ -17,45 +16,45 @@ import opensource.bravest.global.exception.CustomException; import org.springframework.stereotype.Service; -import java.util.Objects; - @Service @Transactional @RequiredArgsConstructor public class ChatMessageService { - private final AnonymousProfileRepository memberRepository; - private final AnonymousRoomRepository chatRoomRepository; - private final ChatMessageRepository chatMessageRepository; + private final AnonymousProfileRepository memberRepository; + private final AnonymousRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; - // 메시지 전송 - public MessageResponse send(MessageRequest request, Long id) { - AnonymousProfile sender = memberRepository.findById(id) - .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); + // 메시지 전송 + public MessageResponse send(MessageRequest request, Long id) { + AnonymousProfile sender = + memberRepository.findById(id).orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); - AnonymousRoom chatRoom = chatRoomRepository.findById(request.getChatRoomId()) + AnonymousRoom chatRoom = + chatRoomRepository + .findById(request.getChatRoomId()) .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); - ChatMessage chatMessage = ChatMessage.builder() - .room(chatRoom) - .sender(sender) - .content(request.getContent()) - .build(); + ChatMessage chatMessage = + ChatMessage.builder().room(chatRoom).sender(sender).content(request.getContent()).build(); - chatMessageRepository.save(chatMessage); + chatMessageRepository.save(chatMessage); - return MessageResponse.from(chatMessage); - } + return MessageResponse.from(chatMessage); + } - @Transactional - public void readMessages(Long chatRoomId, Long memberId) { - AnonymousRoom chatRoom = chatRoomRepository.findById(chatRoomId) + @Transactional + public void readMessages(Long chatRoomId, Long memberId) { + AnonymousRoom chatRoom = + chatRoomRepository + .findById(chatRoomId) .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); -// if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && !Objects.equals(chatRoom.getMember2().getId(), -// memberId)) { -// throw new BaseException(ChatExceptionType.CHAT_ROOM_ACCESS_DENIED); -// } -// messageReceiptRepository.bulkUpdateStatusToRead(chatRoomId, memberId); - } -} \ No newline at end of file + // if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && + // !Objects.equals(chatRoom.getMember2().getId(), + // memberId)) { + // throw new BaseException(ChatExceptionType.CHAT_ROOM_ACCESS_DENIED); + // } + // messageReceiptRepository.bulkUpdateStatusToRead(chatRoomId, memberId); + } +} diff --git a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java index 26a5d5d..6b2923f 100644 --- a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java +++ b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java @@ -10,30 +10,26 @@ import opensource.bravest.global.apiPayload.code.status.SuccessStatus; import org.springframework.web.bind.annotation.*; - @RestController @RequiredArgsConstructor @RequestMapping("/anonymous-profiles") public class AnonymousProfileController { - private final AnonymousProfileService anonymousProfileService; - + private final AnonymousProfileService anonymousProfileService; - @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") - @PostMapping("/rooms/{roomId}") - public ApiResponse createAnonymousProfile( - @PathVariable Long roomId, - @RequestBody CreateAnonymousProfileRequest request - ) { - AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); - AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); - return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); - } + @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") + @PostMapping("/rooms/{roomId}") + public ApiResponse createAnonymousProfile( + @PathVariable Long roomId, @RequestBody CreateAnonymousProfileRequest request) { + AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); + AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); + } - @DeleteMapping("/{profileId}") - @Operation(summary = "익명 프로필 삭제", description = "ID로 특정 익명 프로필을 삭제합니다.") - public ApiResponse deleteAnonymousProfile(@PathVariable Long profileId) { - anonymousProfileService.deleteAnonymousProfile(profileId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + @DeleteMapping("/{profileId}") + @Operation(summary = "익명 프로필 삭제", description = "ID로 특정 익명 프로필을 삭제합니다.") + public ApiResponse deleteAnonymousProfile(@PathVariable Long profileId) { + anonymousProfileService.deleteAnonymousProfile(profileId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } } diff --git a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java index e7d8c8c..95165ea 100644 --- a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java +++ b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java @@ -7,16 +7,17 @@ @Getter @Builder public class AnonymousProfileResponse { - private Long id; - private Long roomId; - private String nickname; - // 필요한 필드만 + private Long id; + private Long roomId; + private String nickname; - public static AnonymousProfileResponse from(AnonymousProfile profile) { - return AnonymousProfileResponse.builder() - .id(profile.getId()) - .roomId(profile.getRoom().getId()) - .nickname(profile.getAnonymousName()) - .build(); - } + // 필요한 필드만 + + public static AnonymousProfileResponse from(AnonymousProfile profile) { + return AnonymousProfileResponse.builder() + .id(profile.getId()) + .roomId(profile.getRoom().getId()) + .nickname(profile.getAnonymousName()) + .build(); + } } diff --git a/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java index 4b06c9e..9c43be9 100644 --- a/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java +++ b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java @@ -6,6 +6,6 @@ @Getter @NoArgsConstructor public class CreateAnonymousProfileRequest { - private Long realUserId; - private String anonymousName; + private Long realUserId; + private String anonymousName; } diff --git a/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java b/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java index 954be53..fe773e5 100644 --- a/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java +++ b/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java @@ -11,20 +11,20 @@ @Builder public class AnonymousProfile { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 어떤 방에 속한 익명 프로필인지 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + // 어떤 방에 속한 익명 프로필인지 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - // 실제 유저 PK (User 테이블 없다면 JWT의 userId 기준으로) - @Column(nullable = false) - private Long realUserId; + // 실제 유저 PK (User 테이블 없다면 JWT의 userId 기준으로) + @Column(nullable = false) + private Long realUserId; - // 방 안에서 보여줄 익명 닉네임 (예: BlueTiger12) - @Column(nullable = false, length = 50) - private String anonymousName; -} \ No newline at end of file + // 방 안에서 보여줄 익명 닉네임 (예: BlueTiger12) + @Column(nullable = false, length = 50) + private String anonymousName; +} diff --git a/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java b/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java index bdef7a7..88bbdbe 100644 --- a/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java +++ b/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java @@ -1,13 +1,12 @@ package opensource.bravest.domain.profile.repository; +import java.util.Optional; import opensource.bravest.domain.profile.entity.AnonymousProfile; import opensource.bravest.domain.room.entity.AnonymousRoom; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface AnonymousProfileRepository extends JpaRepository { - // 같은 방 + 같은 실제 유저라면 익명 프로필 하나만 사용 - Optional findByRoomAndRealUserId(AnonymousRoom room, Long realUserId); -} \ No newline at end of file + // 같은 방 + 같은 실제 유저라면 익명 프로필 하나만 사용 + Optional findByRoomAndRealUserId(AnonymousRoom room, Long realUserId); +} diff --git a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java index 74326d5..0217c7d 100644 --- a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java +++ b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java @@ -1,5 +1,6 @@ package opensource.bravest.domain.profile.service; +import java.util.Optional; import lombok.RequiredArgsConstructor; import opensource.bravest.domain.profile.dto.CreateAnonymousProfileRequest; import opensource.bravest.domain.profile.entity.AnonymousProfile; @@ -9,41 +10,44 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class AnonymousProfileService { - private final AnonymousProfileRepository anonymousProfileRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - - @Transactional - public AnonymousProfile createAnonymousProfile(Long roomId, CreateAnonymousProfileRequest request) { - AnonymousRoom room = anonymousRoomRepository.findById(roomId) - .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); - - // 중복 프로필 체크 - Optional existingProfile = anonymousProfileRepository.findByRoomAndRealUserId(room, request.getRealUserId()); - if (existingProfile.isPresent()) { - throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); - } + private final AnonymousProfileRepository anonymousProfileRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + + @Transactional + public AnonymousProfile createAnonymousProfile( + Long roomId, CreateAnonymousProfileRequest request) { + AnonymousRoom room = + anonymousRoomRepository + .findById(roomId) + .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); + + // 중복 프로필 체크 + Optional existingProfile = + anonymousProfileRepository.findByRoomAndRealUserId(room, request.getRealUserId()); + if (existingProfile.isPresent()) { + throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); + } - AnonymousProfile newProfile = AnonymousProfile.builder() - .room(room) - .realUserId(request.getRealUserId()) - .anonymousName(request.getAnonymousName()) - .build(); + AnonymousProfile newProfile = + AnonymousProfile.builder() + .room(room) + .realUserId(request.getRealUserId()) + .anonymousName(request.getAnonymousName()) + .build(); - return anonymousProfileRepository.save(newProfile); - } + return anonymousProfileRepository.save(newProfile); + } - @Transactional - public void deleteAnonymousProfile(Long profileId) { - if (!anonymousProfileRepository.existsById(profileId)) { - throw new RuntimeException("없는 사용자임. 너~ 누구야!"); - } - anonymousProfileRepository.deleteById(profileId); + @Transactional + public void deleteAnonymousProfile(Long profileId) { + if (!anonymousProfileRepository.existsById(profileId)) { + throw new RuntimeException("없는 사용자임. 너~ 누구야!"); } + anonymousProfileRepository.deleteById(profileId); + } } diff --git a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java index b331461..3a12474 100644 --- a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java +++ b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java @@ -14,67 +14,81 @@ @RequestMapping("/rooms") public class RoomController { - private final RoomService roomService; + private final RoomService roomService; - @PostMapping - @Operation(summary = "채팅방 생성", description = "새로운 채팅방을 생성합니다.") - public ApiResponse createRoom(@RequestBody RoomDto.CreateRoomRequest request) { - AnonymousRoom room = roomService.createRoom(request); - return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PostMapping + @Operation(summary = "채팅방 생성", description = "새로운 채팅방을 생성합니다.") + public ApiResponse createRoom( + @RequestBody RoomDto.CreateRoomRequest request) { + AnonymousRoom room = roomService.createRoom(request); + return ApiResponse.of( + SuccessStatus._CREATED, + SuccessStatus._CREATED.getMessage(), + RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } - @GetMapping("/{roomId}") - @Operation(summary = "채팅방 조회", description = "ID로 특정 채팅방의 정보를 조회합니다.") - public ApiResponse getRoom(@PathVariable Long roomId) { - AnonymousRoom room = roomService.getRoom(roomId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @GetMapping("/{roomId}") + @Operation(summary = "채팅방 조회", description = "ID로 특정 채팅방의 정보를 조회합니다.") + public ApiResponse getRoom(@PathVariable Long roomId) { + AnonymousRoom room = roomService.getRoom(roomId); + return ApiResponse.of( + SuccessStatus._OK, + SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } - @PutMapping("/{roomId}") - @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") - public ApiResponse updateRoom(@PathVariable Long roomId, @RequestBody RoomDto.UpdateRoomRequest request) { - AnonymousRoom room = roomService.updateRoom(roomId, request); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PutMapping("/{roomId}") + @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") + public ApiResponse updateRoom( + @PathVariable Long roomId, @RequestBody RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = roomService.updateRoom(roomId, request); + return ApiResponse.of( + SuccessStatus._OK, + SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } - @DeleteMapping("/{roomId}") - @Operation(summary = "채팅방 삭제", description = "ID로 특정 채팅방을 삭제합니다.") - public ApiResponse deleteRoom(@PathVariable Long roomId) { - roomService.deleteRoom(roomId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + @DeleteMapping("/{roomId}") + @Operation(summary = "채팅방 삭제", description = "ID로 특정 채팅방을 삭제합니다.") + public ApiResponse deleteRoom(@PathVariable Long roomId) { + roomService.deleteRoom(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } - @GetMapping("/{roomId}/invite-code") - @Operation(summary = "초대 코드 조회", description = "ID로 특정 채팅방의 초대 코드를 조회합니다.") - public ApiResponse getInviteCode(@PathVariable Long roomId) { - String inviteCode = roomService.getInviteCode(roomId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), inviteCode); - } + @GetMapping("/{roomId}/invite-code") + @Operation(summary = "초대 코드 조회", description = "ID로 특정 채팅방의 초대 코드를 조회합니다.") + public ApiResponse getInviteCode(@PathVariable Long roomId) { + String inviteCode = roomService.getInviteCode(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), inviteCode); + } - @PostMapping("/join") - @Operation(summary = "초대 코드로 채팅방 참여", description = "초대 코드를 사용하여 특정 채팅방에 참여합니다.") - public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { - AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PostMapping("/join") + @Operation(summary = "초대 코드로 채팅방 참여", description = "초대 코드를 사용하여 특정 채팅방에 참여합니다.") + public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { + AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); + return ApiResponse.of( + SuccessStatus._OK, + SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder() + .id(room.getId()) + .roomCode(room.getRoomCode()) + .title(room.getTitle()) + .createdAt(room.getCreatedAt()) + .build()); + } } diff --git a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java index e1175e2..8655c0c 100644 --- a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java +++ b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java @@ -1,40 +1,39 @@ package opensource.bravest.domain.room.dto; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - public class RoomDto { - @Getter - @NoArgsConstructor - public static class CreateRoomRequest { - private String title; - } + @Getter + @NoArgsConstructor + public static class CreateRoomRequest { + private String title; + } - @Getter - @NoArgsConstructor - public static class UpdateRoomRequest { - private String title; - } + @Getter + @NoArgsConstructor + public static class UpdateRoomRequest { + private String title; + } - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class RoomResponse { - private Long id; - private String roomCode; - private String title; - private LocalDateTime createdAt; - } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RoomResponse { + private Long id; + private String roomCode; + private String title; + private LocalDateTime createdAt; + } - @Getter - @NoArgsConstructor - public static class JoinRoomRequest { - private String roomCode; - } + @Getter + @NoArgsConstructor + public static class JoinRoomRequest { + private String roomCode; + } } diff --git a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java index aae4aa7..3be35e9 100644 --- a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java +++ b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java @@ -1,11 +1,11 @@ package opensource.bravest.domain.room.entity; -import jakarta.persistence.*; -import lombok.*; -import opensource.bravest.domain.profile.entity.AnonymousProfile; +import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import lombok.*; +import opensource.bravest.domain.profile.entity.AnonymousProfile; @Entity @Getter @@ -14,26 +14,25 @@ @Builder public class AnonymousRoom { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - // 친구들에게 공유하는 코드 (예: ABC123) - @Column(nullable = false, unique = true, length = 20) - private String roomCode; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 방 제목 (선택) - @Column(nullable = false, length = 100) - private String title; + // 친구들에게 공유하는 코드 (예: ABC123) + @Column(nullable = false, unique = true, length = 20) + private String roomCode; - @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default - private List profiles = new ArrayList<>(); + // 방 제목 (선택) + @Column(nullable = false, length = 100) + private String title; + @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List profiles = new ArrayList<>(); - private LocalDateTime createdAt; + private LocalDateTime createdAt; - public void updateTitle(String title) { - this.title = title; - } -} \ No newline at end of file + public void updateTitle(String title) { + this.title = title; + } +} diff --git a/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java b/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java index 51b518e..a900571 100644 --- a/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java +++ b/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java @@ -1,13 +1,12 @@ package opensource.bravest.domain.room.repository; +import java.util.Optional; import opensource.bravest.domain.room.entity.AnonymousRoom; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface AnonymousRoomRepository extends JpaRepository { - Optional findByRoomCode(String roomCode); + Optional findByRoomCode(String roomCode); - boolean existsByRoomCode(String roomCode); -} \ No newline at end of file + boolean existsByRoomCode(String roomCode); +} diff --git a/src/main/java/opensource/bravest/domain/room/service/RoomService.java b/src/main/java/opensource/bravest/domain/room/service/RoomService.java index 2ffb4a5..21d1c2c 100644 --- a/src/main/java/opensource/bravest/domain/room/service/RoomService.java +++ b/src/main/java/opensource/bravest/domain/room/service/RoomService.java @@ -1,5 +1,7 @@ package opensource.bravest.domain.room.service; +import java.time.LocalDateTime; +import java.util.UUID; import lombok.RequiredArgsConstructor; import opensource.bravest.domain.room.dto.RoomDto; import opensource.bravest.domain.room.entity.AnonymousRoom; @@ -7,62 +9,62 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.UUID; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class RoomService { - private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousRoomRepository anonymousRoomRepository; - @Transactional - public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { - String roomCode = generateUniqueRoomCode(); - AnonymousRoom room = AnonymousRoom.builder() - .title(request.getTitle()) - .roomCode(roomCode) - .createdAt(LocalDateTime.now()) - .build(); - return anonymousRoomRepository.save(room); - } + @Transactional + public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { + String roomCode = generateUniqueRoomCode(); + AnonymousRoom room = + AnonymousRoom.builder() + .title(request.getTitle()) + .roomCode(roomCode) + .createdAt(LocalDateTime.now()) + .build(); + return anonymousRoomRepository.save(room); + } - public AnonymousRoom getRoom(Long roomId) { - return anonymousRoomRepository.findById(roomId) - .orElseThrow(() -> new RuntimeException("Room not found")); - } + public AnonymousRoom getRoom(Long roomId) { + return anonymousRoomRepository + .findById(roomId) + .orElseThrow(() -> new RuntimeException("Room not found")); + } - @Transactional - public AnonymousRoom updateRoom(Long roomId, RoomDto.UpdateRoomRequest request) { - AnonymousRoom room = getRoom(roomId); - room.updateTitle(request.getTitle()); - return room; - } + @Transactional + public AnonymousRoom updateRoom(Long roomId, RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = getRoom(roomId); + room.updateTitle(request.getTitle()); + return room; + } - @Transactional - public void deleteRoom(Long roomId) { - if (!anonymousRoomRepository.existsById(roomId)) { - throw new RuntimeException("Room not found"); - } - anonymousRoomRepository.deleteById(roomId); + @Transactional + public void deleteRoom(Long roomId) { + if (!anonymousRoomRepository.existsById(roomId)) { + throw new RuntimeException("Room not found"); } + anonymousRoomRepository.deleteById(roomId); + } - public String getInviteCode(Long roomId) { - AnonymousRoom room = getRoom(roomId); - return room.getRoomCode(); - } + public String getInviteCode(Long roomId) { + AnonymousRoom room = getRoom(roomId); + return room.getRoomCode(); + } - public AnonymousRoom joinRoom(String roomCode) { - return anonymousRoomRepository.findByRoomCode(roomCode) - .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); - } + public AnonymousRoom joinRoom(String roomCode) { + return anonymousRoomRepository + .findByRoomCode(roomCode) + .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); + } - private String generateUniqueRoomCode() { - String roomCode; - do { - roomCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); - } while (anonymousRoomRepository.existsByRoomCode(roomCode)); - return roomCode; - } + private String generateUniqueRoomCode() { + String roomCode; + do { + roomCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); + } while (anonymousRoomRepository.existsByRoomCode(roomCode)); + return roomCode; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java index bf40633..e46aca8 100644 --- a/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java +++ b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java @@ -14,49 +14,51 @@ @RequestMapping("/votes") public class VoteController { - private final VoteService voteService; - - @PostMapping - @Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다.") - public ApiResponse createVote(@RequestBody VoteDto.CreateVoteRequest request) { - Vote vote = voteService.createVote(request); - // The response DTO needs to be built manually - VoteDto.VoteResponse responseDto = voteService.getVoteResult(vote.getId()); - return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), responseDto); - } - - @GetMapping("/{voteId}") - @Operation(summary = "투표 조회", description = "ID로 특정 투표의 정보를 조회합니다.") - public ApiResponse getVote(@PathVariable Long voteId) { - VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); - } - - @PostMapping("/{voteId}/cast") - @Operation(summary = "투표 참여", description = "특정 투표 항목에 투표합니다.") - public ApiResponse castVote(@PathVariable Long voteId, @RequestBody VoteDto.CastVoteRequest request) { - voteService.castVote(voteId, request); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } - - @PostMapping("/{voteId}/end") - @Operation(summary = "투표 종료", description = "특정 투표를 종료합니다.") - public ApiResponse endVote(@PathVariable Long voteId) { - voteService.endVote(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } - - @GetMapping("/{voteId}/result") - @Operation(summary = "투표 결과 조회", description = "종료된 투표의 결과를 조회합니다.") - public ApiResponse getVoteResult(@PathVariable Long voteId) { - VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); - } - - @DeleteMapping("/{voteId}") - @Operation(summary = "투표 삭제", description = "ID로 특정 투표를 삭제합니다.") - public ApiResponse deleteVote(@PathVariable Long voteId) { - voteService.deleteVote(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + private final VoteService voteService; + + @PostMapping + @Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다.") + public ApiResponse createVote( + @RequestBody VoteDto.CreateVoteRequest request) { + Vote vote = voteService.createVote(request); + // The response DTO needs to be built manually + VoteDto.VoteResponse responseDto = voteService.getVoteResult(vote.getId()); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), responseDto); + } + + @GetMapping("/{voteId}") + @Operation(summary = "투표 조회", description = "ID로 특정 투표의 정보를 조회합니다.") + public ApiResponse getVote(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @PostMapping("/{voteId}/cast") + @Operation(summary = "투표 참여", description = "특정 투표 항목에 투표합니다.") + public ApiResponse castVote( + @PathVariable Long voteId, @RequestBody VoteDto.CastVoteRequest request) { + voteService.castVote(voteId, request); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @PostMapping("/{voteId}/end") + @Operation(summary = "투표 종료", description = "특정 투표를 종료합니다.") + public ApiResponse endVote(@PathVariable Long voteId) { + voteService.endVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @GetMapping("/{voteId}/result") + @Operation(summary = "투표 결과 조회", description = "종료된 투표의 결과를 조회합니다.") + public ApiResponse getVoteResult(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @DeleteMapping("/{voteId}") + @Operation(summary = "투표 삭제", description = "ID로 특정 투표를 삭제합니다.") + public ApiResponse deleteVote(@PathVariable Long voteId) { + voteService.deleteVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } } diff --git a/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java index 77ac32d..83cd3dd 100644 --- a/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java +++ b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java @@ -1,43 +1,42 @@ package opensource.bravest.domain.vote.dto; +import java.time.LocalDateTime; +import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; -import java.util.List; - public class VoteDto { - @Getter - @NoArgsConstructor - public static class CreateVoteRequest { - private Long roomId; - private List messages; - } + @Getter + @NoArgsConstructor + public static class CreateVoteRequest { + private Long roomId; + private List messages; + } - @Getter - @NoArgsConstructor - public static class CastVoteRequest { - private Long voteOptionId; - private Long anonymousProfileId; - } + @Getter + @NoArgsConstructor + public static class CastVoteRequest { + private Long voteOptionId; + private Long anonymousProfileId; + } - @Getter - @Builder - public static class VoteResponse { - private Long id; - private String title; - private boolean isActive; - private LocalDateTime createdAt; - private List options; - } + @Getter + @Builder + public static class VoteResponse { + private Long id; + private String title; + private boolean isActive; + private LocalDateTime createdAt; + private List options; + } - @Getter - @Builder - public static class VoteOptionResponse { - private Long id; - private String messageContent; - private int voteCount; - } + @Getter + @Builder + public static class VoteOptionResponse { + private Long id; + private String messageContent; + private int voteCount; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java index 2b71b82..f88a73b 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java @@ -11,19 +11,19 @@ @Builder public class UserVote { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_id", nullable = false) - private Vote vote; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_option_id", nullable = false) - private VoteOption voteOption; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "anonymous_profile_id", nullable = false) - private AnonymousProfile voter; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_option_id", nullable = false) + private VoteOption voteOption; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "anonymous_profile_id", nullable = false) + private AnonymousProfile voter; } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/Vote.java b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java index 03ca1d5..3f2df78 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/Vote.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java @@ -1,12 +1,11 @@ package opensource.bravest.domain.vote.entity; import jakarta.persistence.*; -import lombok.*; -import opensource.bravest.domain.room.entity.AnonymousRoom; - import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import lombok.*; +import opensource.bravest.domain.room.entity.AnonymousRoom; @Entity @Getter @@ -15,26 +14,26 @@ @Builder public class Vote { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - @Column(nullable = false, length = 100) - private String title; + @Column(nullable = false, length = 100) + private String title; - @Builder.Default - @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true) - private List options = new ArrayList<>(); + @Builder.Default + @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true) + private List options = new ArrayList<>(); - private boolean isActive; + private boolean isActive; - private LocalDateTime createdAt; + private LocalDateTime createdAt; - public void endVote() { - this.isActive = false; - } + public void endVote() { + this.isActive = false; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java index 2ed3825..70f03c7 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java @@ -2,7 +2,6 @@ import jakarta.persistence.*; import lombok.*; -import opensource.bravest.domain.message.entity.ChatMessage; @Entity @Getter @@ -11,21 +10,21 @@ @Builder public class VoteOption { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_id", nullable = false) - private Vote vote; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; - @Column(name = "message_content", nullable = false) - private String messageContent; + @Column(name = "message_content", nullable = false) + private String messageContent; - @Column(nullable = false) - private int voteCount; + @Column(nullable = false) + private int voteCount; - public void incrementVoteCount() { - this.voteCount++; - } + public void incrementVoteCount() { + this.voteCount++; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java index d9e3579..7fe9954 100644 --- a/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java +++ b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java @@ -1,12 +1,11 @@ package opensource.bravest.domain.vote.repository; +import java.util.Optional; import opensource.bravest.domain.profile.entity.AnonymousProfile; import opensource.bravest.domain.vote.entity.UserVote; import opensource.bravest.domain.vote.entity.Vote; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface UserVoteRepository extends JpaRepository { - Optional findByVoteAndVoter(Vote vote, AnonymousProfile voter); + Optional findByVoteAndVoter(Vote vote, AnonymousProfile voter); } diff --git a/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java index 5b97137..ccc2c49 100644 --- a/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java +++ b/src/main/java/opensource/bravest/domain/vote/repository/VoteOptionRepository.java @@ -3,5 +3,4 @@ import opensource.bravest.domain.vote.entity.VoteOption; import org.springframework.data.jpa.repository.JpaRepository; -public interface VoteOptionRepository extends JpaRepository { -} +public interface VoteOptionRepository extends JpaRepository {} diff --git a/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java index e7b9d91..8dcfded 100644 --- a/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java +++ b/src/main/java/opensource/bravest/domain/vote/repository/VoteRepository.java @@ -3,5 +3,4 @@ import opensource.bravest.domain.vote.entity.Vote; import org.springframework.data.jpa.repository.JpaRepository; -public interface VoteRepository extends JpaRepository { -} +public interface VoteRepository extends JpaRepository {} diff --git a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java index 8d39887..39224b9 100644 --- a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java +++ b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java @@ -1,8 +1,9 @@ package opensource.bravest.domain.vote.service; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import opensource.bravest.domain.message.entity.ChatMessage; -import opensource.bravest.domain.message.repository.ChatMessageRepository; import opensource.bravest.domain.profile.entity.AnonymousProfile; import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; import opensource.bravest.domain.room.entity.AnonymousRoom; @@ -16,112 +17,112 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class VoteService { - private final VoteRepository voteRepository; - private final UserVoteRepository userVoteRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - private final AnonymousProfileRepository anonymousProfileRepository; - - @Transactional - public Vote createVote(VoteDto.CreateVoteRequest request) { - AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) - .orElseThrow(() -> new RuntimeException("Room not found")); - - Vote vote = Vote.builder() - .room(room) - .title(room.getTitle()) - .isActive(true) - .createdAt(LocalDateTime.now()) - .build(); - - List options = request.getMessages().stream() - .map(message -> VoteOption.builder() - .vote(vote) - .messageContent(message) - .voteCount(0) - .build()) - .collect(Collectors.toList()); + private final VoteRepository voteRepository; + private final UserVoteRepository userVoteRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousProfileRepository anonymousProfileRepository; + + @Transactional + public Vote createVote(VoteDto.CreateVoteRequest request) { + AnonymousRoom room = + anonymousRoomRepository + .findById(request.getRoomId()) + .orElseThrow(() -> new RuntimeException("Room not found")); + + Vote vote = + Vote.builder() + .room(room) + .title(room.getTitle()) + .isActive(true) + .createdAt(LocalDateTime.now()) + .build(); + + List options = + request.getMessages().stream() + .map( + message -> + VoteOption.builder().vote(vote).messageContent(message).voteCount(0).build()) + .collect(Collectors.toList()); + + vote.getOptions().addAll(options); + + return voteRepository.save(vote); + } + + @Transactional + public void castVote(Long voteId, VoteDto.CastVoteRequest request) { + Vote vote = + voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + if (!vote.isActive()) { + throw new RuntimeException("Vote is not active"); + } - vote.getOptions().addAll(options); + AnonymousProfile voter = + anonymousProfileRepository + .findById(request.getAnonymousProfileId()) + .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); - return voteRepository.save(vote); + if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { + throw new RuntimeException("User has already voted"); } - @Transactional - public void castVote(Long voteId, VoteDto.CastVoteRequest request) { - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new RuntimeException("Vote not found")); - if (!vote.isActive()) { - throw new RuntimeException("Vote is not active"); - } - - AnonymousProfile voter = anonymousProfileRepository.findById(request.getAnonymousProfileId()) - .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); - - if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { - throw new RuntimeException("User has already voted"); - } - - VoteOption voteOption = vote.getOptions().stream() - .filter(option -> option.getId().equals(request.getVoteOptionId())) - .findFirst() - .orElseThrow(() -> new RuntimeException("VoteOption not found")); - - voteOption.incrementVoteCount(); - - UserVote userVote = UserVote.builder() - .vote(vote) - .voteOption(voteOption) - .voter(voter) - .build(); - userVoteRepository.save(userVote); - } + VoteOption voteOption = + vote.getOptions().stream() + .filter(option -> option.getId().equals(request.getVoteOptionId())) + .findFirst() + .orElseThrow(() -> new RuntimeException("VoteOption not found")); - @Transactional - public void endVote(Long voteId) { - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new RuntimeException("Vote not found")); - vote.endVote(); - } + voteOption.incrementVoteCount(); - public VoteDto.VoteResponse getVoteResult(Long voteId) { - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new RuntimeException("Vote not found")); + UserVote userVote = UserVote.builder().vote(vote).voteOption(voteOption).voter(voter).build(); + userVoteRepository.save(userVote); + } - return buildVoteResponse(vote); - } + @Transactional + public void endVote(Long voteId) { + Vote vote = + voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + vote.endVote(); + } - @Transactional - public void deleteVote(Long voteId) { - if (!voteRepository.existsById(voteId)) { - throw new RuntimeException("Vote not found"); - } - voteRepository.deleteById(voteId); - } + public VoteDto.VoteResponse getVoteResult(Long voteId) { + Vote vote = + voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + + return buildVoteResponse(vote); + } - private VoteDto.VoteResponse buildVoteResponse(Vote vote) { - List optionResponses = vote.getOptions().stream() - .map(option -> VoteDto.VoteOptionResponse.builder() + @Transactional + public void deleteVote(Long voteId) { + if (!voteRepository.existsById(voteId)) { + throw new RuntimeException("Vote not found"); + } + voteRepository.deleteById(voteId); + } + + private VoteDto.VoteResponse buildVoteResponse(Vote vote) { + List optionResponses = + vote.getOptions().stream() + .map( + option -> + VoteDto.VoteOptionResponse.builder() .id(option.getId()) .messageContent(option.getMessageContent()) .voteCount(option.getVoteCount()) .build()) - .collect(Collectors.toList()); - - return VoteDto.VoteResponse.builder() - .id(vote.getId()) - .title(vote.getTitle()) - .isActive(vote.isActive()) - .createdAt(vote.getCreatedAt()) - .options(optionResponses) - .build(); - } + .collect(Collectors.toList()); + + return VoteDto.VoteResponse.builder() + .id(vote.getId()) + .title(vote.getTitle()) + .isActive(vote.isActive()) + .createdAt(vote.getCreatedAt()) + .options(optionResponses) + .build(); + } } diff --git a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java index 098aeec..b577884 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java +++ b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java @@ -1,6 +1,5 @@ package opensource.bravest.global.apiPayload; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -13,10 +12,11 @@ @Getter @AllArgsConstructor -@JsonPropertyOrder({ "isSuccess", "code", "message", "data" }) +@JsonPropertyOrder({"isSuccess", "code", "message", "data"}) public class ApiResponse { @JsonProperty("isSuccess") private final boolean isSuccess; + private final String code; private final String message; @@ -25,27 +25,17 @@ public class ApiResponse { public static ApiResponse onSuccess(T data) { return new ApiResponse<>( - true, - SuccessStatus._OK.getCode(), - SuccessStatus._OK.getMessage(), - data); + true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), data); } public static ApiResponse of(BaseCode code, String message, T data) { return new ApiResponse<>( - true, - code.getReasonHttpStatus().getCode(), - code.getReasonHttpStatus().getMessage(), - data); + true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), data); } public static ApiResponse onFailure(BaseErrorCode errorCode, T data) { ErrorReasonDto reason = errorCode.getReasonHttpStatus(); - return new ApiResponse<>( - reason.getIsSuccess(), - reason.getCode(), - reason.getMessage(), - data); + return new ApiResponse<>(reason.getIsSuccess(), reason.getCode(), reason.getMessage(), data); } public static ApiResponse onFailure(String code, String message, T data) { diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java b/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java index df8f866..00f3dd4 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java @@ -2,5 +2,6 @@ public interface BaseCode { ReasonDto getReason(); + ReasonDto getReasonHttpStatus(); -} \ No newline at end of file +} diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java b/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java index e52b4d4..6f514a7 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java @@ -2,5 +2,6 @@ public interface BaseErrorCode { ErrorReasonDto getReason(); + ErrorReasonDto getReasonHttpStatus(); } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java index 8afd9f2..b0ae3d4 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java @@ -1,6 +1,5 @@ package opensource.bravest.global.apiPayload.code.status; - import lombok.AllArgsConstructor; import lombok.Getter; import opensource.bravest.global.apiPayload.code.BaseErrorCode; @@ -19,7 +18,6 @@ public enum ErrorStatus implements BaseErrorCode { _USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "사용자를 찾을 수 없습니다."), _CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "채팅방을 찾을 수 없습니다."), _CHATLIST_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "리스트를 찾을 수 없습니다."), - ; private final HttpStatus httpStatus; @@ -28,11 +26,7 @@ public enum ErrorStatus implements BaseErrorCode { @Override public ErrorReasonDto getReason() { - return ErrorReasonDto.builder() - .isSuccess(false) - .message(message) - .code(code) - .build(); + return ErrorReasonDto.builder().isSuccess(false).message(message).code(code).build(); } @Override diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java index 2400189..7845515 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java @@ -18,11 +18,7 @@ public enum SuccessStatus implements BaseCode { @Override public ReasonDto getReason() { - return ReasonDto.builder() - .isSuccess(true) - .message(message) - .code(code) - .build(); + return ReasonDto.builder().isSuccess(true).message(message).code(code).build(); } @Override @@ -34,5 +30,4 @@ public ReasonDto getReasonHttpStatus() { .message(message) .build(); } - } diff --git a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java index 48e4c39..25c4cff 100644 --- a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java +++ b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java @@ -13,28 +13,29 @@ @Configuration public class OpenApiConfig { - private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; - @Bean - public OpenAPI baseOpenAPI() { - return new OpenAPI() - // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 - .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) - // 2) JWT Bearer 스키마 정의 - .components(new Components() - .addSecuritySchemes(SECURITY_SCHEME_NAME, - new SecurityScheme() - .name(SECURITY_SCHEME_NAME) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - ) - ) - .info(new Info() - .title("openSource Bravest API") - .description("openSource Bravest 백엔드 API 문서") - .version("v1.0.0") - .license(new License().name("MIT"))) - .externalDocs(new ExternalDocumentation().description("README")); - } + @Bean + public OpenAPI baseOpenAPI() { + return new OpenAPI() + // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + // 2) JWT Bearer 스키마 정의 + .components( + new Components() + .addSecuritySchemes( + SECURITY_SCHEME_NAME, + new SecurityScheme() + .name(SECURITY_SCHEME_NAME) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))) + .info( + new Info() + .title("openSource Bravest API") + .description("openSource Bravest 백엔드 API 문서") + .version("v1.0.0") + .license(new License().name("MIT"))) + .externalDocs(new ExternalDocumentation().description("README")); + } } diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index eb5b4be..35abcef 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -1,5 +1,8 @@ package opensource.bravest.global.config; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import opensource.bravest.global.apiPayload.code.status.ErrorStatus; import opensource.bravest.global.security.jwt.JwtAuthenticationFilter; @@ -17,10 +20,6 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.*; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -29,27 +28,30 @@ public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; // Swagger - private static final String[] SWAGGER = { - "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" - }; + private static final String[] SWAGGER = {"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"}; // 로그인/토큰 교환/리다이렉트/헬스체크 등 공개 경로 private static final String[] PUBLIC = { - "/", "/actuator/health", - "/api/auth/**", // 카카오 코드 교환 API 등 - "/oauth2/**", - "/login/**", "/login/oauth2/**", - "/api/test/auth/**", - "/rooms/**", - "/chatlists/**", - "/anonymous-profiles/**", - "/votes/**", - "/ws-connect/**", "/chat-test", "/pub/**", "/sub/**" + "/", + "/actuator/health", + "/api/auth/**", // 카카오 코드 교환 API 등 + "/oauth2/**", + "/login/**", + "/login/oauth2/**", + "/api/test/auth/**", + "/rooms/**", + "/chatlists/**", + "/anonymous-profiles/**", + "/votes/**", + "/ws-connect/**", + "/chat-test", + "/pub/**", + "/sub/**" }; // 정적 리소스 private static final String[] STATIC = { - "/favicon.ico", "/assets/**", "/css/**", "/js/**", "/images/**" + "/favicon.ico", "/assets/**", "/css/**", "/js/**", "/images/**" }; @Bean @@ -73,37 +75,46 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestCache(cache -> cache.disable()) // 권한 규칙 - .authorizeHttpRequests(auth -> auth - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // CORS preflight 허용 - .requestMatchers(SWAGGER).permitAll() - .requestMatchers(PUBLIC).permitAll() - .requestMatchers(STATIC).permitAll() - .anyRequest().authenticated()) + .authorizeHttpRequests( + auth -> + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // CORS preflight 허용 + .requestMatchers(SWAGGER) + .permitAll() + .requestMatchers(PUBLIC) + .permitAll() + .requestMatchers(STATIC) + .permitAll() + .anyRequest() + .authenticated()) // 인증/인가 실패 공통 응답(JSON) - ApiResponse 형식 - .exceptionHandling(ex -> ex - .authenticationEntryPoint((req, res, ex1) -> { - ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write(String.format( - "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), - errorStatus.getMessage())); - } - }) - .accessDeniedHandler((req, res, ex2) -> { - ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write(String.format( - "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), - errorStatus.getMessage())); - } - })) + .exceptionHandling( + ex -> + ex.authenticationEntryPoint( + (req, res, ex1) -> { + ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write( + String.format( + "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + }) + .accessDeniedHandler( + (req, res, ex2) -> { + ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write( + String.format( + "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + })) // JWT 필터 등록(UsernamePasswordAuthenticationFilter 앞) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); @@ -112,15 +123,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } private static void addAll(List target, String[] arr) { - for (String s : arr) - target.add(s); + for (String s : arr) target.add(s); } // CORS (개발용: 필요 시 도메인 고정/축소) @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration c = new CorsConfiguration(); - c.setAllowedOrigins(List.of("http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173")); + c.setAllowedOrigins( + List.of("http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173")); c.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); c.setAllowedHeaders(List.of("*")); c.setExposedHeaders(List.of("Authorization", "Location")); diff --git a/src/main/java/opensource/bravest/global/config/ValkeyConfig.java b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java index f13aea9..be6c578 100644 --- a/src/main/java/opensource/bravest/global/config/ValkeyConfig.java +++ b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java @@ -2,15 +2,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class ValkeyConfig { - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); - } + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } } diff --git a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java index 87931a0..177db70 100644 --- a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java +++ b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java @@ -14,23 +14,21 @@ @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final StompHandler stompHandler; + private final StompHandler stompHandler; - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws-connect") - .setAllowedOriginPatterns("*") - .withSockJS(); - } + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws-connect").setAllowedOriginPatterns("*").withSockJS(); + } - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/subs"); - registry.setApplicationDestinationPrefixes("/pubs"); - } + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/subs"); + registry.setApplicationDestinationPrefixes("/pubs"); + } - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(stompHandler); - } + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } } diff --git a/src/main/java/opensource/bravest/global/exception/CustomException.java b/src/main/java/opensource/bravest/global/exception/CustomException.java index 1ae5428..3fb913e 100644 --- a/src/main/java/opensource/bravest/global/exception/CustomException.java +++ b/src/main/java/opensource/bravest/global/exception/CustomException.java @@ -1,19 +1,16 @@ package opensource.bravest.global.exception; - import lombok.Getter; import opensource.bravest.global.apiPayload.code.BaseErrorCode; -/** - * 서비스/도메인 레이어에서 표준화된 에러코드를 던지기 위한 예외 - */ +/** 서비스/도메인 레이어에서 표준화된 에러코드를 던지기 위한 예외 */ @Getter public class CustomException extends RuntimeException { - private final BaseErrorCode errorCode; + private final BaseErrorCode errorCode; - public CustomException(BaseErrorCode errorCode) { - super(errorCode.getReason().getMessage()); - this.errorCode = errorCode; - } -} \ No newline at end of file + public CustomException(BaseErrorCode errorCode) { + super(errorCode.getReason().getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java index aeddd45..6faa479 100644 --- a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java @@ -14,8 +14,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) public ResponseEntity> handleCustomException(CustomException e) { log.warn("CustomException: {}", e.getMessage()); - return ResponseEntity - .status(e.getErrorCode().getReasonHttpStatus().getHttpStatus()) + return ResponseEntity.status(e.getErrorCode().getReasonHttpStatus().getHttpStatus()) .body(ApiResponse.onFailure(e.getErrorCode(), null)); } @@ -27,31 +26,31 @@ public ResponseEntity> handleRuntimeException(RuntimeExcepti if (message != null) { if (message.contains("유효하지 않은 초대 코드") || message.contains("가족을 찾을 수 없습니다")) { log.warn("Family not found: {}", message); - return ResponseEntity - .status(ErrorStatus._FAMILY_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) + return ResponseEntity.status( + ErrorStatus._FAMILY_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) .body(ApiResponse.onFailure(ErrorStatus._FAMILY_NOT_FOUND, null)); } if (message.contains("사용자를 찾을 수 없습니다")) { log.warn("User not found: {}", message); - return ResponseEntity - .status(ErrorStatus._USER_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) + return ResponseEntity.status( + ErrorStatus._USER_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) .body(ApiResponse.onFailure(ErrorStatus._USER_NOT_FOUND, null)); } } // 기본값: 500 Internal Server Error log.error("RuntimeException: ", e); - return ResponseEntity - .status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) + return ResponseEntity.status( + ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); } @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception e) { log.error("Unexpected exception: ", e); - return ResponseEntity - .status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) + return ResponseEntity.status( + ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); } } diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index c3ab889..d7ec8d8 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -1,5 +1,7 @@ package opensource.bravest.global.handler; +import java.security.Principal; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import opensource.bravest.domain.profile.repository.AnonymousProfileRepository; @@ -16,136 +18,141 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import java.security.Principal; -import java.util.List; - @Slf4j @Component @RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { - private final AnonymousProfileRepository anonymousProfileRepository; - private final StringRedisTemplate redisTemplate; + private final AnonymousProfileRepository anonymousProfileRepository; + private final StringRedisTemplate redisTemplate; - private static final String USER_SUB_KEY_PREFIX = "ws:subs:user:"; // + anonymousId - private static final String METRIC_TOTAL_SUB = "ws:metrics:sub:total"; - private static final String METRIC_DUP_SUB = "ws:metrics:sub:duplicate"; + private static final String USER_SUB_KEY_PREFIX = "ws:subs:user:"; // + anonymousId + private static final String METRIC_TOTAL_SUB = "ws:metrics:sub:total"; + private static final String METRIC_DUP_SUB = "ws:metrics:sub:duplicate"; - @Override - public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = - MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = + MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - if (accessor == null) { - return message; - } + if (accessor == null) { + return message; + } - StompCommand command = accessor.getCommand(); - - // 1) CONNECT: anonymousId를 Principal로 설정 - if (StompCommand.CONNECT.equals(command)) { - String anonymousId = accessor.getFirstNativeHeader("anonymousId"); - if (anonymousId == null || anonymousId.isBlank()) { - log.warn("STOMP CONNECT: anonymousId missing"); - throw new IllegalArgumentException("anonymousId header is required"); - } - - anonymousProfileRepository.findById(Long.valueOf(anonymousId)) - .ifPresentOrElse(member -> { - Authentication auth = new UsernamePasswordAuthenticationToken( - anonymousId, - null, - List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")) - ); - SecurityContextHolder.getContext().setAuthentication(auth); - accessor.setUser(auth); - log.info("STOMP CONNECT: anonymousId={} principal set", anonymousId); - }, () -> { - log.warn("STOMP CONNECT: invalid anonymousId={}", anonymousId); - throw new IllegalArgumentException("Invalid anonymousId"); - }); - } + StompCommand command = accessor.getCommand(); + + // 1) CONNECT: anonymousId를 Principal로 설정 + if (StompCommand.CONNECT.equals(command)) { + String anonymousId = accessor.getFirstNativeHeader("anonymousId"); + if (anonymousId == null || anonymousId.isBlank()) { + log.warn("STOMP CONNECT: anonymousId missing"); + throw new IllegalArgumentException("anonymousId header is required"); + } + + anonymousProfileRepository + .findById(Long.valueOf(anonymousId)) + .ifPresentOrElse( + member -> { + Authentication auth = + new UsernamePasswordAuthenticationToken( + anonymousId, null, List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); + SecurityContextHolder.getContext().setAuthentication(auth); + accessor.setUser(auth); + log.info("STOMP CONNECT: anonymousId={} principal set", anonymousId); + }, + () -> { + log.warn("STOMP CONNECT: invalid anonymousId={}", anonymousId); + throw new IllegalArgumentException("Invalid anonymousId"); + }); + } - // 2) SUBSCRIBE: Redis를 사용해 anonymousId 기준 중복 구독 방지 + 메트릭 기록 - if (StompCommand.SUBSCRIBE.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - accessor.setUser(auth); - user = auth; - } - } - - String destination = accessor.getDestination(); - - if (user != null && destination != null) { - String anonymousId = user.getName(); - String key = USER_SUB_KEY_PREFIX + anonymousId; - - log.info("[SUBSCRIBE] handling: anonymousId={}, destination={}, key={}", - anonymousId, destination, key); - - try { - Long total = redisTemplate.opsForValue().increment(METRIC_TOTAL_SUB); - Long added = redisTemplate.opsForSet().add(key, destination); - redisTemplate.expire(key, java.time.Duration.ofHours(1)); - - log.info("[SUBSCRIBE] redis result: total={}, added={}", total, added); - - if (added != null && added == 0L) { - Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); - log.warn("[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", - anonymousId, destination, dup); - return null; - } - - log.info("[SUBSCRIBE] stored in Redis: key={}, member={}", key, destination); - - } catch (Exception e) { - log.error("Redis error while handling SUBSCRIBE", e); - } - } else { - log.warn("[SUBSCRIBE] skipped: user or destination is null (user={}, dest={})", - user, destination); - } + // 2) SUBSCRIBE: Redis를 사용해 anonymousId 기준 중복 구독 방지 + 메트릭 기록 + if (StompCommand.SUBSCRIBE.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + user = auth; } + } + + String destination = accessor.getDestination(); + if (user != null && destination != null) { + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; - // 3) SEND: Principal 비어 있으면 SecurityContext에서 복구 - if (StompCommand.SEND.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - accessor.setUser(auth); - } - } + log.info( + "[SUBSCRIBE] handling: anonymousId={}, destination={}, key={}", + anonymousId, + destination, + key); + + try { + Long total = redisTemplate.opsForValue().increment(METRIC_TOTAL_SUB); + Long added = redisTemplate.opsForSet().add(key, destination); + redisTemplate.expire(key, java.time.Duration.ofHours(1)); + + log.info("[SUBSCRIBE] redis result: total={}, added={}", total, added); + + if (added != null && added == 0L) { + Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); + log.warn( + "[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", + anonymousId, + destination, + dup); + return null; + } + + log.info("[SUBSCRIBE] stored in Redis: key={}, member={}", key, destination); + + } catch (Exception e) { + log.error("Redis error while handling SUBSCRIBE", e); } + } else { + log.warn( + "[SUBSCRIBE] skipped: user or destination is null (user={}, dest={})", + user, + destination); + } + } - // 4) DISCONNECT: 유저별 구독 키를 정리할지 여부 (옵션) - // - 전체 방 전체 유저 수가 크지 않다면 TTL만으로도 충분. - if (StompCommand.DISCONNECT.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - user = auth; - } - } - if (user != null) { - String anonymousId = user.getName(); - String key = USER_SUB_KEY_PREFIX + anonymousId; - try { - // 완전히 정리하고 싶으면 delete - redisTemplate.delete(key); - log.info("DISCONNECT: cleared subscriptions for anonymousId={}", anonymousId); - } catch (Exception e) { - log.error("Redis error while handling DISCONNECT", e); - } - } + // 3) SEND: Principal 비어 있으면 SecurityContext에서 복구 + if (StompCommand.SEND.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); } + } + } - return message; + // 4) DISCONNECT: 유저별 구독 키를 정리할지 여부 (옵션) + // - 전체 방 전체 유저 수가 크지 않다면 TTL만으로도 충분. + if (StompCommand.DISCONNECT.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + user = auth; + } + } + if (user != null) { + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; + try { + // 완전히 정리하고 싶으면 delete + redisTemplate.delete(key); + log.info("DISCONNECT: cleared subscriptions for anonymousId={}", anonymousId); + } catch (Exception e) { + log.error("Redis error while handling DISCONNECT", e); + } + } } + + return message; + } } diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java index 5f7c282..34ccece 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java @@ -5,68 +5,67 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - /** - * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - * - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 건너뜀. - * - 토큰이 유효하면 SecurityContext 설정, 아니면 체인 진행 (401은 EntryPoint가 처리) + * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 건너뜀. - 토큰이 유효하면 + * SecurityContext 설정, 아니면 체인 진행 (401은 EntryPoint가 처리) */ public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtTokenProvider jwtTokenProvider; - private final List skipPatterns; // 필터를 스킵할 경로 패턴들(ant style) - private final AntPathMatcher matcher = new AntPathMatcher(); + private final JwtTokenProvider jwtTokenProvider; + private final List skipPatterns; // 필터를 스킵할 경로 패턴들(ant style) + private final AntPathMatcher matcher = new AntPathMatcher(); - public JwtAuthenticationFilter(JwtTokenProvider provider, Collection skipPatterns) { - this.jwtTokenProvider = provider; - this.skipPatterns = skipPatterns == null ? List.of() : List.copyOf(skipPatterns); - } + public JwtAuthenticationFilter(JwtTokenProvider provider, Collection skipPatterns) { + this.jwtTokenProvider = provider; + this.skipPatterns = skipPatterns == null ? List.of() : List.copyOf(skipPatterns); + } - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - // 1) CORS preflight는 항상 스킵 - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 1) CORS preflight는 항상 스킵 + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; - // 2) 화이트리스트 패턴은 스킵 - String path = request.getServletPath(); - for (String p : skipPatterns) { - if (matcher.match(p, path)) return true; - } - return false; + // 2) 화이트리스트 패턴은 스킵 + String path = request.getServletPath(); + for (String p : skipPatterns) { + if (matcher.match(p, path)) return true; } + return false; + } - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); + String header = request.getHeader(HttpHeaders.AUTHORIZATION); - if (header != null && header.startsWith("Bearer ")) { - String token = header.substring(7); - try { - Claims claims = jwtTokenProvider.parseClaims(token); - String subject = claims.getSubject(); - if (subject != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // 필요 시 roles/authorities를 claims에서 꺼내서 넣어도 됨 - var auth = new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } catch (Exception ignored) { - // 유효하지 않으면 그냥 통과 -> 최종적으로 EntryPoint가 401 응답 처리 - } + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + try { + Claims claims = jwtTokenProvider.parseClaims(token); + String subject = claims.getSubject(); + if (subject != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 필요 시 roles/authorities를 claims에서 꺼내서 넣어도 됨 + var auth = + new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()); + SecurityContextHolder.getContext().setAuthentication(auth); } - - chain.doFilter(request, response); + } catch (Exception ignored) { + // 유효하지 않으면 그냥 통과 -> 최종적으로 EntryPoint가 401 응답 처리 + } } -} + chain.doFilter(request, response); + } +} diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java index e2e3b36..f5a8f66 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java @@ -1,99 +1,93 @@ package opensource.bravest.global.security.jwt; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.Claims; +import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Date; import java.util.Map; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; @Component public class JwtTokenProvider { - @Value("${jwt.secret}") - private String secret; - - @Value("${jwt.access-token-validity-seconds}") - private long accessValidity; - - @Value("${jwt.refresh-token-validity-seconds}") - private long refreshValidity; + @Value("${jwt.secret}") + private String secret; - private SecretKey key; + @Value("${jwt.access-token-validity-seconds}") + private long accessValidity; - @PostConstruct - void init() { - if (secret == null || secret.isBlank()) { - throw new IllegalStateException("jwt.secret is not configured. Check your application.yml / env."); - } + @Value("${jwt.refresh-token-validity-seconds}") + private long refreshValidity; - byte[] keyBytes; - try { - // secret이 Base64면 여기서 정상 디코딩 - keyBytes = Decoders.BASE64.decode(secret); - } catch (IllegalArgumentException e) { - // Base64 아니면 그냥 문자열 바이트로 사용 - keyBytes = secret.getBytes(StandardCharsets.UTF_8); - } + private SecretKey key; - this.key = Keys.hmacShaKeyFor(keyBytes); + @PostConstruct + void init() { + if (secret == null || secret.isBlank()) { + throw new IllegalStateException( + "jwt.secret is not configured. Check your application.yml / env."); } - public String createAccessToken(String subject, Map claims) { - Instant now = Instant.now(); - return Jwts.builder() - .subject(subject) - .claims(claims) - .issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(accessValidity))) - .signWith(key) - .compact(); + byte[] keyBytes; + try { + // secret이 Base64면 여기서 정상 디코딩 + keyBytes = Decoders.BASE64.decode(secret); + } catch (IllegalArgumentException e) { + // Base64 아니면 그냥 문자열 바이트로 사용 + keyBytes = secret.getBytes(StandardCharsets.UTF_8); } - public String createRefreshToken(String subject) { - Instant now = Instant.now(); - return Jwts.builder() - .subject(subject) - .issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(refreshValidity))) - .signWith(key) - .compact(); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String createAccessToken(String subject, Map claims) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(subject) + .claims(claims) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(accessValidity))) + .signWith(key) + .compact(); + } + + public String createRefreshToken(String subject) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(subject) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(refreshValidity))) + .signWith(key) + .compact(); + } + + public Long getIdFromToken(String token) { + Claims claims = + Jwts.parser() + .verifyWith(key) // init()에서 만든 key 재사용 + .build() + .parseSignedClaims(token) + .getPayload(); + + return claims.get("id", Long.class); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + return true; + } catch (Exception e) { + return false; } + } - public Long getIdFromToken(String token) { - Claims claims = Jwts.parser() - .verifyWith(key) // init()에서 만든 key 재사용 - .build() - .parseSignedClaims(token) - .getPayload(); - - return claims.get("id", Long.class); - } - - public boolean validateToken(String token) { - try { - Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token); - return true; - } catch (Exception e) { - return false; - } - } - - public Claims parseClaims(String token) { - return Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload(); - } + public Claims parseClaims(String token) { + return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); + } } diff --git a/src/test/java/opensource/bravest/BravestApplicationTests.java b/src/test/java/opensource/bravest/BravestApplicationTests.java index 6494944..bde485a 100644 --- a/src/test/java/opensource/bravest/BravestApplicationTests.java +++ b/src/test/java/opensource/bravest/BravestApplicationTests.java @@ -6,8 +6,6 @@ @SpringBootTest class BravestApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } From 174006516cf301d3351b3e43918e867b90191483 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 04:23:50 +0900 Subject: [PATCH 41/44] [ci/cd] rename ci/cd file --- .github/workflows/code-style-test.yml | 43 +++++++++++++++++++++++++++ .github/workflows/code-stype-test.yml | 43 --------------------------- 2 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/code-style-test.yml delete mode 100644 .github/workflows/code-stype-test.yml diff --git a/.github/workflows/code-style-test.yml b/.github/workflows/code-style-test.yml new file mode 100644 index 0000000..b389c45 --- /dev/null +++ b/.github/workflows/code-style-test.yml @@ -0,0 +1,43 @@ +name: Java Code Style Check + +on: + pull_request: + branches: ['**'] + push: + branches: ['**'] + workflow_dispatch: + +jobs: + style-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Spotless and Checkstyle + run: | + set -e + ./gradlew spotlessCheck --no-daemon + ./gradlew checkstyleMain checkstyleTest --no-daemon + shell: bash + + - name: Show summary + run: | + if [ $? -eq 0 ]; then + echo "🎉 Code style checks passed!" + else + echo "❌ Code style violations detected!" + echo "Please run './gradlew spotlessApply' locally and fix Checkstyle issues." + exit 1 + fi + shell: bash diff --git a/.github/workflows/code-stype-test.yml b/.github/workflows/code-stype-test.yml deleted file mode 100644 index b6551b7..0000000 --- a/.github/workflows/code-stype-test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Code Style Check - -on: - pull_request: - push: - workflow_dispatch: - -jobs: - code-style-check: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Grant permission for Gradle - run: chmod +x gradlew - - - name: Build without tests - run: ./gradlew assemble --no-daemon - - - name: Run Spotless Check - run: ./gradlew spotlessCheck --no-daemon - - - name: Run Checkstyle - run: ./gradlew checkstyleMain checkstyleTest --no-daemon - - - name: Run Gradle Check - run: ./gradlew check --no-daemon - - - name: Summary - if: success() - run: echo "🎉 Code style checks passed successfully!" - - - name: Fail Summary - if: failure() - run: echo "❌ Code style violations found. Please fix them before merging." From 9e6c4ec3abaea3e4e0d5d0917666396c26735368 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 04:38:44 +0900 Subject: [PATCH 42/44] [ci/cd] refactor all of file --- build.gradle | 11 +- .../checkstyle/checkstyle_eclipse_format.xml | 26 +++ config/checkstyle/google_checks.xml | 34 ++- .../bravest/BravestApplication.java | 6 +- .../controller/ChatListController.java | 65 +++--- .../domain/chatList/dto/ChatListDto.java | 74 +++--- .../domain/chatList/entity/ChatList.java | 57 ++--- .../repository/ChatListRepository.java | 4 +- .../chatList/service/ChatListService.java | 73 +++--- .../controller/ChatMessageController.java | 22 +- .../domain/message/dto/MessageDto.java | 52 ++--- .../domain/message/entity/ChatMessage.java | 28 +-- .../repository/ChatMessageRepository.java | 4 +- .../message/service/ChatMessageService.java | 67 +++--- .../AnonymousProfileController.java | 30 +-- .../profile/dto/AnonymousProfileResponse.java | 19 +- .../dto/CreateAnonymousProfileRequest.java | 4 +- .../profile/entity/AnonymousProfile.java | 26 +-- .../AnonymousProfileRepository.java | 4 +- .../service/AnonymousProfileService.java | 55 ++--- .../room/controller/RoomController.java | 117 ++++------ .../bravest/domain/room/dto/RoomDto.java | 50 ++-- .../domain/room/entity/AnonymousRoom.java | 32 +-- .../repository/AnonymousRoomRepository.java | 4 +- .../domain/room/service/RoomService.java | 85 ++++--- .../vote/controller/VoteController.java | 92 ++++---- .../bravest/domain/vote/dto/VoteDto.java | 56 ++--- .../bravest/domain/vote/entity/UserVote.java | 24 +- .../bravest/domain/vote/entity/Vote.java | 32 +-- .../domain/vote/entity/VoteOption.java | 26 +-- .../vote/repository/UserVoteRepository.java | 2 +- .../domain/vote/service/VoteService.java | 156 ++++++------- .../global/apiPayload/ApiResponse.java | 41 ++-- .../global/apiPayload/code/BaseCode.java | 4 +- .../global/apiPayload/code/BaseErrorCode.java | 4 +- .../apiPayload/code/ErrorReasonDto.java | 8 +- .../global/apiPayload/code/ReasonDto.java | 8 +- .../apiPayload/code/status/ErrorStatus.java | 46 ++-- .../apiPayload/code/status/SuccessStatus.java | 31 +-- .../bravest/global/config/OpenApiConfig.java | 38 ++-- .../bravest/global/config/SecurityConfig.java | 209 +++++++---------- .../bravest/global/config/ValkeyConfig.java | 8 +- .../global/config/WebSocketConfig.java | 28 +-- .../global/exception/CustomException.java | 10 +- .../exception/GlobalExceptionHandler.java | 76 +++---- .../bravest/global/handler/StompHandler.java | 214 ++++++++---------- .../security/jwt/JwtAuthenticationFilter.java | 80 +++---- .../global/security/jwt/JwtTokenProvider.java | 108 ++++----- .../bravest/BravestApplicationTests.java | 4 +- 49 files changed, 1066 insertions(+), 1188 deletions(-) create mode 100644 config/checkstyle/checkstyle_eclipse_format.xml diff --git a/build.gradle b/build.gradle index ed442ea..ec2be1e 100644 --- a/build.gradle +++ b/build.gradle @@ -85,7 +85,14 @@ apply plugin: 'com.diffplug.spotless' spotless { java { - googleJavaFormat() target 'src/**/*.java' + + // Checkstyle XML 규칙을 완전히 재현할 수는 없지만, 기본 규칙 적용 + // 들여쓰기 4칸, 중괄호 스타일, 라인 길이 제한 + eclipse().configFile 'config/checkstyle/checkstyle_eclipse_format.xml' + + // 혹은 Google Java Format 사용 + // googleJavaFormat('1.16.0') } -} \ No newline at end of file + +} diff --git a/config/checkstyle/checkstyle_eclipse_format.xml b/config/checkstyle/checkstyle_eclipse_format.xml new file mode 100644 index 0000000..0a278a0 --- /dev/null +++ b/config/checkstyle/checkstyle_eclipse_format.xml @@ -0,0 +1,26 @@ + + + + + + + + + + ``` + + + + + + + + + + + + + + ``` + + diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml index 6eff7fe..3817602 100644 --- a/config/checkstyle/google_checks.xml +++ b/config/checkstyle/google_checks.xml @@ -1,10 +1,38 @@ - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/main/java/opensource/bravest/BravestApplication.java b/src/main/java/opensource/bravest/BravestApplication.java index 144da2e..15912ca 100644 --- a/src/main/java/opensource/bravest/BravestApplication.java +++ b/src/main/java/opensource/bravest/BravestApplication.java @@ -6,7 +6,7 @@ @SpringBootApplication public class BravestApplication { - public static void main(String[] args) { - SpringApplication.run(BravestApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(BravestApplication.class, args); + } } diff --git a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java index 38810d3..2114548 100644 --- a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java +++ b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java @@ -23,37 +23,36 @@ @RequiredArgsConstructor public class ChatListController { - private final ChatListService chatListService; - - @PostMapping - public ApiResponse createChatList( - @Valid @RequestBody ChatListCreateRequest request) { - ChatListResponse response = chatListService.createChatList(request); - return ApiResponse.onSuccess(response); - } - - @GetMapping("/room/{roomId}") - public ApiResponse> getChatListsByRoomId(@PathVariable Long roomId) { - List response = chatListService.getChatListsByRoomId(roomId); - return ApiResponse.onSuccess(response); - } - - @GetMapping("/{id}") - public ApiResponse getChatListById(@PathVariable Long id) { - ChatListResponse response = chatListService.getChatListById(id); - return ApiResponse.onSuccess(response); - } - - @PutMapping("/{id}") - public ApiResponse updateChatList( - @PathVariable Long id, @Valid @RequestBody ChatListUpdateRequest request) { - ChatListResponse response = chatListService.updateChatList(id, request); - return ApiResponse.onSuccess(response); - } - - @DeleteMapping("/{id}") - public ApiResponse deleteChatList(@PathVariable Long id) { - chatListService.deleteChatList(id); - return ApiResponse.onSuccess(null); - } + private final ChatListService chatListService; + + @PostMapping + public ApiResponse createChatList(@Valid @RequestBody ChatListCreateRequest request) { + ChatListResponse response = chatListService.createChatList(request); + return ApiResponse.onSuccess(response); + } + + @GetMapping("/room/{roomId}") + public ApiResponse> getChatListsByRoomId(@PathVariable Long roomId) { + List response = chatListService.getChatListsByRoomId(roomId); + return ApiResponse.onSuccess(response); + } + + @GetMapping("/{id}") + public ApiResponse getChatListById(@PathVariable Long id) { + ChatListResponse response = chatListService.getChatListById(id); + return ApiResponse.onSuccess(response); + } + + @PutMapping("/{id}") + public ApiResponse updateChatList(@PathVariable Long id, + @Valid @RequestBody ChatListUpdateRequest request) { + ChatListResponse response = chatListService.updateChatList(id, request); + return ApiResponse.onSuccess(response); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteChatList(@PathVariable Long id) { + chatListService.deleteChatList(id); + return ApiResponse.onSuccess(null); + } } diff --git a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java index 77baf7d..7452f40 100644 --- a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java +++ b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java @@ -8,44 +8,40 @@ public class ChatListDto { - // 1. 아이디어 생성 요청 DTO (Create Request) - @Getter - @Setter - public static class ChatListCreateRequest { - - private Long roomId; - - private String content; - - private Long registeredBy; - } - - // 2. 아이디어 수정 요청 DTO (Update Request) - @Getter - @Setter - public static class ChatListUpdateRequest { - - // 아이디어 내용 수정만 가정 - private String content; - } - - @Getter - @Builder - public static class ChatListResponse { - private Long id; - private Long roomId; - private String content; - private Long registeredBy; - private LocalDateTime createdAt; - - public static ChatListResponse fromEntity(ChatList chatList) { - return ChatListResponse.builder() - .id(chatList.getId()) - .roomId(chatList.getRoomId()) - .content(chatList.getContent()) - .registeredBy(chatList.getRegisteredBy().getId()) - .createdAt(chatList.getCreatedAt()) - .build(); + // 1. 아이디어 생성 요청 DTO (Create Request) + @Getter + @Setter + public static class ChatListCreateRequest { + + private Long roomId; + + private String content; + + private Long registeredBy; + } + + // 2. 아이디어 수정 요청 DTO (Update Request) + @Getter + @Setter + public static class ChatListUpdateRequest { + + // 아이디어 내용 수정만 가정 + private String content; + } + + @Getter + @Builder + public static class ChatListResponse { + private Long id; + private Long roomId; + private String content; + private Long registeredBy; + private LocalDateTime createdAt; + + public static ChatListResponse fromEntity(ChatList chatList) { + return ChatListResponse.builder().id(chatList.getId()).roomId(chatList.getRoomId()) + .content(chatList.getContent()).registeredBy(chatList.getRegisteredBy().getId()) + .createdAt(chatList.getCreatedAt()).build(); + } } - } } diff --git a/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java b/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java index 63cb45a..d4a93d8 100644 --- a/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java +++ b/src/main/java/opensource/bravest/domain/chatList/entity/ChatList.java @@ -25,40 +25,41 @@ @Table(name = "chat_list") public class ChatList { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - @NotNull - @Column(length = 255) - private String content; + @NotNull + @Column(length = 255) + private String content; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - private AnonymousProfile registeredBy; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private AnonymousProfile registeredBy; - @CreationTimestamp private LocalDateTime createdAt; + @CreationTimestamp + private LocalDateTime createdAt; - @Builder - public ChatList(AnonymousRoom room, String content, AnonymousProfile registeredBy) { - this.room = room; - this.content = content; - this.registeredBy = registeredBy; - } + @Builder + public ChatList(AnonymousRoom room, String content, AnonymousProfile registeredBy) { + this.room = room; + this.content = content; + this.registeredBy = registeredBy; + } - public void updateContent(String content) { - this.content = content; - } + public void updateContent(String content) { + this.content = content; + } - public Long getRoomId() { - return this.room.getId(); - } + public Long getRoomId() { + return this.room.getId(); + } - public Long getProfileId() { - return this.registeredBy.getId(); - } + public Long getProfileId() { + return this.registeredBy.getId(); + } } diff --git a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java index b90efd1..b005041 100644 --- a/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java +++ b/src/main/java/opensource/bravest/domain/chatList/repository/ChatListRepository.java @@ -7,6 +7,6 @@ public interface ChatListRepository extends JpaRepository { - @Query("SELECT c FROM ChatList c WHERE c.room.id = :roomId ORDER BY c.createdAt DESC") - List findAllByRoomId(Long roomId); + @Query("SELECT c FROM ChatList c WHERE c.room.id = :roomId ORDER BY c.createdAt DESC") + List findAllByRoomId(Long roomId); } diff --git a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java index e375a13..5ac8da9 100644 --- a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java +++ b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java @@ -25,55 +25,48 @@ @Transactional(readOnly = true) public class ChatListService { - private final ChatListRepository chatListRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - private final AnonymousProfileRepository anonymousProfileRepository; + private final ChatListRepository chatListRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousProfileRepository anonymousProfileRepository; - @Transactional - public ChatListResponse createChatList(ChatListCreateRequest request) { - AnonymousRoom room = - anonymousRoomRepository - .findById(request.getRoomId()) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + @Transactional + public ChatListResponse createChatList(ChatListCreateRequest request) { + AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); - AnonymousProfile profile = - anonymousProfileRepository - .findById(request.getRegisteredBy()) - .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); + AnonymousProfile profile = anonymousProfileRepository.findById(request.getRegisteredBy()) + .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); - ChatList chatList = - ChatList.builder().room(room).registeredBy(profile).content(request.getContent()).build(); + ChatList chatList = ChatList.builder().room(room).registeredBy(profile).content(request.getContent()).build(); - ChatList savedList = chatListRepository.save(chatList); - return ChatListResponse.fromEntity(savedList); - } + ChatList savedList = chatListRepository.save(chatList); + return ChatListResponse.fromEntity(savedList); + } - public List getChatListsByRoomId(Long roomId) { - List chatLists = chatListRepository.findAllByRoomId(roomId); - return chatLists.stream().map(ChatListResponse::fromEntity).collect(Collectors.toList()); - } + public List getChatListsByRoomId(Long roomId) { + List chatLists = chatListRepository.findAllByRoomId(roomId); + return chatLists.stream().map(ChatListResponse::fromEntity).collect(Collectors.toList()); + } - public ChatListResponse getChatListById(Long id) { - ChatList chatList = - chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); - return ChatListResponse.fromEntity(chatList); - } + public ChatListResponse getChatListById(Long id) { + ChatList chatList = chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); + return ChatListResponse.fromEntity(chatList); + } - @Transactional - public ChatListResponse updateChatList(Long id, ChatListUpdateRequest request) { - ChatList chatList = - chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); + @Transactional + public ChatListResponse updateChatList(Long id, ChatListUpdateRequest request) { + ChatList chatList = chatListRepository.findById(id).orElseThrow(() -> new CustomException(_CHATLIST_NOT_FOUND)); - chatList.updateContent(request.getContent()); + chatList.updateContent(request.getContent()); - return ChatListResponse.fromEntity(chatList); - } + return ChatListResponse.fromEntity(chatList); + } - @Transactional - public void deleteChatList(Long id) { - if (!chatListRepository.existsById(id)) { - throw new CustomException(_CHATLIST_NOT_FOUND); + @Transactional + public void deleteChatList(Long id) { + if (!chatListRepository.existsById(id)) { + throw new CustomException(_CHATLIST_NOT_FOUND); + } + chatListRepository.deleteById(id); } - chatListRepository.deleteById(id); - } } diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java index 609e347..e2872df 100644 --- a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -17,17 +17,17 @@ @Controller @RequiredArgsConstructor public class ChatMessageController { - private final ChatMessageService chatMessageService; - private final SimpMessagingTemplate messagingTemplate; + private final ChatMessageService chatMessageService; + private final SimpMessagingTemplate messagingTemplate; - @MessageMapping("/send") - @SendTo("/subs/chat-rooms") - public void receiveMessage(MessageRequest request, Principal principal) { - Long id = Long.parseLong(principal.getName()); - MessageResponse response = chatMessageService.send(request, id); + @MessageMapping("/send") + @SendTo("/subs/chat-rooms") + public void receiveMessage(MessageRequest request, Principal principal) { + Long id = Long.parseLong(principal.getName()); + MessageResponse response = chatMessageService.send(request, id); - // 특정 채팅방 구독자들에게 메시지 전송 - messagingTemplate.convertAndSend( - "/subs/chat-rooms/" + request.getChatRoomId(), ApiResponse.onSuccess(response)); - } + // 특정 채팅방 구독자들에게 메시지 전송 + messagingTemplate.convertAndSend("/subs/chat-rooms/" + request.getChatRoomId(), + ApiResponse.onSuccess(response)); + } } diff --git a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java index 72b32c5..ac6f7c7 100644 --- a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java +++ b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java @@ -7,36 +7,34 @@ public class MessageDto { - @Getter - public static class SendMessageRequest { - private String content; - } + @Getter + public static class SendMessageRequest { + private String content; + } - @Getter - @RequiredArgsConstructor - public static class MessageResponse { - private final String senderName; // 익명 닉네임 - private final String content; - private final LocalDateTime createdAt; + @Getter + @RequiredArgsConstructor + public static class MessageResponse { + private final String senderName; // 익명 닉네임 + private final String content; + private final LocalDateTime createdAt; - public static MessageResponse from(ChatMessage chatMessage) { - return new MessageResponse( - chatMessage.getSender().getAnonymousName(), - chatMessage.getContent(), - chatMessage.getCreatedAt()); + public static MessageResponse from(ChatMessage chatMessage) { + return new MessageResponse(chatMessage.getSender().getAnonymousName(), chatMessage.getContent(), + chatMessage.getCreatedAt()); + } } - } - @Getter - @RequiredArgsConstructor - public static class MessageRequest { - private final Long chatRoomId; - private final String content; - } + @Getter + @RequiredArgsConstructor + public static class MessageRequest { + private final Long chatRoomId; + private final String content; + } - @Getter - @RequiredArgsConstructor - public static class ChatReadRequest { - private final Long chatRoomId; - } + @Getter + @RequiredArgsConstructor + public static class ChatReadRequest { + private final Long chatRoomId; + } } diff --git a/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java b/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java index 2ab7d59..2544637 100644 --- a/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java +++ b/src/main/java/opensource/bravest/domain/message/entity/ChatMessage.java @@ -13,22 +13,22 @@ @Builder public class ChatMessage { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 어느 방의 메시지인지 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + // 어느 방의 메시지인지 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - // 누가 보냈는지 (익명 프로필 기준) - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "anonymous_profile_id", nullable = false) - private AnonymousProfile sender; + // 누가 보냈는지 (익명 프로필 기준) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "anonymous_profile_id", nullable = false) + private AnonymousProfile sender; - @Column(nullable = false, length = 1000) - private String content; + @Column(nullable = false, length = 1000) + private String content; - private LocalDateTime createdAt; + private LocalDateTime createdAt; } diff --git a/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java b/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java index eaae205..4621641 100644 --- a/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java +++ b/src/main/java/opensource/bravest/domain/message/repository/ChatMessageRepository.java @@ -7,6 +7,6 @@ public interface ChatMessageRepository extends JpaRepository { - // 방 기준으로 최근 메시지 목록 - List findByRoomOrderByCreatedAtAsc(AnonymousRoom room); + // 방 기준으로 최근 메시지 목록 + List findByRoomOrderByCreatedAtAsc(AnonymousRoom room); } diff --git a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java index 57808a6..654d847 100644 --- a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java +++ b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java @@ -21,40 +21,35 @@ @RequiredArgsConstructor public class ChatMessageService { - private final AnonymousProfileRepository memberRepository; - private final AnonymousRoomRepository chatRoomRepository; - private final ChatMessageRepository chatMessageRepository; - - // 메시지 전송 - public MessageResponse send(MessageRequest request, Long id) { - AnonymousProfile sender = - memberRepository.findById(id).orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); - - AnonymousRoom chatRoom = - chatRoomRepository - .findById(request.getChatRoomId()) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); - - ChatMessage chatMessage = - ChatMessage.builder().room(chatRoom).sender(sender).content(request.getContent()).build(); - - chatMessageRepository.save(chatMessage); - - return MessageResponse.from(chatMessage); - } - - @Transactional - public void readMessages(Long chatRoomId, Long memberId) { - AnonymousRoom chatRoom = - chatRoomRepository - .findById(chatRoomId) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); - - // if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && - // !Objects.equals(chatRoom.getMember2().getId(), - // memberId)) { - // throw new BaseException(ChatExceptionType.CHAT_ROOM_ACCESS_DENIED); - // } - // messageReceiptRepository.bulkUpdateStatusToRead(chatRoomId, memberId); - } + private final AnonymousProfileRepository memberRepository; + private final AnonymousRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + + // 메시지 전송 + public MessageResponse send(MessageRequest request, Long id) { + AnonymousProfile sender = memberRepository.findById(id).orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); + + AnonymousRoom chatRoom = chatRoomRepository.findById(request.getChatRoomId()) + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + + ChatMessage chatMessage = ChatMessage.builder().room(chatRoom).sender(sender).content(request.getContent()) + .build(); + + chatMessageRepository.save(chatMessage); + + return MessageResponse.from(chatMessage); + } + + @Transactional + public void readMessages(Long chatRoomId, Long memberId) { + AnonymousRoom chatRoom = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + + // if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && + // !Objects.equals(chatRoom.getMember2().getId(), + // memberId)) { + // throw new BaseException(ChatExceptionType.CHAT_ROOM_ACCESS_DENIED); + // } + // messageReceiptRepository.bulkUpdateStatusToRead(chatRoomId, memberId); + } } diff --git a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java index 6b2923f..a8f4a9f 100644 --- a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java +++ b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java @@ -15,21 +15,21 @@ @RequestMapping("/anonymous-profiles") public class AnonymousProfileController { - private final AnonymousProfileService anonymousProfileService; + private final AnonymousProfileService anonymousProfileService; - @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") - @PostMapping("/rooms/{roomId}") - public ApiResponse createAnonymousProfile( - @PathVariable Long roomId, @RequestBody CreateAnonymousProfileRequest request) { - AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); - AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); - return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); - } + @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") + @PostMapping("/rooms/{roomId}") + public ApiResponse createAnonymousProfile(@PathVariable Long roomId, + @RequestBody CreateAnonymousProfileRequest request) { + AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); + AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); + } - @DeleteMapping("/{profileId}") - @Operation(summary = "익명 프로필 삭제", description = "ID로 특정 익명 프로필을 삭제합니다.") - public ApiResponse deleteAnonymousProfile(@PathVariable Long profileId) { - anonymousProfileService.deleteAnonymousProfile(profileId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + @DeleteMapping("/{profileId}") + @Operation(summary = "익명 프로필 삭제", description = "ID로 특정 익명 프로필을 삭제합니다.") + public ApiResponse deleteAnonymousProfile(@PathVariable Long profileId) { + anonymousProfileService.deleteAnonymousProfile(profileId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } } diff --git a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java index 95165ea..9bf377c 100644 --- a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java +++ b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java @@ -7,17 +7,14 @@ @Getter @Builder public class AnonymousProfileResponse { - private Long id; - private Long roomId; - private String nickname; + private Long id; + private Long roomId; + private String nickname; - // 필요한 필드만 + // 필요한 필드만 - public static AnonymousProfileResponse from(AnonymousProfile profile) { - return AnonymousProfileResponse.builder() - .id(profile.getId()) - .roomId(profile.getRoom().getId()) - .nickname(profile.getAnonymousName()) - .build(); - } + public static AnonymousProfileResponse from(AnonymousProfile profile) { + return AnonymousProfileResponse.builder().id(profile.getId()).roomId(profile.getRoom().getId()) + .nickname(profile.getAnonymousName()).build(); + } } diff --git a/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java index 9c43be9..4b06c9e 100644 --- a/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java +++ b/src/main/java/opensource/bravest/domain/profile/dto/CreateAnonymousProfileRequest.java @@ -6,6 +6,6 @@ @Getter @NoArgsConstructor public class CreateAnonymousProfileRequest { - private Long realUserId; - private String anonymousName; + private Long realUserId; + private String anonymousName; } diff --git a/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java b/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java index fe773e5..1c95fdd 100644 --- a/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java +++ b/src/main/java/opensource/bravest/domain/profile/entity/AnonymousProfile.java @@ -11,20 +11,20 @@ @Builder public class AnonymousProfile { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 어떤 방에 속한 익명 프로필인지 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + // 어떤 방에 속한 익명 프로필인지 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - // 실제 유저 PK (User 테이블 없다면 JWT의 userId 기준으로) - @Column(nullable = false) - private Long realUserId; + // 실제 유저 PK (User 테이블 없다면 JWT의 userId 기준으로) + @Column(nullable = false) + private Long realUserId; - // 방 안에서 보여줄 익명 닉네임 (예: BlueTiger12) - @Column(nullable = false, length = 50) - private String anonymousName; + // 방 안에서 보여줄 익명 닉네임 (예: BlueTiger12) + @Column(nullable = false, length = 50) + private String anonymousName; } diff --git a/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java b/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java index 88bbdbe..103c514 100644 --- a/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java +++ b/src/main/java/opensource/bravest/domain/profile/repository/AnonymousProfileRepository.java @@ -7,6 +7,6 @@ public interface AnonymousProfileRepository extends JpaRepository { - // 같은 방 + 같은 실제 유저라면 익명 프로필 하나만 사용 - Optional findByRoomAndRealUserId(AnonymousRoom room, Long realUserId); + // 같은 방 + 같은 실제 유저라면 익명 프로필 하나만 사용 + Optional findByRoomAndRealUserId(AnonymousRoom room, Long realUserId); } diff --git a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java index 0217c7d..8c03a24 100644 --- a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java +++ b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java @@ -15,39 +15,32 @@ @Transactional(readOnly = true) public class AnonymousProfileService { - private final AnonymousProfileRepository anonymousProfileRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - - @Transactional - public AnonymousProfile createAnonymousProfile( - Long roomId, CreateAnonymousProfileRequest request) { - AnonymousRoom room = - anonymousRoomRepository - .findById(roomId) - .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); - - // 중복 프로필 체크 - Optional existingProfile = - anonymousProfileRepository.findByRoomAndRealUserId(room, request.getRealUserId()); - if (existingProfile.isPresent()) { - throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); - } + private final AnonymousProfileRepository anonymousProfileRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + + @Transactional + public AnonymousProfile createAnonymousProfile(Long roomId, CreateAnonymousProfileRequest request) { + AnonymousRoom room = anonymousRoomRepository.findById(roomId) + .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); - AnonymousProfile newProfile = - AnonymousProfile.builder() - .room(room) - .realUserId(request.getRealUserId()) - .anonymousName(request.getAnonymousName()) - .build(); + // 중복 프로필 체크 + Optional existingProfile = anonymousProfileRepository.findByRoomAndRealUserId(room, + request.getRealUserId()); + if (existingProfile.isPresent()) { + throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); + } - return anonymousProfileRepository.save(newProfile); - } + AnonymousProfile newProfile = AnonymousProfile.builder().room(room).realUserId(request.getRealUserId()) + .anonymousName(request.getAnonymousName()).build(); + + return anonymousProfileRepository.save(newProfile); + } - @Transactional - public void deleteAnonymousProfile(Long profileId) { - if (!anonymousProfileRepository.existsById(profileId)) { - throw new RuntimeException("없는 사용자임. 너~ 누구야!"); + @Transactional + public void deleteAnonymousProfile(Long profileId) { + if (!anonymousProfileRepository.existsById(profileId)) { + throw new RuntimeException("없는 사용자임. 너~ 누구야!"); + } + anonymousProfileRepository.deleteById(profileId); } - anonymousProfileRepository.deleteById(profileId); - } } diff --git a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java index 3a12474..ea09dd7 100644 --- a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java +++ b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java @@ -14,81 +14,56 @@ @RequestMapping("/rooms") public class RoomController { - private final RoomService roomService; + private final RoomService roomService; - @PostMapping - @Operation(summary = "채팅방 생성", description = "새로운 채팅방을 생성합니다.") - public ApiResponse createRoom( - @RequestBody RoomDto.CreateRoomRequest request) { - AnonymousRoom room = roomService.createRoom(request); - return ApiResponse.of( - SuccessStatus._CREATED, - SuccessStatus._CREATED.getMessage(), - RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PostMapping + @Operation(summary = "채팅방 생성", description = "새로운 채팅방을 생성합니다.") + public ApiResponse createRoom(@RequestBody RoomDto.CreateRoomRequest request) { + AnonymousRoom room = roomService.createRoom(request); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) + .createdAt(room.getCreatedAt()).build()); + } - @GetMapping("/{roomId}") - @Operation(summary = "채팅방 조회", description = "ID로 특정 채팅방의 정보를 조회합니다.") - public ApiResponse getRoom(@PathVariable Long roomId) { - AnonymousRoom room = roomService.getRoom(roomId); - return ApiResponse.of( - SuccessStatus._OK, - SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @GetMapping("/{roomId}") + @Operation(summary = "채팅방 조회", description = "ID로 특정 채팅방의 정보를 조회합니다.") + public ApiResponse getRoom(@PathVariable Long roomId) { + AnonymousRoom room = roomService.getRoom(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) + .createdAt(room.getCreatedAt()).build()); + } - @PutMapping("/{roomId}") - @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") - public ApiResponse updateRoom( - @PathVariable Long roomId, @RequestBody RoomDto.UpdateRoomRequest request) { - AnonymousRoom room = roomService.updateRoom(roomId, request); - return ApiResponse.of( - SuccessStatus._OK, - SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PutMapping("/{roomId}") + @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") + public ApiResponse updateRoom(@PathVariable Long roomId, + @RequestBody RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = roomService.updateRoom(roomId, request); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) + .createdAt(room.getCreatedAt()).build()); + } - @DeleteMapping("/{roomId}") - @Operation(summary = "채팅방 삭제", description = "ID로 특정 채팅방을 삭제합니다.") - public ApiResponse deleteRoom(@PathVariable Long roomId) { - roomService.deleteRoom(roomId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + @DeleteMapping("/{roomId}") + @Operation(summary = "채팅방 삭제", description = "ID로 특정 채팅방을 삭제합니다.") + public ApiResponse deleteRoom(@PathVariable Long roomId) { + roomService.deleteRoom(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } - @GetMapping("/{roomId}/invite-code") - @Operation(summary = "초대 코드 조회", description = "ID로 특정 채팅방의 초대 코드를 조회합니다.") - public ApiResponse getInviteCode(@PathVariable Long roomId) { - String inviteCode = roomService.getInviteCode(roomId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), inviteCode); - } + @GetMapping("/{roomId}/invite-code") + @Operation(summary = "초대 코드 조회", description = "ID로 특정 채팅방의 초대 코드를 조회합니다.") + public ApiResponse getInviteCode(@PathVariable Long roomId) { + String inviteCode = roomService.getInviteCode(roomId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), inviteCode); + } - @PostMapping("/join") - @Operation(summary = "초대 코드로 채팅방 참여", description = "초대 코드를 사용하여 특정 채팅방에 참여합니다.") - public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { - AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); - return ApiResponse.of( - SuccessStatus._OK, - SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder() - .id(room.getId()) - .roomCode(room.getRoomCode()) - .title(room.getTitle()) - .createdAt(room.getCreatedAt()) - .build()); - } + @PostMapping("/join") + @Operation(summary = "초대 코드로 채팅방 참여", description = "초대 코드를 사용하여 특정 채팅방에 참여합니다.") + public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { + AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) + .createdAt(room.getCreatedAt()).build()); + } } diff --git a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java index 8655c0c..e0e02dc 100644 --- a/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java +++ b/src/main/java/opensource/bravest/domain/room/dto/RoomDto.java @@ -8,32 +8,32 @@ public class RoomDto { - @Getter - @NoArgsConstructor - public static class CreateRoomRequest { - private String title; - } + @Getter + @NoArgsConstructor + public static class CreateRoomRequest { + private String title; + } - @Getter - @NoArgsConstructor - public static class UpdateRoomRequest { - private String title; - } + @Getter + @NoArgsConstructor + public static class UpdateRoomRequest { + private String title; + } - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class RoomResponse { - private Long id; - private String roomCode; - private String title; - private LocalDateTime createdAt; - } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RoomResponse { + private Long id; + private String roomCode; + private String title; + private LocalDateTime createdAt; + } - @Getter - @NoArgsConstructor - public static class JoinRoomRequest { - private String roomCode; - } + @Getter + @NoArgsConstructor + public static class JoinRoomRequest { + private String roomCode; + } } diff --git a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java index 3be35e9..ce9b74e 100644 --- a/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java +++ b/src/main/java/opensource/bravest/domain/room/entity/AnonymousRoom.java @@ -14,25 +14,25 @@ @Builder public class AnonymousRoom { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - // 친구들에게 공유하는 코드 (예: ABC123) - @Column(nullable = false, unique = true, length = 20) - private String roomCode; + // 친구들에게 공유하는 코드 (예: ABC123) + @Column(nullable = false, unique = true, length = 20) + private String roomCode; - // 방 제목 (선택) - @Column(nullable = false, length = 100) - private String title; + // 방 제목 (선택) + @Column(nullable = false, length = 100) + private String title; - @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default - private List profiles = new ArrayList<>(); + @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List profiles = new ArrayList<>(); - private LocalDateTime createdAt; + private LocalDateTime createdAt; - public void updateTitle(String title) { - this.title = title; - } + public void updateTitle(String title) { + this.title = title; + } } diff --git a/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java b/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java index a900571..d58c838 100644 --- a/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java +++ b/src/main/java/opensource/bravest/domain/room/repository/AnonymousRoomRepository.java @@ -6,7 +6,7 @@ public interface AnonymousRoomRepository extends JpaRepository { - Optional findByRoomCode(String roomCode); + Optional findByRoomCode(String roomCode); - boolean existsByRoomCode(String roomCode); + boolean existsByRoomCode(String roomCode); } diff --git a/src/main/java/opensource/bravest/domain/room/service/RoomService.java b/src/main/java/opensource/bravest/domain/room/service/RoomService.java index 21d1c2c..13e90b5 100644 --- a/src/main/java/opensource/bravest/domain/room/service/RoomService.java +++ b/src/main/java/opensource/bravest/domain/room/service/RoomService.java @@ -14,57 +14,50 @@ @Transactional(readOnly = true) public class RoomService { - private final AnonymousRoomRepository anonymousRoomRepository; - - @Transactional - public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { - String roomCode = generateUniqueRoomCode(); - AnonymousRoom room = - AnonymousRoom.builder() - .title(request.getTitle()) - .roomCode(roomCode) - .createdAt(LocalDateTime.now()) - .build(); - return anonymousRoomRepository.save(room); - } + private final AnonymousRoomRepository anonymousRoomRepository; + + @Transactional + public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { + String roomCode = generateUniqueRoomCode(); + AnonymousRoom room = AnonymousRoom.builder().title(request.getTitle()).roomCode(roomCode) + .createdAt(LocalDateTime.now()).build(); + return anonymousRoomRepository.save(room); + } - public AnonymousRoom getRoom(Long roomId) { - return anonymousRoomRepository - .findById(roomId) - .orElseThrow(() -> new RuntimeException("Room not found")); - } + public AnonymousRoom getRoom(Long roomId) { + return anonymousRoomRepository.findById(roomId).orElseThrow(() -> new RuntimeException("Room not found")); + } - @Transactional - public AnonymousRoom updateRoom(Long roomId, RoomDto.UpdateRoomRequest request) { - AnonymousRoom room = getRoom(roomId); - room.updateTitle(request.getTitle()); - return room; - } + @Transactional + public AnonymousRoom updateRoom(Long roomId, RoomDto.UpdateRoomRequest request) { + AnonymousRoom room = getRoom(roomId); + room.updateTitle(request.getTitle()); + return room; + } - @Transactional - public void deleteRoom(Long roomId) { - if (!anonymousRoomRepository.existsById(roomId)) { - throw new RuntimeException("Room not found"); + @Transactional + public void deleteRoom(Long roomId) { + if (!anonymousRoomRepository.existsById(roomId)) { + throw new RuntimeException("Room not found"); + } + anonymousRoomRepository.deleteById(roomId); } - anonymousRoomRepository.deleteById(roomId); - } - public String getInviteCode(Long roomId) { - AnonymousRoom room = getRoom(roomId); - return room.getRoomCode(); - } + public String getInviteCode(Long roomId) { + AnonymousRoom room = getRoom(roomId); + return room.getRoomCode(); + } - public AnonymousRoom joinRoom(String roomCode) { - return anonymousRoomRepository - .findByRoomCode(roomCode) - .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); - } + public AnonymousRoom joinRoom(String roomCode) { + return anonymousRoomRepository.findByRoomCode(roomCode) + .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); + } - private String generateUniqueRoomCode() { - String roomCode; - do { - roomCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); - } while (anonymousRoomRepository.existsByRoomCode(roomCode)); - return roomCode; - } + private String generateUniqueRoomCode() { + String roomCode; + do { + roomCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); + } while (anonymousRoomRepository.existsByRoomCode(roomCode)); + return roomCode; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java index e46aca8..bf40633 100644 --- a/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java +++ b/src/main/java/opensource/bravest/domain/vote/controller/VoteController.java @@ -14,51 +14,49 @@ @RequestMapping("/votes") public class VoteController { - private final VoteService voteService; - - @PostMapping - @Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다.") - public ApiResponse createVote( - @RequestBody VoteDto.CreateVoteRequest request) { - Vote vote = voteService.createVote(request); - // The response DTO needs to be built manually - VoteDto.VoteResponse responseDto = voteService.getVoteResult(vote.getId()); - return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), responseDto); - } - - @GetMapping("/{voteId}") - @Operation(summary = "투표 조회", description = "ID로 특정 투표의 정보를 조회합니다.") - public ApiResponse getVote(@PathVariable Long voteId) { - VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); - } - - @PostMapping("/{voteId}/cast") - @Operation(summary = "투표 참여", description = "특정 투표 항목에 투표합니다.") - public ApiResponse castVote( - @PathVariable Long voteId, @RequestBody VoteDto.CastVoteRequest request) { - voteService.castVote(voteId, request); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } - - @PostMapping("/{voteId}/end") - @Operation(summary = "투표 종료", description = "특정 투표를 종료합니다.") - public ApiResponse endVote(@PathVariable Long voteId) { - voteService.endVote(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } - - @GetMapping("/{voteId}/result") - @Operation(summary = "투표 결과 조회", description = "종료된 투표의 결과를 조회합니다.") - public ApiResponse getVoteResult(@PathVariable Long voteId) { - VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); - } - - @DeleteMapping("/{voteId}") - @Operation(summary = "투표 삭제", description = "ID로 특정 투표를 삭제합니다.") - public ApiResponse deleteVote(@PathVariable Long voteId) { - voteService.deleteVote(voteId); - return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); - } + private final VoteService voteService; + + @PostMapping + @Operation(summary = "투표 생성", description = "새로운 투표를 생성합니다.") + public ApiResponse createVote(@RequestBody VoteDto.CreateVoteRequest request) { + Vote vote = voteService.createVote(request); + // The response DTO needs to be built manually + VoteDto.VoteResponse responseDto = voteService.getVoteResult(vote.getId()); + return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), responseDto); + } + + @GetMapping("/{voteId}") + @Operation(summary = "투표 조회", description = "ID로 특정 투표의 정보를 조회합니다.") + public ApiResponse getVote(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @PostMapping("/{voteId}/cast") + @Operation(summary = "투표 참여", description = "특정 투표 항목에 투표합니다.") + public ApiResponse castVote(@PathVariable Long voteId, @RequestBody VoteDto.CastVoteRequest request) { + voteService.castVote(voteId, request); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @PostMapping("/{voteId}/end") + @Operation(summary = "투표 종료", description = "특정 투표를 종료합니다.") + public ApiResponse endVote(@PathVariable Long voteId) { + voteService.endVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } + + @GetMapping("/{voteId}/result") + @Operation(summary = "투표 결과 조회", description = "종료된 투표의 결과를 조회합니다.") + public ApiResponse getVoteResult(@PathVariable Long voteId) { + VoteDto.VoteResponse responseDto = voteService.getVoteResult(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), responseDto); + } + + @DeleteMapping("/{voteId}") + @Operation(summary = "투표 삭제", description = "ID로 특정 투표를 삭제합니다.") + public ApiResponse deleteVote(@PathVariable Long voteId) { + voteService.deleteVote(voteId); + return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), null); + } } diff --git a/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java index 83cd3dd..7c31382 100644 --- a/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java +++ b/src/main/java/opensource/bravest/domain/vote/dto/VoteDto.java @@ -8,35 +8,35 @@ public class VoteDto { - @Getter - @NoArgsConstructor - public static class CreateVoteRequest { - private Long roomId; - private List messages; - } + @Getter + @NoArgsConstructor + public static class CreateVoteRequest { + private Long roomId; + private List messages; + } - @Getter - @NoArgsConstructor - public static class CastVoteRequest { - private Long voteOptionId; - private Long anonymousProfileId; - } + @Getter + @NoArgsConstructor + public static class CastVoteRequest { + private Long voteOptionId; + private Long anonymousProfileId; + } - @Getter - @Builder - public static class VoteResponse { - private Long id; - private String title; - private boolean isActive; - private LocalDateTime createdAt; - private List options; - } + @Getter + @Builder + public static class VoteResponse { + private Long id; + private String title; + private boolean isActive; + private LocalDateTime createdAt; + private List options; + } - @Getter - @Builder - public static class VoteOptionResponse { - private Long id; - private String messageContent; - private int voteCount; - } + @Getter + @Builder + public static class VoteOptionResponse { + private Long id; + private String messageContent; + private int voteCount; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java index f88a73b..21c4b9e 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/UserVote.java @@ -11,19 +11,19 @@ @Builder public class UserVote { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_id", nullable = false) - private Vote vote; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_option_id", nullable = false) - private VoteOption voteOption; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_option_id", nullable = false) + private VoteOption voteOption; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "anonymous_profile_id", nullable = false) - private AnonymousProfile voter; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "anonymous_profile_id", nullable = false) + private AnonymousProfile voter; } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/Vote.java b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java index 3f2df78..cb115db 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/Vote.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/Vote.java @@ -14,26 +14,26 @@ @Builder public class Vote { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private AnonymousRoom room; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private AnonymousRoom room; - @Column(nullable = false, length = 100) - private String title; + @Column(nullable = false, length = 100) + private String title; - @Builder.Default - @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true) - private List options = new ArrayList<>(); + @Builder.Default + @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true) + private List options = new ArrayList<>(); - private boolean isActive; + private boolean isActive; - private LocalDateTime createdAt; + private LocalDateTime createdAt; - public void endVote() { - this.isActive = false; - } + public void endVote() { + this.isActive = false; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java index 70f03c7..50a9ea0 100644 --- a/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java +++ b/src/main/java/opensource/bravest/domain/vote/entity/VoteOption.java @@ -10,21 +10,21 @@ @Builder public class VoteOption { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_id", nullable = false) - private Vote vote; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "vote_id", nullable = false) + private Vote vote; - @Column(name = "message_content", nullable = false) - private String messageContent; + @Column(name = "message_content", nullable = false) + private String messageContent; - @Column(nullable = false) - private int voteCount; + @Column(nullable = false) + private int voteCount; - public void incrementVoteCount() { - this.voteCount++; - } + public void incrementVoteCount() { + this.voteCount++; + } } diff --git a/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java index 7fe9954..9ead3ee 100644 --- a/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java +++ b/src/main/java/opensource/bravest/domain/vote/repository/UserVoteRepository.java @@ -7,5 +7,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface UserVoteRepository extends JpaRepository { - Optional findByVoteAndVoter(Vote vote, AnonymousProfile voter); + Optional findByVoteAndVoter(Vote vote, AnonymousProfile voter); } diff --git a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java index 39224b9..145b5df 100644 --- a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java +++ b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java @@ -22,107 +22,79 @@ @Transactional(readOnly = true) public class VoteService { - private final VoteRepository voteRepository; - private final UserVoteRepository userVoteRepository; - private final AnonymousRoomRepository anonymousRoomRepository; - private final AnonymousProfileRepository anonymousProfileRepository; - - @Transactional - public Vote createVote(VoteDto.CreateVoteRequest request) { - AnonymousRoom room = - anonymousRoomRepository - .findById(request.getRoomId()) - .orElseThrow(() -> new RuntimeException("Room not found")); - - Vote vote = - Vote.builder() - .room(room) - .title(room.getTitle()) - .isActive(true) - .createdAt(LocalDateTime.now()) - .build(); - - List options = - request.getMessages().stream() - .map( - message -> - VoteOption.builder().vote(vote).messageContent(message).voteCount(0).build()) - .collect(Collectors.toList()); - - vote.getOptions().addAll(options); - - return voteRepository.save(vote); - } - - @Transactional - public void castVote(Long voteId, VoteDto.CastVoteRequest request) { - Vote vote = - voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); - if (!vote.isActive()) { - throw new RuntimeException("Vote is not active"); - } + private final VoteRepository voteRepository; + private final UserVoteRepository userVoteRepository; + private final AnonymousRoomRepository anonymousRoomRepository; + private final AnonymousProfileRepository anonymousProfileRepository; + + @Transactional + public Vote createVote(VoteDto.CreateVoteRequest request) { + AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) + .orElseThrow(() -> new RuntimeException("Room not found")); + + Vote vote = Vote.builder().room(room).title(room.getTitle()).isActive(true).createdAt(LocalDateTime.now()) + .build(); + + List options = request.getMessages().stream() + .map(message -> VoteOption.builder().vote(vote).messageContent(message).voteCount(0).build()) + .collect(Collectors.toList()); - AnonymousProfile voter = - anonymousProfileRepository - .findById(request.getAnonymousProfileId()) - .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); + vote.getOptions().addAll(options); - if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { - throw new RuntimeException("User has already voted"); + return voteRepository.save(vote); } - VoteOption voteOption = - vote.getOptions().stream() - .filter(option -> option.getId().equals(request.getVoteOptionId())) - .findFirst() - .orElseThrow(() -> new RuntimeException("VoteOption not found")); + @Transactional + public void castVote(Long voteId, VoteDto.CastVoteRequest request) { + Vote vote = voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + if (!vote.isActive()) { + throw new RuntimeException("Vote is not active"); + } - voteOption.incrementVoteCount(); + AnonymousProfile voter = anonymousProfileRepository.findById(request.getAnonymousProfileId()) + .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); - UserVote userVote = UserVote.builder().vote(vote).voteOption(voteOption).voter(voter).build(); - userVoteRepository.save(userVote); - } + if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { + throw new RuntimeException("User has already voted"); + } - @Transactional - public void endVote(Long voteId) { - Vote vote = - voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); - vote.endVote(); - } + VoteOption voteOption = vote.getOptions().stream() + .filter(option -> option.getId().equals(request.getVoteOptionId())).findFirst() + .orElseThrow(() -> new RuntimeException("VoteOption not found")); + + voteOption.incrementVoteCount(); + + UserVote userVote = UserVote.builder().vote(vote).voteOption(voteOption).voter(voter).build(); + userVoteRepository.save(userVote); + } - public VoteDto.VoteResponse getVoteResult(Long voteId) { - Vote vote = - voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + @Transactional + public void endVote(Long voteId) { + Vote vote = voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + vote.endVote(); + } + + public VoteDto.VoteResponse getVoteResult(Long voteId) { + Vote vote = voteRepository.findById(voteId).orElseThrow(() -> new RuntimeException("Vote not found")); + + return buildVoteResponse(vote); + } + + @Transactional + public void deleteVote(Long voteId) { + if (!voteRepository.existsById(voteId)) { + throw new RuntimeException("Vote not found"); + } + voteRepository.deleteById(voteId); + } - return buildVoteResponse(vote); - } + private VoteDto.VoteResponse buildVoteResponse(Vote vote) { + List optionResponses = vote.getOptions().stream() + .map(option -> VoteDto.VoteOptionResponse.builder().id(option.getId()) + .messageContent(option.getMessageContent()).voteCount(option.getVoteCount()).build()) + .collect(Collectors.toList()); - @Transactional - public void deleteVote(Long voteId) { - if (!voteRepository.existsById(voteId)) { - throw new RuntimeException("Vote not found"); + return VoteDto.VoteResponse.builder().id(vote.getId()).title(vote.getTitle()).isActive(vote.isActive()) + .createdAt(vote.getCreatedAt()).options(optionResponses).build(); } - voteRepository.deleteById(voteId); - } - - private VoteDto.VoteResponse buildVoteResponse(Vote vote) { - List optionResponses = - vote.getOptions().stream() - .map( - option -> - VoteDto.VoteOptionResponse.builder() - .id(option.getId()) - .messageContent(option.getMessageContent()) - .voteCount(option.getVoteCount()) - .build()) - .collect(Collectors.toList()); - - return VoteDto.VoteResponse.builder() - .id(vote.getId()) - .title(vote.getTitle()) - .isActive(vote.isActive()) - .createdAt(vote.getCreatedAt()) - .options(optionResponses) - .build(); - } } diff --git a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java index b577884..8b479a6 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java +++ b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java @@ -14,31 +14,30 @@ @AllArgsConstructor @JsonPropertyOrder({"isSuccess", "code", "message", "data"}) public class ApiResponse { - @JsonProperty("isSuccess") - private final boolean isSuccess; + @JsonProperty("isSuccess") + private final boolean isSuccess; - private final String code; - private final String message; + private final String code; + private final String message; - @JsonInclude(JsonInclude.Include.NON_NULL) - private T data; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T data; - public static ApiResponse onSuccess(T data) { - return new ApiResponse<>( - true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), data); - } + public static ApiResponse onSuccess(T data) { + return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), data); + } - public static ApiResponse of(BaseCode code, String message, T data) { - return new ApiResponse<>( - true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), data); - } + public static ApiResponse of(BaseCode code, String message, T data) { + return new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), + data); + } - public static ApiResponse onFailure(BaseErrorCode errorCode, T data) { - ErrorReasonDto reason = errorCode.getReasonHttpStatus(); - return new ApiResponse<>(reason.getIsSuccess(), reason.getCode(), reason.getMessage(), data); - } + public static ApiResponse onFailure(BaseErrorCode errorCode, T data) { + ErrorReasonDto reason = errorCode.getReasonHttpStatus(); + return new ApiResponse<>(reason.getIsSuccess(), reason.getCode(), reason.getMessage(), data); + } - public static ApiResponse onFailure(String code, String message, T data) { - return new ApiResponse<>(false, code, message, data); - } + public static ApiResponse onFailure(String code, String message, T data) { + return new ApiResponse<>(false, code, message, data); + } } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java b/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java index 00f3dd4..81444ee 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/BaseCode.java @@ -1,7 +1,7 @@ package opensource.bravest.global.apiPayload.code; public interface BaseCode { - ReasonDto getReason(); + ReasonDto getReason(); - ReasonDto getReasonHttpStatus(); + ReasonDto getReasonHttpStatus(); } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java b/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java index 6f514a7..6607ad0 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/BaseErrorCode.java @@ -1,7 +1,7 @@ package opensource.bravest.global.apiPayload.code; public interface BaseErrorCode { - ErrorReasonDto getReason(); + ErrorReasonDto getReason(); - ErrorReasonDto getReasonHttpStatus(); + ErrorReasonDto getReasonHttpStatus(); } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/ErrorReasonDto.java b/src/main/java/opensource/bravest/global/apiPayload/code/ErrorReasonDto.java index bda7a57..2172a3c 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/ErrorReasonDto.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/ErrorReasonDto.java @@ -7,8 +7,8 @@ @Getter @Builder public class ErrorReasonDto { - private HttpStatus httpStatus; - private final Boolean isSuccess; - private final String message; - private final String code; + private HttpStatus httpStatus; + private final Boolean isSuccess; + private final String message; + private final String code; } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/ReasonDto.java b/src/main/java/opensource/bravest/global/apiPayload/code/ReasonDto.java index df56f57..e8ba0a0 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/ReasonDto.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/ReasonDto.java @@ -7,8 +7,8 @@ @Getter @Builder public class ReasonDto { - private HttpStatus httpStatus; - private final Boolean isSuccess; - private final String code; - private final String message; + private HttpStatus httpStatus; + private final Boolean isSuccess; + private final String code; + private final String message; } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java index b0ae3d4..18831ea 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java @@ -9,33 +9,27 @@ @Getter @AllArgsConstructor public enum ErrorStatus implements BaseErrorCode { - _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), - _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), - _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), - _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), - _NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "요청한 리소스를 찾을 수 없습니다."), - _FAMILY_NOT_FOUND(HttpStatus.NOT_FOUND, "FAMILY404", "유효하지 않은 초대 코드입니다."), - _USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "사용자를 찾을 수 없습니다."), - _CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "채팅방을 찾을 수 없습니다."), - _CHATLIST_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "리스트를 찾을 수 없습니다."), - ; + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), _BAD_REQUEST( + HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", + "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), _NOT_FOUND( + HttpStatus.NOT_FOUND, "COMMON404", + "요청한 리소스를 찾을 수 없습니다."), _FAMILY_NOT_FOUND(HttpStatus.NOT_FOUND, "FAMILY404", + "유효하지 않은 초대 코드입니다."), _USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", + "사용자를 찾을 수 없습니다."), _CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", + "채팅방을 찾을 수 없습니다."), _CHATLIST_NOT_FOUND(HttpStatus.NOT_FOUND, + "USER404", "리스트를 찾을 수 없습니다."),; - private final HttpStatus httpStatus; - private final String code; - private final String message; + private final HttpStatus httpStatus; + private final String code; + private final String message; - @Override - public ErrorReasonDto getReason() { - return ErrorReasonDto.builder().isSuccess(false).message(message).code(code).build(); - } + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder().isSuccess(false).message(message).code(code).build(); + } - @Override - public ErrorReasonDto getReasonHttpStatus() { - return ErrorReasonDto.builder() - .httpStatus(httpStatus) - .isSuccess(false) - .code(code) - .message(message) - .build(); - } + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder().httpStatus(httpStatus).isSuccess(false).code(code).message(message).build(); + } } diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java index 7845515..25d02ed 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/SuccessStatus.java @@ -9,25 +9,18 @@ @Getter @AllArgsConstructor public enum SuccessStatus implements BaseCode { - _OK(HttpStatus.OK, "COMMON2000", "성공입니다."), - _CREATED(HttpStatus.CREATED, "COMMON201", "생성되었습니다."), - ; - private final HttpStatus httpStatus; - private final String code; - private final String message; + _OK(HttpStatus.OK, "COMMON2000", "성공입니다."), _CREATED(HttpStatus.CREATED, "COMMON201", "생성되었습니다."),; + private final HttpStatus httpStatus; + private final String code; + private final String message; - @Override - public ReasonDto getReason() { - return ReasonDto.builder().isSuccess(true).message(message).code(code).build(); - } + @Override + public ReasonDto getReason() { + return ReasonDto.builder().isSuccess(true).message(message).code(code).build(); + } - @Override - public ReasonDto getReasonHttpStatus() { - return ReasonDto.builder() - .httpStatus(httpStatus) - .isSuccess(true) - .code(code) - .message(message) - .build(); - } + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder().httpStatus(httpStatus).isSuccess(true).code(code).message(message).build(); + } } diff --git a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java index 25c4cff..f6a1b36 100644 --- a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java +++ b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java @@ -13,29 +13,19 @@ @Configuration public class OpenApiConfig { - private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; - @Bean - public OpenAPI baseOpenAPI() { - return new OpenAPI() - // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 - .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) - // 2) JWT Bearer 스키마 정의 - .components( - new Components() - .addSecuritySchemes( - SECURITY_SCHEME_NAME, - new SecurityScheme() - .name(SECURITY_SCHEME_NAME) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT"))) - .info( - new Info() - .title("openSource Bravest API") - .description("openSource Bravest 백엔드 API 문서") - .version("v1.0.0") - .license(new License().name("MIT"))) - .externalDocs(new ExternalDocumentation().description("README")); - } + @Bean + public OpenAPI baseOpenAPI() { + return new OpenAPI() + // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + // 2) JWT Bearer 스키마 정의 + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme().name(SECURITY_SCHEME_NAME).type(SecurityScheme.Type.HTTP).scheme("bearer") + .bearerFormat("JWT"))) + .info(new Info().title("openSource Bravest API").description("openSource Bravest 백엔드 API 문서") + .version("v1.0.0").license(new License().name("MIT"))) + .externalDocs(new ExternalDocumentation().description("README")); + } } diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index 35abcef..c37b2c8 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -25,126 +25,91 @@ @RequiredArgsConstructor public class SecurityConfig { - private final JwtTokenProvider jwtTokenProvider; - - // Swagger - private static final String[] SWAGGER = {"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"}; - - // 로그인/토큰 교환/리다이렉트/헬스체크 등 공개 경로 - private static final String[] PUBLIC = { - "/", - "/actuator/health", - "/api/auth/**", // 카카오 코드 교환 API 등 - "/oauth2/**", - "/login/**", - "/login/oauth2/**", - "/api/test/auth/**", - "/rooms/**", - "/chatlists/**", - "/anonymous-profiles/**", - "/votes/**", - "/ws-connect/**", - "/chat-test", - "/pub/**", - "/sub/**" - }; - - // 정적 리소스 - private static final String[] STATIC = { - "/favicon.ico", "/assets/**", "/css/**", "/js/**", "/images/**" - }; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // Jwt 필터에서 건너뛸(스킵) 경로 패턴 통합 - List skip = new ArrayList<>(); - addAll(skip, SWAGGER); - addAll(skip, PUBLIC); - addAll(skip, STATIC); - - JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtTokenProvider, skip); - - http - // REST API 기본 세팅 - .csrf(csrf -> csrf.disable()) - .cors(Customizer.withDefaults()) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .httpBasic(basic -> basic.disable()) - .formLogin(form -> form.disable()) - .logout(lo -> lo.disable()) - .requestCache(cache -> cache.disable()) - - // 권한 규칙 - .authorizeHttpRequests( - auth -> - auth.requestMatchers(HttpMethod.OPTIONS, "/**") - .permitAll() // CORS preflight 허용 - .requestMatchers(SWAGGER) - .permitAll() - .requestMatchers(PUBLIC) - .permitAll() - .requestMatchers(STATIC) - .permitAll() - .anyRequest() - .authenticated()) - - // 인증/인가 실패 공통 응답(JSON) - ApiResponse 형식 - .exceptionHandling( - ex -> - ex.authenticationEntryPoint( - (req, res, ex1) -> { - ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write( - String.format( - "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), errorStatus.getMessage())); - } - }) - .accessDeniedHandler( - (req, res, ex2) -> { - ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write( - String.format( - "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), errorStatus.getMessage())); - } - })) - - // JWT 필터 등록(UsernamePasswordAuthenticationFilter 앞) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - private static void addAll(List target, String[] arr) { - for (String s : arr) target.add(s); - } - - // CORS (개발용: 필요 시 도메인 고정/축소) - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration c = new CorsConfiguration(); - c.setAllowedOrigins( - List.of("http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173")); - c.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - c.setAllowedHeaders(List.of("*")); - c.setExposedHeaders(List.of("Authorization", "Location")); - c.setAllowCredentials(true); - c.setMaxAge(3600L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", c); - return source; - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + private final JwtTokenProvider jwtTokenProvider; + + // Swagger + private static final String[] SWAGGER = {"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"}; + + // 로그인/토큰 교환/리다이렉트/헬스체크 등 공개 경로 + private static final String[] PUBLIC = {"/", "/actuator/health", "/api/auth/**", // 카카오 코드 교환 API 등 + "/oauth2/**", "/login/**", "/login/oauth2/**", "/api/test/auth/**", "/rooms/**", "/chatlists/**", + "/anonymous-profiles/**", "/votes/**", "/ws-connect/**", "/chat-test", "/pub/**", "/sub/**"}; + + // 정적 리소스 + private static final String[] STATIC = {"/favicon.ico", "/assets/**", "/css/**", "/js/**", "/images/**"}; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // Jwt 필터에서 건너뛸(스킵) 경로 패턴 통합 + List skip = new ArrayList<>(); + addAll(skip, SWAGGER); + addAll(skip, PUBLIC); + addAll(skip, STATIC); + + JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtTokenProvider, skip); + + http + // REST API 기본 세팅 + .csrf(csrf -> csrf.disable()).cors(Customizer.withDefaults()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .httpBasic(basic -> basic.disable()).formLogin(form -> form.disable()).logout(lo -> lo.disable()) + .requestCache(cache -> cache.disable()) + + // 권한 규칙 + .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // CORS + // preflight + // 허용 + .requestMatchers(SWAGGER).permitAll().requestMatchers(PUBLIC).permitAll() + .requestMatchers(STATIC).permitAll().anyRequest().authenticated()) + + // 인증/인가 실패 공통 응답(JSON) - ApiResponse 형식 + .exceptionHandling(ex -> ex.authenticationEntryPoint((req, res, ex1) -> { + ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write(String.format("{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + }).accessDeniedHandler((req, res, ex2) -> { + ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write(String.format("{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + })) + + // JWT 필터 등록(UsernamePasswordAuthenticationFilter 앞) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + private static void addAll(List target, String[] arr) { + for (String s : arr) + target.add(s); + } + + // CORS (개발용: 필요 시 도메인 고정/축소) + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration c = new CorsConfiguration(); + c.setAllowedOrigins(List.of("http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173")); + c.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + c.setAllowedHeaders(List.of("*")); + c.setExposedHeaders(List.of("Authorization", "Location")); + c.setAllowCredentials(true); + c.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", c); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/src/main/java/opensource/bravest/global/config/ValkeyConfig.java b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java index be6c578..2be6ffa 100644 --- a/src/main/java/opensource/bravest/global/config/ValkeyConfig.java +++ b/src/main/java/opensource/bravest/global/config/ValkeyConfig.java @@ -8,8 +8,8 @@ @Configuration public class ValkeyConfig { - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); - } + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } } diff --git a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java index 177db70..449d017 100644 --- a/src/main/java/opensource/bravest/global/config/WebSocketConfig.java +++ b/src/main/java/opensource/bravest/global/config/WebSocketConfig.java @@ -14,21 +14,21 @@ @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final StompHandler stompHandler; + private final StompHandler stompHandler; - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws-connect").setAllowedOriginPatterns("*").withSockJS(); - } + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws-connect").setAllowedOriginPatterns("*").withSockJS(); + } - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/subs"); - registry.setApplicationDestinationPrefixes("/pubs"); - } + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/subs"); + registry.setApplicationDestinationPrefixes("/pubs"); + } - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(stompHandler); - } + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } } diff --git a/src/main/java/opensource/bravest/global/exception/CustomException.java b/src/main/java/opensource/bravest/global/exception/CustomException.java index 3fb913e..1b8ab2e 100644 --- a/src/main/java/opensource/bravest/global/exception/CustomException.java +++ b/src/main/java/opensource/bravest/global/exception/CustomException.java @@ -7,10 +7,10 @@ @Getter public class CustomException extends RuntimeException { - private final BaseErrorCode errorCode; + private final BaseErrorCode errorCode; - public CustomException(BaseErrorCode errorCode) { - super(errorCode.getReason().getMessage()); - this.errorCode = errorCode; - } + public CustomException(BaseErrorCode errorCode) { + super(errorCode.getReason().getMessage()); + this.errorCode = errorCode; + } } diff --git a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java index 6faa479..b0bbb26 100644 --- a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java @@ -11,46 +11,42 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(CustomException.class) - public ResponseEntity> handleCustomException(CustomException e) { - log.warn("CustomException: {}", e.getMessage()); - return ResponseEntity.status(e.getErrorCode().getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(e.getErrorCode(), null)); - } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity> handleRuntimeException(RuntimeException e) { - String message = e.getMessage(); - - // 메시지에 따라 적절한 에러 코드 결정 - if (message != null) { - if (message.contains("유효하지 않은 초대 코드") || message.contains("가족을 찾을 수 없습니다")) { - log.warn("Family not found: {}", message); - return ResponseEntity.status( - ErrorStatus._FAMILY_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._FAMILY_NOT_FOUND, null)); - } - - if (message.contains("사용자를 찾을 수 없습니다")) { - log.warn("User not found: {}", message); - return ResponseEntity.status( - ErrorStatus._USER_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._USER_NOT_FOUND, null)); - } + @ExceptionHandler(CustomException.class) + public ResponseEntity> handleCustomException(CustomException e) { + log.warn("CustomException: {}", e.getMessage()); + return ResponseEntity.status(e.getErrorCode().getReasonHttpStatus().getHttpStatus()) + .body(ApiResponse.onFailure(e.getErrorCode(), null)); } - // 기본값: 500 Internal Server Error - log.error("RuntimeException: ", e); - return ResponseEntity.status( - ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception e) { - log.error("Unexpected exception: ", e); - return ResponseEntity.status( - ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); - } + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException e) { + String message = e.getMessage(); + + // 메시지에 따라 적절한 에러 코드 결정 + if (message != null) { + if (message.contains("유효하지 않은 초대 코드") || message.contains("가족을 찾을 수 없습니다")) { + log.warn("Family not found: {}", message); + return ResponseEntity.status(ErrorStatus._FAMILY_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) + .body(ApiResponse.onFailure(ErrorStatus._FAMILY_NOT_FOUND, null)); + } + + if (message.contains("사용자를 찾을 수 없습니다")) { + log.warn("User not found: {}", message); + return ResponseEntity.status(ErrorStatus._USER_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) + .body(ApiResponse.onFailure(ErrorStatus._USER_NOT_FOUND, null)); + } + } + + // 기본값: 500 Internal Server Error + log.error("RuntimeException: ", e); + return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) + .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + log.error("Unexpected exception: ", e); + return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) + .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); + } } diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index d7ec8d8..8d10752 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -23,136 +23,120 @@ @RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { - private final AnonymousProfileRepository anonymousProfileRepository; - private final StringRedisTemplate redisTemplate; + private final AnonymousProfileRepository anonymousProfileRepository; + private final StringRedisTemplate redisTemplate; - private static final String USER_SUB_KEY_PREFIX = "ws:subs:user:"; // + anonymousId - private static final String METRIC_TOTAL_SUB = "ws:metrics:sub:total"; - private static final String METRIC_DUP_SUB = "ws:metrics:sub:duplicate"; + private static final String USER_SUB_KEY_PREFIX = "ws:subs:user:"; // + anonymousId + private static final String METRIC_TOTAL_SUB = "ws:metrics:sub:total"; + private static final String METRIC_DUP_SUB = "ws:metrics:sub:duplicate"; - @Override - public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = - MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - if (accessor == null) { - return message; - } + if (accessor == null) { + return message; + } + + StompCommand command = accessor.getCommand(); - StompCommand command = accessor.getCommand(); - - // 1) CONNECT: anonymousId를 Principal로 설정 - if (StompCommand.CONNECT.equals(command)) { - String anonymousId = accessor.getFirstNativeHeader("anonymousId"); - if (anonymousId == null || anonymousId.isBlank()) { - log.warn("STOMP CONNECT: anonymousId missing"); - throw new IllegalArgumentException("anonymousId header is required"); - } - - anonymousProfileRepository - .findById(Long.valueOf(anonymousId)) - .ifPresentOrElse( - member -> { - Authentication auth = - new UsernamePasswordAuthenticationToken( - anonymousId, null, List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); + // 1) CONNECT: anonymousId를 Principal로 설정 + if (StompCommand.CONNECT.equals(command)) { + String anonymousId = accessor.getFirstNativeHeader("anonymousId"); + if (anonymousId == null || anonymousId.isBlank()) { + log.warn("STOMP CONNECT: anonymousId missing"); + throw new IllegalArgumentException("anonymousId header is required"); + } + + anonymousProfileRepository.findById(Long.valueOf(anonymousId)).ifPresentOrElse(member -> { + Authentication auth = new UsernamePasswordAuthenticationToken(anonymousId, null, + List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); SecurityContextHolder.getContext().setAuthentication(auth); accessor.setUser(auth); log.info("STOMP CONNECT: anonymousId={} principal set", anonymousId); - }, - () -> { + }, () -> { log.warn("STOMP CONNECT: invalid anonymousId={}", anonymousId); throw new IllegalArgumentException("Invalid anonymousId"); - }); - } - - // 2) SUBSCRIBE: Redis를 사용해 anonymousId 기준 중복 구독 방지 + 메트릭 기록 - if (StompCommand.SUBSCRIBE.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - accessor.setUser(auth); - user = auth; + }); } - } - - String destination = accessor.getDestination(); - - if (user != null && destination != null) { - String anonymousId = user.getName(); - String key = USER_SUB_KEY_PREFIX + anonymousId; - - log.info( - "[SUBSCRIBE] handling: anonymousId={}, destination={}, key={}", - anonymousId, - destination, - key); - try { - Long total = redisTemplate.opsForValue().increment(METRIC_TOTAL_SUB); - Long added = redisTemplate.opsForSet().add(key, destination); - redisTemplate.expire(key, java.time.Duration.ofHours(1)); - - log.info("[SUBSCRIBE] redis result: total={}, added={}", total, added); - - if (added != null && added == 0L) { - Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); - log.warn( - "[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", - anonymousId, - destination, - dup); - return null; - } - - log.info("[SUBSCRIBE] stored in Redis: key={}, member={}", key, destination); - - } catch (Exception e) { - log.error("Redis error while handling SUBSCRIBE", e); + // 2) SUBSCRIBE: Redis를 사용해 anonymousId 기준 중복 구독 방지 + 메트릭 기록 + if (StompCommand.SUBSCRIBE.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + user = auth; + } + } + + String destination = accessor.getDestination(); + + if (user != null && destination != null) { + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; + + log.info("[SUBSCRIBE] handling: anonymousId={}, destination={}, key={}", anonymousId, destination, key); + + try { + Long total = redisTemplate.opsForValue().increment(METRIC_TOTAL_SUB); + Long added = redisTemplate.opsForSet().add(key, destination); + redisTemplate.expire(key, java.time.Duration.ofHours(1)); + + log.info("[SUBSCRIBE] redis result: total={}, added={}", total, added); + + if (added != null && added == 0L) { + Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); + log.warn("[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", anonymousId, + destination, dup); + return null; + } + + log.info("[SUBSCRIBE] stored in Redis: key={}, member={}", key, destination); + + } catch (Exception e) { + log.error("Redis error while handling SUBSCRIBE", e); + } + } else { + log.warn("[SUBSCRIBE] skipped: user or destination is null (user={}, dest={})", user, destination); + } } - } else { - log.warn( - "[SUBSCRIBE] skipped: user or destination is null (user={}, dest={})", - user, - destination); - } - } - // 3) SEND: Principal 비어 있으면 SecurityContext에서 복구 - if (StompCommand.SEND.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - accessor.setUser(auth); + // 3) SEND: Principal 비어 있으면 SecurityContext에서 복구 + if (StompCommand.SEND.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + accessor.setUser(auth); + } + } } - } - } - // 4) DISCONNECT: 유저별 구독 키를 정리할지 여부 (옵션) - // - 전체 방 전체 유저 수가 크지 않다면 TTL만으로도 충분. - if (StompCommand.DISCONNECT.equals(command)) { - Principal user = accessor.getUser(); - if (user == null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null) { - user = auth; - } - } - if (user != null) { - String anonymousId = user.getName(); - String key = USER_SUB_KEY_PREFIX + anonymousId; - try { - // 완전히 정리하고 싶으면 delete - redisTemplate.delete(key); - log.info("DISCONNECT: cleared subscriptions for anonymousId={}", anonymousId); - } catch (Exception e) { - log.error("Redis error while handling DISCONNECT", e); + // 4) DISCONNECT: 유저별 구독 키를 정리할지 여부 (옵션) + // - 전체 방 전체 유저 수가 크지 않다면 TTL만으로도 충분. + if (StompCommand.DISCONNECT.equals(command)) { + Principal user = accessor.getUser(); + if (user == null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + user = auth; + } + } + if (user != null) { + String anonymousId = user.getName(); + String key = USER_SUB_KEY_PREFIX + anonymousId; + try { + // 완전히 정리하고 싶으면 delete + redisTemplate.delete(key); + log.info("DISCONNECT: cleared subscriptions for anonymousId={}", anonymousId); + } catch (Exception e) { + log.error("Redis error while handling DISCONNECT", e); + } + } } - } - } - return message; - } + return message; + } } diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java index 34ccece..6fc09a3 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java @@ -16,56 +16,56 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 건너뜀. - 토큰이 유효하면 - * SecurityContext 설정, 아니면 체인 진행 (401은 EntryPoint가 처리) + * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 + * 건너뜀. - 토큰이 유효하면 SecurityContext 설정, 아니면 체인 진행 (401은 EntryPoint가 처리) */ public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtTokenProvider jwtTokenProvider; - private final List skipPatterns; // 필터를 스킵할 경로 패턴들(ant style) - private final AntPathMatcher matcher = new AntPathMatcher(); + private final JwtTokenProvider jwtTokenProvider; + private final List skipPatterns; // 필터를 스킵할 경로 패턴들(ant style) + private final AntPathMatcher matcher = new AntPathMatcher(); - public JwtAuthenticationFilter(JwtTokenProvider provider, Collection skipPatterns) { - this.jwtTokenProvider = provider; - this.skipPatterns = skipPatterns == null ? List.of() : List.copyOf(skipPatterns); - } + public JwtAuthenticationFilter(JwtTokenProvider provider, Collection skipPatterns) { + this.jwtTokenProvider = provider; + this.skipPatterns = skipPatterns == null ? List.of() : List.copyOf(skipPatterns); + } - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - // 1) CORS preflight는 항상 스킵 - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) return true; + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 1) CORS preflight는 항상 스킵 + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) + return true; - // 2) 화이트리스트 패턴은 스킵 - String path = request.getServletPath(); - for (String p : skipPatterns) { - if (matcher.match(p, path)) return true; + // 2) 화이트리스트 패턴은 스킵 + String path = request.getServletPath(); + for (String p : skipPatterns) { + if (matcher.match(p, path)) + return true; + } + return false; } - return false; - } - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); + String header = request.getHeader(HttpHeaders.AUTHORIZATION); - if (header != null && header.startsWith("Bearer ")) { - String token = header.substring(7); - try { - Claims claims = jwtTokenProvider.parseClaims(token); - String subject = claims.getSubject(); - if (subject != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // 필요 시 roles/authorities를 claims에서 꺼내서 넣어도 됨 - var auth = - new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()); - SecurityContextHolder.getContext().setAuthentication(auth); + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + try { + Claims claims = jwtTokenProvider.parseClaims(token); + String subject = claims.getSubject(); + if (subject != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 필요 시 roles/authorities를 claims에서 꺼내서 넣어도 됨 + var auth = new UsernamePasswordAuthenticationToken(subject, null, Collections.emptyList()); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (Exception ignored) { + // 유효하지 않으면 그냥 통과 -> 최종적으로 EntryPoint가 401 응답 처리 + } } - } catch (Exception ignored) { - // 유효하지 않으면 그냥 통과 -> 최종적으로 EntryPoint가 401 응답 처리 - } - } - chain.doFilter(request, response); - } + chain.doFilter(request, response); + } } diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java index f5a8f66..f475b19 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java @@ -16,78 +16,64 @@ @Component public class JwtTokenProvider { - @Value("${jwt.secret}") - private String secret; + @Value("${jwt.secret}") + private String secret; - @Value("${jwt.access-token-validity-seconds}") - private long accessValidity; + @Value("${jwt.access-token-validity-seconds}") + private long accessValidity; - @Value("${jwt.refresh-token-validity-seconds}") - private long refreshValidity; + @Value("${jwt.refresh-token-validity-seconds}") + private long refreshValidity; - private SecretKey key; + private SecretKey key; - @PostConstruct - void init() { - if (secret == null || secret.isBlank()) { - throw new IllegalStateException( - "jwt.secret is not configured. Check your application.yml / env."); - } + @PostConstruct + void init() { + if (secret == null || secret.isBlank()) { + throw new IllegalStateException("jwt.secret is not configured. Check your application.yml / env."); + } - byte[] keyBytes; - try { - // secret이 Base64면 여기서 정상 디코딩 - keyBytes = Decoders.BASE64.decode(secret); - } catch (IllegalArgumentException e) { - // Base64 아니면 그냥 문자열 바이트로 사용 - keyBytes = secret.getBytes(StandardCharsets.UTF_8); - } + byte[] keyBytes; + try { + // secret이 Base64면 여기서 정상 디코딩 + keyBytes = Decoders.BASE64.decode(secret); + } catch (IllegalArgumentException e) { + // Base64 아니면 그냥 문자열 바이트로 사용 + keyBytes = secret.getBytes(StandardCharsets.UTF_8); + } - this.key = Keys.hmacShaKeyFor(keyBytes); - } + this.key = Keys.hmacShaKeyFor(keyBytes); + } - public String createAccessToken(String subject, Map claims) { - Instant now = Instant.now(); - return Jwts.builder() - .subject(subject) - .claims(claims) - .issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(accessValidity))) - .signWith(key) - .compact(); - } + public String createAccessToken(String subject, Map claims) { + Instant now = Instant.now(); + return Jwts.builder().subject(subject).claims(claims).issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(accessValidity))).signWith(key).compact(); + } - public String createRefreshToken(String subject) { - Instant now = Instant.now(); - return Jwts.builder() - .subject(subject) - .issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(refreshValidity))) - .signWith(key) - .compact(); - } + public String createRefreshToken(String subject) { + Instant now = Instant.now(); + return Jwts.builder().subject(subject).issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(refreshValidity))).signWith(key).compact(); + } - public Long getIdFromToken(String token) { - Claims claims = - Jwts.parser() - .verifyWith(key) // init()에서 만든 key 재사용 - .build() - .parseSignedClaims(token) - .getPayload(); + public Long getIdFromToken(String token) { + Claims claims = Jwts.parser().verifyWith(key) // init()에서 만든 key 재사용 + .build().parseSignedClaims(token).getPayload(); - return claims.get("id", Long.class); - } + return claims.get("id", Long.class); + } - public boolean validateToken(String token) { - try { - Jwts.parser().verifyWith(key).build().parseSignedClaims(token); - return true; - } catch (Exception e) { - return false; + public boolean validateToken(String token) { + try { + Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + return true; + } catch (Exception e) { + return false; + } } - } - public Claims parseClaims(String token) { - return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); - } + public Claims parseClaims(String token) { + return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); + } } diff --git a/src/test/java/opensource/bravest/BravestApplicationTests.java b/src/test/java/opensource/bravest/BravestApplicationTests.java index bde485a..ca77007 100644 --- a/src/test/java/opensource/bravest/BravestApplicationTests.java +++ b/src/test/java/opensource/bravest/BravestApplicationTests.java @@ -6,6 +6,6 @@ @SpringBootTest class BravestApplicationTests { - @Test - void contextLoads() {} + @Test + void contextLoads() {} } From c1daf6f4b0f9b1f71bc81fde5a4534017a8592a6 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Mon, 1 Dec 2025 15:31:07 +0900 Subject: [PATCH 43/44] [ci/cd] fix check_style file and refactor all of file --- build.gradle | 28 +++---- .../checkstyle/checkstyle_eclipse_format.xml | 44 +++++++---- config/checkstyle/google_checks.xml | 50 +++++++------ .../controller/ChatListController.java | 2 +- .../domain/chatList/dto/ChatListDto.java | 4 +- .../chatList/service/ChatListService.java | 4 +- .../controller/ChatMessageController.java | 2 +- .../domain/message/dto/MessageDto.java | 2 +- .../message/service/ChatMessageService.java | 6 +- .../AnonymousProfileController.java | 2 +- .../profile/dto/AnonymousProfileResponse.java | 2 +- .../service/AnonymousProfileService.java | 6 +- .../room/controller/RoomController.java | 18 ++--- .../domain/room/service/RoomService.java | 4 +- .../domain/vote/service/VoteService.java | 23 +++--- .../global/apiPayload/ApiResponse.java | 2 +- .../apiPayload/code/status/ErrorStatus.java | 22 ++++-- .../bravest/global/config/OpenApiConfig.java | 18 ++--- .../bravest/global/config/SecurityConfig.java | 74 ++++++++++--------- .../exception/GlobalExceptionHandler.java | 10 +-- .../bravest/global/handler/StompHandler.java | 4 +- .../security/jwt/JwtAuthenticationFilter.java | 6 +- .../global/security/jwt/JwtTokenProvider.java | 6 +- 23 files changed, 184 insertions(+), 155 deletions(-) diff --git a/build.gradle b/build.gradle index ec2be1e..a011a6a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.springframework.boot' version '3.5.7' id 'io.spring.dependency-management' version '1.1.7' id 'com.diffplug.spotless' version '6.25.0' + id 'checkstyle' } group = 'opensource' @@ -66,33 +67,28 @@ tasks.named('test') { useJUnitPlatform() } -apply plugin: 'checkstyle' +tasks.register("checkstyle") { + dependsOn("checkstyleMain", "checkstyleTest") +} checkstyle { - toolVersion = '10.12.0' - configFile = file('config/checkstyle/google_checks.xml') + toolVersion = "10.12.0" + + configFile = file("${rootDir}/config/checkstyle/google_checks.xml") } -tasks.withType(Checkstyle) { +tasks.withType(Checkstyle).configureEach { reports { xml.required.set(true) html.required.set(true) } } -// Spotless 적용 -apply plugin: 'com.diffplug.spotless' - spotless { java { + eclipse().configFile file("${rootDir}/config/checkstyle/checkstyle_eclipse_format.xml") target 'src/**/*.java' - - // Checkstyle XML 규칙을 완전히 재현할 수는 없지만, 기본 규칙 적용 - // 들여쓰기 4칸, 중괄호 스타일, 라인 길이 제한 - eclipse().configFile 'config/checkstyle/checkstyle_eclipse_format.xml' - - // 혹은 Google Java Format 사용 - // googleJavaFormat('1.16.0') + trimTrailingWhitespace() + endWithNewline() } - -} +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle_eclipse_format.xml b/config/checkstyle/checkstyle_eclipse_format.xml index 0a278a0..b13dbcb 100644 --- a/config/checkstyle/checkstyle_eclipse_format.xml +++ b/config/checkstyle/checkstyle_eclipse_format.xml @@ -1,26 +1,42 @@ - - + - - + + - - ``` - - + + + + + + + + + + + + + + + + + - - - - - ``` + + + + + + + - + + + \ No newline at end of file diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml index 3817602..1013d86 100644 --- a/config/checkstyle/google_checks.xml +++ b/config/checkstyle/google_checks.xml @@ -4,35 +4,43 @@ "https://checkstyle.org/dtds/configuration_1_3.dtd"> - - - - + - - + + + - - + + + + + + + + + + - - + + + - - - - + - - - - - - - + + + + + + + + + + + diff --git a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java index 2114548..8c51ad4 100644 --- a/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java +++ b/src/main/java/opensource/bravest/domain/chatList/controller/ChatListController.java @@ -45,7 +45,7 @@ public ApiResponse getChatListById(@PathVariable Long id) { @PutMapping("/{id}") public ApiResponse updateChatList(@PathVariable Long id, - @Valid @RequestBody ChatListUpdateRequest request) { + @Valid @RequestBody ChatListUpdateRequest request) { ChatListResponse response = chatListService.updateChatList(id, request); return ApiResponse.onSuccess(response); } diff --git a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java index 7452f40..ff012ac 100644 --- a/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java +++ b/src/main/java/opensource/bravest/domain/chatList/dto/ChatListDto.java @@ -40,8 +40,8 @@ public static class ChatListResponse { public static ChatListResponse fromEntity(ChatList chatList) { return ChatListResponse.builder().id(chatList.getId()).roomId(chatList.getRoomId()) - .content(chatList.getContent()).registeredBy(chatList.getRegisteredBy().getId()) - .createdAt(chatList.getCreatedAt()).build(); + .content(chatList.getContent()).registeredBy(chatList.getRegisteredBy().getId()) + .createdAt(chatList.getCreatedAt()).build(); } } } diff --git a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java index 5ac8da9..4443986 100644 --- a/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java +++ b/src/main/java/opensource/bravest/domain/chatList/service/ChatListService.java @@ -32,10 +32,10 @@ public class ChatListService { @Transactional public ChatListResponse createChatList(ChatListCreateRequest request) { AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); AnonymousProfile profile = anonymousProfileRepository.findById(request.getRegisteredBy()) - .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); + .orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); ChatList chatList = ChatList.builder().room(room).registeredBy(profile).content(request.getContent()).build(); diff --git a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java index e2872df..a80df92 100644 --- a/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java +++ b/src/main/java/opensource/bravest/domain/message/controller/ChatMessageController.java @@ -28,6 +28,6 @@ public void receiveMessage(MessageRequest request, Principal principal) { // 특정 채팅방 구독자들에게 메시지 전송 messagingTemplate.convertAndSend("/subs/chat-rooms/" + request.getChatRoomId(), - ApiResponse.onSuccess(response)); + ApiResponse.onSuccess(response)); } } diff --git a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java index ac6f7c7..f44edbb 100644 --- a/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java +++ b/src/main/java/opensource/bravest/domain/message/dto/MessageDto.java @@ -21,7 +21,7 @@ public static class MessageResponse { public static MessageResponse from(ChatMessage chatMessage) { return new MessageResponse(chatMessage.getSender().getAnonymousName(), chatMessage.getContent(), - chatMessage.getCreatedAt()); + chatMessage.getCreatedAt()); } } diff --git a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java index 654d847..a5a64de 100644 --- a/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java +++ b/src/main/java/opensource/bravest/domain/message/service/ChatMessageService.java @@ -30,10 +30,10 @@ public MessageResponse send(MessageRequest request, Long id) { AnonymousProfile sender = memberRepository.findById(id).orElseThrow(() -> new CustomException(_USER_NOT_FOUND)); AnonymousRoom chatRoom = chatRoomRepository.findById(request.getChatRoomId()) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); ChatMessage chatMessage = ChatMessage.builder().room(chatRoom).sender(sender).content(request.getContent()) - .build(); + .build(); chatMessageRepository.save(chatMessage); @@ -43,7 +43,7 @@ public MessageResponse send(MessageRequest request, Long id) { @Transactional public void readMessages(Long chatRoomId, Long memberId) { AnonymousRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); + .orElseThrow(() -> new CustomException(_CHATROOM_NOT_FOUND)); // if (!Objects.equals(chatRoom.getMember1().getId(), memberId) && // !Objects.equals(chatRoom.getMember2().getId(), diff --git a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java index a8f4a9f..02dcb41 100644 --- a/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java +++ b/src/main/java/opensource/bravest/domain/profile/controller/AnonymousProfileController.java @@ -20,7 +20,7 @@ public class AnonymousProfileController { @Operation(summary = "익명 프로필 생성", description = "특정 채팅방에 대한 새로운 익명 프로필을 생성합니다.") @PostMapping("/rooms/{roomId}") public ApiResponse createAnonymousProfile(@PathVariable Long roomId, - @RequestBody CreateAnonymousProfileRequest request) { + @RequestBody CreateAnonymousProfileRequest request) { AnonymousProfile profile = anonymousProfileService.createAnonymousProfile(roomId, request); AnonymousProfileResponse response = AnonymousProfileResponse.from(profile); return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), response); diff --git a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java index 9bf377c..de5ba8c 100644 --- a/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java +++ b/src/main/java/opensource/bravest/domain/profile/dto/AnonymousProfileResponse.java @@ -15,6 +15,6 @@ public class AnonymousProfileResponse { public static AnonymousProfileResponse from(AnonymousProfile profile) { return AnonymousProfileResponse.builder().id(profile.getId()).roomId(profile.getRoom().getId()) - .nickname(profile.getAnonymousName()).build(); + .nickname(profile.getAnonymousName()).build(); } } diff --git a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java index 8c03a24..6547577 100644 --- a/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java +++ b/src/main/java/opensource/bravest/domain/profile/service/AnonymousProfileService.java @@ -21,17 +21,17 @@ public class AnonymousProfileService { @Transactional public AnonymousProfile createAnonymousProfile(Long roomId, CreateAnonymousProfileRequest request) { AnonymousRoom room = anonymousRoomRepository.findById(roomId) - .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); + .orElseThrow(() -> new RuntimeException("방을 찾을 수 없음.뿡")); // 중복 프로필 체크 Optional existingProfile = anonymousProfileRepository.findByRoomAndRealUserId(room, - request.getRealUserId()); + request.getRealUserId()); if (existingProfile.isPresent()) { throw new RuntimeException("이미 방에 존재하는 유저임. 다른걸로 접속하셈."); } AnonymousProfile newProfile = AnonymousProfile.builder().room(room).realUserId(request.getRealUserId()) - .anonymousName(request.getAnonymousName()).build(); + .anonymousName(request.getAnonymousName()).build(); return anonymousProfileRepository.save(newProfile); } diff --git a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java index ea09dd7..d0faf85 100644 --- a/src/main/java/opensource/bravest/domain/room/controller/RoomController.java +++ b/src/main/java/opensource/bravest/domain/room/controller/RoomController.java @@ -21,8 +21,8 @@ public class RoomController { public ApiResponse createRoom(@RequestBody RoomDto.CreateRoomRequest request) { AnonymousRoom room = roomService.createRoom(request); return ApiResponse.of(SuccessStatus._CREATED, SuccessStatus._CREATED.getMessage(), - RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) - .createdAt(room.getCreatedAt()).build()); + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()) + .title(room.getTitle()).createdAt(room.getCreatedAt()).build()); } @GetMapping("/{roomId}") @@ -30,18 +30,18 @@ public ApiResponse createRoom(@RequestBody RoomDto.CreateR public ApiResponse getRoom(@PathVariable Long roomId) { AnonymousRoom room = roomService.getRoom(roomId); return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) - .createdAt(room.getCreatedAt()).build()); + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()) + .title(room.getTitle()).createdAt(room.getCreatedAt()).build()); } @PutMapping("/{roomId}") @Operation(summary = "채팅방 정보 수정", description = "ID로 특정 채팅방의 정보를 수정합니다.") public ApiResponse updateRoom(@PathVariable Long roomId, - @RequestBody RoomDto.UpdateRoomRequest request) { + @RequestBody RoomDto.UpdateRoomRequest request) { AnonymousRoom room = roomService.updateRoom(roomId, request); return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) - .createdAt(room.getCreatedAt()).build()); + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()) + .title(room.getTitle()).createdAt(room.getCreatedAt()).build()); } @DeleteMapping("/{roomId}") @@ -63,7 +63,7 @@ public ApiResponse getInviteCode(@PathVariable Long roomId) { public ApiResponse joinRoom(@RequestBody RoomDto.JoinRoomRequest request) { AnonymousRoom room = roomService.joinRoom(request.getRoomCode()); return ApiResponse.of(SuccessStatus._OK, SuccessStatus._OK.getMessage(), - RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()).title(room.getTitle()) - .createdAt(room.getCreatedAt()).build()); + RoomDto.RoomResponse.builder().id(room.getId()).roomCode(room.getRoomCode()) + .title(room.getTitle()).createdAt(room.getCreatedAt()).build()); } } diff --git a/src/main/java/opensource/bravest/domain/room/service/RoomService.java b/src/main/java/opensource/bravest/domain/room/service/RoomService.java index 13e90b5..5ea299b 100644 --- a/src/main/java/opensource/bravest/domain/room/service/RoomService.java +++ b/src/main/java/opensource/bravest/domain/room/service/RoomService.java @@ -20,7 +20,7 @@ public class RoomService { public AnonymousRoom createRoom(RoomDto.CreateRoomRequest request) { String roomCode = generateUniqueRoomCode(); AnonymousRoom room = AnonymousRoom.builder().title(request.getTitle()).roomCode(roomCode) - .createdAt(LocalDateTime.now()).build(); + .createdAt(LocalDateTime.now()).build(); return anonymousRoomRepository.save(room); } @@ -50,7 +50,7 @@ public String getInviteCode(Long roomId) { public AnonymousRoom joinRoom(String roomCode) { return anonymousRoomRepository.findByRoomCode(roomCode) - .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); + .orElseThrow(() -> new RuntimeException("Room not found with code: " + roomCode)); } private String generateUniqueRoomCode() { diff --git a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java index 145b5df..0a05107 100644 --- a/src/main/java/opensource/bravest/domain/vote/service/VoteService.java +++ b/src/main/java/opensource/bravest/domain/vote/service/VoteService.java @@ -30,14 +30,14 @@ public class VoteService { @Transactional public Vote createVote(VoteDto.CreateVoteRequest request) { AnonymousRoom room = anonymousRoomRepository.findById(request.getRoomId()) - .orElseThrow(() -> new RuntimeException("Room not found")); + .orElseThrow(() -> new RuntimeException("Room not found")); Vote vote = Vote.builder().room(room).title(room.getTitle()).isActive(true).createdAt(LocalDateTime.now()) - .build(); + .build(); List options = request.getMessages().stream() - .map(message -> VoteOption.builder().vote(vote).messageContent(message).voteCount(0).build()) - .collect(Collectors.toList()); + .map(message -> VoteOption.builder().vote(vote).messageContent(message).voteCount(0).build()) + .collect(Collectors.toList()); vote.getOptions().addAll(options); @@ -52,15 +52,15 @@ public void castVote(Long voteId, VoteDto.CastVoteRequest request) { } AnonymousProfile voter = anonymousProfileRepository.findById(request.getAnonymousProfileId()) - .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); + .orElseThrow(() -> new RuntimeException("AnonymousProfile not found")); if (userVoteRepository.findByVoteAndVoter(vote, voter).isPresent()) { throw new RuntimeException("User has already voted"); } VoteOption voteOption = vote.getOptions().stream() - .filter(option -> option.getId().equals(request.getVoteOptionId())).findFirst() - .orElseThrow(() -> new RuntimeException("VoteOption not found")); + .filter(option -> option.getId().equals(request.getVoteOptionId())).findFirst() + .orElseThrow(() -> new RuntimeException("VoteOption not found")); voteOption.incrementVoteCount(); @@ -90,11 +90,12 @@ public void deleteVote(Long voteId) { private VoteDto.VoteResponse buildVoteResponse(Vote vote) { List optionResponses = vote.getOptions().stream() - .map(option -> VoteDto.VoteOptionResponse.builder().id(option.getId()) - .messageContent(option.getMessageContent()).voteCount(option.getVoteCount()).build()) - .collect(Collectors.toList()); + .map(option -> VoteDto.VoteOptionResponse.builder().id(option.getId()) + .messageContent(option.getMessageContent()).voteCount(option.getVoteCount()) + .build()) + .collect(Collectors.toList()); return VoteDto.VoteResponse.builder().id(vote.getId()).title(vote.getTitle()).isActive(vote.isActive()) - .createdAt(vote.getCreatedAt()).options(optionResponses).build(); + .createdAt(vote.getCreatedAt()).options(optionResponses).build(); } } diff --git a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java index 8b479a6..859bbaa 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java +++ b/src/main/java/opensource/bravest/global/apiPayload/ApiResponse.java @@ -29,7 +29,7 @@ public static ApiResponse onSuccess(T data) { public static ApiResponse of(BaseCode code, String message, T data) { return new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), - data); + data); } public static ApiResponse onFailure(BaseErrorCode errorCode, T data) { diff --git a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java index 18831ea..3b7b59e 100644 --- a/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/opensource/bravest/global/apiPayload/code/status/ErrorStatus.java @@ -10,14 +10,20 @@ @AllArgsConstructor public enum ErrorStatus implements BaseErrorCode { _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), _BAD_REQUEST( - HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", - "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), _NOT_FOUND( - HttpStatus.NOT_FOUND, "COMMON404", - "요청한 리소스를 찾을 수 없습니다."), _FAMILY_NOT_FOUND(HttpStatus.NOT_FOUND, "FAMILY404", - "유효하지 않은 초대 코드입니다."), _USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", - "사용자를 찾을 수 없습니다."), _CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", - "채팅방을 찾을 수 없습니다."), _CHATLIST_NOT_FOUND(HttpStatus.NOT_FOUND, - "USER404", "리스트를 찾을 수 없습니다."),; + HttpStatus.BAD_REQUEST, "COMMON400", + "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), _FORBIDDEN( + HttpStatus.FORBIDDEN, "COMMON403", + "금지된 요청입니다."), _NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", + "요청한 리소스를 찾을 수 없습니다."), _FAMILY_NOT_FOUND(HttpStatus.NOT_FOUND, + "FAMILY404", "유효하지 않은 초대 코드입니다."), _USER_NOT_FOUND( + HttpStatus.NOT_FOUND, "USER404", + "사용자를 찾을 수 없습니다."), _CHATROOM_NOT_FOUND( + HttpStatus.NOT_FOUND, + "USER404", + "채팅방을 찾을 수 없습니다."), _CHATLIST_NOT_FOUND( + HttpStatus.NOT_FOUND, + "USER404", + "리스트를 찾을 수 없습니다."),; private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java index f6a1b36..c791d78 100644 --- a/src/main/java/opensource/bravest/global/config/OpenApiConfig.java +++ b/src/main/java/opensource/bravest/global/config/OpenApiConfig.java @@ -18,14 +18,14 @@ public class OpenApiConfig { @Bean public OpenAPI baseOpenAPI() { return new OpenAPI() - // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 - .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) - // 2) JWT Bearer 스키마 정의 - .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, - new SecurityScheme().name(SECURITY_SCHEME_NAME).type(SecurityScheme.Type.HTTP).scheme("bearer") - .bearerFormat("JWT"))) - .info(new Info().title("openSource Bravest API").description("openSource Bravest 백엔드 API 문서") - .version("v1.0.0").license(new License().name("MIT"))) - .externalDocs(new ExternalDocumentation().description("README")); + // 1) 전역으로 "이 API는 이 인증 방식을 쓴다" 선언 + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + // 2) JWT Bearer 스키마 정의 + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme().name(SECURITY_SCHEME_NAME).type(SecurityScheme.Type.HTTP) + .scheme("bearer").bearerFormat("JWT"))) + .info(new Info().title("openSource Bravest API").description("openSource Bravest 백엔드 API 문서") + .version("v1.0.0").license(new License().name("MIT"))) + .externalDocs(new ExternalDocumentation().description("README")); } } diff --git a/src/main/java/opensource/bravest/global/config/SecurityConfig.java b/src/main/java/opensource/bravest/global/config/SecurityConfig.java index c37b2c8..e087085 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -32,8 +32,8 @@ public class SecurityConfig { // 로그인/토큰 교환/리다이렉트/헬스체크 등 공개 경로 private static final String[] PUBLIC = {"/", "/actuator/health", "/api/auth/**", // 카카오 코드 교환 API 등 - "/oauth2/**", "/login/**", "/login/oauth2/**", "/api/test/auth/**", "/rooms/**", "/chatlists/**", - "/anonymous-profiles/**", "/votes/**", "/ws-connect/**", "/chat-test", "/pub/**", "/sub/**"}; + "/oauth2/**", "/login/**", "/login/oauth2/**", "/api/test/auth/**", "/rooms/**", "/chatlists/**", + "/anonymous-profiles/**", "/votes/**", "/ws-connect/**", "/chat-test", "/pub/**", "/sub/**"}; // 정적 리소스 private static final String[] STATIC = {"/favicon.ico", "/assets/**", "/css/**", "/js/**", "/images/**"}; @@ -49,40 +49,42 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtTokenProvider, skip); http - // REST API 기본 세팅 - .csrf(csrf -> csrf.disable()).cors(Customizer.withDefaults()) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .httpBasic(basic -> basic.disable()).formLogin(form -> form.disable()).logout(lo -> lo.disable()) - .requestCache(cache -> cache.disable()) - - // 권한 규칙 - .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // CORS - // preflight - // 허용 - .requestMatchers(SWAGGER).permitAll().requestMatchers(PUBLIC).permitAll() - .requestMatchers(STATIC).permitAll().anyRequest().authenticated()) - - // 인증/인가 실패 공통 응답(JSON) - ApiResponse 형식 - .exceptionHandling(ex -> ex.authenticationEntryPoint((req, res, ex1) -> { - ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write(String.format("{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), errorStatus.getMessage())); - } - }).accessDeniedHandler((req, res, ex2) -> { - ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; - res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); - res.setContentType("application/json;charset=UTF-8"); - try (PrintWriter w = res.getWriter()) { - w.write(String.format("{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", - errorStatus.getCode(), errorStatus.getMessage())); - } - })) - - // JWT 필터 등록(UsernamePasswordAuthenticationFilter 앞) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + // REST API 기본 세팅 + .csrf(csrf -> csrf.disable()).cors(Customizer.withDefaults()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .httpBasic(basic -> basic.disable()).formLogin(form -> form.disable()) + .logout(lo -> lo.disable()).requestCache(cache -> cache.disable()) + + // 권한 규칙 + .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // CORS + // preflight + // 허용 + .requestMatchers(SWAGGER).permitAll().requestMatchers(PUBLIC).permitAll() + .requestMatchers(STATIC).permitAll().anyRequest().authenticated()) + + // 인증/인가 실패 공통 응답(JSON) - ApiResponse 형식 + .exceptionHandling(ex -> ex.authenticationEntryPoint((req, res, ex1) -> { + ErrorStatus errorStatus = ErrorStatus._UNAUTHORIZED; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write(String.format( + "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + }).accessDeniedHandler((req, res, ex2) -> { + ErrorStatus errorStatus = ErrorStatus._FORBIDDEN; + res.setStatus(errorStatus.getReasonHttpStatus().getHttpStatus().value()); + res.setContentType("application/json;charset=UTF-8"); + try (PrintWriter w = res.getWriter()) { + w.write(String.format( + "{\"isSuccess\":false,\"code\":\"%s\",\"message\":\"%s\",\"data\":null}", + errorStatus.getCode(), errorStatus.getMessage())); + } + })) + + // JWT 필터 등록(UsernamePasswordAuthenticationFilter 앞) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java index b0bbb26..be2b96b 100644 --- a/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/opensource/bravest/global/exception/GlobalExceptionHandler.java @@ -15,7 +15,7 @@ public class GlobalExceptionHandler { public ResponseEntity> handleCustomException(CustomException e) { log.warn("CustomException: {}", e.getMessage()); return ResponseEntity.status(e.getErrorCode().getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(e.getErrorCode(), null)); + .body(ApiResponse.onFailure(e.getErrorCode(), null)); } @ExceptionHandler(RuntimeException.class) @@ -27,26 +27,26 @@ public ResponseEntity> handleRuntimeException(RuntimeExcepti if (message.contains("유효하지 않은 초대 코드") || message.contains("가족을 찾을 수 없습니다")) { log.warn("Family not found: {}", message); return ResponseEntity.status(ErrorStatus._FAMILY_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._FAMILY_NOT_FOUND, null)); + .body(ApiResponse.onFailure(ErrorStatus._FAMILY_NOT_FOUND, null)); } if (message.contains("사용자를 찾을 수 없습니다")) { log.warn("User not found: {}", message); return ResponseEntity.status(ErrorStatus._USER_NOT_FOUND.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._USER_NOT_FOUND, null)); + .body(ApiResponse.onFailure(ErrorStatus._USER_NOT_FOUND, null)); } } // 기본값: 500 Internal Server Error log.error("RuntimeException: ", e); return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); + .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); } @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception e) { log.error("Unexpected exception: ", e); return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus().getHttpStatus()) - .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); + .body(ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR, null)); } } diff --git a/src/main/java/opensource/bravest/global/handler/StompHandler.java b/src/main/java/opensource/bravest/global/handler/StompHandler.java index 8d10752..ff026ed 100644 --- a/src/main/java/opensource/bravest/global/handler/StompHandler.java +++ b/src/main/java/opensource/bravest/global/handler/StompHandler.java @@ -50,7 +50,7 @@ public Message preSend(Message message, MessageChannel channel) { anonymousProfileRepository.findById(Long.valueOf(anonymousId)).ifPresentOrElse(member -> { Authentication auth = new UsernamePasswordAuthenticationToken(anonymousId, null, - List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); + List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); SecurityContextHolder.getContext().setAuthentication(auth); accessor.setUser(auth); log.info("STOMP CONNECT: anonymousId={} principal set", anonymousId); @@ -89,7 +89,7 @@ public Message preSend(Message message, MessageChannel channel) { if (added != null && added == 0L) { Long dup = redisTemplate.opsForValue().increment(METRIC_DUP_SUB); log.warn("[SUBSCRIBE] duplicate detected: anonymousId={}, dest={}, dupCount={}", anonymousId, - destination, dup); + destination, dup); return null; } diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java index 6fc09a3..89e00b7 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtAuthenticationFilter.java @@ -16,8 +16,8 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 - * 건너뜀. - 토큰이 유효하면 SecurityContext 설정, 아니면 체인 진행 (401은 EntryPoint가 처리) + * JWT가 필요한 보호 경로에만 동작하도록 만든 필터. - 화이트리스트(permitAll) 경로와 OPTIONS 프리플라이트는 필터를 건너뜀. - 토큰이 유효하면 SecurityContext 설정, 아니면 체인 + * 진행 (401은 EntryPoint가 처리) */ public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -47,7 +47,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { + throws ServletException, IOException { String header = request.getHeader(HttpHeaders.AUTHORIZATION); diff --git a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java index f475b19..6640e7a 100644 --- a/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/opensource/bravest/global/security/jwt/JwtTokenProvider.java @@ -48,18 +48,18 @@ void init() { public String createAccessToken(String subject, Map claims) { Instant now = Instant.now(); return Jwts.builder().subject(subject).claims(claims).issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(accessValidity))).signWith(key).compact(); + .expiration(Date.from(now.plusSeconds(accessValidity))).signWith(key).compact(); } public String createRefreshToken(String subject) { Instant now = Instant.now(); return Jwts.builder().subject(subject).issuedAt(Date.from(now)) - .expiration(Date.from(now.plusSeconds(refreshValidity))).signWith(key).compact(); + .expiration(Date.from(now.plusSeconds(refreshValidity))).signWith(key).compact(); } public Long getIdFromToken(String token) { Claims claims = Jwts.parser().verifyWith(key) // init()에서 만든 key 재사용 - .build().parseSignedClaims(token).getPayload(); + .build().parseSignedClaims(token).getPayload(); return claims.get("id", Long.class); } From b98c75ce939930f3f3d12afa6c5868c3d1cdaef1 Mon Sep 17 00:00:00 2001 From: JangYeongHu Date: Tue, 9 Dec 2025 23:47:44 +0900 Subject: [PATCH 44/44] [ci/cd] fix style --- .../java/opensource/bravest/config/SwaggerConfig.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/opensource/bravest/config/SwaggerConfig.java b/src/main/java/opensource/bravest/config/SwaggerConfig.java index c207f19..91fb3e6 100644 --- a/src/main/java/opensource/bravest/config/SwaggerConfig.java +++ b/src/main/java/opensource/bravest/config/SwaggerConfig.java @@ -13,15 +13,10 @@ public class SwaggerConfig { @Bean public OpenAPI openAPI() { - return new OpenAPI() - .components(new Components()) - .info(apiInfo()); + return new OpenAPI().components(new Components()).info(apiInfo()); } private Info apiInfo() { - return new Info() - .title("Bravest") - .description("오픈소스 프로젝트 Bravest api 명세서입니다.") - .version("1.0.0"); + return new Info().title("Bravest").description("오픈소스 프로젝트 Bravest api 명세서입니다.").version("1.0.0"); } -} \ No newline at end of file +}