diff --git a/src/backend/chat-server/Dockerfile b/src/backend/chat-server/Dockerfile index f90496f6..ec6944bd 100644 --- a/src/backend/chat-server/Dockerfile +++ b/src/backend/chat-server/Dockerfile @@ -1,22 +1,9 @@ -# 1️⃣ 베이스 이미지로 OpenJDK 17 사용 -FROM openjdk:17-jdk-slim AS build - -# 2️⃣ 작업 디렉토리 설정 -WORKDIR /app - -# 3️⃣ 프로젝트 복사 -COPY . . - -# 4️⃣ Gradle 빌드 수행 (테스트는 제외) -RUN chmod +x gradlew -RUN ./gradlew build -x test - -# 5️⃣ 런타임 이미지로 OpenJDK 17 사용 +# 베이스 이미지로 OpenJDK 17 사용 FROM openjdk:17-jdk-slim -WORKDIR /app -# 6️⃣ 빌드된 JAR 파일 복사 -COPY --from=build /app/build/libs/*.jar app.jar +# 애플리케이션 파일을 컨테이너에 복사 +ARG JAR_FILE=stomp_chat-0.0.1-SNAPSHOT.jar +COPY ${JAR_FILE} app.jar -# 7️⃣ 컨테이너 실행 시 Spring Boot 애플리케이션 실행 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +# 애플리케이션 실행 +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/config/KafkaConsumerConfig.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/config/KafkaConsumerConfig.java index 011ebc01..47521d83 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/config/KafkaConsumerConfig.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/config/KafkaConsumerConfig.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.DefaultKafkaConsumerFactory; diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/RoomController.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/RoomController.java index 37f2b537..427210b8 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/RoomController.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/RoomController.java @@ -23,6 +23,9 @@ public RoomController(SimpMessagingTemplate messagingTemplate, RoomManager roomM @MessageMapping("/joinRoom") public void joinRoom(String payload, SimpMessageHeaderAccessor headerAccessor) throws Exception { + + // TODO[SMG-C]: jsonNode, map 사용 지양 + // objectmapper.read 활용해서 object로 사용 JsonNode jsonNode = objectMapper.readTree(payload); String roomId = jsonNode.get("roomId").asText(); String userId = jsonNode.get("userId").asText(); @@ -52,6 +55,11 @@ public void sendMessage(String payload, SimpMessageHeaderAccessor headerAccessor //messagingTemplate.convertAndSend("/topic/" + roomId, response); } + + // TODO[SMG-C]: record 사용 + // MessageResponse이거를 아래처럼 간단히 가능합니다. + record MessageResponseTest(String userId, String message) {} + public static class MessageResponse { private String userId; private String message; diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/RoomEvent.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/RoomEvent.java index 6cee8868..890a8a63 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/RoomEvent.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/RoomEvent.java @@ -8,6 +8,8 @@ @Setter @AllArgsConstructor public class RoomEvent { + // TODO[SMG-C]: enum 사용 지향 + // string 보다는 enum 사용하시면 switch case 문 쓸 때도 편해요 private String eventType; // 이벤트 구분 private Object data; // 이벤트 데이터 diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/KafkaConsumerService.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/KafkaConsumerService.java index f207d3b3..636183cf 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/KafkaConsumerService.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/KafkaConsumerService.java @@ -1,11 +1,13 @@ package kickzo.stomp_chat.service; import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; import kickzo.stomp_chat.dto.PlaylistUpdateEvent; import kickzo.stomp_chat.dto.RoomEvent; import lombok.RequiredArgsConstructor; @@ -20,6 +22,21 @@ public class KafkaConsumerService { private final RoomEventHandler roomEventHandler; private final PlaylistEventHandler playlistEventHandler; + @Value("${server.port}") + private String serverPort; + + @Value("${spring.kafka.bootstrap-servers}") + private String KAFKA_BROKER; // Kafka 브로커 주소 + + @Value(("${spring.kafka.consumer.group-id}")) + private String groupId; + + // 서버 포트에 맞춰서 groupId를 동적으로 설정하는 메서드 + @PostConstruct + public void init() { + groupId = "my-group-" + serverPort; // 서버 포트에 따라 groupId 설정 + } + @KafkaListener(topics = "playlist") public void consumePlaylistEvents(ConsumerRecord record) { try { diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/PlaylistEventHandler.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/PlaylistEventHandler.java index cdcf9ab0..b09616d8 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/PlaylistEventHandler.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/PlaylistEventHandler.java @@ -19,6 +19,8 @@ public class PlaylistEventHandler { private final MessagingService messagingService; public void handleEvent(PlaylistUpdateEvent event) throws JsonProcessingException { + // TODO[SMG-C]: Map 사용 보다는 Object + // object 로 만들고 관리해야 추후에 변경 추적이 쉬워요 Map playlistResponse = new LinkedHashMap<>(); playlistResponse.put("roomId", event.getRoomId()); playlistResponse.put("playlistJson", event.getPlaylistJson()); diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/RoomEventHandler.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/RoomEventHandler.java index 7ce52140..ae0adcb3 100644 --- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/RoomEventHandler.java +++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/RoomEventHandler.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import kickzo.stomp_chat.dto.NewUserJoinEvent; import kickzo.stomp_chat.dto.RoleChangeEvent; import kickzo.stomp_chat.dto.RoomData; import kickzo.stomp_chat.dto.RoomEvent; @@ -22,15 +21,13 @@ public class RoomEventHandler { public void handleEvent(RoomEvent event) throws JsonProcessingException { switch (event.getEventType()) { + case "room-update": handleRoomUpdate(event); break; case "role-change": handleRoleChange(event); break; - case "user-list": - handleUserList(event); - break; default: log.warn("Unknown event type: {}", event.getEventType()); } @@ -45,9 +42,4 @@ private void handleRoleChange(RoomEvent event) throws JsonProcessingException { RoleChangeEvent roleChange = objectMapper.convertValue(event.getData(), RoleChangeEvent.class); messagingService.sendMessage("role-change", roleChange.getRoomId(), roleChange); } - - private void handleUserList(RoomEvent event) throws JsonProcessingException { - NewUserJoinEvent userList = objectMapper.convertValue(event.getData(), NewUserJoinEvent.class); - messagingService.sendMessage("user-list", userList.getRoomId(), userList); - } } \ No newline at end of file diff --git a/src/backend/history-server/src/main/java/history/kickzo/config/KafkaConsumerConfig.java b/src/backend/history-server/src/main/java/history/kickzo/config/KafkaConsumerConfig.java index 20e9d707..5430564f 100644 --- a/src/backend/history-server/src/main/java/history/kickzo/config/KafkaConsumerConfig.java +++ b/src/backend/history-server/src/main/java/history/kickzo/config/KafkaConsumerConfig.java @@ -62,6 +62,7 @@ public ConcurrentMessageListenerContainer messageListenerCo @Override public void onMessage(ConsumerRecord record) { String message = record.value(); + // TODO[SMG-C]: logger 권장. @Slf4j 사용하시면 편합니다. System.out.println("1: Received Kafka message: " + message); chatMessageService.saveMessage(message); } diff --git a/src/backend/history-server/src/main/java/history/kickzo/model/ChatMessage.java b/src/backend/history-server/src/main/java/history/kickzo/model/ChatMessage.java index 882a3673..4402fdc0 100644 --- a/src/backend/history-server/src/main/java/history/kickzo/model/ChatMessage.java +++ b/src/backend/history-server/src/main/java/history/kickzo/model/ChatMessage.java @@ -61,6 +61,8 @@ public long getTimestamp() { return timestamp; } + // TODO[SMG-C]: setter 사용 지양 + // 참고 : https://velog.io/@langoustine/setter-%EC%A7%80%EC%96%91-%EC%9D%B4%EC%9C%A0 public void setTimestamp(long timestamp) { this.timestamp = timestamp; } diff --git a/src/backend/history-server/src/main/java/history/kickzo/repository/ChatMessageRepository.java b/src/backend/history-server/src/main/java/history/kickzo/repository/ChatMessageRepository.java index 5aaf9f98..885f4567 100644 --- a/src/backend/history-server/src/main/java/history/kickzo/repository/ChatMessageRepository.java +++ b/src/backend/history-server/src/main/java/history/kickzo/repository/ChatMessageRepository.java @@ -8,6 +8,7 @@ @Repository public interface ChatMessageRepository extends MongoRepository { + // TODO[SMG-Q]: 왜 채팅은 mongoDB 사용 했나요? // 특정 방의 메시지들을 타임스탬프 기준으로 정렬하여 가져오는 메서드 List findByRoomIdOrderByTimestampDesc(String roomId); diff --git a/src/backend/history-server/src/main/java/history/kickzo/service/ChatMessageService.java b/src/backend/history-server/src/main/java/history/kickzo/service/ChatMessageService.java index 43588704..e019b165 100644 --- a/src/backend/history-server/src/main/java/history/kickzo/service/ChatMessageService.java +++ b/src/backend/history-server/src/main/java/history/kickzo/service/ChatMessageService.java @@ -4,6 +4,7 @@ import history.kickzo.model.ChatMessage; import history.kickzo.repository.ChatMessageRepository; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; import java.util.List; @@ -12,6 +13,7 @@ public class ChatMessageService { private final ChatMessageRepository repository; private final ObjectMapper objectMapper; + // TODO[SMG-C]: @RequiredArgsConstructor 사용 권장. 이게 편해요 public ChatMessageService(ChatMessageRepository repository, ObjectMapper objectMapper) { this.repository = repository; this.objectMapper = objectMapper; @@ -24,6 +26,7 @@ public void saveMessage(String message) { chatMessage.setTimestamp(System.currentTimeMillis()); // 현재 시간으로 타임스탬프 설정 (필요 시) // 저장할 데이터 출력 + // TODO[SMG-C]: logger 사용 권장. ex. @Slf4j System.out.println("Saving message to DB: "); System.out.println("ID: " + chatMessage.getId()); System.out.println("User: " + chatMessage.getUserId()); @@ -35,6 +38,8 @@ public void saveMessage(String message) { } catch (Exception e) { e.printStackTrace(); System.out.println("Error parsing message: " + e.getMessage()); + // TODO[SMG-C]: logger 사용 권장. ex. @Slf4j + // log.error("Error parsing message: " + e.getMessage(), e); } } diff --git a/src/backend/main-server/main/build.gradle b/src/backend/main-server/main/build.gradle index 58129c04..8df075e0 100644 --- a/src/backend/main-server/main/build.gradle +++ b/src/backend/main-server/main/build.gradle @@ -35,6 +35,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageApi.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageApi.java index 888f2ef2..fade03b2 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageApi.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageApi.java @@ -21,8 +21,7 @@ public interface MainPageApi { @Operation(summary = "모든 방 조회", description = "페이지 번호와 사이즈로 방 목록을 조회합니다.") - @ApiResponses( - value = { + @ApiResponses({ @ApiResponse(responseCode = "200", description = "전체 방 목록 조회 성공"), @ApiResponse(responseCode = "DB-500-001", description = ApiResponseConstants.DATABASE_ERROR_MESSAGE), @ApiResponse(responseCode = "JSON-500-001", description = ApiResponseConstants.JSON_PROCESSING_ERROR_MESSAGE), @@ -46,7 +45,7 @@ ResponseEntity> getUserRooms( ); @Operation(summary = "방 생성", description = "사용자가 새로운 방을 생성합니다.") - @ApiResponses(value = { + @ApiResponses({ @ApiResponse(responseCode = "200", description = "새로운 방 생성 성공"), @ApiResponse(responseCode = "MAIN-400-001", description = ApiResponseConstants.INVALID_INPUT_MESSAGE), @ApiResponse(responseCode = "MAIN-404-002", description = ApiResponseConstants.USER_NOT_FOUND_MESSAGE), diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageController.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageController.java index 20d32b59..8ab772d2 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageController.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/MainPageController.java @@ -6,7 +6,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.kickzo.main.dto.request.CreateRoomRequestDto; @@ -27,22 +30,27 @@ public class MainPageController implements MainPageApi { private final MainPageService mainPageService; @Override - @GetMapping("/list/all") - public ResponseEntity> getAllRooms(int page, int size) { + @GetMapping("/all") + public ResponseEntity> getAllRooms( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { log.info("get all rooms. page: {}, size: {}", page, size); return ResponseEntity.ok(mainPageService.getAllRooms(PageRequest.of(page, size))); } @Override - @GetMapping("/list/my") - public ResponseEntity> getUserRooms(Long userId) { + @GetMapping("/me") + public ResponseEntity> getUserRooms( + @RequestHeader(value = "x-user-id", required = true) Long userId) { log.info("get user rooms. userId: {}", userId); return ResponseEntity.ok(mainPageService.getUserRooms(userId)); } @Override @PostMapping("/create-room") - public ResponseEntity createRoom(Long userId, @Valid CreateRoomRequestDto requestDto) { + public ResponseEntity createRoom( + @RequestHeader(value = "x-user-id", required = true) Long userId, + @Valid @RequestBody CreateRoomRequestDto requestDto) { log.info("create room for userId: {}", userId); return ResponseEntity.ok(mainPageService.createRoom(userId, requestDto)); } diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomApi.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomApi.java index e84fa9b6..28add8b3 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomApi.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomApi.java @@ -1,8 +1,13 @@ package com.kickzo.main.controller; +import java.util.List; + import com.kickzo.main.constants.ApiResponseConstants; +import com.kickzo.main.dto.request.RoleChangeRequestDto; +import com.kickzo.main.dto.request.RoomJoinRequestDto; import com.kickzo.main.dto.request.RoomUpdateRequestDto; import com.kickzo.main.dto.response.RoomEntryResponseDto; +import com.kickzo.main.dto.response.UserListDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -29,7 +34,7 @@ public interface RoomApi { }) ResponseEntity joinRoom( @RequestHeader(value = "x-user-id", required = false) Long userId, - @RequestParam String roomCode + @RequestBody RoomJoinRequestDto roomJoinRequestDto ); @Operation(summary = "방 정보 수정", description = "특정 방의 제목, 설명, 공개여부를 수정합니다.") @@ -71,7 +76,7 @@ ResponseEntity updateRoomInfo( }) ResponseEntity savePlaylist( @RequestHeader(value = "x-user-id", required = true) Long userId, - @RequestParam Long roomId, + @RequestBody Long roomId, @RequestBody String playlistJson ); @@ -92,8 +97,11 @@ ResponseEntity savePlaylist( }) ResponseEntity changeUserRole( @RequestHeader(value = "x-user-id", required = true) Long userId, - @RequestParam Long roomId, - @RequestParam Long targetUserId, - @RequestParam int newRole + @RequestBody RoleChangeRequestDto roleChangeRequestDto + ); + + @Operation(summary = "방의 userList 제공", description = "방에 소속한 participant의 userList를 제공합니다.") + ResponseEntity> getRoomParticipants( + @RequestParam Long roomId ); } diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomController.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomController.java index d05946d1..59b4c8e3 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomController.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/controller/RoomController.java @@ -1,6 +1,9 @@ package com.kickzo.main.controller; +import java.util.List; + import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -9,9 +12,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.kickzo.main.dto.request.RoleChangeRequestDto; +import com.kickzo.main.dto.request.RoomJoinRequestDto; import com.kickzo.main.dto.request.RoomUpdateRequestDto; -import com.kickzo.main.dto.response.RoomDetailsDto; import com.kickzo.main.dto.response.RoomEntryResponseDto; +import com.kickzo.main.dto.response.UserListDto; import com.kickzo.main.service.PlaylistService; import com.kickzo.main.service.RoomService; import com.kickzo.main.service.RoomUserService; @@ -39,22 +44,12 @@ public class RoomController implements RoomApi { @PostMapping("/join") public ResponseEntity joinRoom( @RequestHeader(value = "x-user-id", required = false) Long userId, - @RequestParam String roomCode) { - log.info("UserId = {}, RoomCode = {}", userId, roomCode); + @RequestBody RoomJoinRequestDto roomJoinRequestDto) { - int myRole; - if (userId == null) { - // 비로그인 유저는 role = 99 - myRole = 99; - } else { - // 유저의 Role 및 방 참여 상태 확인 - myRole = roomService.getUserRole(roomCode, userId); - log.info("myRole: " + myRole); - } - - RoomDetailsDto roomDetails = roomService.getRoomDetails(myRole, roomCode); + String roomCode = roomJoinRequestDto.getRoomCode(); + log.info("UserId = {}, RoomCode = {}", userId, roomCode); - RoomEntryResponseDto response = new RoomEntryResponseDto(myRole, roomDetails); + RoomEntryResponseDto response = roomService.getRoomJoinResponse(roomCode, userId); return ResponseEntity.ok(response); } @@ -72,7 +67,7 @@ public ResponseEntity updateRoomInfo( @PostMapping("/playlist") public ResponseEntity savePlaylist( @RequestHeader(value = "x-user-id", required = true) Long userId, - @RequestParam Long roomId, + @RequestBody Long roomId, @RequestBody String playlistJson) { log.info("Saving playlist for room: {}, playlist: {}", roomId, playlistJson); roomUserService.checkAccessRole(userId, roomId); @@ -84,10 +79,15 @@ public ResponseEntity savePlaylist( @PatchMapping("/change-role") public ResponseEntity changeUserRole( @RequestHeader(value = "x-user-id", required = true) Long userId, - @RequestParam Long roomId, - @RequestParam Long targetUserId, - @RequestParam int newRole) { - roomUserService.changeUserRole(userId, roomId, targetUserId, newRole); + @RequestBody RoleChangeRequestDto roleChangeRequestDto) { + roomUserService.changeUserRole(userId, roleChangeRequestDto); return ResponseEntity.ok("User role updated successfully."); } + + @Override + @GetMapping("/participants") + public ResponseEntity> getRoomParticipants( + @RequestParam Long roomId) { + return ResponseEntity.ok(roomService.getRoomParticipants(roomId)); + } } diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/KafkaProducerService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/KafkaProducerService.java index 45e47bcd..fe87951c 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/KafkaProducerService.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/KafkaProducerService.java @@ -44,6 +44,10 @@ public void sendRoleChangeEvent(Long roomId, Long targetUserId, int newRole) { RoleChangeEvent event = new RoleChangeEvent(roomId, targetUserId, newRole); RoomEvent roomEvent = new RoomEvent("role-change", event); try { + // TODO[SMG-C]: kafka send 역할 분리 + // KafkaRepository로 분리해서 사용하면 좋을거 같아요 + // KafkaRepository 에서 object를 string 으로 바꾸고 보내기까지 세트로 해주면 될거 같아요 + // kafkaRepository.send(topci, messageObject); String message = objectMapper.writeValueAsString(roomEvent); kafkaTemplate.send(TOPIC_ROOM, message); log.info("Kafka Role Change Event Sent: {}", message); diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java index 9e288b53..560c481b 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java @@ -41,6 +41,9 @@ public class MainPageService { private static final int MAX_ROOMS_PER_USER = 5; private static final int ROLE_CREATOR = 0; + // TODO[SMG-C]: bean 사용 권장 + // JacksonAutoConfiguration 에서 objectMapper 빈 등록해줘서 재활용 해주시면 좋을거 같아요 + // objectMapper는 기본적으로 thread-safe 해서, 프로젝트에서 하나 만든거를 재활용 하는게 좋아요 static final ObjectMapper objectMapper = new ObjectMapper(); // 메인 페이지 방 list 제공 diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java index c24b6a50..b48acb0e 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java @@ -17,6 +17,7 @@ import com.kickzo.main.dto.event.RoomUpdateEvent; import com.kickzo.main.dto.request.RoomUpdateRequestDto; import com.kickzo.main.dto.response.RoomDetailsDto; +import com.kickzo.main.dto.response.RoomEntryResponseDto; import com.kickzo.main.dto.response.RoomInfoDto; import com.kickzo.main.dto.response.UserListDto; import com.kickzo.main.entity.Room; @@ -45,55 +46,11 @@ public class RoomService { private static final int ROLE_MEMBER = 2; - // roomCode에 따른 방의 정보와 유저 list 전달 @Transactional - public RoomDetailsDto getRoomDetails(int myRole, String roomCode) { - System.out.println("RoomCode : " + roomCode); - if (roomCode == null || roomCode.isBlank()) { - throw new CustomException(CustomErrorCode.INVALID_ROOM_CODE); - } - // Step 1: RoomCode로 RoomID 검색 - Long roomId = getRoomId(roomCode); - - // Step 2: RoomID로 UserID와 Role 목록 검색 - List userIdRoles = roomUserRepository.findUsersByRoomId(roomId); - - // Step 3: UserID로 Nickname 검색 - List userList = getUserInfoList(userIdRoles); - - // Step 4: RoomID로 Room 정보와 Playlist Order 검색 - List roomInfo = getRoomInfoByRoomId(roomId); - List playlist = null; - try { - playlist = getPlaylistByRoomId(roomId); - } catch (JsonProcessingException e) { - throw new CustomException(CustomErrorCode.JSON_PROCESSING_ERROR); - } - - if (myRole == ROLE_MEMBER) { - kafkaProducerService.sendRoomUserList(roomId, userList); - } - - // 결과를 조합하여 반환 - return new RoomDetailsDto(userList, roomInfo, playlist); - } - - // 방에 유저 소속 여부에 따른 작업과 role 전달 - @Transactional - public int getUserRole(String roomCode, Long userId) { - Long roomId = getRoomId(roomCode); - // Step 1: 해당 방에서 유저의 역할(Role)을 찾음 - Integer role = roomUserRepository.findRoleByUserIdAndRoomId(roomId, userId); - if (role != null) { - // Step 2: 역할(Role)이 존재하면 반환 - return role; - } else { - // Step 3: 역할(Role)이 존재하지 않으면 새 사용자 추가 - saveUserCount(roomId); - saveNewRoomUser(roomId, userId); - // 새로운 사람이 들어왔으므로 kafka로 현재 방의 UserList 보내기 - return ROLE_MEMBER; - } + public RoomEntryResponseDto getRoomJoinResponse(String roomCode, Long userId){ + int myRole = determineUserRole(roomCode, userId); + RoomDetailsDto roomDetails = assembleRoomDetails(myRole, roomCode); + return new RoomEntryResponseDto(myRole, roomDetails); } @Transactional @@ -129,33 +86,74 @@ public void updateRoomInfo(RoomUpdateRequestDto updateRequestDto) { kafkaProducerService.sendRoomUpdateMessage(event); } + @Transactional(readOnly = true) + public List getRoomParticipants(Long roomId) { + return fetchUserList(roomId); + } + /** * roomCode에 따른 방의 정보와 유저 list 전달 - * 1. roomCode로 roomId 뽑아오기 : getRoomId - * 2. UserId를 기반으로 닉네임 매핑 : mapNicknames - * 3. room 테이블에서 받아온 data를 dto로 변환 : getRoomInfoByRoomId - * 4. playlist 테이블에서 받아온 data를 dto로 변환 : getPlaylistByRoomId + * 1. 사용자 역할(Role) 확인, 비로그인 유저인 경우 기본 Role(99) 반환 : determineUserRole + * 2. 사용자 역할(Role) 조회, 존재하지 않으면 새 사용자로 등록 후 기본 Role(ROLE_MEMBER) 반환 : findOrAssignUserRole + * 3. 주어진 역할(Role)과 방 코드를 기반으로 Room의 전체 세부 정보 생성 : assembleRoomDetails + * 4. Room ID를 기준으로 방에 참여한 유저의 ID, Role, 닉네임, 프로필 이미지 조회 : fetchUserList + * 5. Room ID로 방의 정보를 조회하고 RoomInfoDto 리스트로 변환 : fetchRoomInfo + * 6. Room ID를 기반으로 Playlist 정보를 JSON에서 List 형태로 변환 : fetchPlaylist + * 7. Room Code로 Room ID 조회, 존재하지 않으면 예외 발생 : getRoomId + * 8. 방 생성자의 닉네임을 기반으로 프로필 이미지 URL 조회, 없으면 기본 이미지 반환 : getCreatorProfileImage + * 9. 유저 ID를 기반으로 프로필 이미지 URL 조회, 없으면 기본 이미지 반환 : getUserProfileImage + * 10. Room ID를 기준으로 방의 유저 수를 1 증가시키고, 변경된 정보 저장 : saveUserCount + * 11. RoomUser 엔티티를 생성하여 새로운 사용자를 방에 추가하고 기본 Role(ROLE_MEMBER)로 저장 : saveNewRoomUser */ - private Long getRoomId(String roomCode) { - return Optional.ofNullable(roomRepository.findRoomIdByRoomCode(roomCode)) - .orElseThrow(() -> new CustomException(CustomErrorCode.ROOM_NOT_FOUND)); + private int determineUserRole(String roomCode, Long userId) { + if (userId == null) { + return 99; // 비로그인 유저는 role = 99 + } + Long roomId = getRoomId(roomCode); + return findOrAssignUserRole(roomId, userId); + } + + private int findOrAssignUserRole(Long roomId, Long userId) { + // Step 1: 해당 방에서 유저의 역할(Role)을 찾음 + Integer role = roomUserRepository.findRoleByUserIdAndRoomId(roomId, userId); + if (role != null) { + // Step 2: 역할(Role)이 존재하면 반환 + return role; + } + // Step 3: 역할(Role)이 존재하지 않으면 새 사용자 추가 + saveUserCount(roomId); + saveNewRoomUser(roomId, userId); + return ROLE_MEMBER; + } + + private RoomDetailsDto assembleRoomDetails(int myRole, String roomCode) { + Long roomId = getRoomId(roomCode); + + List userList = fetchUserList(roomId); + List roomInfo = fetchRoomInfo(roomId); + List playlist = fetchPlaylist(roomId); + + if (myRole == ROLE_MEMBER) { + kafkaProducerService.sendRoomUserList(roomId, userList); + } + + return new RoomDetailsDto(userList, roomInfo, playlist); } - private List getUserInfoList(List userIdRoles) { - // UserID를 기반으로 Nickname을 매핑하는 로직 + private List fetchUserList(Long roomId) { + List userIdRoles = roomUserRepository.findUsersByRoomId(roomId); return userIdRoles.stream() .map(userRole -> { - Long userId = (Long)userRole[0]; - int role = (int)userRole[1]; - // UserRepository를 통해 UserID로 Nickname 조회 + Long userId = (Long) userRole[0]; + int role = (int) userRole[1]; String nickname = userRepository.findNicknameById(userId); - String profileImageUrl= getUserProfileImage(userId); + String profileImageUrl = getUserProfileImage(userId); return new UserListDto(userId, role, nickname, profileImageUrl); }) .collect(Collectors.toList()); } - private List getRoomInfoByRoomId(Long roomId) { + private List fetchRoomInfo(Long roomId) { return roomRepository.findRoomById(roomId) .stream() .map(room -> RoomInfoDto.builder() @@ -170,12 +168,22 @@ private List getRoomInfoByRoomId(Long roomId) { .collect(Collectors.toList()); } - private List getPlaylistByRoomId(Long roomId) throws JsonProcessingException { + private List fetchPlaylist(Long roomId) { String playlistJson = playlistRepository.findOrderById(roomId); if (playlistJson == null || playlistJson.isBlank()) { - return new ArrayList<>(); // null 또는 빈 값 처리 + return new ArrayList<>(); + } + + try { + return objectMapper.readValue(playlistJson, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new CustomException(CustomErrorCode.JSON_PROCESSING_ERROR); } - return objectMapper.readValue(playlistJson, new TypeReference<>() {}); + } + + private Long getRoomId(String roomCode) { + return Optional.ofNullable(roomRepository.findRoomIdByRoomCode(roomCode)) + .orElseThrow(() -> new CustomException(CustomErrorCode.ROOM_NOT_FOUND)); } private String getCreatorProfileImage(String creator) { @@ -188,10 +196,6 @@ private String getUserProfileImage(Long userId) { .orElse("default-profile-image-url"); // 기본 이미지 설정 } - /** - * 새로 들어온 user -> count 증가 - * 새로 들어온 user -> room_user DB에 저장 - */ private void saveUserCount(Long roomId){ Room room = roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(CustomErrorCode.ROOM_NOT_FOUND)); diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomUserService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomUserService.java index 1c00ae24..6c57eeb6 100644 --- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomUserService.java +++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomUserService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.kickzo.main.dto.request.RoleChangeRequestDto; import com.kickzo.main.entity.RoomUser; import com.kickzo.main.entity.RoomUserId; import com.kickzo.main.exception.CustomErrorCode; @@ -23,7 +24,11 @@ public class RoomUserService { private static final int ROLE_MEMBER = 2; @Transactional - public void changeUserRole(Long userId, Long roomId, Long targetUserId, int newRole) { + public void changeUserRole(Long userId, RoleChangeRequestDto roleChangeRequestDto) { + Long roomId = roleChangeRequestDto.getRoomId(); + Long targetUserId = roleChangeRequestDto.getTargetUserId(); + int newRole = roleChangeRequestDto.getNewRole(); + checkAccessRole(userId, roomId); RoomUserId targetId = new RoomUserId(roomId, targetUserId); diff --git a/src/backend/main-server/main/src/main/resources/application-dev.properties b/src/backend/main-server/main/src/main/resources/application-dev.properties index 63ae0134..0fb57c01 100644 --- a/src/backend/main-server/main/src/main/resources/application-dev.properties +++ b/src/backend/main-server/main/src/main/resources/application-dev.properties @@ -1,4 +1,9 @@ spring.datasource.url=jdbc:mysql://localhost:7001/kickzo spring.datasource.username=kickzo spring.datasource.password=test123 -spring.kafka.bootstrap-servers=localhost:19092 \ No newline at end of file +spring.kafka.bootstrap-servers=localhost:19092 + +spring.data.redis.database=3 +spring.data.redis.host=localhost +spring.data.redis.port=7003 +spring.data.redis.timeout=60000 \ No newline at end of file diff --git a/src/backend/main-server/main/src/main/resources/application-docker.properties b/src/backend/main-server/main/src/main/resources/application-docker.properties index d4d7b7d3..7dfa5ddb 100644 --- a/src/backend/main-server/main/src/main/resources/application-docker.properties +++ b/src/backend/main-server/main/src/main/resources/application-docker.properties @@ -1,4 +1,9 @@ spring.datasource.url=jdbc:mysql://mysql:3306/kickzo spring.datasource.username=kickzo spring.datasource.password=test123 -spring.kafka.bootstrap-servers=kafka:9092 \ No newline at end of file +spring.kafka.bootstrap-servers=kafka:9092 + +spring.data.redis.database=3 +spring.data.redis.host=redis +spring.data.redis.port=6379 +spring.data.redis.timeout=60000 \ No newline at end of file diff --git a/src/backend/state-server/src/main/java/com/example/state/config/KafkaProducerConfig.java b/src/backend/state-server/src/main/java/com/example/state/config/KafkaProducerConfig.java index 31005360..7847fe2b 100644 --- a/src/backend/state-server/src/main/java/com/example/state/config/KafkaProducerConfig.java +++ b/src/backend/state-server/src/main/java/com/example/state/config/KafkaProducerConfig.java @@ -48,4 +48,27 @@ public ProducerFactory userStatusProducerFactory() public KafkaTemplate userStatusKafkaTemplate() { return new KafkaTemplate<>(userStatusProducerFactory()); } + + + // TODO[SMG-C]: kafka message용 interface + // dto 마다 factory를 만들지 않고, kafka message 공통 interface를 만들어서 받으시면 편하실거 같아요 + // 보낼 떄는 kafkaMessageTemplate.send(topic, message)로 보내시면 됩니다. + interface KafkaMessage { + String getTopic(); + String getVersion(); + } + + public ProducerFactory kafkaMessageProducerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_BROKER); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); // JSON 직렬화 + + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaMessageTemplate() { + return new KafkaTemplate<>(kafkaMessageProducerFactory()); + } } \ No newline at end of file diff --git a/src/backend/state-server/src/main/java/com/example/state/entity/Friend.java b/src/backend/state-server/src/main/java/com/example/state/entity/Friend.java index 1b13d28f..45f71900 100644 --- a/src/backend/state-server/src/main/java/com/example/state/entity/Friend.java +++ b/src/backend/state-server/src/main/java/com/example/state/entity/Friend.java @@ -13,6 +13,9 @@ @AllArgsConstructor @IdClass(FriendId.class) public class Friend { + // TODO[SMG-Q]: 1,2 구분이 뭔지랑, Friend 엔티티랑 뭔 차이인가요? + // TODO[SMG-C]: 변수명 변경 + // friend1, 2가 아니라 조금 더 역할에 맞는 이름으로 지어주시면 좋을거 같아요. 추가로 설명도 같이 있으면 좋을거 같아요 @Id @Column(name = "friend_1") diff --git a/src/backend/state-server/src/main/java/com/example/state/entity/FriendId.java b/src/backend/state-server/src/main/java/com/example/state/entity/FriendId.java index c76f852c..4921ad95 100644 --- a/src/backend/state-server/src/main/java/com/example/state/entity/FriendId.java +++ b/src/backend/state-server/src/main/java/com/example/state/entity/FriendId.java @@ -6,6 +6,7 @@ public class FriendId implements Serializable { private Long friend1; private Long friend2; + // TODO[SMG-Q]: 1,2 구분이 뭔지랑, Friend 엔티티랑 뭔 차이인가요? public FriendId() {} diff --git a/src/backend/state-server/src/main/java/com/example/state/service/KafkaProducerService.java b/src/backend/state-server/src/main/java/com/example/state/service/KafkaProducerService.java index 4990c8ac..167ba9fb 100644 --- a/src/backend/state-server/src/main/java/com/example/state/service/KafkaProducerService.java +++ b/src/backend/state-server/src/main/java/com/example/state/service/KafkaProducerService.java @@ -8,6 +8,10 @@ @Service @RequiredArgsConstructor public class KafkaProducerService { + // TODO[SMG-C]: 네이밍 변경 + // Service/Repository 계층으로 사용중이시면 kafka는 인프라 계층이고, 이를 활용하는 역할이라 KafkaRepository로 보는게 맞을거 같아요 + // 참고 : https://medium.com/@ankitpal181/service-repository-pattern-802540254019 + private final KafkaTemplate userStatusKafkaTemplate; private static final String TOPIC = "friend_online_notify"; diff --git a/src/backend/state-server/src/main/java/com/example/state/service/RedisService.java b/src/backend/state-server/src/main/java/com/example/state/service/RedisService.java index 446197c9..efda3e03 100644 --- a/src/backend/state-server/src/main/java/com/example/state/service/RedisService.java +++ b/src/backend/state-server/src/main/java/com/example/state/service/RedisService.java @@ -12,6 +12,9 @@ @Service public class RedisService { + // TODO[SMG-C]: 네이밍 변경 + // Service/Repository 계층으로 사용중이시면 redis는 인프라 database 계층이고, 이를 활용하는 역할이라 RedisRepository로 보는게 맞을거 같아요 + // 참고 : https://medium.com/@ankitpal181/service-repository-pattern-802540254019 private static final Logger logger = LoggerFactory.getLogger(RedisService.class); private final StringRedisTemplate redisTemplate; diff --git a/src/backend/state-server/src/main/java/com/example/state/service/StateManager.java b/src/backend/state-server/src/main/java/com/example/state/service/StateManager.java index 89584106..c4202794 100644 --- a/src/backend/state-server/src/main/java/com/example/state/service/StateManager.java +++ b/src/backend/state-server/src/main/java/com/example/state/service/StateManager.java @@ -9,6 +9,9 @@ @Service public class StateManager { + // TODO[SMG-C]: 네이밍 변경 + // 여러 repository를 활용해서 비즈니스 로직을 구현한 계층이라 Service 네이밍을 넣으면 좋을거 같아요 + // 참고 : https://medium.com/@ankitpal181/service-repository-pattern-802540254019 private final RedisService redisService; private final ObjectMapper objectMapper; @@ -27,12 +30,17 @@ public StateManager(RedisService redisService, ObjectMapper objectMapper, public void consumeMessage(String message) { // 메시지 처리 try { + // TODO[SMG-C]: json > model 변환 + // object 변환해서 사용하는게 더 편하실거에요 + // ex. objectMapper.readValue(message, Message.class); JsonNode jsonNode = objectMapper.readTree(message); String eventType = jsonNode.get("eventType").asText(); String userId = jsonNode.get("userId").asText(); String serverPort = jsonNode.get("serverPort").asText(); String timestamp = jsonNode.get("timestamp").asText(); + // TODO[SMG-C]: logger 사용 권장 + // system out 대신 logger 사용 권장. @Slf4j 참고 System.out.println("Processing event: " + eventType + " for user: " + userId); if ("JOIN".equals(eventType)) { @@ -52,6 +60,9 @@ public void consumeMessage(String message) { } } catch (Exception e) { e.printStackTrace(); + + // TODO[SMG-C]: logger 사용 권장 + // ex. log.error("Error processing message: {}", e.getMessage(), e); } } } \ No newline at end of file diff --git a/src/backend/user-server/src/user/user.controller.ts b/src/backend/user-server/src/user/user.controller.ts index a6f802aa..fc2a8bd0 100644 --- a/src/backend/user-server/src/user/user.controller.ts +++ b/src/backend/user-server/src/user/user.controller.ts @@ -14,6 +14,7 @@ import { BadRequestException, Delete, Patch, + DefaultValuePipe, } from "@nestjs/common"; import { UserService } from "./user.service"; import { CreateUserDto } from "./dto/create-user.dto"; @@ -43,17 +44,22 @@ export class UserController { return await this.userService.delete(+userId); } - @Get() - async findAll() { - return this.userService.findAll(); - } + @Get("exists") + @UsePipes(new ValidationPipe({ transform: true })) + async checkExists(@Query() query: CheckExistsDto) { + if (query.nickname && query.email) { + throw new BadRequestException(MESSAGES.NICKNAME_AND_EMAIL); + } - @Get("profile/:id") - async getUserById(@Param("id", ParseIntPipe) id: string) { - return this.userService.getUserById(+id); + if (query.nickname) { + return this.userService.checkNicknameExists(query.nickname); + } + if (query.email) { + return this.userService.checkEmailExists(query.email); + } } - @Get("profile") + @Get("me") async getMyInfo(@Req() req: Request) { const id = req.headers["x-user-id"]; if (!id) { @@ -62,7 +68,7 @@ export class UserController { return this.userService.getUserById(+id); } - @Patch("profile") + @Patch("me") @UsePipes(ValidationPipe) async updateProfile( @Req() req: Request, @@ -78,6 +84,19 @@ export class UserController { return await this.userService.updateProfile(+userId, updateUserDto); } + @Get() + async findAll( + @Query("page", new DefaultValuePipe(0), ParseIntPipe) page: number = 0, + @Query("size", new DefaultValuePipe(10), ParseIntPipe) size: number = 10, + ) { + return this.userService.findAll(page, size); + } + + @Get(":id") + async getUserById(@Param("id", ParseIntPipe) id: string) { + return this.userService.getUserById(+id); + } + @MessagePattern({ cmd: "get_user_by_email" }) @UsePipes(ValidationPipe) async getUserByEmail(@Payload() payload: { email: string }) { @@ -98,19 +117,4 @@ export class UserController { }; return userWithPassword; } - - @Get("exists") - @UsePipes(new ValidationPipe({ transform: true })) - async checkExists(@Query() query: CheckExistsDto) { - if (query.nickname && query.email) { - throw new BadRequestException(MESSAGES.NICKNAME_AND_EMAIL); - } - - if (query.nickname) { - return this.userService.checkNicknameExists(query.nickname); - } - if (query.email) { - return this.userService.checkEmailExists(query.email); - } - } } diff --git a/src/backend/user-server/src/user/user.service.ts b/src/backend/user-server/src/user/user.service.ts index be2de344..5fbc93ca 100644 --- a/src/backend/user-server/src/user/user.service.ts +++ b/src/backend/user-server/src/user/user.service.ts @@ -69,8 +69,15 @@ export class UserService { } // NOTE: pagination 필요할 경우 추가 - async findAll() { - return this.userRepository.find(); + async findAll(page: number = 0, size: number = 10) { + const [users, total] = await this.userRepository.findAndCount({ + skip: page * size, + take: size, + }); + return { + users, + total, + }; } async getUserById(id: number) { diff --git a/src/frontend/package.json b/src/frontend/package.json index 0b0605ed..30aaad97 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -15,10 +15,14 @@ "@sentry/browser": "^8.48.0", "@sentry/react": "^8.48.0", "@tanstack/react-query": "^5.64.1", + "@types/sockjs-client": "^1.5.4", + "@types/stompjs": "^2.3.9", "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.1.3", + "sockjs-client": "1.5.1", + "stompjs": "^2.3.3", "storybook": "^8.4.7", "styled-components": "^6.1.14", "zustand": "^5.0.3" diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml index 9ad33551..b3e047a4 100644 --- a/src/frontend/pnpm-lock.yaml +++ b/src/frontend/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@tanstack/react-query': specifier: ^5.64.1 version: 5.64.1(react@18.3.1) + '@types/sockjs-client': + specifier: ^1.5.4 + version: 1.5.4 + '@types/stompjs': + specifier: ^2.3.9 + version: 2.3.9 axios: specifier: ^1.7.9 version: 1.7.9 @@ -29,9 +35,15 @@ importers: react-router-dom: specifier: ^7.1.3 version: 7.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sockjs-client: + specifier: 1.5.1 + version: 1.5.1 + stompjs: + specifier: ^2.3.3 + version: 2.3.3 storybook: specifier: ^8.4.7 - version: 8.4.7(prettier@3.4.2) + version: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) styled-components: specifier: ^6.1.14 version: 6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -41,37 +53,37 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: ^3.2.3 - version: 3.2.3(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) + version: 3.2.3(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@eslint/js': specifier: ^9.17.0 version: 9.18.0 '@storybook/addon-essentials': specifier: ^8.4.7 - version: 8.4.7(@types/react@18.3.18)(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(@types/react@18.3.18)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/addon-interactions': specifier: ^8.4.7 - version: 8.4.7(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/addon-onboarding': specifier: ^8.4.7 - version: 8.4.7(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/addons': specifier: ^7.6.17 version: 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/blocks': specifier: ^8.4.7 - version: 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/react': specifier: ^8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.6.3) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(typescript@5.6.3) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.6.3)(vite@6.0.7(@types/node@22.10.6)) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(typescript@5.6.3)(vite@6.0.7(@types/node@22.10.6)) '@storybook/test': specifier: ^8.4.7 - version: 8.4.7(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/theming': specifier: ^8.4.7 - version: 8.4.7(storybook@8.4.7(prettier@3.4.2)) + version: 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -116,7 +128,7 @@ importers: version: 15.14.0 jsdom: specifier: ^26.0.0 - version: 26.0.0 + version: 26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) prettier: specifier: ^3.4.2 version: 3.4.2 @@ -134,7 +146,7 @@ importers: version: 6.0.7(@types/node@22.10.6) vitest: specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.6)(jsdom@26.0.0) + version: 2.1.8(@types/node@22.10.6)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) packages: @@ -1120,6 +1132,12 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/sockjs-client@1.5.4': + resolution: {integrity: sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==} + + '@types/stompjs@2.3.9': + resolution: {integrity: sha512-fu/GgkRdxwyEJ+JeUsGhDxGwmZQi+xeNElradGQ4ehWiG2z/o89gsi5Y7Gv0KC6VK1v78Cjh8zj3VF+RvqCGSA==} + '@types/styled-components@5.1.34': resolution: {integrity: sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==} @@ -1312,6 +1330,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1412,10 +1434,30 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1490,6 +1532,17 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -1572,6 +1625,10 @@ packages: jiti: optional: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@10.3.0: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1603,10 +1660,20 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventsource@1.1.2: + resolution: {integrity: sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==} + engines: {node: '>=0.12.0'} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1626,6 +1693,10 @@ packages: fastq@1.18.0: resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1752,6 +1823,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-parser-js@0.5.9: + resolution: {integrity: sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1831,6 +1905,9 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -1872,6 +1949,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json3@3.3.3: + resolution: {integrity: sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1962,6 +2042,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1973,6 +2056,13 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -2086,6 +2176,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2158,6 +2251,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2195,6 +2291,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -2255,6 +2354,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sockjs-client@1.5.1: + resolution: {integrity: sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2273,6 +2375,9 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stompjs@2.3.3: + resolution: {integrity: sha512-5l/Ogz0DTFW7TrpHF0LAETGqM/so8UxNJvYZjJKqcX31EVprSQgnGkO80tZctPC/lFBDUrSFiTG3xd0R27XAIA==} + store2@2.14.4: resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} @@ -2415,6 +2520,12 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typescript-eslint@8.20.0: resolution: {integrity: sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2447,6 +2558,13 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2569,6 +2687,18 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + websocket@1.0.35: + resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} + engines: {node: '>=4.0.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2626,6 +2756,10 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaeti@0.0.6: + resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} + engines: {node: '>=0.10.32'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2790,13 +2924,13 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@chromatic-com/storybook@3.2.3(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))': + '@chromatic-com/storybook@3.2.3(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: chromatic: 11.24.0 filesize: 10.1.6 jsonfile: 6.1.0 react-confetti: 6.2.2(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -3179,99 +3313,99 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 - '@storybook/addon-actions@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-actions@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-backgrounds@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-controls@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.4.7(@types/react@18.3.18)(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-docs@8.4.7(@types/react@18.3.18)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.18)(react@18.3.1) - '@storybook/blocks': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) - '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/react-dom-shim': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) + '@storybook/blocks': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/react-dom-shim': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.4.7(@types/react@18.3.18)(storybook@8.4.7(prettier@3.4.2))': - dependencies: - '@storybook/addon-actions': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-backgrounds': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-controls': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-docs': 8.4.7(@types/react@18.3.18)(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-highlight': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-measure': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-outline': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-toolbars': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/addon-viewport': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - storybook: 8.4.7(prettier@3.4.2) + '@storybook/addon-essentials@8.4.7(@types/react@18.3.18)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': + dependencies: + '@storybook/addon-actions': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-backgrounds': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-controls': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-docs': 8.4.7(@types/react@18.3.18)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-highlight': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-measure': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-outline': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-toolbars': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/addon-viewport': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-highlight@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) - '@storybook/addon-interactions@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-interactions@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/test': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/instrumenter': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/test': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) polished: 4.3.1 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 - '@storybook/addon-measure@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-measure@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) tiny-invariant: 1.3.3 - '@storybook/addon-onboarding@8.4.7(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-onboarding@8.4.7(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: react-confetti: 6.2.2(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - react - '@storybook/addon-outline@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-outline@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-toolbars@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) - '@storybook/addon-viewport@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/addon-viewport@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: memoizerific: 1.11.3 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/addons@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -3282,21 +3416,21 @@ snapshots: - react - react-dom - '@storybook/blocks@8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))': + '@storybook/blocks@8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/csf': 0.1.13 '@storybook/icons': 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.0.7(@types/node@22.10.6))': + '@storybook/builder-vite@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(vite@6.0.7(@types/node@22.10.6))': dependencies: - '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) browser-assert: 1.2.1 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 vite: 6.0.7(@types/node@22.10.6) @@ -3313,15 +3447,15 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/components@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/components@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/core-events@7.6.17': dependencies: ts-dedent: 2.2.0 - '@storybook/core@8.4.7(prettier@3.4.2)': + '@storybook/core@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)': dependencies: '@storybook/csf': 0.1.13 better-opn: 3.0.2 @@ -3333,7 +3467,7 @@ snapshots: recast: 0.23.9 semver: 7.6.3 util: 0.12.5 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: prettier: 3.4.2 transitivePeerDependencies: @@ -3341,9 +3475,9 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/csf-plugin@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) unplugin: 1.16.1 '@storybook/csf@0.1.13': @@ -3357,11 +3491,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/instrumenter@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 2.1.8 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/manager-api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -3383,9 +3517,9 @@ snapshots: - react - react-dom - '@storybook/manager-api@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/manager-api@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/preview-api@7.6.17': dependencies: @@ -3404,29 +3538,29 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/preview-api@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/preview-api@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) - '@storybook/react-dom-shim@8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))': + '@storybook/react-dom-shim@8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) - '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.6.3)(vite@6.0.7(@types/node@22.10.6))': + '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(typescript@5.6.3)(vite@6.0.7(@types/node@22.10.6))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.6.3)(vite@6.0.7(@types/node@22.10.6)) '@rollup/pluginutils': 5.1.4(rollup@4.30.1) - '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@6.0.7(@types/node@22.10.6)) - '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.6.3) + '@storybook/builder-vite': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(vite@6.0.7(@types/node@22.10.6)) + '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(typescript@5.6.3) find-up: 5.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 7.1.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) tsconfig-paths: 4.2.0 vite: 6.0.7(@types/node@22.10.6) transitivePeerDependencies: @@ -3435,19 +3569,19 @@ snapshots: - supports-color - typescript - '@storybook/react@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2))(typescript@5.6.3)': + '@storybook/react@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))(typescript@5.6.3)': dependencies: - '@storybook/components': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/components': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/preview-api': 8.4.7(storybook@8.4.7(prettier@3.4.2)) - '@storybook/react-dom-shim': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.4.2)) - '@storybook/theming': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/manager-api': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/preview-api': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/react-dom-shim': 8.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) + '@storybook/theming': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) optionalDependencies: - '@storybook/test': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/test': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) typescript: 5.6.3 '@storybook/router@7.6.17': @@ -3456,17 +3590,17 @@ snapshots: memoizerific: 1.11.3 qs: 6.14.0 - '@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/test@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/csf': 0.1.13 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.4.7(storybook@8.4.7(prettier@3.4.2)) + '@storybook/instrumenter': 8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10)) '@testing-library/dom': 10.4.0 '@testing-library/jest-dom': 6.5.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/expect': 2.0.5 '@vitest/spy': 2.0.5 - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/theming@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -3477,9 +3611,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/theming@8.4.7(storybook@8.4.7(prettier@3.4.2))': + '@storybook/theming@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: - storybook: 8.4.7(prettier@3.4.2) + storybook: 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) '@storybook/types@7.6.17': dependencies: @@ -3637,6 +3771,12 @@ snapshots: '@types/node': 22.10.6 '@types/send': 0.17.4 + '@types/sockjs-client@1.5.4': {} + + '@types/stompjs@2.3.9': + dependencies: + '@types/node': 22.10.6 + '@types/styled-components@5.1.34': dependencies: '@types/hoist-non-react-statics': 3.3.6 @@ -3880,6 +4020,11 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + optional: true + cac@6.7.14: {} call-bind-apply-helpers@1.0.1: @@ -3972,11 +4117,26 @@ snapshots: csstype@3.1.3: {} + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + optional: true + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.1.0 + debug@2.6.9: + dependencies: + ms: 2.0.0 + optional: true + + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.0: dependencies: ms: 2.1.3 @@ -4029,6 +4189,27 @@ snapshots: dependencies: es-errors: 1.3.0 + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + optional: true + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + optional: true + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + optional: true + esbuild-register@3.6.0(esbuild@0.24.2): dependencies: debug: 4.4.0 @@ -4173,6 +4354,14 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + optional: true + espree@10.3.0: dependencies: acorn: 8.14.0 @@ -4199,8 +4388,21 @@ snapshots: esutils@2.0.3: {} + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + optional: true + + eventsource@1.1.2: {} + expect-type@1.1.0: {} + ext@1.7.0: + dependencies: + type: 2.7.3 + optional: true + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -4221,6 +4423,10 @@ snapshots: dependencies: reusify: 1.0.4 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4337,6 +4543,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-parser-js@0.5.9: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 @@ -4411,6 +4619,9 @@ snapshots: dependencies: which-typed-array: 1.1.18 + is-typedarray@1.0.0: + optional: true + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -4425,7 +4636,7 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} - jsdom@26.0.0: + jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: cssstyle: 4.2.1 data-urls: 5.0.0 @@ -4446,7 +4657,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.1.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -4461,6 +4672,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json3@3.3.3: {} + json5@2.2.3: {} jsonfile@6.1.0: @@ -4541,12 +4754,21 @@ snapshots: minimist@1.2.8: {} + ms@2.0.0: + optional: true + ms@2.1.3: {} nanoid@3.3.8: {} natural-compare@1.4.0: {} + next-tick@1.1.0: + optional: true + + node-gyp-build@4.8.4: + optional: true + node-releases@2.0.19: {} nwsapi@2.2.16: {} @@ -4644,6 +4866,8 @@ snapshots: dependencies: side-channel: 1.1.0 + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} ramda@0.29.0: {} @@ -4721,6 +4945,8 @@ snapshots: require-directory@2.1.1: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve@1.22.10: @@ -4771,6 +4997,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: dependencies: call-bound: 1.0.3 @@ -4840,6 +5068,17 @@ snapshots: siginfo@2.0.0: {} + sockjs-client@1.5.1: + dependencies: + debug: 3.2.7 + eventsource: 1.1.2 + faye-websocket: 0.11.4 + inherits: 2.0.4 + json3: 3.3.3 + url-parse: 1.5.10 + transitivePeerDependencies: + - supports-color + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -4850,11 +5089,17 @@ snapshots: std-env@3.8.0: {} + stompjs@2.3.3: + optionalDependencies: + websocket: 1.0.35 + transitivePeerDependencies: + - supports-color + store2@2.14.4: {} - storybook@8.4.7(prettier@3.4.2): + storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10): dependencies: - '@storybook/core': 8.4.7(prettier@3.4.2) + '@storybook/core': 8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10) optionalDependencies: prettier: 3.4.2 transitivePeerDependencies: @@ -4977,6 +5222,14 @@ snapshots: type-fest@2.19.0: {} + type@2.7.3: + optional: true + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + optional: true + typescript-eslint@8.20.0(eslint@9.18.0)(typescript@5.6.3): dependencies: '@typescript-eslint/eslint-plugin': 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.6.3))(eslint@9.18.0)(typescript@5.6.3) @@ -5008,6 +5261,16 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.4 + optional: true + util-deprecate@1.0.2: {} util@0.12.5: @@ -5056,7 +5319,7 @@ snapshots: '@types/node': 22.10.6 fsevents: 2.3.3 - vitest@2.1.8(@types/node@22.10.6)(jsdom@26.0.0): + vitest@2.1.8(@types/node@22.10.6)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@vitest/expect': 2.1.8 '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.6)) @@ -5080,7 +5343,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.6 - jsdom: 26.0.0 + jsdom: 26.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - less - lightningcss @@ -5100,6 +5363,26 @@ snapshots: webpack-virtual-modules@0.6.2: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.9 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + websocket@1.0.35: + dependencies: + bufferutil: 4.0.9 + debug: 2.6.9 + es5-ext: 0.10.64 + typedarray-to-buffer: 3.1.5 + utf-8-validate: 5.0.10 + yaeti: 0.0.6 + transitivePeerDependencies: + - supports-color + optional: true + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -5137,7 +5420,10 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - ws@8.18.0: {} + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 xml-name-validator@5.0.0: {} @@ -5145,6 +5431,9 @@ snapshots: y18n@5.0.8: {} + yaeti@0.0.6: + optional: true + yallist@3.1.1: {} yargs-parser@21.1.1: {} diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 126f525a..351a690d 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -59,6 +59,7 @@ color: var(--palette-static-black); } +textarea::placeholder, input::placeholder { color: var(--palette-label-assistive); } diff --git a/src/frontend/src/api/endpoints/user/user.api.ts b/src/frontend/src/api/endpoints/user/user.api.ts index b6a3e0b3..6aa2b375 100644 --- a/src/frontend/src/api/endpoints/user/user.api.ts +++ b/src/frontend/src/api/endpoints/user/user.api.ts @@ -4,19 +4,25 @@ import { UpdateUserRequestDto, UserResponseDto } from './user.interface'; export const userApi = { // 내 프로필 조회 getMyProfile: async () => { - const { data } = await instance.get('users/profile'); + const { data } = await instance.get('users/me'); return data; }, // 내 프로필 수정 updateMyProfile: async (updateUserRequestDto: UpdateUserRequestDto) => { - const { data } = await instance.patch(`users/profile`, updateUserRequestDto); + const { data } = await instance.patch(`users/me`, updateUserRequestDto); + return data; + }, + + // 전체 유저 조회 + getUsers: async (page: number = 0, size: number = 10) => { + const { data } = await instance.get(`users`, { params: { page, size } }); return data; }, // 프로필 조회 getProfile: async (userId: string) => { - const { data } = await instance.get(`users/profile/${userId}`); + const { data } = await instance.get(`users/${userId}`); return data; }, diff --git a/src/frontend/src/components/LeftNavBar/index.tsx b/src/frontend/src/components/LeftNavBar/index.tsx index 0a38c2ba..f5764111 100644 --- a/src/frontend/src/components/LeftNavBar/index.tsx +++ b/src/frontend/src/components/LeftNavBar/index.tsx @@ -28,7 +28,7 @@ export const LeftNavBar = () => { my Room -

마이

+

MY

diff --git a/src/frontend/src/components/Modal/RoomCreateModal/index.css.tsx b/src/frontend/src/components/Modal/RoomCreateModal/index.css.tsx index e112c369..6d81d84f 100644 --- a/src/frontend/src/components/Modal/RoomCreateModal/index.css.tsx +++ b/src/frontend/src/components/Modal/RoomCreateModal/index.css.tsx @@ -16,6 +16,31 @@ export const CommonInput = styled.input` } `; +export const TitleLength = styled.div` + width: 100%; + text-align: right; + font-size: 0.875rem; + color: var(--palette-font-gray); + margin: -0.625rem 0.625rem 0.625rem 0; +`; + +export const TextArea = styled.textarea` + width: 100%; + padding: 0.625rem; + margin: 0.625rem 0 1.25rem; + background: var(--palette-fill-normal); + border: 1px solid var(--palette-line-normal-normal); + border-radius: 0.5rem; + font-size: 1rem; + color: var(--palette-font-gray); + outline: none; + resize: none; + + &:focus-within { + border: 1px solid var(--palette-interaction-inactive); + } +`; + export const PrivacyToggleContainer = styled.div` display: flex; justify-content: center; diff --git a/src/frontend/src/components/Modal/RoomCreateModal/index.tsx b/src/frontend/src/components/Modal/RoomCreateModal/index.tsx index 824ed7c7..ddfbd838 100644 --- a/src/frontend/src/components/Modal/RoomCreateModal/index.tsx +++ b/src/frontend/src/components/Modal/RoomCreateModal/index.tsx @@ -2,7 +2,13 @@ import { ButtonColor } from '@/types/enums/ButtonColor'; import { ModalPortal } from '@/components/Modal/ModalPortal'; import { IModal } from '@/components/Modal'; import { Background, ButtonContainer, ModalContainer, Title } from '@/components/Modal/index.css'; -import { CommonInput, PrivacyButton, PrivacyToggleContainer } from './index.css'; +import { + CommonInput, + PrivacyButton, + PrivacyToggleContainer, + TextArea, + TitleLength, +} from './index.css'; import { CommonButton } from '@/components/common/Button'; import { useRef, useState } from 'react'; import VideoIcon from '@/assets/img/Video.svg'; @@ -10,7 +16,8 @@ import UserLineIcon from '@/assets/img/UsersLine.svg'; import UserLineWhiteIcon from '@/assets/img/UsersLine_W.svg'; import DisableEyeIcon from '@/assets/img/DisableEye.svg'; import DisableEyeWhiteIcon from '@/assets/img/DisableEye_W.svg'; - +import { getByteLength } from '@/utils/stringUtils'; +import { useCreateRoom } from '@/hooks/queries/useCreateRoom'; interface IRoomCreateModal { onCancel: () => void; } @@ -18,13 +25,34 @@ interface IRoomCreateModal { export const RoomCreateModal = ({ onCancel }: IRoomCreateModal) => { const titleRef = useRef(null); const [isPublic, setIsPublic] = useState(true); + const [titleLength, setTitleLength] = useState(0); + const createRoom = useCreateRoom(); const handleTitleChange = () => { - console.log('title:', titleRef.current?.value); + const title = titleRef.current?.value || ''; + const titleByteLength = getByteLength(title); + if (titleByteLength > 60) { + titleRef.current?.setCustomValidity('제목은 한글 20자, 영어 60자 이하로 입력해주세요.'); + titleRef.current?.reportValidity(); + setTitleLength(titleByteLength); + } else { + titleRef.current?.setCustomValidity(''); + setTitleLength(titleByteLength); + } }; const handleCreation = () => { - console.log('생성'); + if (titleRef.current?.value === '') { + titleRef.current?.setCustomValidity('제목을 입력해주세요.'); + titleRef.current?.reportValidity(); + return; + } + + createRoom.mutate({ + title: titleRef.current?.value || '', + description: '', + isPublic: isPublic, + }); onCancel(); }; @@ -52,6 +80,8 @@ export const RoomCreateModal = ({ onCancel }: IRoomCreateModal) => { onChange={handleTitleChange} required /> + {`${titleLength} / 60`} +