diff --git a/build.gradle b/build.gradle index ed442ea..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,26 +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 { - googleJavaFormat() + eclipse().configFile file("${rootDir}/config/checkstyle/checkstyle_eclipse_format.xml") target 'src/**/*.java' + trimTrailingWhitespace() + endWithNewline() } } \ 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..b13dbcb --- /dev/null +++ b/config/checkstyle/checkstyle_eclipse_format.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml index 6eff7fe..1013d86 100644 --- a/config/checkstyle/google_checks.xml +++ b/config/checkstyle/google_checks.xml @@ -1,10 +1,46 @@ - + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + - \ 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..8c51ad4 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..ff012ac 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..4443986 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..a80df92 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..f44edbb 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..a5a64de 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..02dcb41 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..de5ba8c 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..6547577 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..d0faf85 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..5ea299b 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..0a05107 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,80 @@ @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..859bbaa 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..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 @@ -9,33 +9,33 @@ @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..c791d78 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..e087085 100644 --- a/src/main/java/opensource/bravest/global/config/SecurityConfig.java +++ b/src/main/java/opensource/bravest/global/config/SecurityConfig.java @@ -25,126 +25,93 @@ @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())); - } + 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(); - } + // 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..be2b96b 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..ff026ed 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..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,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..6640e7a 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() {} }