diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
new file mode 100644
index 0000000..2b837ef
--- /dev/null
+++ b/.storybook/preview-head.html
@@ -0,0 +1,4 @@
+
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
new file mode 100644
index 0000000..1c183a6
--- /dev/null
+++ b/.storybook/preview.ts
@@ -0,0 +1,21 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import type { Preview } from '@storybook/react';
+import { initialize, mswLoader } from 'msw-storybook-addon';
+
+// MSW 초기화
+initialize();
+
+const preview: Preview = {
+ parameters: {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/
+ }
+ }
+ },
+ loaders: [mswLoader]
+};
+
+export default preview;
diff --git a/package.json b/package.json
index d41146d..a7c2c66 100644
--- a/package.json
+++ b/package.json
@@ -9,11 +9,13 @@
"init": "^0.1.2",
"jest": "^29.7.0",
"moment": "^2.30.1",
+ "msw": "^2.6.0",
"react": "^18.3.1",
"react-calendar": "^5.1.0",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"recharts": "^2.13.0",
+ "react-spinners": "^0.14.1",
"storybook": "^8.3.0",
"styled-components": "^6.1.13",
"webpack-merge": "^6.0.1",
@@ -57,6 +59,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"husky": "^8.0.0",
+ "msw-storybook-addon": "^2.0.3",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
@@ -75,5 +78,10 @@
"format": "prettier --cache --write .",
"lint": "eslint --cache .",
"test": "jest"
+ },
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
}
}
diff --git a/public/index.html b/public/index.html
index 2de31c1..2d39747 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,6 +9,10 @@
type="text/css"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"
/>
+
Webpack without CRA
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
new file mode 100644
index 0000000..fc45489
--- /dev/null
+++ b/public/mockServiceWorker.js
@@ -0,0 +1,295 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const PACKAGE_VERSION = '2.6.0';
+const INTEGRITY_CHECKSUM = '07a8241b182f8a246a7cd39894799a9e';
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
+const activeClientIds = new Set();
+
+self.addEventListener('install', function () {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id;
+
+ if (!clientId || !self.clients) {
+ return;
+ }
+
+ const client = await self.clients.get(clientId);
+
+ if (!client) {
+ return;
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window'
+ });
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE'
+ });
+ break;
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM
+ }
+ });
+ break;
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId);
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: {
+ client: {
+ id: client.id,
+ frameType: client.frameType
+ }
+ }
+ });
+ break;
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId);
+ break;
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId);
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId;
+ });
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister();
+ }
+
+ break;
+ }
+ }
+});
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event;
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return;
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return;
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return;
+ }
+
+ // Generate unique request ID.
+ const requestId = crypto.randomUUID();
+ event.respondWith(handleRequest(event, requestId));
+});
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event);
+ const response = await getResponse(event, client, requestId);
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ (async function () {
+ const responseClone = response.clone();
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ body: responseClone.body,
+ headers: Object.fromEntries(
+ responseClone.headers.entries()
+ )
+ }
+ },
+ [responseClone.body]
+ );
+ })();
+ }
+
+ return response;
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId);
+
+ if (activeClientIds.has(event.clientId)) {
+ return client;
+ }
+
+ if (client?.frameType === 'top-level') {
+ return client;
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window'
+ });
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible';
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id);
+ });
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event;
+
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = request.clone();
+
+ function passthrough() {
+ const headers = Object.fromEntries(requestClone.headers.entries());
+
+ // Remove internal MSW request header so the passthrough request
+ // complies with any potential CORS preflight checks on the server.
+ // Some servers forbid unknown request headers.
+ delete headers['x-msw-intention'];
+
+ return fetch(requestClone, { headers });
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough();
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough();
+ }
+
+ // Notify the client that a request has been intercepted.
+ const requestBuffer = await request.arrayBuffer();
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: requestBuffer,
+ keepalive: request.keepalive
+ }
+ },
+ [requestBuffer]
+ );
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data);
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough();
+ }
+ }
+
+ return passthrough();
+}
+
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error);
+ }
+
+ resolve(event.data);
+ };
+
+ client.postMessage(
+ message,
+ [channel.port2].concat(transferrables.filter(Boolean))
+ );
+ });
+}
+
+async function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error();
+ }
+
+ const mockedResponse = new Response(response.body, response);
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true
+ });
+
+ return mockedResponse;
+}
diff --git a/src/entities/music/api/fetchMusicList.ts b/src/entities/music/api/fetchMusicList.ts
new file mode 100644
index 0000000..d29a8bd
--- /dev/null
+++ b/src/entities/music/api/fetchMusicList.ts
@@ -0,0 +1,101 @@
+import { MusicItem, YouTubeResponse, SpotifyResponse } from '../model/type';
+import defaultApi from '../../../shared/api/api';
+import axios from 'axios';
+import React from 'react';
+
+/**
+ * 스포티파이 토큰 발급
+ * TODO - 토큰 캐싱 로직 추가 => useSearchMusic
+ * TODO - 토큰 만료 체크 로직 추가
+ * @returns
+ */
+const getSpotifyToken = async () => {
+ const clientId = process.env.REACT_APP_SPOTIFY_CLIENT_ID;
+ const clientSecret = process.env.REACT_APP_SPOTIFY_CLIENT_SECRET;
+
+ try {
+ const response = await axios.post(
+ 'https://accounts.spotify.com/api/token',
+ 'grant_type=client_credentials',
+ {
+ headers: {
+ Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ }
+ );
+ return response.data.access_token;
+ } catch (error) {
+ console.error('토큰 발급 에러 :', error);
+ throw error;
+ }
+};
+
+/**
+ * 스포티파이 api 인스턴스
+ */
+const createSpotifyApi = async () => {
+ const token = await getSpotifyToken();
+ return defaultApi({
+ baseURL: 'https://api.spotify.com/v1',
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ });
+};
+
+/**
+ * 유튜브 api 인스턴스
+ */
+const youtubeApi = defaultApi({
+ baseURL: 'https://www.googleapis.com/youtube/v3',
+ params: {
+ key: process.env.REACT_APP_YOUTUBE_API_KEY
+ }
+});
+
+/**
+ * 음악 검색 함수
+ * @param query 검색어 - gpt 응답 / 사용자 직접 입력
+ * @returns
+ */
+export const searchMusic = async (query: string): Promise => {
+ try {
+ // 스포티파이 검색
+ const spotifyApi = await createSpotifyApi();
+ const spotifyResponse = await spotifyApi.get('/search', {
+ params: {
+ q: query,
+ type: 'track',
+ limit: 1
+ }
+ });
+
+ const spotifyTrack = spotifyResponse.data.tracks
+ .items[0] as SpotifyResponse;
+
+ // 유튜브 검색
+ const youtubeResponse = await youtubeApi.get('/search', {
+ params: {
+ q: `${spotifyTrack.name} ${spotifyTrack.artists[0].name}`,
+ part: 'snippet',
+ type: 'video',
+ maxResults: 1
+ }
+ });
+
+ const youtubeVideo = youtubeResponse.data.items[0] as YouTubeResponse;
+
+ return {
+ title: spotifyTrack.name,
+ artist: spotifyTrack.artists[0].name,
+ thumbnailUrl: spotifyTrack.album.images[0].url,
+ // youtubeId: 'M5aEiDSx7kI'
+ // api 할당량 제한으로 그냥 아무 영상 id로 고정하고 테스트 중입니다.
+ youtubeId: youtubeVideo.id.videoId
+ };
+ } catch (error) {
+ console.error('스포티파이 음악 검색 실패 :', error);
+ throw error;
+ }
+};
diff --git a/src/entities/music/hooks/useSearchMusic.ts b/src/entities/music/hooks/useSearchMusic.ts
new file mode 100644
index 0000000..8a604dc
--- /dev/null
+++ b/src/entities/music/hooks/useSearchMusic.ts
@@ -0,0 +1,13 @@
+// import { useQuery } from '@tanstack/react-query';
+// import { searchMusic } from '../api/fetchMusicList';
+
+// export const useSearchMusic = (query: string) => {
+// return useQuery({
+// queryKey: ['music-search', query],
+// queryFn: () => searchMusic(query),
+// enabled: !!query,
+// staleTime: 10 * 60 * 1000,
+// gcTime: 30 * 60 * 1000,
+// retry: 2
+// });
+// };
diff --git a/src/entities/music/index.ts b/src/entities/music/index.ts
new file mode 100644
index 0000000..9d6d66a
--- /dev/null
+++ b/src/entities/music/index.ts
@@ -0,0 +1,4 @@
+export { MusicCard } from './ui/MusicCard';
+export { EmptyMusicCard } from './ui/EmptyMusicCard';
+// export { useSearchMusic } from './hooks/useSearchMusic';
+export { searchMusic } from './api/fetchMusicList';
diff --git a/src/entities/music/model/type.ts b/src/entities/music/model/type.ts
new file mode 100644
index 0000000..1a2e1e9
--- /dev/null
+++ b/src/entities/music/model/type.ts
@@ -0,0 +1,40 @@
+export interface MusicItem {
+ youtubeId: string;
+ thumbnailUrl: string;
+ title: string;
+ artist: string;
+}
+
+export interface MusicCardListProps {
+ musicList: MusicItem[];
+}
+
+export interface MusicCardProps {
+ youtubeId: string; // 유튜브 ID
+ thumbnailUrl: string; // 스포티파이 썸네일
+ title: string; // 곡 제목
+ artist: string; // 곡 아티스트
+ $isPlaying?: boolean; // 재생 상태
+ onPlay?: (youtubeId: string) => void; // 재생 함수
+ onClick: () => void; // 클릭시 아이템 셋팅
+ $isSelected?: boolean;
+}
+
+export interface MusicListQueryParams {
+ title: string;
+ artist: string;
+}
+
+export interface SpotifyResponse {
+ name: string;
+ artists: Array<{ name: string }>;
+ album: {
+ images: Array<{ url: string }>;
+ };
+}
+
+export interface YouTubeResponse {
+ id: {
+ videoId: string;
+ };
+}
diff --git a/src/entities/music/model/useMusicStore.ts b/src/entities/music/model/useMusicStore.ts
new file mode 100644
index 0000000..52872f6
--- /dev/null
+++ b/src/entities/music/model/useMusicStore.ts
@@ -0,0 +1,17 @@
+import { create } from 'zustand';
+import { MusicItem } from './type';
+import React from 'react';
+
+interface MusicStore {
+ selectedMusic: MusicItem | null;
+ setSelectedMusic: (music: MusicItem | null) => void;
+ clearSelectedMusic: () => void;
+}
+
+export const useMusicStore = create((set) => ({
+ selectedMusic: null,
+ setSelectedMusic: (music) => set({ selectedMusic: music }),
+ clearSelectedMusic: () => set({ selectedMusic: null })
+}));
+
+export default useMusicStore;
diff --git a/src/entities/music/ui/EmptyMusicCard.styled.tsx b/src/entities/music/ui/EmptyMusicCard.styled.tsx
new file mode 100644
index 0000000..0e822af
--- /dev/null
+++ b/src/entities/music/ui/EmptyMusicCard.styled.tsx
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ width: 250px;
+ height: 300px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 10px;
+ padding: 1rem;
+ justify-content: center;
+`;
+
+export const TextWrap = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/src/entities/music/ui/EmptyMusicCard.tsx b/src/entities/music/ui/EmptyMusicCard.tsx
new file mode 100644
index 0000000..9276e5b
--- /dev/null
+++ b/src/entities/music/ui/EmptyMusicCard.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Container, TextWrap } from './EmptyMusicCard.styled';
+
+export const EmptyMusicCard = () => {
+ return (
+
+ 제목으로 노래를 검색해주세요!
+
+ );
+};
diff --git a/src/entities/music/ui/MusicCard.stories.tsx b/src/entities/music/ui/MusicCard.stories.tsx
new file mode 100644
index 0000000..3a76c23
--- /dev/null
+++ b/src/entities/music/ui/MusicCard.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import {MusicCard} from './MusicCard';
+import React from 'react';
+
+const meta: Meta = {
+ title: 'entities/music/MusicCard',
+ component: MusicCard,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+// 이미지 테스트
+const sampleImageUrl = "/samplecover.jpg";
+
+export const Default: Story = {
+ args: {
+ thumbnailUrl: sampleImageUrl,
+ title: "Supernatural",
+ artist: "뉴진스",
+ $isPlaying: false,
+ onPlay: () => {},
+ },
+};
+
+export const Playing: Story = {
+ args: {
+ thumbnailUrl: sampleImageUrl,
+ title: "뉴진스",
+ artist: "Supernatural",
+ $isPlaying: true,
+ onPlay: () => {},
+ },
+};
\ No newline at end of file
diff --git a/src/entities/music/ui/MusicCard.styled.tsx b/src/entities/music/ui/MusicCard.styled.tsx
new file mode 100644
index 0000000..7411d56
--- /dev/null
+++ b/src/entities/music/ui/MusicCard.styled.tsx
@@ -0,0 +1,155 @@
+import styled, { keyframes, css } from 'styled-components';
+
+interface ContainerProps {
+ $isSelected: boolean;
+}
+
+interface ThumbnailProps {
+ $isPlaying: boolean;
+}
+
+const colorAnimation = keyframes`
+ 0% {
+ border-color: #FF480E;
+ }
+ 50% {
+ border-color: #ffc5b3;
+ }
+ 100% {
+ border-color: #FF480E;
+ }
+`;
+
+export const Container = styled.div`
+ width: 250px;
+ /* height: 300px; */
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 10px;
+ transition: all 0.2s ease;
+ border: ${(props) => (props.$isSelected ? '2px solid #a6a6a6' : 'none')};
+ background-color: ${(props) => (props.$isSelected ? '#efefef' : 'none')};
+ &:hover {
+ background-color: #f2f2f2;
+ cursor: pointer;
+ }
+ padding: 1rem;
+ justify-content: center;
+ gap: 0.5rem;
+`;
+
+export const ThumbnailContainer = styled.div`
+ width: 250px;
+ height: 250px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const IconWrapper = styled.div<{ $show: boolean }>`
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 3;
+ opacity: ${(props) => (props.$show ? 1 : 0)};
+ transition: opacity 0.2s ease;
+ background-color: rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ padding: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .material-symbols-outlined {
+ font-size: 35px;
+ color: #ffffff;
+ }
+`;
+
+export const Thumbnail = styled.div`
+ border-radius: 100%;
+ width: 220px;
+ height: 220px;
+ overflow: hidden;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+
+ ${IconWrapper} {
+ opacity: ${(props) => (props.$isPlaying ? 1 : 0)};
+ }
+
+ &:hover ${IconWrapper} {
+ opacity: 1;
+ }
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -10px;
+ left: -10px;
+ right: -10px;
+ bottom: -10px;
+ border-radius: 100%;
+ border: 15px solid transparent;
+ pointer-events: none;
+ transition: all 0.2s ease;
+ z-index: 2;
+
+ ${(props) =>
+ props.$isPlaying &&
+ css`
+ animation: ${colorAnimation} 2s ease-in-out infinite;
+ `}
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: ${(props) =>
+ props.$isPlaying ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0)'};
+ transition: background-color 0.2s ease;
+ z-index: 1;
+ }
+
+ &:hover {
+ cursor: pointer;
+ &::after {
+ background-color: ${(props) =>
+ props.$isPlaying ? 'rgba(0, 0, 0, 0.1)' : 'rgba(0, 0, 0, 0.2)'};
+ }
+ }
+`;
+
+export const StyledImg = styled.img`
+ object-fit: cover;
+ width: 220px;
+ height: 220px;
+ z-index: 0;
+`;
+
+export const TextWrap = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+`;
+
+export const Title = styled.div`
+ font-weight: bold;
+ font-size: 20px;
+ display: flex;
+ width: 100%;
+ text-align: center;
+`;
+
+export const Artist = styled.div`
+ color: #939393;
+ font-size: 14px;
+`;
diff --git a/src/entities/music/ui/MusicCard.tsx b/src/entities/music/ui/MusicCard.tsx
new file mode 100644
index 0000000..44befcb
--- /dev/null
+++ b/src/entities/music/ui/MusicCard.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import {
+ Container,
+ ThumbnailContainer,
+ Thumbnail,
+ StyledImg,
+ TextWrap,
+ Title,
+ Artist,
+ IconWrapper
+} from './MusicCard.styled';
+import { MusicCardProps } from '../model/type';
+
+export const MusicCard = ({
+ youtubeId,
+ thumbnailUrl,
+ title,
+ artist,
+ $isPlaying = false,
+ onPlay,
+ onClick,
+ $isSelected = false
+}: MusicCardProps) => {
+ /**
+ * 이벤트 버블링을 막아서 썸네일을 클릭했을 때는 노래 재생만 처리합니다.
+ * @param e
+ */
+ const handlePlay = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (onPlay) {
+ onPlay(youtubeId);
+ }
+ };
+ return (
+
+
+
+
+
+ {$isPlaying ? (
+
+ pause
+
+ ) : (
+
+ play_arrow
+
+ )}
+
+
+
+
+ {title}
+ {artist}
+
+
+ );
+};
diff --git a/src/features/diary-write/index.ts b/src/features/diary-write/index.ts
index 7d8d809..8bc9c91 100644
--- a/src/features/diary-write/index.ts
+++ b/src/features/diary-write/index.ts
@@ -1,2 +1,4 @@
export { VisibilityButton } from './visibility/ui/VisibilityButton';
export { ConditionButtonGroup } from './condition/ui/ConditionButtonGroup';
+export { SearchModeButtonGroup } from './search-mode-selector/ui/SearchModeButtonGroup';
+export { MusicSearchInput } from './music-search-input/ui/MusicSearchInput';
diff --git a/src/features/diary-write/music-search-input/model/type.ts b/src/features/diary-write/music-search-input/model/type.ts
new file mode 100644
index 0000000..70dd11f
--- /dev/null
+++ b/src/features/diary-write/music-search-input/model/type.ts
@@ -0,0 +1,3 @@
+export interface MusicSearchInputProps {
+ onSearch: (keyword: string) => void;
+}
diff --git a/src/features/diary-write/music-search-input/ui/MusicSearchInput.styled.tsx b/src/features/diary-write/music-search-input/ui/MusicSearchInput.styled.tsx
new file mode 100644
index 0000000..f7eb6af
--- /dev/null
+++ b/src/features/diary-write/music-search-input/ui/MusicSearchInput.styled.tsx
@@ -0,0 +1,15 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+`;
+
+export const SearchFormWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ align-items: flex-end;
+ gap: 0.5rem;
+`;
diff --git a/src/features/diary-write/music-search-input/ui/MusicSearchInput.tsx b/src/features/diary-write/music-search-input/ui/MusicSearchInput.tsx
new file mode 100644
index 0000000..709ae5c
--- /dev/null
+++ b/src/features/diary-write/music-search-input/ui/MusicSearchInput.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import InputForm from '@/shared/ui/InputForm/InputForm';
+import { Container, SearchFormWrapper } from './MusicSearchInput.styled';
+import { useState } from 'react';
+import { MusicSearchInputProps } from '../model/type';
+import Button from '@/shared/ui/Button/Button';
+
+export const MusicSearchInput = ({ onSearch }: MusicSearchInputProps) => {
+ const [searchKeyword, setSearchKeyword] = useState('');
+
+ // TODO - 디바운싱
+ const handleContentChange = (value: string) => {
+ setSearchKeyword(value);
+ };
+
+ const handleSearchClick = () => {
+ onSearch(searchKeyword);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && searchKeyword.trim()) {
+ onSearch(searchKeyword);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/diary-write/musicList/model/type.ts b/src/features/diary-write/musicList/model/type.ts
new file mode 100644
index 0000000..268c74a
--- /dev/null
+++ b/src/features/diary-write/musicList/model/type.ts
@@ -0,0 +1,12 @@
+export interface MusicItem {
+ youtubeId: string;
+ thumbnailUrl: string;
+ title: string;
+ artist: string;
+}
+
+export interface MusicCardListProps {
+ type: string; // 리스트 타입
+ responseMusicList: MusicItem[];
+ // onChange: (music: MusicItem) => void;
+}
diff --git a/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx b/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx
new file mode 100644
index 0000000..63189d7
--- /dev/null
+++ b/src/features/diary-write/musicList/ui/MusicCardList.stories.tsx
@@ -0,0 +1,53 @@
+// MusicCardList.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react';
+import { MusicCardList } from './MusicCardList';
+import { SEARCH_TYPE } from '@/features/diary-write/search-mode-selector/model/type';
+import { MusicItem } from '@/entities/music/model/type';
+
+const meta = {
+ title: 'Features/DiaryWrite/MusicCardList',
+ component: MusicCardList,
+ tags: ['autodocs']
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const sampleMusicList: MusicItem[] = [
+ {
+ title: "Sample Music 1",
+ thumbnailUrl: "thumbnail_url_1",
+ artist: "Artist 1",
+ youtubeId: "video_id_1"
+ },
+ {
+ title: "Sample Music 2",
+ thumbnailUrl: "thumbnail_url_2",
+ artist: "Artist 2",
+ youtubeId: "video_id_2"
+ }
+];
+
+export const GPTRecommend: Story = {
+ args: {
+ type: SEARCH_TYPE.GPT,
+ responseMusicList: sampleMusicList,
+ // onChange: (item: MusicItem) => console.log('Selected:', item)
+ }
+};
+
+export const UserSearch: Story = {
+ args: {
+ type: SEARCH_TYPE.USER,
+ responseMusicList: sampleMusicList,
+ // onChange: (item: MusicItem) => console.log('Selected:', item)
+ }
+};
+
+export const Empty: Story = {
+ args: {
+ type: SEARCH_TYPE.USER,
+ responseMusicList: [],
+ // onChange: (item: MusicItem) => console.log('Selected:', item)
+ }
+};
\ No newline at end of file
diff --git a/src/features/diary-write/musicList/ui/MusicCardList.styled.tsx b/src/features/diary-write/musicList/ui/MusicCardList.styled.tsx
new file mode 100644
index 0000000..40d5d1d
--- /dev/null
+++ b/src/features/diary-write/musicList/ui/MusicCardList.styled.tsx
@@ -0,0 +1,21 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 35px;
+ width: 100%;
+ /* padding: 2rem; */
+`;
+
+export const HiddenYoutubeContainer = styled.div`
+ position: fixed;
+ top: -9999px;
+ left: -9999px;
+ width: 1px;
+ height: 1px;
+ visibility: hidden;
+`;
diff --git a/src/features/diary-write/musicList/ui/MusicCardList.tsx b/src/features/diary-write/musicList/ui/MusicCardList.tsx
new file mode 100644
index 0000000..94426a9
--- /dev/null
+++ b/src/features/diary-write/musicList/ui/MusicCardList.tsx
@@ -0,0 +1,82 @@
+import React, { useEffect, useState } from 'react';
+import { Container, HiddenYoutubeContainer } from './MusicCardList.styled';
+import { MusicItem, MusicCardListProps } from '../model/type';
+import { EmptyMusicCard, MusicCard } from '../../../../entities/music';
+import useMusicStore from '@/entities/music/model/useMusicStore';
+
+export const MusicCardList = ({
+ responseMusicList
+ // onChange
+}: MusicCardListProps) => {
+ const [nowPlaying, setNowPaying] = useState(null);
+ const { selectedMusic, setSelectedMusic, clearSelectedMusic } =
+ useMusicStore();
+
+ /**
+ * iframe에 비디오 아이디를 셋팅합니다.
+ * @param videoId :string
+ */
+ const handlePlay = (youtubeId: string) => {
+ if (nowPlaying === youtubeId) {
+ setNowPaying(null);
+ console.log('노래 재생 중지');
+ } else {
+ setNowPaying(youtubeId);
+ console.log('노래 재생 중 : ', youtubeId);
+ }
+ };
+
+ /**
+ * 사용자가 선택한 음악 정보를 셋팅합니다.
+ * @param item :MusicItem 클릭한 카드 컴포넌트 음악 정보
+ */
+ const handleClick = (item: MusicItem) => {
+ if (item === selectedMusic) {
+ clearSelectedMusic();
+ } else {
+ setSelectedMusic(item);
+ }
+ };
+
+ // 테스트
+ useEffect(() => {
+ console.log(selectedMusic);
+ }, [selectedMusic]);
+
+ // TODO - iframe 유튜브 api 모듈로 변경
+ return (
+
+
+ {nowPlaying && (
+
+ )}
+
+ {responseMusicList.length === 0 ? (
+
+ ) : (
+ responseMusicList.map((music) => (
+ handleClick(music)}
+ $isSelected={
+ selectedMusic?.youtubeId === music.youtubeId
+ }
+ />
+ ))
+ )}
+
+ );
+};
diff --git a/src/features/diary-write/search-mode-selector/model/type.ts b/src/features/diary-write/search-mode-selector/model/type.ts
new file mode 100644
index 0000000..e893676
--- /dev/null
+++ b/src/features/diary-write/search-mode-selector/model/type.ts
@@ -0,0 +1,11 @@
+export const SEARCH_TYPE = {
+ GPT: 'gptSearch',
+ USER: 'userSearch'
+} as const;
+
+export type SearchType = (typeof SEARCH_TYPE)[keyof typeof SEARCH_TYPE];
+
+export interface SearchModeButtonGroupProps {
+ selectedType: SearchType;
+ onChange: (type: SearchType) => void;
+}
diff --git a/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.stories.tsx b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.stories.tsx
new file mode 100644
index 0000000..8b72ce1
--- /dev/null
+++ b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.stories.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { SearchModeButtonGroup } from './SearchModeButtonGroup';
+import { SEARCH_TYPE } from '../model/type';
+
+const meta = {
+ title: 'Features/DiaryWrite/SearchModeButtonGroup',
+ component: SearchModeButtonGroup,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ selectedType: {
+ control: 'select',
+ options: Object.values(SEARCH_TYPE),
+ description: '현재 선택되어있는 기분',
+ },
+ onChange: {
+ description: '기분 버튼 클릭시 호출 : 부모 컴포넌트로 선택된 기분 전달'
+ }
+ }
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+// 기본 - gpt
+export const Default: Story = {
+ args: {
+ selectedType: SEARCH_TYPE.GPT,
+ onChange: (condition) => {
+ console.log(condition);
+ },
+ },
+};
+
+// 유저 검색
+export const Selected: Story = {
+ args: {
+ selectedType: SEARCH_TYPE.USER,
+ onChange: (condition) => {
+ console.log(condition);
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.styled.tsx b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.styled.tsx
new file mode 100644
index 0000000..662652c
--- /dev/null
+++ b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.styled.tsx
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+`;
+
+interface ButtonProps {
+ isActive?: boolean;
+}
+
+export const StyledButton = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 60px;
+ border-radius: 5px;
+ transition: all 0.2s ease;
+ background-color: ${(props) => (props.isActive ? '#FFF4F1' : '#ffffff')};
+ border: 1px solid
+ ${(props) => (props.isActive ? '#FF480E' : 'rgba(0, 0, 0, 0.1)')};
+ &:hover {
+ cursor: pointer;
+ background-color: ${(props) =>
+ props.isActive ? '#FFF4F1' : 'rgba(0, 0, 0, 0.1)'};
+ }
+`;
diff --git a/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.tsx b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.tsx
new file mode 100644
index 0000000..62d74a6
--- /dev/null
+++ b/src/features/diary-write/search-mode-selector/ui/SearchModeButtonGroup.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Container, StyledButton } from './SearchModeButtonGroup.styled';
+import { SEARCH_TYPE, SearchModeButtonGroupProps } from '../model/type';
+
+export const SearchModeButtonGroup = ({
+ selectedType,
+ onChange
+}: SearchModeButtonGroupProps) => {
+ return (
+
+ onChange(SEARCH_TYPE.GPT)}
+ type="button"
+ aria-label="AI에게 추천받기"
+ >
+ AI에게 추천받기
+
+ onChange(SEARCH_TYPE.USER)}
+ type="button"
+ aria-label="제목으로 검색하기"
+ >
+ 직접 검색하기
+
+
+ );
+};
diff --git a/src/shared/api/mocks/handlers.ts b/src/shared/api/mocks/handlers.ts
new file mode 100644
index 0000000..bd85142
--- /dev/null
+++ b/src/shared/api/mocks/handlers.ts
@@ -0,0 +1,26 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { http, HttpResponse } from 'msw';
+import { MusicItem } from '../../../entities/music/model/type';
+
+const mockMusicData: MusicItem = {
+ title: '테스트 음악',
+ artist: '테스트 아티스트',
+ thumbnailUrl: 'https://via.placeholder.com/300',
+ youtubeId: 'test-youtube-id'
+};
+
+export const handlers = [
+ http.get('/search', async () => {
+ return HttpResponse.json(mockMusicData, {
+ status: 200,
+ statusText: 'OK'
+ });
+ }),
+
+ http.get('/search-error', async () => {
+ return HttpResponse.json(
+ { message: '에러가 발생했습니다' },
+ { status: 500 }
+ );
+ })
+];
diff --git a/src/shared/ui/InputForm/InputForm.styled.tsx b/src/shared/ui/InputForm/InputForm.styled.tsx
index 0e11e7a..83a5b2b 100644
--- a/src/shared/ui/InputForm/InputForm.styled.tsx
+++ b/src/shared/ui/InputForm/InputForm.styled.tsx
@@ -28,7 +28,8 @@ export const StyledInput = styled.input<{
height: string;
}>`
height: ${(props) => props.height};
- width: calc(100% - 32px);
+ width: 100%;
+ box-sizing: border-box;
padding: 0 16px;
font-size: 16px;
color: #000000;
diff --git a/src/shared/ui/InputForm/InputForm.tsx b/src/shared/ui/InputForm/InputForm.tsx
index ace7ad5..31550fc 100644
--- a/src/shared/ui/InputForm/InputForm.tsx
+++ b/src/shared/ui/InputForm/InputForm.tsx
@@ -24,7 +24,8 @@ const InputForm = ({
isDropdown = false,
isTextarea = false,
options = ['남성', '여성'],
- onChange
+ onChange,
+ onKeyDown
}: InputFormProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
@@ -85,6 +86,7 @@ const InputForm = ({
placeholder={placeholder}
height={height}
onChange={handleInputChange}
+ onKeyDown={onKeyDown}
/>
{isPassword && (
void; // 값 변경 시 호출되는 함수
+ onKeyDown?: (e: React.KeyboardEvent) => void; // 엔터키 이벤트
}
diff --git a/src/widgets/select-music/ui/SelectMusicContainer.stories.tsx b/src/widgets/select-music/ui/SelectMusicContainer.stories.tsx
new file mode 100644
index 0000000..e53d793
--- /dev/null
+++ b/src/widgets/select-music/ui/SelectMusicContainer.stories.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { SelectMusicContainer } from './SelectMusicContainer';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+// QueryClient 인스턴스 생성
+const queryClient = new QueryClient();
+
+// Wrapper 컴포넌트
+const WithQueryClient = (Story: React.ComponentType) => (
+
+
+
+);
+
+const meta = {
+ title: 'Widgets/SelectMusicContainer',
+ component: SelectMusicContainer,
+ decorators: [
+ (Story) => WithQueryClient(Story)
+ ],
+ parameters: {
+ layout: 'centered'
+ },
+ tags: ['autodocs']
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+// 기본 스토리
+export const Default: Story = {
+ args: {}
+};
\ No newline at end of file
diff --git a/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx b/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx
new file mode 100644
index 0000000..2f2da4c
--- /dev/null
+++ b/src/widgets/select-music/ui/SelectMusicContainer.styled.tsx
@@ -0,0 +1,9 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ width: 500px; // 임시 너비
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+`;
diff --git a/src/widgets/select-music/ui/SelectMusicContainer.tsx b/src/widgets/select-music/ui/SelectMusicContainer.tsx
new file mode 100644
index 0000000..8862838
--- /dev/null
+++ b/src/widgets/select-music/ui/SelectMusicContainer.tsx
@@ -0,0 +1,85 @@
+import React, { useEffect, useState } from 'react';
+import { Container } from './SelectMusicContainer.styled';
+import {
+ MusicSearchInput,
+ SearchModeButtonGroup
+} from '@/features/diary-write';
+import { MusicCardList } from '@/features/diary-write/musicList/ui/MusicCardList';
+import { MusicItem } from '@/entities/music/model/type';
+import {
+ SearchType,
+ SEARCH_TYPE
+} from '@/features/diary-write/search-mode-selector/model/type';
+import { searchMusic } from '@/entities/music';
+
+// GPT 테스트용 리스트입니다
+// 실제로는 API가 내려준 리스트가 들어옵니다.
+const testList = ['뉴진스 supernatural'];
+
+// TODO - 로딩스피너 추가
+export const SelectMusicContainer = () => {
+ const [selectedType, setSelectedType] = useState(
+ SEARCH_TYPE.GPT
+ );
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [responseUserMusicList, setresponseUserMusicList] = useState<
+ MusicItem[]
+ >([]);
+ const [responseGptMusicList, setresponseGptMusicList] = useState<
+ MusicItem[]
+ >([]);
+
+ // 타입 셋팅
+ const handleTypeChange = (type: SearchType) => {
+ setSelectedType(type);
+ };
+
+ // 검색어 처리
+ const handleSearchChange = (value: string) => {
+ console.log('검색어 : ', value);
+ setSearchKeyword(value);
+ };
+
+ // 검색
+ useEffect(() => {
+ const fetchMusicData = async () => {
+ try {
+ let searchResult: Promise[];
+ if (selectedType === SEARCH_TYPE.USER) {
+ searchResult = [searchMusic(searchKeyword)];
+ setresponseUserMusicList(await Promise.all(searchResult));
+ } else if (selectedType === SEARCH_TYPE.GPT) {
+ searchResult = testList.map((keyword) => {
+ return searchMusic(keyword);
+ });
+ setresponseGptMusicList(await Promise.all(searchResult));
+ } else {
+ console.log('리스트 불러오기 에러');
+ }
+ } catch (fetcherr) {
+ console.log('음악 리스트 불러오기 에러 : ', fetcherr);
+ }
+ };
+ fetchMusicData();
+ }, [selectedType, searchKeyword]);
+
+ return (
+
+
+ {selectedType === SEARCH_TYPE.USER && (
+
+ )}
+
+
+ );
+};
diff --git a/yarn.lock b/yarn.lock
index cccef4c..cd33764 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1382,6 +1382,28 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+"@bundled-es-modules/cookie@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz#c3b82703969a61cf6a46e959a012b2c257f6b164"
+ integrity sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==
+ dependencies:
+ cookie "^0.5.0"
+
+"@bundled-es-modules/statuses@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872"
+ integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==
+ dependencies:
+ statuses "^2.0.1"
+
+"@bundled-es-modules/tough-cookie@^0.1.6":
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3"
+ integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==
+ dependencies:
+ "@types/tough-cookie" "^4.0.5"
+ tough-cookie "^4.1.4"
+
"@chromatic-com/storybook@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-1.9.0.tgz#d95eb3474783bcc17a830a7627c3f099c1f75ba5"
@@ -1623,6 +1645,39 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
+"@inquirer/confirm@^5.0.0":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.0.1.tgz#35e0aa0f9fdaadee3acb1c42024e707af308fced"
+ integrity sha512-6ycMm7k7NUApiMGfVc32yIPp28iPKxhGRMqoNDiUjq2RyTAkbs5Fx0TdzBqhabcKvniDdAAvHCmsRjnNfTsogw==
+ dependencies:
+ "@inquirer/core" "^10.0.1"
+ "@inquirer/type" "^3.0.0"
+
+"@inquirer/core@^10.0.1":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.0.1.tgz#22068da87d8f6317452172dfd521e811ccbcb90e"
+ integrity sha512-KKTgjViBQUi3AAssqjUFMnMO3CM3qwCHvePV9EW+zTKGKafFGFF01sc1yOIYjLJ7QU52G/FbzKc+c01WLzXmVQ==
+ dependencies:
+ "@inquirer/figures" "^1.0.7"
+ "@inquirer/type" "^3.0.0"
+ ansi-escapes "^4.3.2"
+ cli-width "^4.1.0"
+ mute-stream "^2.0.0"
+ signal-exit "^4.1.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^6.2.0"
+ yoctocolors-cjs "^2.1.2"
+
+"@inquirer/figures@^1.0.7":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.7.tgz#d050ccc0eabfacc0248c4ff647a9dfba1b01594b"
+ integrity sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==
+
+"@inquirer/type@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.0.tgz#1762ebe667ec1d838012b20bf0cf90b841ba68bc"
+ integrity sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1903,6 +1958,18 @@
dependencies:
"@types/mdx" "^2.0.0"
+"@mswjs/interceptors@^0.36.5":
+ version "0.36.6"
+ resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.36.6.tgz#97560cca0d7f42c41d185ad404205fe14735cd30"
+ integrity sha512-issnYydStyH0wPEeU7CMwfO7kI668ffVtzKRMRS7H7BliOYuPuwEZxh9dwiXV+oeHBxT5SXT0wPwV8T7V2PJUA==
+ dependencies:
+ "@open-draft/deferred-promise" "^2.2.0"
+ "@open-draft/logger" "^0.3.0"
+ "@open-draft/until" "^2.0.0"
+ is-node-process "^1.2.0"
+ outvariant "^1.4.3"
+ strict-event-emitter "^0.5.1"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1929,6 +1996,24 @@
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
+"@open-draft/deferred-promise@^2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd"
+ integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==
+
+"@open-draft/logger@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954"
+ integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==
+ dependencies:
+ is-node-process "^1.2.0"
+ outvariant "^1.4.0"
+
+"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
+ integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
+
"@pkgr/core@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
@@ -2572,6 +2657,11 @@
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+"@types/cookie@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
+ integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
+
"@types/doctrine@^0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f"
@@ -2821,11 +2911,21 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
+"@types/statuses@^2.0.4":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63"
+ integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==
+
"@types/stylis@4.2.5":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df"
integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==
+"@types/tough-cookie@^4.0.5":
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
+ integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
+
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
@@ -3267,7 +3367,7 @@ ajv@^8.0.0, ajv@^8.9.0:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
-ansi-escapes@^4.2.1:
+ansi-escapes@^4.2.1, ansi-escapes@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
@@ -3890,6 +3990,11 @@ clean-webpack-plugin@^4.0.0:
dependencies:
del "^4.1.1"
+cli-width@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5"
+ integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
+
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@@ -4051,6 +4156,11 @@ cookie@0.6.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
+cookie@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
+ integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+
copy-webpack-plugin@^12.0.2:
version "12.0.2"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz#935e57b8e6183c82f95bd937df658a59f6a2da28"
@@ -5575,6 +5685,11 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+graphql@^16.8.1:
+ version "16.9.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f"
+ integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==
+
handle-thing@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
@@ -5652,6 +5767,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+headers-polyfill@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07"
+ integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==
+
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -6030,6 +6150,11 @@ is-network-error@^1.0.0:
resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997"
integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==
+is-node-process@^1.0.1, is-node-process@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134"
+ integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==
+
is-number-object@^1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
@@ -7035,6 +7160,37 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+msw-storybook-addon@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/msw-storybook-addon/-/msw-storybook-addon-2.0.3.tgz#6a9ccf19f89ec9dde1d17e4a3be71d93b42e857a"
+ integrity sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==
+ dependencies:
+ is-node-process "^1.0.1"
+
+msw@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/msw/-/msw-2.6.0.tgz#ab50752feb5c63de90d70e8165746e2ea84580dd"
+ integrity sha512-n3tx2w0MZ3H4pxY0ozrQ4sNPzK/dGtlr2cIIyuEsgq2Bhy4wvcW6ZH2w/gXM9+MEUY6HC1fWhqtcXDxVZr5Jxw==
+ dependencies:
+ "@bundled-es-modules/cookie" "^2.0.0"
+ "@bundled-es-modules/statuses" "^1.0.1"
+ "@bundled-es-modules/tough-cookie" "^0.1.6"
+ "@inquirer/confirm" "^5.0.0"
+ "@mswjs/interceptors" "^0.36.5"
+ "@open-draft/deferred-promise" "^2.2.0"
+ "@open-draft/until" "^2.1.0"
+ "@types/cookie" "^0.6.0"
+ "@types/statuses" "^2.0.4"
+ chalk "^4.1.2"
+ graphql "^16.8.1"
+ headers-polyfill "^4.0.2"
+ is-node-process "^1.2.0"
+ outvariant "^1.4.3"
+ path-to-regexp "^6.3.0"
+ strict-event-emitter "^0.5.1"
+ type-fest "^4.26.1"
+ yargs "^17.7.2"
+
multicast-dns@^7.2.5:
version "7.2.5"
resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
@@ -7043,6 +7199,11 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
+mute-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b"
+ integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==
+
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
@@ -7235,6 +7396,11 @@ p-defer@^1.0.0:
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==
+outvariant@^1.4.0, outvariant@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873"
+ integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==
+
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@@ -7374,6 +7540,11 @@ path-to-regexp@0.1.10:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
+path-to-regexp@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
+ integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -7594,12 +7765,17 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+psl@^1.1.33:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
+ integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
+
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
-punycode@^2.1.0:
+punycode@^2.1.0, punycode@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -7623,6 +7799,11 @@ qs@6.13.0, qs@^6.12.3:
dependencies:
side-channel "^1.0.6"
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -7754,6 +7935,11 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
+react-spinners@^0.14.1:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.14.1.tgz#de7d7d6b3e6d4f29d9620c65495b502c7dd90812"
+ integrity sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==
+
"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@@ -8296,6 +8482,11 @@ signal-exit@^3.0.3, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+signal-exit@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@@ -8391,7 +8582,7 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"
-statuses@2.0.1:
+statuses@2.0.1, statuses@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
@@ -8408,6 +8599,11 @@ storybook@^8.3.0:
dependencies:
"@storybook/core" "8.3.0"
+strict-event-emitter@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93"
+ integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==
+
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@@ -8710,6 +8906,16 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+tough-cookie@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
+ integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
tree-dump@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac"
@@ -8824,6 +9030,11 @@ type-fest@^2.19.0, type-fest@~2.19:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+type-fest@^4.26.1:
+ version "4.26.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
+ integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==
+
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -8948,6 +9159,11 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
+universalify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
+ integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
+
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
@@ -8981,6 +9197,14 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+url-parse@^1.5.3:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+ integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
url@^0.11.0:
version "0.11.4"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c"
@@ -9345,6 +9569,15 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@@ -9392,7 +9625,7 @@ yargs-parser@^21.1.1:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-yargs@^17.3.1:
+yargs@^17.3.1, yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
@@ -9415,6 +9648,11 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110"
integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==
+yoctocolors-cjs@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
+ integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
+
zustand@^4.5.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1"