diff --git a/FSD.md b/FSD.md
new file mode 100644
index 000000000..aefa81d7a
--- /dev/null
+++ b/FSD.md
@@ -0,0 +1,517 @@
+# Feature-Sliced Design (FSD) 가이드
+
+## 목차
+1. [FSD 개요](#fsd-개요)
+2. [FSD의 주요 개념](#fsd의-주요-개념)
+3. [FSD 설계 방법](#fsd-설계-방법)
+4. [FSD의 장점](#fsd의-장점)
+5. [점진적 도입 방법](#점진적-도입-방법)
+6. [실제 적용 예시](#실제-적용-예시)
+7. [참고 자료](#참고-자료)
+
+---
+
+## FSD 개요
+
+**Feature-Sliced Design (FSD)**는 프론트엔드 애플리케이션의 코드를 구조화하기 위한 아키텍처 방법론입니다.
+
+### 목적
+- **요구사항이 바뀌어도 코드 구조가 무너지지 않고**, 새 기능을 쉽게 추가할 수 있는 프로젝트를 만드는 것
+- 코드를 **얼마나 많은 책임을 가지는지**와 **다른 모듈에 얼마나 의존하는지**에 따라 계층화
+
+### 적용 대상
+FSD는 웹, 모바일, 데스크톱 등 **프론트엔드 애플리케이션을 만드는 프로젝트**에 잘 어울립니다.
+- 단순한 라이브러리보다는 **애플리케이션**에 더 적합
+- 특정 언어나 프레임워크에 제한이 없음
+- Monorepo 환경에서도 단계적으로 적용 가능
+
+### 언제 사용하면 좋을까?
+다음과 같은 상황이라면 FSD가 도움이 될 수 있습니다:
+- 프로젝트가 커지면서 구조가 얽히고, 유지보수 속도가 느려졌을 때
+- 새로 합류한 팀원이 폴더 구조를 이해하기 힘들어할 때
+
+> ⚠️ **주의**: 지금 구조에 특별한 문제가 없다면 굳이 바꿀 필요는 없습니다. 각 페이지가 독립적인 특성을 가진 프로젝트에서는 오히려 구조가 복잡해질 수 있습니다.
+
+---
+
+## FSD의 주요 개념
+
+FSD는 **3단계 계층 구조**를 따릅니다:
+
+```
+Layer (계층)
+ └── Slice (슬라이스)
+ └── Segment (세그먼트)
+```
+
+### 1. Layer (계층)
+
+Layer는 모든 FSD 프로젝트의 표준 최상위 폴더입니다. 프로젝트의 기능적 역할에 따라 코드를 수직적으로 분리합니다.
+
+#### 주요 Layer
+
+1. **App** (`app`)
+ - Routing, Entrypoint, Global Styles, Provider 등 앱을 실행하는 모든 요소
+ - Slice 없이 Segment로만 구성
+
+2. **Pages** (`pages`)
+ - Route 기준으로 구성된 주요 화면 단위
+ - 사용자와 상호작용하는 실제 페이지 컴포넌트
+
+3. **Widgets** (`widgets`)
+ - 크고 독립적으로 동작하는 UI 구성 단위
+ - 일반적으로 하나의 완결된 화면 기능(use case)을 제공
+
+4. **Features** (`features`)
+ - 사용자에게 비즈니스 가치를 제공하는 액션을 구현한 재사용 가능한 제품 기능 단위
+ - 예: 좋아요 기능, 댓글 작성 기능, 검색 기능
+
+5. **Entities** (`entities`)
+ - 프로젝트가 다루는 핵심 비즈니스 Entity
+ - 예: User, Post, Comment, Product
+
+6. **Shared** (`shared`)
+ - 모든 Layer에서 재사용되는 코드(라이브러리, 유틸리티 등)
+ - Slice 없이 Segment로만 구성
+
+#### Layer 간 의존성 규칙
+
+**중요**: 상위 Layer는 자신보다 하위 Layer를 참조할 수 있지만, **하위 Layer가 상위 Layer를 참조하는 것은 금지**됩니다.
+
+```
+app → pages → widgets → features → entities → shared
+```
+
+예를 들어:
+- ✅ `pages`는 `features`나 `entities`의 모듈을 참조할 수 있음
+- ❌ `features`가 `pages`를 참조하는 것은 금지
+- ✅ 모든 Layer는 `shared`를 참조할 수 있음
+
+### 2. Slice (슬라이스)
+
+Slice는 Layer 내부를 **비즈니스 도메인별**로 나눕니다.
+
+#### 특징
+- 이름/개수에 제한이 없음
+- 같은 Layer 내 다른 Slice를 참조할 수 없음 (높은 응집도와 낮은 결합도 보장)
+
+#### 예시
+```
+entities/
+ ├── user/ # user 도메인
+ ├── post/ # post 도메인
+ └── comment/ # comment 도메인
+
+features/
+ ├── post-search/ # 게시물 검색 기능
+ ├── comment-like/ # 댓글 좋아요 기능
+ └── post-create/ # 게시물 생성 기능
+```
+
+### 3. Segment (세그먼트)
+
+Slice와 App/Shared Layer는 Segment로 세분화되어, **코드의 역할**에 따라 코드를 그룹화합니다.
+
+#### 주요 Segment
+
+1. **`ui`** - UI Components, date formatter, styles 등 UI 표현과 직접 관련된 코드
+2. **`api`** - request functions, data types, mappers 등 백엔드 통신 및 데이터 로직
+3. **`model`** - schema, interfaces, store, business logic 등 애플리케이션 도메인 모델
+4. **`lib`** - 해당 Slice에서 여러 모듈이 함께 사용하는 공통 library code
+5. **`config`** - configuration files, feature flags 등 환경/기능 설정
+
+#### Segment 구조 예시
+
+```
+entities/post/
+ ├── ui/ # PostCard, PostList 등 UI 컴포넌트
+ ├── api/ # fetchPost, createPost 등 API 함수
+ ├── model/ # Post 타입, Post 스토어, 비즈니스 로직
+ ├── lib/ # Post 관련 유틸리티 함수
+ └── config/ # Post 관련 설정
+```
+
+---
+
+## FSD 설계 방법
+
+### 1단계: 기본 Layer 구성
+
+프로젝트 루트에 기본 Layer 폴더를 생성합니다.
+
+```
+src/
+ ├── app/ # 앱 초기화 및 전역 설정
+ ├── pages/ # 페이지 컴포넌트
+ ├── widgets/ # 복잡한 UI 블록
+ ├── features/ # 비즈니스 기능
+ ├── entities/ # 비즈니스 엔티티
+ └── shared/ # 공통 코드
+```
+
+### 2단계: 도메인별 Slice 정의
+
+각 Layer 내에서 비즈니스 도메인에 따라 Slice를 나눕니다.
+
+**Entities Layer 예시:**
+```
+entities/
+ ├── user/
+ ├── post/
+ └── comment/
+```
+
+**Features Layer 예시:**
+```
+features/
+ ├── post-search/
+ ├── post-create/
+ ├── comment-like/
+ └── comment-create/
+```
+
+### 3단계: Segment 분류
+
+각 Slice 내에서 코드의 역할에 따라 Segment로 분류합니다.
+
+```
+entities/post/
+ ├── ui/
+ │ ├── PostCard.tsx
+ │ └── PostList.tsx
+ ├── api/
+ │ ├── postApi.ts
+ │ └── types.ts
+ ├── model/
+ │ ├── types.ts
+ │ └── store.ts
+ └── lib/
+ └── utils.ts
+```
+
+### 4단계: 의존성 규칙 준수
+
+- 상위 Layer는 하위 Layer를 참조할 수 있음
+- 하위 Layer는 상위 Layer를 참조할 수 없음
+- 같은 Layer 내 다른 Slice를 참조할 수 없음
+- 모든 Layer는 `shared`를 참조할 수 있음
+
+### 5단계: Public API 정의
+
+각 Slice는 `index.ts` 파일을 통해 Public API를 노출합니다.
+
+```typescript
+// entities/post/index.ts
+export { PostCard, PostList } from './ui'
+export { usePost, usePosts } from './model'
+export type { Post } from './model/types'
+```
+
+---
+
+## FSD의 장점
+
+### 1. 일관성
+- 구조가 표준화되어 팀 간 협업과 신규 멤버 온보딩이 쉬워집니다
+- 모든 프로젝트에서 동일한 구조를 사용하여 학습 곡선을 줄입니다
+
+### 2. 격리성
+- Layer와 Slice 간 의존성을 제한하여, 특정 모듈만 안전하게 수정할 수 있습니다
+- 한 기능의 변경이 다른 기능에 영향을 주지 않습니다
+
+### 3. 재사용 범위 제어
+- 재사용 가능한 코드를 필요한 범위에서만 활용할 수 있어, **DRY 원칙**과 실용성을 균형 있게 유지합니다
+- 과도한 추상화를 방지합니다
+
+### 4. 도메인 중심 구조
+- 비즈니스 용어 기반의 구조로 되어 있어, 전체 코드를 몰라도 특정 기능을 독립적으로 구현할 수 있습니다
+- 비즈니스 로직과 코드 구조가 일치합니다
+
+### 5. 확장성
+- 새로운 기능을 추가할 때 기존 코드에 영향을 최소화하면서 확장할 수 있습니다
+- 프로젝트가 커져도 구조가 복잡해지지 않습니다
+
+---
+
+## 점진적 도입 방법
+
+기존 프로젝트에 FSD를 도입하는 방법:
+
+### 1단계: 기반 다지기
+`app`, `shared` Layer를 먼저 정리하며 기반을 다집니다.
+
+```
+src/
+ ├── app/
+ │ ├── providers/
+ │ ├── router/
+ │ └── styles/
+ └── shared/
+ ├── ui/
+ ├── lib/
+ └── api/
+```
+
+### 2단계: UI 분배
+기존 UI를 `widgets`, `pages` Layer로 분배합니다.
+- 이 과정에서 FSD 규칙을 위반해도 괜찮습니다 (점진적 도입)
+
+### 3단계: 로직 분리
+Import 위반을 하나씩 해결하면서, 코드에서 로직을 분리해 `entities`와 `features`로 옮깁니다.
+
+```
+기존 코드 분석
+ ↓
+entities 정의 (User, Post, Comment 등)
+ ↓
+features 정의 (검색, 좋아요, 댓글 작성 등)
+ ↓
+Import 경로 수정
+```
+
+### 4단계: 안정화
+- 새로운 대규모 Entity나 복잡한 기능을 추가하지 않는 것이 좋습니다
+- 구조를 안정적으로 정리하는 데 집중합니다
+
+> 💡 **팁**: 도입 단계에서는 완벽한 FSD 구조를 만들려고 하지 말고, 점진적으로 개선해 나가는 것이 중요합니다.
+
+---
+
+## 실제 적용 예시
+
+### 게시물 관리 시스템 예시
+
+#### 1. Entities Layer
+
+```typescript
+// entities/post/model/types.ts
+export interface Post {
+ id: number
+ title: string
+ body: string
+ userId: number
+ tags?: string[]
+ reactions?: {
+ likes: number
+ dislikes: number
+ }
+ author?: User
+}
+
+// entities/post/api/postApi.ts
+export const postApi = {
+ getPosts: (params: { limit: number; skip: number }) =>
+ fetch(`/api/posts?limit=${params.limit}&skip=${params.skip}`),
+
+ getPost: (id: number) =>
+ fetch(`/api/posts/${id}`),
+
+ createPost: (post: CreatePostDto) =>
+ fetch('/api/posts/add', { method: 'POST', ... }),
+
+ updatePost: (id: number, post: UpdatePostDto) =>
+ fetch(`/api/posts/${id}`, { method: 'PUT', ... }),
+
+ deletePost: (id: number) =>
+ fetch(`/api/posts/${id}`, { method: 'DELETE' }),
+}
+
+// entities/post/ui/PostCard.tsx
+export const PostCard = ({ post }: { post: Post }) => {
+ return (
+
+
+ {post.title}
+
+
+ {post.body}
+
+
+ )
+}
+```
+
+#### 2. Features Layer
+
+```typescript
+// features/post-search/ui/SearchInput.tsx
+export const SearchInput = () => {
+ const [query, setQuery] = useState('')
+ const { searchPosts } = usePostSearch()
+
+ return (
+ setQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && searchPosts(query)}
+ />
+ )
+}
+
+// features/post-search/model/usePostSearch.ts
+export const usePostSearch = () => {
+ const searchPosts = async (query: string) => {
+ // 검색 로직
+ }
+
+ return { searchPosts }
+}
+```
+
+#### 3. Widgets Layer
+
+```typescript
+// widgets/post-list/ui/PostList.tsx
+export const PostList = () => {
+ const { posts, loading } = usePosts()
+
+ return (
+
+ {posts.map(post => (
+
+ ))}
+
+ )
+}
+```
+
+#### 4. Pages Layer
+
+```typescript
+// pages/posts-manager/ui/PostsManagerPage.tsx
+export const PostsManagerPage = () => {
+ return (
+
+
+ 게시물 관리자
+
+
+
+
+
+
+ )
+}
+```
+
+### 폴더 구조 예시
+
+```
+src/
+├── app/
+│ ├── providers/
+│ │ └── AppProvider.tsx
+│ ├── router/
+│ │ └── router.tsx
+│ └── styles/
+│ └── global.css
+│
+├── pages/
+│ └── posts-manager/
+│ └── ui/
+│ └── PostsManagerPage.tsx
+│
+├── widgets/
+│ └── post-list/
+│ ├── ui/
+│ │ └── PostList.tsx
+│ └── model/
+│ └── usePostList.ts
+│
+├── features/
+│ ├── post-search/
+│ │ ├── ui/
+│ │ │ └── SearchInput.tsx
+│ │ └── model/
+│ │ └── usePostSearch.ts
+│ └── post-create/
+│ ├── ui/
+│ │ └── CreatePostDialog.tsx
+│ └── model/
+│ └── usePostCreate.ts
+│
+├── entities/
+│ ├── post/
+│ │ ├── ui/
+│ │ │ ├── PostCard.tsx
+│ │ │ └── PostList.tsx
+│ │ ├── api/
+│ │ │ ├── postApi.ts
+│ │ │ └── types.ts
+│ │ ├── model/
+│ │ │ ├── types.ts
+│ │ │ └── store.ts
+│ │ └── index.ts
+│ ├── user/
+│ │ └── ...
+│ └── comment/
+│ └── ...
+│
+└── shared/
+ ├── ui/
+ │ ├── Button.tsx
+ │ ├── Card.tsx
+ │ └── Input.tsx
+ ├── lib/
+ │ └── utils.ts
+ └── api/
+ └── client.ts
+```
+
+---
+
+## 체크리스트
+
+FSD를 적용할 때 다음 사항들을 확인해보세요:
+
+### 공통 컴포넌트 및 로직
+- [ ] `shared` 공통 컴포넌트를 분리했나요?
+- [ ] `shared` 공통 로직을 분리했나요?
+
+### Entities
+- [ ] `entities`를 중심으로 type을 정의하고 model을 분리했나요?
+- [ ] `entities`를 중심으로 ui를 분리했나요?
+- [ ] `entities`를 중심으로 api를 분리했나요?
+
+### Features
+- [ ] `feature`를 중심으로 사용자행동(이벤트 처리)를 분리했나요?
+- [ ] `feature`를 중심으로 ui를 분리했나요?
+- [ ] `feature`를 중심으로 api를 분리했나요?
+
+### Widgets
+- [ ] `widget`을 중심으로 데이터를 재사용가능한 형태로 분리했나요?
+
+### 의존성 규칙
+- [ ] 상위 Layer가 하위 Layer를 참조하는 구조인가요?
+- [ ] 하위 Layer가 상위 Layer를 참조하지 않는가요?
+- [ ] 같은 Layer 내 다른 Slice를 참조하지 않는가요?
+
+### 상태 관리
+- [ ] 전역상태관리를 사용해서 상태를 분리하고 관리했나요?
+- [ ] Props Drilling을 최소화했나요?
+
+---
+
+## 참고 자료
+
+- [Feature-Sliced Design 공식 문서 (한국어)](https://feature-sliced.github.io/documentation/kr/docs/get-started/overview)
+- [Feature-Sliced Design 공식 문서 (영어)](https://feature-sliced.github.io/documentation/)
+- [FSD 예제 모음](https://feature-sliced.github.io/documentation/kr/docs/guides/examples)
+- [FSD Migration 가이드](https://feature-sliced.github.io/documentation/kr/docs/guides/migration)
+
+---
+
+## 결론
+
+Feature-Sliced Design은 프론트엔드 프로젝트의 구조를 체계적으로 관리하기 위한 강력한 방법론입니다.
+
+**핵심 원칙:**
+1. **Layer**: 기능적 역할에 따른 수직 분리
+2. **Slice**: 비즈니스 도메인에 따른 수평 분리
+3. **Segment**: 코드의 역할에 따른 세분화
+4. **의존성 규칙**: 상위 → 하위 방향으로만 의존
+
+FSD를 적용하면 코드의 일관성, 격리성, 재사용성, 확장성을 모두 확보할 수 있으며, 팀 협업과 유지보수가 훨씬 쉬워집니다.
+
+> 💡 **기억하세요**: FSD는 완벽한 구조를 만드는 것이 아니라, 프로젝트를 더 나은 방향으로 점진적으로 개선해 나가는 것입니다.
+
diff --git a/package.json b/package.json
index 30461eb42..58d622cf5 100644
--- a/package.json
+++ b/package.json
@@ -3,17 +3,23 @@
"private": true,
"version": "0.0.0",
"type": "module",
+ "homepage": "https://toeam.github.io/front_7th_chapter3-3/",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
- "coverage": "vitest run --coverage"
+ "coverage": "vitest run --coverage",
+ "predeploy": "pnpm run build",
+ "deploy": "gh-pages -d dist"
},
"dependencies": {
+ "@tanstack/react-query": "^5.90.12",
+ "@tanstack/react-query-devtools": "^5.91.1",
"react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "react-dom": "^19.2.1",
+ "zustand": "^5.0.9"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -30,6 +36,7 @@
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
+ "gh-pages": "^6.3.0",
"globals": "^16.5.0",
"jsdom": "^27.2.0",
"lucide-react": "^0.556.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2ba290a26..65ad49449 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,103 +8,114 @@ importers:
.:
dependencies:
+ '@tanstack/react-query':
+ specifier: ^5.90.12
+ version: 5.90.12(react@19.2.1)
+ '@tanstack/react-query-devtools':
+ specifier: ^5.91.1
+ version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)
react:
- specifier: ^19.2.0
- version: 19.2.0
+ specifier: ^19.2.1
+ version: 19.2.1
react-dom:
- specifier: ^19.2.0
- version: 19.2.0(react@19.2.0)
+ specifier: ^19.2.1
+ version: 19.2.1(react@19.2.1)
+ zustand:
+ specifier: ^5.0.9
+ version: 5.0.9(@types/react@19.2.7)(react@19.2.1)
devDependencies:
'@eslint/js':
- specifier: ^9.37.0
- version: 9.37.0
+ specifier: ^9.39.1
+ version: 9.39.1
'@radix-ui/react-dialog':
specifier: ^1.1.15
- version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@radix-ui/react-select':
specifier: ^2.2.6
- version: 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@testing-library/jest-dom':
specifier: ^6.9.1
version: 6.9.1
'@testing-library/react':
specifier: ^16.3.0
- version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.0)
'@types/react':
- specifier: ^19.2.2
- version: 19.2.2
+ specifier: ^19.2.7
+ version: 19.2.7
'@types/react-dom':
- specifier: ^19.2.1
- version: 19.2.1(@types/react@19.2.2)
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.7)
'@vitejs/plugin-react':
- specifier: ^5.0.4
- version: 5.0.4(vite@7.1.9(@types/node@22.8.1))
+ specifier: ^5.1.1
+ version: 5.1.2(vite@7.2.7(@types/node@22.8.1))
axios:
- specifier: ^1.12.2
- version: 1.12.2
+ specifier: ^1.13.2
+ version: 1.13.2
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
eslint:
- specifier: ^9.37.0
- version: 9.37.0
+ specifier: ^9.39.1
+ version: 9.39.1
eslint-plugin-react-hooks:
- specifier: ^7.0.0
- version: 7.0.0(eslint@9.37.0)
+ specifier: ^7.0.1
+ version: 7.0.1(eslint@9.39.1)
eslint-plugin-react-refresh:
- specifier: ^0.4.23
- version: 0.4.23(eslint@9.37.0)
+ specifier: ^0.4.24
+ version: 0.4.24(eslint@9.39.1)
+ gh-pages:
+ specifier: ^6.3.0
+ version: 6.3.0
globals:
- specifier: ^16.4.0
- version: 16.4.0
+ specifier: ^16.5.0
+ version: 16.5.0
jsdom:
- specifier: ^27.0.0
- version: 27.0.0(postcss@8.5.6)
+ specifier: ^27.2.0
+ version: 27.3.0(postcss@8.5.6)
lucide-react:
- specifier: ^0.545.0
- version: 0.545.0(react@19.2.0)
+ specifier: ^0.556.0
+ version: 0.556.0(react@19.2.1)
msw:
- specifier: ^2.11.5
- version: 2.11.5(@types/node@22.8.1)(typescript@5.9.3)
+ specifier: ^2.12.4
+ version: 2.12.4(@types/node@22.8.1)(typescript@5.9.3)
prettier:
- specifier: ^3.6.2
- version: 3.6.2
+ specifier: ^3.7.4
+ version: 3.7.4
react-router-dom:
- specifier: ^7.9.4
- version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ specifier: ^7.10.1
+ version: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
typescript:
specifier: ~5.9.3
version: 5.9.3
typescript-eslint:
- specifier: ^8.46.0
- version: 8.46.0(eslint@9.37.0)(typescript@5.9.3)
+ specifier: ^8.48.1
+ version: 8.49.0(eslint@9.39.1)(typescript@5.9.3)
vite:
- specifier: ^7.1.9
- version: 7.1.9(@types/node@22.8.1)
+ specifier: ^7.2.6
+ version: 7.2.7(@types/node@22.8.1)
vitest:
- specifier: ^3.2.4
- version: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@27.0.0(postcss@8.5.6))(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))
+ specifier: ^4.0.15
+ version: 4.0.15(@types/node@22.8.1)(jsdom@27.3.0(postcss@8.5.6))(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3))
vitest-browser-react:
- specifier: ^1.0.1
- version: 1.0.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(@vitest/browser@2.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vitest@3.2.4)
+ specifier: ^2.0.2
+ version: 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vitest@4.0.15(@types/node@22.8.1)(jsdom@27.3.0(postcss@8.5.6))(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3)))
packages:
+ '@acemir/cssom@0.9.28':
+ resolution: {integrity: sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==}
+
'@adobe/css-tools@4.4.0':
resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
- '@ampproject/remapping@2.3.0':
- resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
- engines: {node: '>=6.0.0'}
-
- '@asamuzakjp/css-color@4.0.5':
- resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==}
+ '@asamuzakjp/css-color@4.1.0':
+ resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==}
- '@asamuzakjp/dom-selector@6.6.1':
- resolution: {integrity: sha512-8QT9pokVe1fUt1C8IrJketaeFOdRfTOS96DL3EBjE8CRZm3eHnwMlQe2NPoOSEYPwJ5Q25uYoX1+m9044l3ysQ==}
+ '@asamuzakjp/dom-selector@6.7.6':
+ resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
@@ -117,22 +128,22 @@ packages:
resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
engines: {node: '>=6.9.0'}
- '@babel/core@7.28.0':
- resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==}
- engines: {node: '>=6.9.0'}
-
'@babel/core@7.28.4':
resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.28.0':
- resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==}
+ '@babel/core@7.28.5':
+ resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.28.3':
resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
engines: {node: '>=6.9.0'}
+ '@babel/generator@7.28.5':
+ resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
@@ -145,12 +156,6 @@ packages:
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-transforms@7.27.3':
- resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0
-
'@babel/helper-module-transforms@7.28.3':
resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
engines: {node: '>=6.9.0'}
@@ -169,25 +174,25 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-option@7.27.1':
- resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.28.2':
- resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==}
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.28.4':
resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
engines: {node: '>=6.9.0'}
- '@babel/parser@7.28.0':
- resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
+ '@babel/parser@7.28.4':
+ resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/parser@7.28.4':
- resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
+ '@babel/parser@7.28.5':
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -211,22 +216,22 @@ packages:
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.28.0':
- resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
- engines: {node: '>=6.9.0'}
-
'@babel/traverse@7.28.4':
resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.28.2':
- resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
+ '@babel/traverse@7.28.5':
+ resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.4':
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.28.5':
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+ engines: {node: '>=6.9.0'}
+
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -411,12 +416,6 @@ packages:
cpu: [x64]
os: [win32]
- '@eslint-community/eslint-utils@4.7.0':
- resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -427,32 +426,32 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
- '@eslint/config-array@0.21.0':
- resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
+ '@eslint/config-array@0.21.1':
+ resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/config-helpers@0.4.0':
- resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==}
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/core@0.16.0':
- resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==}
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.1':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/js@9.37.0':
- resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==}
+ '@eslint/js@9.39.1':
+ resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/object-schema@2.1.6':
- resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/plugin-kit@0.4.0':
- resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.6.8':
@@ -523,11 +522,14 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
- '@mswjs/interceptors@0.39.5':
- resolution: {integrity: sha512-B9nHSJYtsv79uo7QdkZ/b/WoKm20IkVSmTc/WCKarmDtFwM0dRx2ouEniqwNkzCSLn3fydzKmnMzjtfdOWt3VQ==}
+ '@mswjs/interceptors@0.40.0':
+ resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==}
engines: {node: '>=18'}
'@nodelib/fs.scandir@2.1.5':
@@ -551,9 +553,6 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
- '@polka/url@1.0.0-next.28':
- resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
-
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -832,8 +831,8 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
- '@rolldown/pluginutils@1.0.0-beta.38':
- resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==}
+ '@rolldown/pluginutils@1.0.0-beta.53':
+ resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
'@rollup/rollup-android-arm-eabi@4.46.2':
resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
@@ -935,6 +934,26 @@ packages:
cpu: [x64]
os: [win32]
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
+ '@tanstack/query-core@5.90.12':
+ resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==}
+
+ '@tanstack/query-devtools@5.91.1':
+ resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==}
+
+ '@tanstack/react-query-devtools@5.91.1':
+ resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.90.10
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.90.12':
+ resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==}
+ peerDependencies:
+ react: ^18 || ^19
+
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -994,143 +1013,110 @@ packages:
'@types/node@22.8.1':
resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==}
- '@types/react-dom@19.2.1':
- resolution: {integrity: sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==}
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
- '@types/react@19.2.2':
- resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==}
+ '@types/react@19.2.7':
+ resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
- '@types/statuses@2.0.5':
- resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
+ '@types/statuses@2.0.6':
+ resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
- '@typescript-eslint/eslint-plugin@8.46.0':
- resolution: {integrity: sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==}
+ '@typescript-eslint/eslint-plugin@8.49.0':
+ resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.46.0
+ '@typescript-eslint/parser': ^8.49.0
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/parser@8.46.0':
- resolution: {integrity: sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==}
+ '@typescript-eslint/parser@8.49.0':
+ resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/project-service@8.46.0':
- resolution: {integrity: sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==}
+ '@typescript-eslint/project-service@8.49.0':
+ resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/scope-manager@8.46.0':
- resolution: {integrity: sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==}
+ '@typescript-eslint/scope-manager@8.49.0':
+ resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.46.0':
- resolution: {integrity: sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==}
+ '@typescript-eslint/tsconfig-utils@8.49.0':
+ resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/type-utils@8.46.0':
- resolution: {integrity: sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==}
+ '@typescript-eslint/type-utils@8.49.0':
+ resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/types@8.46.0':
- resolution: {integrity: sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==}
+ '@typescript-eslint/types@8.49.0':
+ resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.46.0':
- resolution: {integrity: sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==}
+ '@typescript-eslint/typescript-estree@8.49.0':
+ resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/utils@8.46.0':
- resolution: {integrity: sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==}
+ '@typescript-eslint/utils@8.49.0':
+ resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/visitor-keys@8.46.0':
- resolution: {integrity: sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==}
+ '@typescript-eslint/visitor-keys@8.49.0':
+ resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@vitejs/plugin-react@5.0.4':
- resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==}
+ '@vitejs/plugin-react@5.1.2':
+ resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
- '@vitest/browser@2.1.3':
- resolution: {integrity: sha512-PQ2kLLc9q8ukJutuuYsynHSr31E78/dtYEvPy4jCHLht1LmITqXTVTqu7THWdZ1kXNGrWwtdMqtt3z2mvSKdIg==}
- peerDependencies:
- playwright: '*'
- safaridriver: '*'
- vitest: 2.1.3
- webdriverio: '*'
- peerDependenciesMeta:
- playwright:
- optional: true
- safaridriver:
- optional: true
- webdriverio:
- optional: true
-
- '@vitest/expect@3.2.4':
- resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
+ '@vitest/expect@4.0.15':
+ resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==}
- '@vitest/mocker@2.1.3':
- resolution: {integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==}
- peerDependencies:
- '@vitest/spy': 2.1.3
- msw: ^2.3.5
- vite: ^5.0.0
- peerDependenciesMeta:
- msw:
- optional: true
- vite:
- optional: true
-
- '@vitest/mocker@3.2.4':
- resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
+ '@vitest/mocker@4.0.15':
+ resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==}
peerDependencies:
msw: ^2.4.9
- vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ vite: ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
- '@vitest/pretty-format@2.1.3':
- resolution: {integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==}
-
- '@vitest/pretty-format@3.2.4':
- resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
+ '@vitest/pretty-format@4.0.15':
+ resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==}
- '@vitest/runner@3.2.4':
- resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+ '@vitest/runner@4.0.15':
+ resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==}
- '@vitest/snapshot@3.2.4':
- resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+ '@vitest/snapshot@4.0.15':
+ resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==}
- '@vitest/spy@3.2.4':
- resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+ '@vitest/spy@4.0.15':
+ resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==}
- '@vitest/utils@2.1.3':
- resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==}
-
- '@vitest/utils@3.2.4':
- resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+ '@vitest/utils@4.0.15':
+ resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@@ -1175,15 +1161,18 @@ packages:
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
- assertion-error@2.0.1:
- resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
- engines: {node: '>=12'}
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ async@3.2.6:
+ resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
- axios@1.12.2:
- resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
+ axios@1.13.2:
+ resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1206,10 +1195,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
- cac@6.7.14:
- resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
- engines: {node: '>=8'}
-
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1221,18 +1206,14 @@ packages:
caniuse-lite@1.0.30001669:
resolution: {integrity: sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==}
- chai@5.2.0:
- resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
- engines: {node: '>=12'}
+ chai@6.2.1:
+ resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
+ engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
- check-error@2.1.1:
- resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
- engines: {node: '>= 16'}
-
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -1259,6 +1240,13 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ commander@13.1.0:
+ resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+ engines: {node: '>=18'}
+
+ commondir@1.0.1:
+ resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1280,12 +1268,12 @@ packages:
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
- cssstyle@5.3.1:
- resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==}
+ cssstyle@5.3.4:
+ resolution: {integrity: sha512-KyOS/kJMEq5O9GdPnaf82noigg5X5DYn0kZPJTaAsCUaBizp6Xa1y9D4Qoqf/JazEXWuruErHgVXwjN5391ZJw==}
engines: {node: '>=20'}
- csstype@3.1.3:
- resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
data-urls@6.0.0:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
@@ -1300,12 +1288,8 @@ packages:
supports-color:
optional: true
- decimal.js@10.5.0:
- resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
-
- deep-eql@5.0.2:
- resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
- engines: {node: '>=6'}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -1321,6 +1305,10 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -1334,6 +1322,9 @@ packages:
electron-to-chromium@1.5.45:
resolution: {integrity: sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==}
+ email-addresses@5.0.0:
+ resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1369,18 +1360,22 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
- eslint-plugin-react-hooks@7.0.0:
- resolution: {integrity: sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==}
+ eslint-plugin-react-hooks@7.0.1:
+ resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
engines: {node: '>=18'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
- eslint-plugin-react-refresh@0.4.23:
- resolution: {integrity: sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==}
+ eslint-plugin-react-refresh@0.4.24:
+ resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==}
peerDependencies:
eslint: '>=8.40'
@@ -1396,8 +1391,8 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- eslint@9.37.0:
- resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==}
+ eslint@9.39.1:
+ resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@@ -1429,15 +1424,15 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
- expect-type@1.2.1:
- resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- fast-glob@3.3.2:
- resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fast-json-stable-stringify@2.1.0:
@@ -1446,16 +1441,8 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
- fastq@1.17.1:
- resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
-
- fdir@6.4.6:
- resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
- peerDependencies:
- picomatch: ^3 || ^4
- peerDependenciesMeta:
- picomatch:
- optional: true
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
@@ -1470,10 +1457,26 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ filename-reserved-regex@2.0.0:
+ resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
+ engines: {node: '>=4'}
+
+ filenamify@4.3.0:
+ resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
+ engines: {node: '>=8'}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-cache-dir@3.3.2:
+ resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
+ engines: {node: '>=8'}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -1498,6 +1501,10 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
+ fs-extra@11.3.2:
+ resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
+ engines: {node: '>=14.14'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1526,6 +1533,11 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
+ gh-pages@6.3.0:
+ resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1538,19 +1550,23 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
- globals@16.4.0:
- resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
+ globals@16.5.0:
+ resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
engines: {node: '>=18'}
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
- graphemer@1.4.0:
- resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- graphql@16.9.0:
- resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==}
+ graphql@16.12.0:
+ resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
has-flag@4.0.0:
@@ -1642,16 +1658,13 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-tokens@9.0.1:
- resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
-
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
- jsdom@27.0.0:
- resolution: {integrity: sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==}
- engines: {node: '>=20'}
+ jsdom@27.3.0:
+ resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
@@ -1677,6 +1690,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1684,6 +1700,10 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1691,21 +1711,19 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
- loupe@3.1.3:
- resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
-
- loupe@3.2.0:
- resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
-
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
+ lru-cache@11.2.4:
+ resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
- lucide-react@0.545.0:
- resolution: {integrity: sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==}
+ lucide-react@0.556.0:
+ resolution: {integrity: sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -1713,8 +1731,12 @@ packages:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
- magic-string@0.30.17:
- resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ make-dir@3.1.0:
+ resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+ engines: {node: '>=8'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
@@ -1750,15 +1772,11 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
- mrmime@2.0.0:
- resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
- engines: {node: '>=10'}
-
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- msw@2.11.5:
- resolution: {integrity: sha512-atFI4GjKSJComxcigz273honh8h4j5zzpk5kwG4tGm0TPcYne6bqmVrufeRll6auBeouIkXqZYXxVbWSWxM3RA==}
+ msw@2.12.4:
+ resolution: {integrity: sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
@@ -1782,6 +1800,9 @@ packages:
node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -1789,20 +1810,32 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
- parse5@7.3.0:
- resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
@@ -1815,13 +1848,13 @@ packages:
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
- pathval@2.0.0:
- resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
- engines: {node: '>= 14.16'}
-
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1829,14 +1862,14 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
- picomatch@4.0.2:
- resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
- engines: {node: '>=12'}
-
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
+ pkg-dir@4.2.0:
+ resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
+ engines: {node: '>=8'}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -1845,8 +1878,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
- prettier@3.6.2:
- resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
+ prettier@3.7.4:
+ resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'}
hasBin: true
@@ -1864,16 +1897,16 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- react-dom@19.2.0:
- resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+ react-dom@19.2.1:
+ resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
peerDependencies:
- react: ^19.2.0
+ react: ^19.2.1
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
- react-refresh@0.17.0:
- resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
react-remove-scroll-bar@2.3.8:
@@ -1896,15 +1929,15 @@ packages:
'@types/react':
optional: true
- react-router-dom@7.9.4:
- resolution: {integrity: sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==}
+ react-router-dom@7.10.1:
+ resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
- react-router@7.9.4:
- resolution: {integrity: sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==}
+ react-router@7.10.1:
+ resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -1923,8 +1956,8 @@ packages:
'@types/react':
optional: true
- react@19.2.0:
- resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+ react@19.2.1:
+ resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
engines: {node: '>=0.10.0'}
redent@3.0.0:
@@ -1949,8 +1982,8 @@ packages:
rettime@0.7.0:
resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==}
- reusify@1.0.4:
- resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup@4.46.2:
@@ -1958,9 +1991,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
- rrweb-cssom@0.8.0:
- resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
-
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2001,9 +2031,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
- sirv@2.0.4:
- resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
- engines: {node: '>= 10'}
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
@@ -2016,8 +2046,8 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
- std-env@3.9.0:
- resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
@@ -2038,8 +2068,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
- strip-literal@3.0.0:
- resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
+ strip-outer@1.0.1:
+ resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
+ engines: {node: '>=0.10.0'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
@@ -2048,34 +2079,23 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+ tagged-tag@1.0.0:
+ resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
+ engines: {node: '>=20'}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
- tinyexec@0.3.2:
- resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
-
- tinyglobby@0.2.14:
- resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
- engines: {node: '>=12.0.0'}
+ tinyexec@1.0.2:
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
+ engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
- tinypool@1.1.1:
- resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
- engines: {node: ^18.0.0 || >=20.0.0}
-
- tinyrainbow@1.2.0:
- resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
- engines: {node: '>=14.0.0'}
-
- tinyrainbow@2.0.0:
- resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
- engines: {node: '>=14.0.0'}
-
- tinyspy@4.0.3:
- resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
+ tinyrainbow@3.0.3:
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
tldts-core@7.0.17:
@@ -2089,10 +2109,6 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
- totalist@3.0.1:
- resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
- engines: {node: '>=6'}
-
tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'}
@@ -2101,6 +2117,10 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
+ trim-repeated@1.0.0:
+ resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
+ engines: {node: '>=0.10.0'}
+
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -2118,12 +2138,12 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
- type-fest@4.26.1:
- resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==}
- engines: {node: '>=16'}
+ type-fest@5.3.1:
+ resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==}
+ engines: {node: '>=20'}
- typescript-eslint@8.46.0:
- resolution: {integrity: sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==}
+ typescript-eslint@8.49.0:
+ resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -2137,6 +2157,10 @@ packages:
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
until-async@3.0.2:
resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
@@ -2169,13 +2193,8 @@ packages:
'@types/react':
optional: true
- vite-node@3.2.4:
- resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
- engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
- hasBin: true
-
- vite@7.1.9:
- resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==}
+ vite@7.2.7:
+ resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -2214,42 +2233,46 @@ packages:
yaml:
optional: true
- vitest-browser-react@1.0.1:
- resolution: {integrity: sha512-LqiGFCdknrbMoSDWXTCTrPsED3SvdIXIgYOOZyYUNj2dkJusW2eF6NENOlBlxwq+FBQqzNK1X59b+b03pXFpAQ==}
- engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ vitest-browser-react@2.0.2:
+ resolution: {integrity: sha512-zuSgTe/CKODU3ip+w4ls6Qm4xZ9+A4OHmDf0obt/mwAqavpOtqtq2YcioZt8nfDQE50EWmhdnRfDmpS1jCsbTQ==}
peerDependencies:
'@types/react': ^18.0.0 || ^19.0.0
'@types/react-dom': ^18.0.0 || ^19.0.0
- '@vitest/browser': ^2.1.0 || ^3.0.0 || ^4.0.0-0
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
- vitest: ^2.1.0 || ^3.0.0 || ^4.0.0-0
+ vitest: ^4.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
- vitest@3.2.4:
- resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
- engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ vitest@4.0.15:
+ resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
- '@types/debug': ^4.1.12
- '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
- '@vitest/browser': 3.2.4
- '@vitest/ui': 3.2.4
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.15
+ '@vitest/browser-preview': 4.0.15
+ '@vitest/browser-webdriverio': 4.0.15
+ '@vitest/ui': 4.0.15
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
- '@types/debug':
+ '@opentelemetry/api':
optional: true
'@types/node':
optional: true
- '@vitest/browser':
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
optional: true
'@vitest/ui':
optional: true
@@ -2300,18 +2323,6 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
- ws@8.18.0:
- resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: '>=5.0.2'
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
-
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@@ -2363,16 +2374,31 @@ packages:
zod@4.1.12:
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
+ zustand@5.0.9:
+ resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
- '@adobe/css-tools@4.4.0': {}
+ '@acemir/cssom@0.9.28': {}
- '@ampproject/remapping@2.3.0':
- dependencies:
- '@jridgewell/gen-mapping': 0.3.12
- '@jridgewell/trace-mapping': 0.3.29
+ '@adobe/css-tools@4.4.0': {}
- '@asamuzakjp/css-color@4.0.5':
+ '@asamuzakjp/css-color@4.1.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -2380,13 +2406,13 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
lru-cache: 11.2.2
- '@asamuzakjp/dom-selector@6.6.1':
+ '@asamuzakjp/dom-selector@6.7.6':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
css-tree: 3.1.0
is-potential-custom-element-name: 1.0.1
- lru-cache: 11.2.2
+ lru-cache: 11.2.4
'@asamuzakjp/nwsapi@2.3.9': {}
@@ -2398,18 +2424,18 @@ snapshots:
'@babel/compat-data@7.28.0': {}
- '@babel/core@7.28.0':
+ '@babel/core@7.28.4':
dependencies:
- '@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.0
+ '@babel/generator': 7.28.3
'@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
- '@babel/helpers': 7.28.2
- '@babel/parser': 7.28.0
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)
+ '@babel/helpers': 7.28.4
+ '@babel/parser': 7.28.4
'@babel/template': 7.27.2
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/traverse': 7.28.4
+ '@babel/types': 7.28.4
+ '@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.1
gensync: 1.0.0-beta.2
@@ -2418,17 +2444,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/core@7.28.4':
+ '@babel/core@7.28.5':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.3
+ '@babel/generator': 7.28.5
'@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
'@babel/helpers': 7.28.4
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.28.5
'@babel/template': 7.27.2
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.1
@@ -2438,18 +2464,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/generator@7.28.0':
+ '@babel/generator@7.28.3':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
jsesc: 3.0.2
- '@babel/generator@7.28.3':
+ '@babel/generator@7.28.5':
dependencies:
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
jsesc: 3.0.2
@@ -2466,26 +2492,26 @@ snapshots:
'@babel/helper-module-imports@7.27.1':
dependencies:
- '@babel/traverse': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)':
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)':
dependencies:
- '@babel/core': 7.28.0
+ '@babel/core': 7.28.4
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
- '@babel/traverse': 7.28.0
+ '@babel/traverse': 7.28.5
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)':
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
dependencies:
- '@babel/core': 7.28.4
+ '@babel/core': 7.28.5
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
- '@babel/traverse': 7.28.4
+ '@babel/traverse': 7.28.5
transitivePeerDependencies:
- supports-color
@@ -2495,34 +2521,31 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {}
- '@babel/helper-validator-option@7.27.1': {}
+ '@babel/helper-validator-identifier@7.28.5': {}
- '@babel/helpers@7.28.2':
- dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.2
+ '@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.28.4':
dependencies:
'@babel/template': 7.27.2
- '@babel/types': 7.28.4
-
- '@babel/parser@7.28.0':
- dependencies:
- '@babel/types': 7.28.2
+ '@babel/types': 7.28.5
'@babel/parser@7.28.4':
dependencies:
'@babel/types': 7.28.4
- '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)':
+ '@babel/parser@7.28.5':
dependencies:
- '@babel/core': 7.28.4
+ '@babel/types': 7.28.5
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
'@babel/helper-plugin-utils': 7.27.1
- '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)':
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
dependencies:
- '@babel/core': 7.28.4
+ '@babel/core': 7.28.5
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.26.0':
@@ -2532,42 +2555,42 @@ snapshots:
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
- '@babel/traverse@7.28.0':
+ '@babel/traverse@7.28.4':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.0
+ '@babel/generator': 7.28.3
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.0
+ '@babel/parser': 7.28.4
'@babel/template': 7.27.2
- '@babel/types': 7.28.2
+ '@babel/types': 7.28.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
- '@babel/traverse@7.28.4':
+ '@babel/traverse@7.28.5':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.3
+ '@babel/generator': 7.28.5
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.28.5
'@babel/template': 7.27.2
- '@babel/types': 7.28.4
+ '@babel/types': 7.28.5
debug: 4.4.1
transitivePeerDependencies:
- supports-color
- '@babel/types@7.28.2':
+ '@babel/types@7.28.4':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
- '@babel/types@7.28.4':
+ '@babel/types@7.28.5':
dependencies:
'@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
'@csstools/color-helpers@5.1.0': {}
@@ -2668,31 +2691,26 @@ snapshots:
'@esbuild/win32-x64@0.25.3':
optional: true
- '@eslint-community/eslint-utils@4.7.0(eslint@9.37.0)':
+ '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)':
dependencies:
- eslint: 9.37.0
- eslint-visitor-keys: 3.4.3
-
- '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0)':
- dependencies:
- eslint: 9.37.0
+ eslint: 9.39.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
- '@eslint/config-array@0.21.0':
+ '@eslint/config-array@0.21.1':
dependencies:
- '@eslint/object-schema': 2.1.6
+ '@eslint/object-schema': 2.1.7
debug: 4.4.1
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
- '@eslint/config-helpers@0.4.0':
+ '@eslint/config-helpers@0.4.2':
dependencies:
- '@eslint/core': 0.16.0
+ '@eslint/core': 0.17.0
- '@eslint/core@0.16.0':
+ '@eslint/core@0.17.0':
dependencies:
'@types/json-schema': 7.0.15
@@ -2710,13 +2728,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint/js@9.37.0': {}
+ '@eslint/js@9.39.1': {}
- '@eslint/object-schema@2.1.6': {}
+ '@eslint/object-schema@2.1.7': {}
- '@eslint/plugin-kit@0.4.0':
+ '@eslint/plugin-kit@0.4.1':
dependencies:
- '@eslint/core': 0.16.0
+ '@eslint/core': 0.17.0
levn: 0.4.1
'@floating-ui/core@1.6.8':
@@ -2728,11 +2746,11 @@ snapshots:
'@floating-ui/core': 1.6.8
'@floating-ui/utils': 0.2.8
- '@floating-ui/react-dom@2.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@floating-ui/react-dom@2.1.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@floating-ui/dom': 1.6.11
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
'@floating-ui/utils@0.2.8': {}
@@ -2789,12 +2807,14 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.0': {}
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
'@jridgewell/trace-mapping@0.3.29':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
- '@mswjs/interceptors@0.39.5':
+ '@mswjs/interceptors@0.40.0':
dependencies:
'@open-draft/deferred-promise': 2.2.0
'@open-draft/logger': 0.3.0
@@ -2813,7 +2833,7 @@ snapshots:
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
- fastq: 1.17.1
+ fastq: 1.19.1
'@open-draft/deferred-promise@2.2.0': {}
@@ -2824,259 +2844,257 @@ snapshots:
'@open-draft/until@2.1.0': {}
- '@polka/url@1.0.0-next.28': {}
-
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
- '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1)
aria-hidden: 1.2.4
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- react-remove-scroll: 2.6.3(@types/react@19.2.2)(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ react-remove-scroll: 2.6.3(@types/react@19.2.7)(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
-
- '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
- dependencies:
- '@floating-ui/react-dom': 2.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0)
+ '@types/react': 19.2.7
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1)
'@radix-ui/rect': 1.1.1
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
aria-hidden: 1.2.4
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- react-remove-scroll: 2.6.3(@types/react@19.2.2)(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ react-remove-scroll: 2.6.3(@types/react@19.2.7)(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/rect': 1.1.1
- react: 19.2.0
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-use-size@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
+ react: 19.2.1
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
'@radix-ui/rect@1.1.1': {}
- '@rolldown/pluginutils@1.0.0-beta.38': {}
+ '@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/rollup-android-arm-eabi@4.46.2':
optional: true
@@ -3138,6 +3156,23 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.46.2':
optional: true
+ '@standard-schema/spec@1.0.0': {}
+
+ '@tanstack/query-core@5.90.12': {}
+
+ '@tanstack/query-devtools@5.91.1': {}
+
+ '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)':
+ dependencies:
+ '@tanstack/query-devtools': 5.91.1
+ '@tanstack/react-query': 5.90.12(react@19.2.1)
+ react: 19.2.1
+
+ '@tanstack/react-query@5.90.12(react@19.2.1)':
+ dependencies:
+ '@tanstack/query-core': 5.90.12
+ react: 19.2.1
+
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.27.1
@@ -3158,15 +3193,15 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
- '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@babel/runtime': 7.26.0
'@testing-library/dom': 10.4.0
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)':
dependencies:
@@ -3176,24 +3211,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.6
'@types/babel__generator@7.6.8':
dependencies:
- '@babel/types': 7.28.2
+ '@babel/types': 7.28.4
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.28.0
- '@babel/types': 7.28.2
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
'@types/babel__traverse@7.20.6':
dependencies:
- '@babel/types': 7.28.2
+ '@babel/types': 7.28.4
'@types/chai@5.2.2':
dependencies:
@@ -3209,26 +3244,25 @@ snapshots:
dependencies:
undici-types: 6.19.8
- '@types/react-dom@19.2.1(@types/react@19.2.2)':
+ '@types/react-dom@19.2.3(@types/react@19.2.7)':
dependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- '@types/react@19.2.2':
+ '@types/react@19.2.7':
dependencies:
- csstype: 3.1.3
+ csstype: 3.2.3
- '@types/statuses@2.0.5': {}
+ '@types/statuses@2.0.6': {}
- '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript@5.9.3)':
+ '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.46.0
- '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
- '@typescript-eslint/utils': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.46.0
- eslint: 9.37.0
- graphemer: 1.4.0
+ '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.49.0
+ '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.49.0
+ eslint: 9.39.1
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -3236,175 +3270,132 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.46.0(eslint@9.37.0)(typescript@5.9.3)':
+ '@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.46.0
- '@typescript-eslint/types': 8.46.0
- '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.46.0
+ '@typescript-eslint/scope-manager': 8.49.0
+ '@typescript-eslint/types': 8.49.0
+ '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.49.0
debug: 4.4.1
- eslint: 9.37.0
+ eslint: 9.39.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.46.0(typescript@5.9.3)':
+ '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.46.0
+ '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.49.0
debug: 4.4.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.46.0':
+ '@typescript-eslint/scope-manager@8.49.0':
dependencies:
- '@typescript-eslint/types': 8.46.0
- '@typescript-eslint/visitor-keys': 8.46.0
+ '@typescript-eslint/types': 8.49.0
+ '@typescript-eslint/visitor-keys': 8.49.0
- '@typescript-eslint/tsconfig-utils@8.46.0(typescript@5.9.3)':
+ '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
- '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0)(typescript@5.9.3)':
+ '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1)(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/types': 8.46.0
- '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/utils': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
+ '@typescript-eslint/types': 8.49.0
+ '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
debug: 4.4.1
- eslint: 9.37.0
+ eslint: 9.39.1
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.46.0': {}
+ '@typescript-eslint/types@8.49.0': {}
- '@typescript-eslint/typescript-estree@8.46.0(typescript@5.9.3)':
+ '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/project-service': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.46.0
- '@typescript-eslint/visitor-keys': 8.46.0
+ '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.49.0
+ '@typescript-eslint/visitor-keys': 8.49.0
debug: 4.4.1
- fast-glob: 3.3.2
- is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.6.3
+ tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.46.0(eslint@9.37.0)(typescript@5.9.3)':
+ '@typescript-eslint/utils@8.49.0(eslint@9.39.1)(typescript@5.9.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.7.0(eslint@9.37.0)
- '@typescript-eslint/scope-manager': 8.46.0
- '@typescript-eslint/types': 8.46.0
- '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3)
- eslint: 9.37.0
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
+ '@typescript-eslint/scope-manager': 8.49.0
+ '@typescript-eslint/types': 8.49.0
+ '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
+ eslint: 9.39.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.46.0':
+ '@typescript-eslint/visitor-keys@8.49.0':
dependencies:
- '@typescript-eslint/types': 8.46.0
+ '@typescript-eslint/types': 8.49.0
eslint-visitor-keys: 4.2.1
- '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@22.8.1))':
+ '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@22.8.1))':
dependencies:
- '@babel/core': 7.28.4
- '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
- '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4)
- '@rolldown/pluginutils': 1.0.0-beta.38
+ '@babel/core': 7.28.5
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
+ '@rolldown/pluginutils': 1.0.0-beta.53
'@types/babel__core': 7.20.5
- react-refresh: 0.17.0
- vite: 7.1.9(@types/node@22.8.1)
+ react-refresh: 0.18.0
+ vite: 7.2.7(@types/node@22.8.1)
transitivePeerDependencies:
- supports-color
- '@vitest/browser@2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.3)(vite@7.1.9(@types/node@22.8.1))(vitest@3.2.4)':
- dependencies:
- '@testing-library/dom': 10.4.0
- '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0)
- '@vitest/mocker': 2.1.3(@vitest/spy@3.2.4)(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))(vite@7.1.9(@types/node@22.8.1))
- '@vitest/utils': 2.1.3
- magic-string: 0.30.17
- msw: 2.11.5(@types/node@22.8.1)(typescript@5.9.3)
- sirv: 2.0.4
- tinyrainbow: 1.2.0
- vitest: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@27.0.0(postcss@8.5.6))(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))
- ws: 8.18.0
- transitivePeerDependencies:
- - '@types/node'
- - '@vitest/spy'
- - bufferutil
- - typescript
- - utf-8-validate
- - vite
-
- '@vitest/expect@3.2.4':
+ '@vitest/expect@4.0.15':
dependencies:
+ '@standard-schema/spec': 1.0.0
'@types/chai': 5.2.2
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
- chai: 5.2.0
- tinyrainbow: 2.0.0
+ '@vitest/spy': 4.0.15
+ '@vitest/utils': 4.0.15
+ chai: 6.2.1
+ tinyrainbow: 3.0.3
- '@vitest/mocker@2.1.3(@vitest/spy@3.2.4)(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))(vite@7.1.9(@types/node@22.8.1))':
+ '@vitest/mocker@4.0.15(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3))(vite@7.2.7(@types/node@22.8.1))':
dependencies:
- '@vitest/spy': 3.2.4
+ '@vitest/spy': 4.0.15
estree-walker: 3.0.3
- magic-string: 0.30.17
+ magic-string: 0.30.21
optionalDependencies:
- msw: 2.11.5(@types/node@22.8.1)(typescript@5.9.3)
- vite: 7.1.9(@types/node@22.8.1)
+ msw: 2.12.4(@types/node@22.8.1)(typescript@5.9.3)
+ vite: 7.2.7(@types/node@22.8.1)
- '@vitest/mocker@3.2.4(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))(vite@7.1.9(@types/node@22.8.1))':
+ '@vitest/pretty-format@4.0.15':
dependencies:
- '@vitest/spy': 3.2.4
- estree-walker: 3.0.3
- magic-string: 0.30.17
- optionalDependencies:
- msw: 2.11.5(@types/node@22.8.1)(typescript@5.9.3)
- vite: 7.1.9(@types/node@22.8.1)
-
- '@vitest/pretty-format@2.1.3':
- dependencies:
- tinyrainbow: 1.2.0
-
- '@vitest/pretty-format@3.2.4':
- dependencies:
- tinyrainbow: 2.0.0
+ tinyrainbow: 3.0.3
- '@vitest/runner@3.2.4':
+ '@vitest/runner@4.0.15':
dependencies:
- '@vitest/utils': 3.2.4
+ '@vitest/utils': 4.0.15
pathe: 2.0.3
- strip-literal: 3.0.0
- '@vitest/snapshot@3.2.4':
+ '@vitest/snapshot@4.0.15':
dependencies:
- '@vitest/pretty-format': 3.2.4
- magic-string: 0.30.17
+ '@vitest/pretty-format': 4.0.15
+ magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@3.2.4':
- dependencies:
- tinyspy: 4.0.3
-
- '@vitest/utils@2.1.3':
- dependencies:
- '@vitest/pretty-format': 2.1.3
- loupe: 3.2.0
- tinyrainbow: 1.2.0
+ '@vitest/spy@4.0.15': {}
- '@vitest/utils@3.2.4':
+ '@vitest/utils@4.0.15':
dependencies:
- '@vitest/pretty-format': 3.2.4
- loupe: 3.2.0
- tinyrainbow: 2.0.0
+ '@vitest/pretty-format': 4.0.15
+ tinyrainbow: 3.0.3
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
@@ -3443,11 +3434,13 @@ snapshots:
dependencies:
dequal: 2.0.3
- assertion-error@2.0.1: {}
+ array-union@2.1.0: {}
+
+ async@3.2.6: {}
asynckit@0.4.0: {}
- axios@1.12.2:
+ axios@1.13.2:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.4
@@ -3481,8 +3474,6 @@ snapshots:
node-releases: 2.0.18
update-browserslist-db: 1.1.1(browserslist@4.24.2)
- cac@6.7.14: {}
-
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -3492,21 +3483,13 @@ snapshots:
caniuse-lite@1.0.30001669: {}
- chai@5.2.0:
- dependencies:
- assertion-error: 2.0.1
- check-error: 2.1.1
- deep-eql: 5.0.2
- loupe: 3.1.3
- pathval: 2.0.0
+ chai@6.2.1: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
- check-error@2.1.1: {}
-
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -3531,6 +3514,10 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
+ commander@13.1.0: {}
+
+ commondir@1.0.1: {}
+
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
@@ -3550,15 +3537,15 @@ snapshots:
css.escape@1.5.1: {}
- cssstyle@5.3.1(postcss@8.5.6):
+ cssstyle@5.3.4(postcss@8.5.6):
dependencies:
- '@asamuzakjp/css-color': 4.0.5
+ '@asamuzakjp/css-color': 4.1.0
'@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6)
css-tree: 3.1.0
transitivePeerDependencies:
- postcss
- csstype@3.1.3: {}
+ csstype@3.2.3: {}
data-urls@6.0.0:
dependencies:
@@ -3569,9 +3556,7 @@ snapshots:
dependencies:
ms: 2.1.3
- decimal.js@10.5.0: {}
-
- deep-eql@5.0.2: {}
+ decimal.js@10.6.0: {}
deep-is@0.1.4: {}
@@ -3581,6 +3566,10 @@ snapshots:
detect-node-es@1.1.0: {}
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -3593,6 +3582,8 @@ snapshots:
electron-to-chromium@1.5.45: {}
+ email-addresses@5.0.0: {}
+
emoji-regex@8.0.0: {}
entities@6.0.1: {}
@@ -3644,22 +3635,24 @@ snapshots:
escalade@3.2.0: {}
+ escape-string-regexp@1.0.5: {}
+
escape-string-regexp@4.0.0: {}
- eslint-plugin-react-hooks@7.0.0(eslint@9.37.0):
+ eslint-plugin-react-hooks@7.0.1(eslint@9.39.1):
dependencies:
- '@babel/core': 7.28.0
- '@babel/parser': 7.28.0
- eslint: 9.37.0
+ '@babel/core': 7.28.4
+ '@babel/parser': 7.28.4
+ eslint: 9.39.1
hermes-parser: 0.25.1
zod: 4.1.12
zod-validation-error: 4.0.2(zod@4.1.12)
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-refresh@0.4.23(eslint@9.37.0):
+ eslint-plugin-react-refresh@0.4.24(eslint@9.39.1):
dependencies:
- eslint: 9.37.0
+ eslint: 9.39.1
eslint-scope@8.4.0:
dependencies:
@@ -3670,21 +3663,20 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
- eslint@9.37.0:
+ eslint@9.39.1:
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
'@eslint-community/regexpp': 4.12.1
- '@eslint/config-array': 0.21.0
- '@eslint/config-helpers': 0.4.0
- '@eslint/core': 0.16.0
+ '@eslint/config-array': 0.21.1
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.1
- '@eslint/js': 9.37.0
- '@eslint/plugin-kit': 0.4.0
+ '@eslint/js': 9.39.1
+ '@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.2
'@types/estree': 1.0.8
- '@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
@@ -3732,11 +3724,11 @@ snapshots:
esutils@2.0.3: {}
- expect-type@1.2.1: {}
+ expect-type@1.3.0: {}
fast-deep-equal@3.1.3: {}
- fast-glob@3.3.2:
+ fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
@@ -3748,13 +3740,9 @@ snapshots:
fast-levenshtein@2.0.6: {}
- fastq@1.17.1:
+ fastq@1.19.1:
dependencies:
- reusify: 1.0.4
-
- fdir@6.4.6(picomatch@4.0.3):
- optionalDependencies:
- picomatch: 4.0.3
+ reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
@@ -3764,10 +3752,29 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ filename-reserved-regex@2.0.0: {}
+
+ filenamify@4.3.0:
+ dependencies:
+ filename-reserved-regex: 2.0.0
+ strip-outer: 1.0.1
+ trim-repeated: 1.0.0
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
+ find-cache-dir@3.3.2:
+ dependencies:
+ commondir: 1.0.1
+ make-dir: 3.1.0
+ pkg-dir: 4.2.0
+
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -3790,6 +3797,12 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
+ fs-extra@11.3.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
fsevents@2.3.3:
optional: true
@@ -3819,6 +3832,16 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
+ gh-pages@6.3.0:
+ dependencies:
+ async: 3.2.6
+ commander: 13.1.0
+ email-addresses: 5.0.0
+ filenamify: 4.3.0
+ find-cache-dir: 3.3.2
+ fs-extra: 11.3.2
+ globby: 11.1.0
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -3829,13 +3852,22 @@ snapshots:
globals@14.0.0: {}
- globals@16.4.0: {}
+ globals@16.5.0: {}
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
gopd@1.2.0: {}
- graphemer@1.4.0: {}
+ graceful-fs@4.2.11: {}
- graphql@16.9.0: {}
+ graphql@16.12.0: {}
has-flag@4.0.0: {}
@@ -3910,24 +3942,22 @@ snapshots:
js-tokens@4.0.0: {}
- js-tokens@9.0.1: {}
-
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
- jsdom@27.0.0(postcss@8.5.6):
+ jsdom@27.3.0(postcss@8.5.6):
dependencies:
- '@asamuzakjp/dom-selector': 6.6.1
- cssstyle: 5.3.1(postcss@8.5.6)
+ '@acemir/cssom': 0.9.28
+ '@asamuzakjp/dom-selector': 6.7.6
+ cssstyle: 5.3.4(postcss@8.5.6)
data-urls: 6.0.0
- decimal.js: 10.5.0
+ decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
- parse5: 7.3.0
- rrweb-cssom: 0.8.0
+ parse5: 8.0.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.0
@@ -3954,6 +3984,12 @@ snapshots:
json5@2.2.3: {}
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -3963,31 +3999,37 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lodash.merge@4.6.2: {}
- loupe@3.1.3: {}
-
- loupe@3.2.0: {}
-
lru-cache@11.2.2: {}
+ lru-cache@11.2.4: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
- lucide-react@0.545.0(react@19.2.0):
+ lucide-react@0.556.0(react@19.2.1):
dependencies:
- react: 19.2.0
+ react: 19.2.1
lz-string@1.5.0: {}
- magic-string@0.30.17:
+ magic-string@0.30.21:
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ make-dir@3.1.0:
+ dependencies:
+ semver: 6.3.1
math-intrinsics@1.1.0: {}
@@ -4016,18 +4058,16 @@ snapshots:
dependencies:
brace-expansion: 2.0.1
- mrmime@2.0.0: {}
-
ms@2.1.3: {}
- msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3):
+ msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3):
dependencies:
'@inquirer/confirm': 5.0.1(@types/node@22.8.1)
- '@mswjs/interceptors': 0.39.5
+ '@mswjs/interceptors': 0.40.0
'@open-draft/deferred-promise': 2.2.0
- '@types/statuses': 2.0.5
+ '@types/statuses': 2.0.6
cookie: 1.0.2
- graphql: 16.9.0
+ graphql: 16.12.0
headers-polyfill: 4.0.3
is-node-process: 1.2.0
outvariant: 1.4.3
@@ -4037,7 +4077,7 @@ snapshots:
statuses: 2.0.2
strict-event-emitter: 0.5.1
tough-cookie: 6.0.0
- type-fest: 4.26.1
+ type-fest: 5.3.1
until-async: 3.0.2
yargs: 17.7.2
optionalDependencies:
@@ -4053,6 +4093,8 @@ snapshots:
node-releases@2.0.18: {}
+ obug@2.1.1: {}
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -4064,19 +4106,29 @@ snapshots:
outvariant@1.4.3: {}
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
+ p-try@2.2.0: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
- parse5@7.3.0:
+ parse5@8.0.0:
dependencies:
entities: 6.0.1
@@ -4086,18 +4138,20 @@ snapshots:
path-to-regexp@6.3.0: {}
- pathe@2.0.3: {}
+ path-type@4.0.0: {}
- pathval@2.0.0: {}
+ pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
- picomatch@4.0.2: {}
-
picomatch@4.0.3: {}
+ pkg-dir@4.2.0:
+ dependencies:
+ find-up: 4.1.0
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -4106,7 +4160,7 @@ snapshots:
prelude-ls@1.2.1: {}
- prettier@3.6.2: {}
+ prettier@3.7.4: {}
pretty-format@27.5.1:
dependencies:
@@ -4120,57 +4174,57 @@ snapshots:
queue-microtask@1.2.3: {}
- react-dom@19.2.0(react@19.2.0):
+ react-dom@19.2.1(react@19.2.1):
dependencies:
- react: 19.2.0
+ react: 19.2.1
scheduler: 0.27.0
react-is@17.0.2: {}
- react-refresh@0.17.0: {}
+ react-refresh@0.18.0: {}
- react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0):
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.1):
dependencies:
- react: 19.2.0
- react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0)
+ react: 19.2.1
+ react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.1)
tslib: 2.8.0
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- react-remove-scroll@2.6.3(@types/react@19.2.2)(react@19.2.0):
+ react-remove-scroll@2.6.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
- react: 19.2.0
- react-remove-scroll-bar: 2.3.8(@types/react@19.2.2)(react@19.2.0)
- react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0)
+ react: 19.2.1
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.7)(react@19.2.1)
+ react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.1)
tslib: 2.8.0
- use-callback-ref: 1.3.3(@types/react@19.2.2)(react@19.2.0)
- use-sidecar: 1.1.3(@types/react@19.2.2)(react@19.2.0)
+ use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.1)
+ use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.1)
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- react-router-dom@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
dependencies:
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- react-router: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ react-router: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ react-router@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
dependencies:
cookie: 1.0.2
- react: 19.2.0
+ react: 19.2.1
set-cookie-parser: 2.7.1
optionalDependencies:
- react-dom: 19.2.0(react@19.2.0)
+ react-dom: 19.2.1(react@19.2.1)
- react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0):
+ react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
get-nonce: 1.0.1
- react: 19.2.0
+ react: 19.2.1
tslib: 2.8.0
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- react@19.2.0: {}
+ react@19.2.1: {}
redent@3.0.0:
dependencies:
@@ -4187,7 +4241,7 @@ snapshots:
rettime@0.7.0: {}
- reusify@1.0.4: {}
+ reusify@1.1.0: {}
rollup@4.46.2:
dependencies:
@@ -4215,8 +4269,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.46.2
fsevents: 2.3.3
- rrweb-cssom@0.8.0: {}
-
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -4245,11 +4297,7 @@ snapshots:
signal-exit@4.1.0: {}
- sirv@2.0.4:
- dependencies:
- '@polka/url': 1.0.0-next.28
- mrmime: 2.0.0
- totalist: 3.0.1
+ slash@3.0.0: {}
source-map-js@1.2.1: {}
@@ -4257,7 +4305,7 @@ snapshots:
statuses@2.0.2: {}
- std-env@3.9.0: {}
+ std-env@3.10.0: {}
strict-event-emitter@0.5.1: {}
@@ -4277,9 +4325,9 @@ snapshots:
strip-json-comments@3.1.1: {}
- strip-literal@3.0.0:
+ strip-outer@1.0.1:
dependencies:
- js-tokens: 9.0.1
+ escape-string-regexp: 1.0.5
supports-color@7.2.0:
dependencies:
@@ -4287,27 +4335,18 @@ snapshots:
symbol-tree@3.2.4: {}
+ tagged-tag@1.0.0: {}
+
tinybench@2.9.0: {}
- tinyexec@0.3.2: {}
-
- tinyglobby@0.2.14:
- dependencies:
- fdir: 6.4.6(picomatch@4.0.3)
- picomatch: 4.0.3
+ tinyexec@1.0.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
- tinypool@1.1.1: {}
-
- tinyrainbow@1.2.0: {}
-
- tinyrainbow@2.0.0: {}
-
- tinyspy@4.0.3: {}
+ tinyrainbow@3.0.3: {}
tldts-core@7.0.17: {}
@@ -4319,8 +4358,6 @@ snapshots:
dependencies:
is-number: 7.0.0
- totalist@3.0.1: {}
-
tough-cookie@6.0.0:
dependencies:
tldts: 7.0.17
@@ -4329,6 +4366,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ trim-repeated@1.0.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+
ts-api-utils@2.1.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -4341,15 +4382,17 @@ snapshots:
type-fest@0.21.3: {}
- type-fest@4.26.1: {}
+ type-fest@5.3.1:
+ dependencies:
+ tagged-tag: 1.0.0
- typescript-eslint@8.46.0(eslint@9.37.0)(typescript@5.9.3):
+ typescript-eslint@8.49.0(eslint@9.39.1)(typescript@5.9.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript@5.9.3)
- '@typescript-eslint/parser': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
- '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3)
- '@typescript-eslint/utils': 8.46.0(eslint@9.37.0)(typescript@5.9.3)
- eslint: 9.37.0
+ '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
+ eslint: 9.39.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -4358,6 +4401,8 @@ snapshots:
undici-types@6.19.8: {}
+ universalify@2.0.1: {}
+
until-async@3.0.2: {}
update-browserslist-db@1.1.1(browserslist@4.24.2):
@@ -4370,43 +4415,22 @@ snapshots:
dependencies:
punycode: 2.3.1
- use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.0):
+ use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
- react: 19.2.0
+ react: 19.2.1
tslib: 2.8.0
optionalDependencies:
- '@types/react': 19.2.2
+ '@types/react': 19.2.7
- use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.0):
+ use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
detect-node-es: 1.1.0
- react: 19.2.0
+ react: 19.2.1
tslib: 2.8.0
optionalDependencies:
- '@types/react': 19.2.2
-
- vite-node@3.2.4(@types/node@22.8.1):
- dependencies:
- cac: 6.7.14
- debug: 4.4.1
- es-module-lexer: 1.7.0
- pathe: 2.0.3
- vite: 7.1.9(@types/node@22.8.1)
- transitivePeerDependencies:
- - '@types/node'
- - jiti
- - less
- - lightningcss
- - sass
- - sass-embedded
- - stylus
- - sugarss
- - supports-color
- - terser
- - tsx
- - yaml
+ '@types/react': 19.2.7
- vite@7.1.9(@types/node@22.8.1):
+ vite@7.2.7(@types/node@22.8.1):
dependencies:
esbuild: 0.25.3
fdir: 6.5.0(picomatch@4.0.3)
@@ -4418,45 +4442,40 @@ snapshots:
'@types/node': 22.8.1
fsevents: 2.3.3
- vitest-browser-react@1.0.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(@vitest/browser@2.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vitest@3.2.4):
+ vitest-browser-react@2.0.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vitest@4.0.15(@types/node@22.8.1)(jsdom@27.3.0(postcss@8.5.6))(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3))):
dependencies:
- '@vitest/browser': 2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.3)(vite@7.1.9(@types/node@22.8.1))(vitest@3.2.4)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- vitest: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@27.0.0(postcss@8.5.6))(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+ vitest: 4.0.15(@types/node@22.8.1)(jsdom@27.3.0(postcss@8.5.6))(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3))
optionalDependencies:
- '@types/react': 19.2.2
- '@types/react-dom': 19.2.1(@types/react@19.2.2)
-
- vitest@3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@27.0.0(postcss@8.5.6))(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3)):
- dependencies:
- '@types/chai': 5.2.2
- '@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(msw@2.11.5(@types/node@22.8.1)(typescript@5.9.3))(vite@7.1.9(@types/node@22.8.1))
- '@vitest/pretty-format': 3.2.4
- '@vitest/runner': 3.2.4
- '@vitest/snapshot': 3.2.4
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
- chai: 5.2.0
- debug: 4.4.1
- expect-type: 1.2.1
- magic-string: 0.30.17
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+ vitest@4.0.15(@types/node@22.8.1)(jsdom@27.3.0(postcss@8.5.6))(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3)):
+ dependencies:
+ '@vitest/expect': 4.0.15
+ '@vitest/mocker': 4.0.15(msw@2.12.4(@types/node@22.8.1)(typescript@5.9.3))(vite@7.2.7(@types/node@22.8.1))
+ '@vitest/pretty-format': 4.0.15
+ '@vitest/runner': 4.0.15
+ '@vitest/snapshot': 4.0.15
+ '@vitest/spy': 4.0.15
+ '@vitest/utils': 4.0.15
+ es-module-lexer: 1.7.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
pathe: 2.0.3
- picomatch: 4.0.2
- std-env: 3.9.0
+ picomatch: 4.0.3
+ std-env: 3.10.0
tinybench: 2.9.0
- tinyexec: 0.3.2
- tinyglobby: 0.2.14
- tinypool: 1.1.1
- tinyrainbow: 2.0.0
- vite: 7.1.9(@types/node@22.8.1)
- vite-node: 3.2.4(@types/node@22.8.1)
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.2.7(@types/node@22.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.8.1
- '@vitest/browser': 2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.3)(vite@7.1.9(@types/node@22.8.1))(vitest@3.2.4)
- jsdom: 27.0.0(postcss@8.5.6)
+ jsdom: 27.3.0(postcss@8.5.6)
transitivePeerDependencies:
- jiti
- less
@@ -4466,7 +4485,6 @@ snapshots:
- sass-embedded
- stylus
- sugarss
- - supports-color
- terser
- tsx
- yaml
@@ -4511,8 +4529,6 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
- ws@8.18.0: {}
-
ws@8.18.3: {}
xml-name-validator@5.0.0: {}
@@ -4544,3 +4560,8 @@ snapshots:
zod: 4.1.12
zod@4.1.12: {}
+
+ zustand@5.0.9(@types/react@19.2.7)(react@19.2.1):
+ optionalDependencies:
+ '@types/react': 19.2.7
+ react: 19.2.1
diff --git a/src/App.tsx b/src/App.tsx
index 0c0032aab..cce47b0b1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,19 +1,25 @@
import { BrowserRouter as Router } from "react-router-dom"
-import Header from "./components/Header.tsx"
-import Footer from "./components/Footer.tsx"
+import { QueryClientProvider } from "@tanstack/react-query"
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
+import { queryClient } from "./shared/lib"
+import Header from "./shared/ui/Header.tsx"
+import Footer from "./shared/ui/Footer.tsx"
import PostsManagerPage from "./pages/PostsManagerPage.tsx"
const App = () => {
return (
-
-
-
+
+
+
+
+ {import.meta.env.DEV && }
+
)
}
diff --git a/src/components/index.tsx b/src/components/index.tsx
index 8495817d3..a9ab70553 100644
--- a/src/components/index.tsx
+++ b/src/components/index.tsx
@@ -42,49 +42,61 @@ export const Button = forwardRef(({ className, v
Button.displayName = "Button"
// 입력 컴포넌트
-export const Input = forwardRef(({ className, type, ...props }, ref) => {
- return (
-
- )
-})
+export const Input = forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
Input.displayName = "Input"
// 카드 컴포넌트
-export const Card = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const Card = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
Card.displayName = "Card"
-export const CardHeader = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const CardHeader = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardHeader.displayName = "CardHeader"
-export const CardTitle = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const CardTitle = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardTitle.displayName = "CardTitle"
-export const CardContent = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const CardContent = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardContent.displayName = "CardContent"
// 텍스트 영역 컴포넌트
-export const Textarea = forwardRef(({ className, ...props }, ref) => {
- return (
-
- )
-})
+export const Textarea = forwardRef>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
Textarea.displayName = "Textarea"
// 선택 컴포넌트
@@ -92,7 +104,10 @@ export const Select = SelectPrimitive.Root
export const SelectGroup = SelectPrimitive.Group
export const SelectValue = SelectPrimitive.Value
-export const SelectTrigger = forwardRef(({ className, children, ...props }, ref) => (
+export const SelectTrigger = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
(
+export const SelectContent = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
(
+export const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
)
DialogHeader.displayName = "DialogHeader"
-export const DialogTitle = forwardRef(({ className, ...props }, ref) => (
+export const DialogTitle = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
(
DialogTitle.displayName = DialogPrimitive.Title.displayName
// 테이블 컴포넌트
-export const Table = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const Table = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
Table.displayName = "Table"
-export const TableHeader = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const TableHeader = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
TableHeader.displayName = "TableHeader"
-export const TableBody = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const TableBody = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
TableBody.displayName = "TableBody"
-export const TableRow = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const TableRow = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
TableRow.displayName = "TableRow"
-export const TableHead = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const TableHead = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
TableHead.displayName = "TableHead"
-export const TableCell = forwardRef(({ className, ...props }, ref) => (
-
-))
+export const TableCell = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
TableCell.displayName = "TableCell"
diff --git a/src/entities/comment/api/commentApi.ts b/src/entities/comment/api/commentApi.ts
new file mode 100644
index 000000000..f3eeb4559
--- /dev/null
+++ b/src/entities/comment/api/commentApi.ts
@@ -0,0 +1,88 @@
+import type { Comment } from '../model/types'
+import { API_BASE_URL } from '../../../shared/config'
+
+export interface CreateCommentDto {
+ body: string
+ postId: number
+ userId: number
+}
+
+export interface UpdateCommentDto {
+ body?: string
+}
+
+export interface CommentsResponse {
+ comments: Comment[]
+ total: number
+ skip: number
+ limit: number
+}
+
+export const commentApi = {
+ // 댓글 목록 조회 (게시물별)
+ getCommentsByPost: async (postId: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/post/${postId}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch comments for post: ${postId}`)
+ }
+ return response.json()
+ },
+
+ // 댓글 단건 조회
+ getComment: async (id: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/${id}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch comment: ${id}`)
+ }
+ return response.json()
+ },
+
+ // 댓글 생성
+ createComment: async (comment: CreateCommentDto): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/add`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(comment),
+ })
+ if (!response.ok) {
+ throw new Error('Failed to create comment')
+ }
+ return response.json()
+ },
+
+ // 댓글 수정
+ updateComment: async (id: number, comment: UpdateCommentDto): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(comment),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to update comment: ${id}`)
+ }
+ return response.json()
+ },
+
+ // 댓글 삭제
+ deleteComment: async (id: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/${id}`, {
+ method: "DELETE",
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to delete comment: ${id}`)
+ }
+ },
+
+ // 댓글 좋아요
+ likeComment: async (id: number, likes: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/comments/${id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ likes }),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to like comment: ${id}`)
+ }
+ return response.json()
+ },
+}
diff --git a/src/entities/comment/api/index.ts b/src/entities/comment/api/index.ts
new file mode 100644
index 000000000..10512d71f
--- /dev/null
+++ b/src/entities/comment/api/index.ts
@@ -0,0 +1,3 @@
+export { commentApi } from './commentApi'
+export type { CreateCommentDto, UpdateCommentDto, CommentsResponse } from './commentApi'
+
diff --git a/src/entities/comment/index.ts b/src/entities/comment/index.ts
new file mode 100644
index 000000000..2c28de610
--- /dev/null
+++ b/src/entities/comment/index.ts
@@ -0,0 +1,6 @@
+export type { Comment } from './model/types'
+export { commentApi } from './api'
+export type { CreateCommentDto, UpdateCommentDto, CommentsResponse } from './api'
+export { useCommentStore } from './model/store'
+export * from './ui'
+
diff --git a/src/entities/comment/model/store.ts b/src/entities/comment/model/store.ts
new file mode 100644
index 000000000..49404fcec
--- /dev/null
+++ b/src/entities/comment/model/store.ts
@@ -0,0 +1,171 @@
+import { create } from 'zustand'
+import { commentApi, type CreateCommentDto, type UpdateCommentDto, type CommentsResponse } from '../api'
+import type { Comment } from './types'
+
+interface CommentStore {
+ // 상태 (postId별로 댓글 관리)
+ comments: Record // { [postId]: Comment[] }
+ loading: boolean
+ error: string | null
+
+ // API 호출 + 상태 관리 함수들 (순수 Comment 도메인만)
+ fetchCommentsByPost: (postId: number) => Promise
+ createComment: (comment: CreateCommentDto) => Promise
+ updateComment: (id: number, comment: UpdateCommentDto) => Promise
+ deleteComment: (id: number, postId: number) => Promise
+ likeComment: (id: number, postId: number, currentLikes: number) => Promise
+
+ // 상태 조작 함수들
+ setComments: (postId: number, comments: Comment[]) => void
+ setLoading: (loading: boolean) => void
+ setError: (error: string | null) => void
+ clearError: () => void
+ reset: () => void
+}
+
+const initialState = {
+ comments: {} as Record,
+ loading: false,
+ error: null,
+}
+
+export const useCommentStore = create((set) => ({
+ ...initialState,
+
+ // 게시물별 댓글 조회 (순수 Comment API만 호출)
+ fetchCommentsByPost: async (postId: number) => {
+ set({ loading: true, error: null })
+
+ try {
+ const response = await commentApi.getCommentsByPost(postId)
+ set((state) => ({
+ comments: {
+ ...state.comments,
+ [postId]: response.comments,
+ },
+ loading: false,
+ }))
+ return response
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '댓글을 불러오는데 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 댓글 생성 (순수 Comment API만 호출)
+ createComment: async (comment: CreateCommentDto) => {
+ set({ loading: true, error: null })
+ try {
+ const createdComment = await commentApi.createComment(comment)
+ set((state) => ({
+ comments: {
+ ...state.comments,
+ [comment.postId]: [
+ ...(state.comments[comment.postId] || []),
+ createdComment,
+ ],
+ },
+ loading: false,
+ }))
+ return createdComment
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '댓글 생성에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 댓글 수정 (순수 Comment API만 호출)
+ updateComment: async (id: number, comment: UpdateCommentDto) => {
+ set({ loading: true, error: null })
+ try {
+ const updatedComment = await commentApi.updateComment(id, comment)
+
+ // postId를 찾아서 업데이트
+ set((state) => {
+ const newComments = { ...state.comments }
+ for (const postId in newComments) {
+ const index = newComments[postId].findIndex((c) => c.id === id)
+ if (index !== -1) {
+ newComments[postId] = [
+ ...newComments[postId].slice(0, index),
+ updatedComment,
+ ...newComments[postId].slice(index + 1),
+ ]
+ break
+ }
+ }
+ return { comments: newComments, loading: false }
+ })
+
+ return updatedComment
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '댓글 수정에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 댓글 삭제 (순수 Comment API만 호출)
+ deleteComment: async (id: number, postId: number) => {
+ set({ loading: true, error: null })
+ try {
+ await commentApi.deleteComment(id)
+ set((state) => ({
+ comments: {
+ ...state.comments,
+ [postId]: (state.comments[postId] || []).filter((c) => c.id !== id),
+ },
+ loading: false,
+ }))
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '댓글 삭제에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 댓글 좋아요 (순수 Comment API만 호출)
+ likeComment: async (id: number, postId: number, currentLikes: number) => {
+ set({ loading: true, error: null })
+ try {
+ const updatedComment = await commentApi.likeComment(id, currentLikes + 1)
+ set((state) => ({
+ comments: {
+ ...state.comments,
+ [postId]: (state.comments[postId] || []).map((c) =>
+ c.id === id ? { ...updatedComment, likes: currentLikes + 1 } : c
+ ),
+ },
+ loading: false,
+ }))
+ return updatedComment
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '댓글 좋아요에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 상태 조작 함수들
+ setComments: (postId: number, comments: Comment[]) =>
+ set((state) => ({
+ comments: { ...state.comments, [postId]: comments },
+ })),
+ setLoading: (loading: boolean) => set({ loading }),
+ setError: (error: string | null) => set({ error }),
+ clearError: () => set({ error: null }),
+ reset: () => set(initialState),
+}))
+
diff --git a/src/entities/comment/model/types.ts b/src/entities/comment/model/types.ts
new file mode 100644
index 000000000..e6f8a7428
--- /dev/null
+++ b/src/entities/comment/model/types.ts
@@ -0,0 +1,11 @@
+export interface Comment {
+ id: number
+ body: string
+ postId: number
+ likes: number
+ user: {
+ id: number
+ username: string
+ fullName: string
+ }
+}
diff --git a/src/entities/comment/ui/CommentItem.tsx b/src/entities/comment/ui/CommentItem.tsx
new file mode 100644
index 000000000..4b32f0a61
--- /dev/null
+++ b/src/entities/comment/ui/CommentItem.tsx
@@ -0,0 +1,48 @@
+import { ThumbsUp, Edit2, Trash2 } from "lucide-react"
+import type { Comment } from '../model/types'
+import { highlightText } from '../../../shared/lib'
+import { Button } from '../../../shared/ui'
+
+interface CommentItemProps {
+ comment: Comment
+ postId: number
+ searchQuery?: string
+ onLikeComment: (commentId: number, postId: number) => void
+ onEditComment: (comment: Comment) => void
+ onDeleteComment: (commentId: number, postId: number) => void
+}
+
+/**
+ * 댓글 아이템 컴포넌트
+ * 개별 댓글을 표시하고 관리합니다.
+ */
+export const CommentItem = ({
+ comment,
+ postId,
+ searchQuery = "",
+ onLikeComment,
+ onEditComment,
+ onDeleteComment,
+}: CommentItemProps) => {
+ return (
+
+
+ {comment.user.username}:
+ {highlightText(comment.body, searchQuery)}
+
+
+ onLikeComment(comment.id, postId)}>
+
+ {comment.likes}
+
+ onEditComment(comment)}>
+
+
+ onDeleteComment(comment.id, postId)}>
+
+
+
+
+ )
+}
+
diff --git a/src/entities/comment/ui/index.ts b/src/entities/comment/ui/index.ts
new file mode 100644
index 000000000..be223e3c2
--- /dev/null
+++ b/src/entities/comment/ui/index.ts
@@ -0,0 +1,2 @@
+export { CommentItem } from './CommentItem'
+
diff --git a/src/entities/post/api/index.ts b/src/entities/post/api/index.ts
new file mode 100644
index 000000000..4939e01e8
--- /dev/null
+++ b/src/entities/post/api/index.ts
@@ -0,0 +1,4 @@
+export { postApi } from './postApi'
+export type { CreatePostDto, UpdatePostDto, PostsResponse, Tag } from './postApi'
+export type { Post } from '../model/types'
+
diff --git a/src/entities/post/api/postApi.ts b/src/entities/post/api/postApi.ts
new file mode 100644
index 000000000..8d9880bfe
--- /dev/null
+++ b/src/entities/post/api/postApi.ts
@@ -0,0 +1,108 @@
+import type { Post } from '../model/types'
+import { API_BASE_URL } from '../../../shared/config'
+
+export interface CreatePostDto {
+ title: string
+ body: string
+ userId: number
+}
+
+export interface UpdatePostDto {
+ title?: string
+ body?: string
+}
+
+export interface PostsResponse {
+ posts: Post[]
+ total: number
+ skip: number
+ limit: number
+}
+
+export interface Tag {
+ url: string
+ slug: string
+}
+
+export const postApi = {
+ // 게시물 목록 조회
+ getPosts: async (limit: number, skip: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts?limit=${limit}&skip=${skip}`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch posts')
+ }
+ return response.json()
+ },
+
+ // 게시물 단건 조회
+ getPost: async (id: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/${id}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch post: ${id}`)
+ }
+ return response.json()
+ },
+
+ // 게시물 생성
+ createPost: async (post: CreatePostDto): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/add`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(post),
+ })
+ if (!response.ok) {
+ throw new Error('Failed to create post')
+ }
+ return response.json()
+ },
+
+ // 게시물 수정
+ updatePost: async (id: number, post: UpdatePostDto): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(post),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to update post: ${id}`)
+ }
+ return response.json()
+ },
+
+ // 게시물 삭제
+ deletePost: async (id: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/${id}`, {
+ method: "DELETE",
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to delete post: ${id}`)
+ }
+ },
+
+ // 게시물 검색
+ searchPosts: async (query: string): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/search?q=${query}`)
+ if (!response.ok) {
+ throw new Error('Failed to search posts')
+ }
+ return response.json()
+ },
+
+ // 태그별 게시물 조회
+ getPostsByTag: async (tag: string): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/tag/${tag}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch posts by tag: ${tag}`)
+ }
+ return response.json()
+ },
+
+ // 태그 목록 조회
+ getTags: async (): Promise => {
+ const response = await fetch(`${API_BASE_URL}/posts/tags`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch tags')
+ }
+ return response.json()
+ },
+}
diff --git a/src/entities/post/index.ts b/src/entities/post/index.ts
new file mode 100644
index 000000000..5ce9902d2
--- /dev/null
+++ b/src/entities/post/index.ts
@@ -0,0 +1,6 @@
+export type { Post, PostAuthor } from './model/types'
+export { postApi } from './api'
+export type { CreatePostDto, UpdatePostDto, PostsResponse, Tag } from './api'
+export { usePostStore } from './model/store'
+export * from './ui'
+
diff --git a/src/entities/post/model/store.ts b/src/entities/post/model/store.ts
new file mode 100644
index 000000000..258eecc7f
--- /dev/null
+++ b/src/entities/post/model/store.ts
@@ -0,0 +1,197 @@
+import { create } from 'zustand'
+import { postApi, type CreatePostDto, type UpdatePostDto, type PostsResponse } from '../api'
+import type { Post } from './types'
+
+interface PostStore {
+ // 상태
+ posts: Post[]
+ total: number
+ loading: boolean
+ error: string | null
+ selectedPost: Post | null
+
+ // API 호출 + 상태 관리 함수들 (순수 Post 도메인만)
+ fetchPosts: (limit: number, skip: number) => Promise
+ fetchPost: (id: number) => Promise
+ createPost: (post: CreatePostDto) => Promise
+ updatePost: (id: number, post: UpdatePostDto) => Promise
+ deletePost: (id: number) => Promise
+ searchPosts: (query: string) => Promise
+ getPostsByTag: (tag: string) => Promise
+
+ // 상태 조작 함수들
+ setPosts: (posts: Post[]) => void
+ setTotal: (total: number) => void
+ setLoading: (loading: boolean) => void
+ setError: (error: string | null) => void
+ setSelectedPost: (post: Post | null) => void
+ clearError: () => void
+ reset: () => void
+}
+
+const initialState = {
+ posts: [],
+ total: 0,
+ loading: false,
+ error: null,
+ selectedPost: null,
+}
+
+export const usePostStore = create((set) => ({
+ ...initialState,
+
+ // 게시물 목록 조회 (순수 Post API만 호출)
+ fetchPosts: async (limit: number, skip: number) => {
+ set({ loading: true, error: null })
+
+ try {
+ const response = await postApi.getPosts(limit, skip)
+ set({
+ posts: response.posts,
+ total: response.total,
+ loading: false,
+ })
+ return response
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '게시물을 불러오는데 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 게시물 단건 조회
+ fetchPost: async (id: number) => {
+ set({ loading: true, error: null })
+ try {
+ const post = await postApi.getPost(id)
+ set({ selectedPost: post, loading: false })
+ return post
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '게시물을 불러오는데 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 게시물 생성 (API 호출 + 상태 관리)
+ createPost: async (post: CreatePostDto) => {
+ set({ loading: true, error: null })
+ try {
+ const createdPost = await postApi.createPost(post)
+ set((state) => ({
+ posts: [createdPost, ...state.posts],
+ total: state.total + 1,
+ loading: false,
+ }))
+ return createdPost
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '게시물 생성에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 게시물 수정 (API 호출 + 상태 관리)
+ updatePost: async (id: number, post: UpdatePostDto) => {
+ set({ loading: true, error: null })
+ try {
+ const updatedPost = await postApi.updatePost(id, post)
+ set((state) => ({
+ posts: state.posts.map((p) => (p.id === id ? updatedPost : p)),
+ selectedPost: state.selectedPost?.id === id ? updatedPost : state.selectedPost,
+ loading: false,
+ }))
+ return updatedPost
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '게시물 수정에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 게시물 삭제 (API 호출 + 상태 관리)
+ deletePost: async (id: number) => {
+ set({ loading: true, error: null })
+ try {
+ await postApi.deletePost(id)
+ set((state) => ({
+ posts: state.posts.filter((p) => p.id !== id),
+ total: state.total - 1,
+ selectedPost: state.selectedPost?.id === id ? null : state.selectedPost,
+ loading: false,
+ }))
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '게시물 삭제에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 게시물 검색 (순수 Post API만 호출)
+ searchPosts: async (query: string) => {
+ if (!query.trim()) {
+ throw new Error('검색어를 입력해주세요')
+ }
+
+ set({ loading: true, error: null })
+ try {
+ const response = await postApi.searchPosts(query)
+ set({
+ posts: response.posts,
+ total: response.total,
+ loading: false,
+ })
+ return response
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '검색에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 태그별 게시물 조회 (순수 Post API만 호출)
+ getPostsByTag: async (tag: string) => {
+ if (!tag || tag === "all") {
+ throw new Error('태그를 선택해주세요')
+ }
+
+ set({ loading: true, error: null })
+ try {
+ const response = await postApi.getPostsByTag(tag)
+ set({
+ posts: response.posts,
+ total: response.total,
+ loading: false,
+ })
+ return response
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '태그별 게시물 조회에 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 상태 조작 함수들
+ setPosts: (posts: Post[]) => set({ posts }),
+ setTotal: (total: number) => set({ total }),
+ setLoading: (loading: boolean) => set({ loading }),
+ setError: (error: string | null) => set({ error }),
+ setSelectedPost: (post: Post | null) => set({ selectedPost: post }),
+ clearError: () => set({ error: null }),
+ reset: () => set(initialState),
+}))
+
diff --git a/src/entities/post/model/types.ts b/src/entities/post/model/types.ts
new file mode 100644
index 000000000..c7915ea6a
--- /dev/null
+++ b/src/entities/post/model/types.ts
@@ -0,0 +1,22 @@
+// Post의 author는 User 엔티티를 참조하지만,
+// FSD 규칙상 같은 Layer 내 다른 Slice를 직접 import할 수 없으므로
+// 필요한 필드만 별도로 정의합니다.
+export interface PostAuthor {
+ id: number
+ username: string
+ image: string
+}
+
+export interface Post {
+ id: number
+ title: string
+ body: string
+ tags?: string[]
+ reactions?: {
+ likes: number
+ dislikes: number
+ }
+ views?: number
+ userId: number
+ author?: PostAuthor
+}
diff --git a/src/entities/post/ui/PostAuthor.tsx b/src/entities/post/ui/PostAuthor.tsx
new file mode 100644
index 000000000..10b89a3fc
--- /dev/null
+++ b/src/entities/post/ui/PostAuthor.tsx
@@ -0,0 +1,27 @@
+import type { PostAuthor as PostAuthorType } from '../model/types'
+
+interface PostAuthorProps {
+ author: PostAuthorType
+ onClick?: () => void
+}
+
+/**
+ * 게시물 작성자 아바타 컴포넌트
+ * 게시물 작성자의 이미지와 이름을 표시합니다.
+ */
+export const PostAuthor = ({ author, onClick }: PostAuthorProps) => {
+ return (
+
+
+
{author.username}
+
+ )
+}
+
diff --git a/src/entities/post/ui/PostRow.tsx b/src/entities/post/ui/PostRow.tsx
new file mode 100644
index 000000000..448487b17
--- /dev/null
+++ b/src/entities/post/ui/PostRow.tsx
@@ -0,0 +1,83 @@
+import { ThumbsDown, ThumbsUp, MessageSquare, Edit2, Trash2 } from "lucide-react"
+import type { Post } from '../model/types'
+import { highlightText } from '../../../shared/lib'
+import { Button, TableCell, TableRow } from '../../../shared/ui'
+import { PostTag } from './PostTag'
+import { PostAuthor } from './PostAuthor'
+
+interface PostRowProps {
+ post: Post
+ searchQuery?: string
+ selectedTag?: string
+ onPostDetail: (post: Post) => void
+ onPostEdit: (post: Post) => void
+ onPostDelete: (postId: number) => void
+ onUserClick: (author: Post['author']) => void
+ onTagClick: (tag: string) => void
+}
+
+/**
+ * 게시물 테이블 행 컴포넌트
+ * 게시물 정보를 테이블 행 형태로 표시합니다.
+ */
+export const PostRow = ({
+ post,
+ searchQuery = "",
+ selectedTag,
+ onPostDetail,
+ onPostEdit,
+ onPostDelete,
+ onUserClick,
+ onTagClick,
+}: PostRowProps) => {
+ return (
+
+ {post.id}
+
+
+
{highlightText(post.title, searchQuery)}
+
+ {post.tags?.map((tag) => (
+
onTagClick(tag)}
+ />
+ ))}
+
+
+
+
+ {post.author && (
+ onUserClick(post.author)}
+ />
+ )}
+
+
+
+
+ {post.reactions?.likes || 0}
+
+ {post.reactions?.dislikes || 0}
+
+
+
+
+ onPostDetail(post)}>
+
+
+ onPostEdit(post)}>
+
+
+ onPostDelete(post.id)}>
+
+
+
+
+
+ )
+}
+
diff --git a/src/entities/post/ui/PostTag.tsx b/src/entities/post/ui/PostTag.tsx
new file mode 100644
index 000000000..f515a8e94
--- /dev/null
+++ b/src/entities/post/ui/PostTag.tsx
@@ -0,0 +1,25 @@
+interface PostTagProps {
+ tag: string
+ isSelected?: boolean
+ onClick?: () => void
+}
+
+/**
+ * 게시물 태그 컴포넌트
+ * 태그를 표시하고 클릭 이벤트를 처리합니다.
+ */
+export const PostTag = ({ tag, isSelected = false, onClick }: PostTagProps) => {
+ return (
+
+ {tag}
+
+ )
+}
+
diff --git a/src/entities/post/ui/index.ts b/src/entities/post/ui/index.ts
new file mode 100644
index 000000000..c0585708c
--- /dev/null
+++ b/src/entities/post/ui/index.ts
@@ -0,0 +1,4 @@
+export { PostRow } from './PostRow'
+export { PostTag } from './PostTag'
+export { PostAuthor } from './PostAuthor'
+
diff --git a/src/entities/user/api/index.ts b/src/entities/user/api/index.ts
new file mode 100644
index 000000000..9e66d0246
--- /dev/null
+++ b/src/entities/user/api/index.ts
@@ -0,0 +1,3 @@
+export { userApi } from './userApi'
+export type { UsersResponse, UserSelectFields } from './userApi'
+
diff --git a/src/entities/user/api/userApi.ts b/src/entities/user/api/userApi.ts
new file mode 100644
index 000000000..1b2ead981
--- /dev/null
+++ b/src/entities/user/api/userApi.ts
@@ -0,0 +1,44 @@
+import type { User } from '../model/types'
+import { API_BASE_URL } from '../../../shared/config'
+
+export interface UsersResponse {
+ users: User[]
+ total: number
+ skip: number
+ limit: number
+}
+
+export interface UserSelectFields {
+ username?: boolean
+ image?: boolean
+ // 필요한 필드만 선택
+}
+
+export const userApi = {
+ // 사용자 목록 조회
+ getUsers: async (params?: {
+ limit?: number
+ skip?: number
+ select?: string
+ }): Promise => {
+ const queryParams = new URLSearchParams()
+ if (params?.limit !== undefined) queryParams.set('limit', params.limit.toString())
+ if (params?.skip !== undefined) queryParams.set('skip', params.skip.toString())
+ if (params?.select) queryParams.set('select', params.select)
+
+ const response = await fetch(`${API_BASE_URL}/users?${queryParams.toString()}`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch users')
+ }
+ return response.json()
+ },
+
+ // 사용자 단건 조회
+ getUser: async (id: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/users/${id}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch user: ${id}`)
+ }
+ return response.json()
+ },
+}
diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts
new file mode 100644
index 000000000..99f7b863f
--- /dev/null
+++ b/src/entities/user/index.ts
@@ -0,0 +1,6 @@
+export type { User } from './model/types'
+export { userApi } from './api'
+export type { UsersResponse, UserSelectFields } from './api'
+export { useUserStore } from './model/store'
+export * from './ui'
+
diff --git a/src/entities/user/model/store.ts b/src/entities/user/model/store.ts
new file mode 100644
index 000000000..5366e8518
--- /dev/null
+++ b/src/entities/user/model/store.ts
@@ -0,0 +1,54 @@
+import { create } from 'zustand'
+import { userApi } from '../api'
+import type { User } from './types'
+
+interface UserStore {
+ // 상태
+ selectedUser: User | null
+ loading: boolean
+ error: string | null
+
+ // API 호출 + 상태 관리 함수들 (순수 User 도메인만)
+ fetchUser: (id: number) => Promise
+
+ // 상태 조작 함수들
+ setSelectedUser: (user: User | null) => void
+ setLoading: (loading: boolean) => void
+ setError: (error: string | null) => void
+ clearError: () => void
+ reset: () => void
+}
+
+const initialState = {
+ selectedUser: null,
+ loading: false,
+ error: null,
+}
+
+export const useUserStore = create((set) => ({
+ ...initialState,
+
+ // 사용자 단건 조회 (순수 User API만 호출)
+ fetchUser: async (id: number) => {
+ set({ loading: true, error: null })
+ try {
+ const user = await userApi.getUser(id)
+ set({ selectedUser: user, loading: false })
+ return user
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : '사용자 정보를 불러오는데 실패했습니다',
+ loading: false,
+ })
+ throw error
+ }
+ },
+
+ // 상태 조작 함수들
+ setSelectedUser: (user: User | null) => set({ selectedUser: user }),
+ setLoading: (loading: boolean) => set({ loading }),
+ setError: (error: string | null) => set({ error }),
+ clearError: () => set({ error: null }),
+ reset: () => set(initialState),
+}))
+
diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts
new file mode 100644
index 000000000..8621f3131
--- /dev/null
+++ b/src/entities/user/model/types.ts
@@ -0,0 +1,70 @@
+export interface User {
+ id: number
+ firstName: string
+ lastName: string
+ maidenName: string
+ age: number
+ gender: string
+ email: string
+ phone: string
+ username: string
+ password: string
+ birthDate: string
+ image: string
+ bloodGroup: string
+ height: number
+ weight: number
+ eyeColor: string
+ hair: {
+ color: string
+ type: string
+ }
+ ip: string
+ address: {
+ address: string
+ city: string
+ state: string
+ stateCode: string
+ postalCode: string
+ coordinates: {
+ lat: number
+ lng: number
+ }
+ country: string
+ }
+ macAddress: string
+ university: string
+ bank: {
+ cardExpire: string
+ cardNumber: string
+ cardType: string
+ currency: string
+ iban: string
+ }
+ company: {
+ department: string
+ name: string
+ title: string
+ address: {
+ address: string
+ city: string
+ state: string
+ stateCode: string
+ postalCode: string
+ coordinates: {
+ lat: number
+ lng: number
+ }
+ country: string
+ }
+ }
+ ein: string
+ ssn: string
+ userAgent: string
+ crypto: {
+ coin: string
+ wallet: string
+ network: string
+ }
+ role: string
+ }
diff --git a/src/entities/user/ui/index.ts b/src/entities/user/ui/index.ts
new file mode 100644
index 000000000..0c01be7b6
--- /dev/null
+++ b/src/entities/user/ui/index.ts
@@ -0,0 +1,2 @@
+// UserAvatar는 PostAuthor로 대체되어 entities/post/ui로 이동했습니다.
+
diff --git a/src/features/comment/index.ts b/src/features/comment/index.ts
new file mode 100644
index 000000000..ce0c1d039
--- /dev/null
+++ b/src/features/comment/index.ts
@@ -0,0 +1,6 @@
+export { useCommentCreate } from './model/useCommentCreate'
+export { useCommentUpdate } from './model/useCommentUpdate'
+export { useCommentDelete } from './model/useCommentDelete'
+export { useCommentLike } from './model/useCommentLike'
+export { useCommentList } from './model/useCommentList'
+
diff --git a/src/features/comment/model/useCommentCreate.ts b/src/features/comment/model/useCommentCreate.ts
new file mode 100644
index 000000000..45ed2232a
--- /dev/null
+++ b/src/features/comment/model/useCommentCreate.ts
@@ -0,0 +1,21 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { commentApi, type CreateCommentDto } from '../../../entities/comment/api'
+import { commentKeys } from '../../../shared/lib'
+
+/**
+ * 댓글 생성 기능
+ */
+export const useCommentCreate = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (comment: CreateCommentDto) => commentApi.createComment(comment),
+ onSuccess: (_data, variables) => {
+ // 해당 게시물의 댓글 목록 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: commentKeys.list(variables.postId) })
+ },
+ onError: (error) => {
+ console.error('댓글 생성 오류:', error)
+ },
+ })
+}
diff --git a/src/features/comment/model/useCommentDelete.ts b/src/features/comment/model/useCommentDelete.ts
new file mode 100644
index 000000000..7288d94fd
--- /dev/null
+++ b/src/features/comment/model/useCommentDelete.ts
@@ -0,0 +1,22 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { commentApi } from '../../../entities/comment/api'
+import { commentKeys } from '../../../shared/lib'
+
+/**
+ * 댓글 삭제 기능
+ */
+export const useCommentDelete = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ id }: { id: number; postId: number }) =>
+ commentApi.deleteComment(id),
+ onSuccess: (_, variables) => {
+ // 해당 게시물의 댓글 목록 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: commentKeys.list(variables.postId) })
+ },
+ onError: (error) => {
+ console.error('댓글 삭제 오류:', error)
+ },
+ })
+}
diff --git a/src/features/comment/model/useCommentLike.ts b/src/features/comment/model/useCommentLike.ts
new file mode 100644
index 000000000..3aa0c8d65
--- /dev/null
+++ b/src/features/comment/model/useCommentLike.ts
@@ -0,0 +1,57 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { commentApi } from '../../../entities/comment/api'
+import { commentKeys } from '../../../shared/lib'
+import type { Comment } from '../../../entities/comment'
+
+/**
+ * 댓글 좋아요 기능
+ * 낙관적 업데이트를 적용합니다.
+ */
+export const useCommentLike = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ id, likes }: { id: number; likes: number; postId: number }) =>
+ commentApi.likeComment(id, likes),
+ // 낙관적 업데이트: 서버 응답 전에 UI 업데이트
+ onMutate: async ({ id, likes, postId }) => {
+ // 진행 중인 쿼리 취소
+ await queryClient.cancelQueries({ queryKey: commentKeys.list(postId) })
+
+ // 이전 데이터 백업
+ const previousComments = queryClient.getQueryData<{ comments: Comment[] }>(
+ commentKeys.list(postId)
+ )
+
+ // 낙관적 업데이트: 즉시 좋아요 수 증가
+ queryClient.setQueryData<{ comments: Comment[] }>(
+ commentKeys.list(postId),
+ (old) => {
+ if (!old) return old
+ return {
+ ...old,
+ comments: old.comments.map((comment) =>
+ comment.id === id ? { ...comment, likes: likes + 1 } : comment
+ ),
+ }
+ }
+ )
+
+ return { previousComments }
+ },
+ // 에러 발생 시 롤백
+ onError: (error, variables, context) => {
+ if (context?.previousComments) {
+ queryClient.setQueryData(
+ commentKeys.list(variables.postId),
+ context.previousComments
+ )
+ }
+ console.error('댓글 좋아요 오류:', error)
+ },
+ // 성공 시 쿼리 무효화
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({ queryKey: commentKeys.list(variables.postId) })
+ },
+ })
+}
diff --git a/src/features/comment/model/useCommentList.ts b/src/features/comment/model/useCommentList.ts
new file mode 100644
index 000000000..9bd763337
--- /dev/null
+++ b/src/features/comment/model/useCommentList.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query'
+import { commentApi } from '../../../entities/comment/api'
+import { commentKeys } from '../../../shared/lib'
+
+/**
+ * 댓글 목록 조회 기능
+ */
+export const useCommentList = (postId: number) => {
+ return useQuery({
+ queryKey: commentKeys.list(postId),
+ queryFn: () => commentApi.getCommentsByPost(postId),
+ enabled: postId > 0, // postId가 유효할 때만 실행
+ })
+}
diff --git a/src/features/comment/model/useCommentUpdate.ts b/src/features/comment/model/useCommentUpdate.ts
new file mode 100644
index 000000000..6130cea8e
--- /dev/null
+++ b/src/features/comment/model/useCommentUpdate.ts
@@ -0,0 +1,22 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { commentApi, type UpdateCommentDto } from '../../../entities/comment/api'
+import { commentKeys } from '../../../shared/lib'
+
+/**
+ * 댓글 수정 기능
+ */
+export const useCommentUpdate = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ id, comment }: { id: number; comment: UpdateCommentDto }) =>
+ commentApi.updateComment(id, comment),
+ onSuccess: (data) => {
+ // 해당 게시물의 댓글 목록 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: commentKeys.list(data.postId) })
+ },
+ onError: (error) => {
+ console.error('댓글 수정 오류:', error)
+ },
+ })
+}
diff --git a/src/features/post/index.ts b/src/features/post/index.ts
new file mode 100644
index 000000000..525bcab81
--- /dev/null
+++ b/src/features/post/index.ts
@@ -0,0 +1,17 @@
+export { usePostDelete } from './model/usePostDelete'
+export { usePostCreate } from './model/usePostCreate'
+export { usePostUpdate } from './model/usePostUpdate'
+export { usePostSearch } from './model/usePostSearch'
+export { usePostSort } from './model/usePostSort'
+export { useTags, usePostsByTag } from './model/useTagFilter'
+export { usePostList } from './model/usePostList'
+export { usePostFilters } from './model/usePostFilters'
+export { usePostPagination } from './model/usePostPagination'
+export { usePostCreateModal } from './model/usePostCreateModal'
+export { usePostEditModal } from './model/usePostEditModal'
+export { usePostDetailModal } from './model/usePostDetailModal'
+
+export type { SortBy, SortOrder } from './model/usePostSort'
+
+export * from './ui'
+
diff --git a/src/features/post/model/usePostCreate.ts b/src/features/post/model/usePostCreate.ts
new file mode 100644
index 000000000..86cc55ada
--- /dev/null
+++ b/src/features/post/model/usePostCreate.ts
@@ -0,0 +1,21 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { postApi, type CreatePostDto } from '../../../entities/post/api'
+import { postKeys } from '../../../shared/lib'
+
+/**
+ * 게시물 생성 기능
+ */
+export const usePostCreate = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (post: CreatePostDto) => postApi.createPost(post),
+ onSuccess: () => {
+ // 게시물 목록 쿼리 무효화하여 자동 리프레시
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ },
+ onError: (error) => {
+ console.error('게시물 생성 오류:', error)
+ },
+ })
+}
diff --git a/src/features/post/model/usePostCreateModal.ts b/src/features/post/model/usePostCreateModal.ts
new file mode 100644
index 000000000..e219b3bb4
--- /dev/null
+++ b/src/features/post/model/usePostCreateModal.ts
@@ -0,0 +1,56 @@
+import { useState } from 'react'
+import type { CreatePostDto } from '../../../entities/post'
+
+/**
+ * 게시물 생성 모달 기능
+ * 게시물 생성 모달의 상태 및 폼 관리를 담당합니다.
+ */
+export const usePostCreateModal = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const [formData, setFormData] = useState({
+ title: "",
+ body: "",
+ userId: 1,
+ })
+
+ const openModal = () => {
+ setIsOpen(true)
+ }
+
+ const closeModal = () => {
+ setIsOpen(false)
+ resetForm()
+ }
+
+ const resetForm = () => {
+ setFormData({
+ title: "",
+ body: "",
+ userId: 1,
+ })
+ }
+
+ const updateTitle = (title: string) => {
+ setFormData((prev) => ({ ...prev, title }))
+ }
+
+ const updateBody = (body: string) => {
+ setFormData((prev) => ({ ...prev, body }))
+ }
+
+ const updateUserId = (userId: number) => {
+ setFormData((prev) => ({ ...prev, userId }))
+ }
+
+ return {
+ isOpen,
+ formData,
+ openModal,
+ closeModal,
+ resetForm,
+ updateTitle,
+ updateBody,
+ updateUserId,
+ }
+}
+
diff --git a/src/features/post/model/usePostDelete.ts b/src/features/post/model/usePostDelete.ts
new file mode 100644
index 000000000..1410eb2b8
--- /dev/null
+++ b/src/features/post/model/usePostDelete.ts
@@ -0,0 +1,52 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { postApi } from '../../../entities/post/api'
+import { postKeys } from '../../../shared/lib'
+import type { Post } from '../../../entities/post'
+
+/**
+ * 게시물 삭제 기능
+ * 낙관적 업데이트를 적용합니다.
+ */
+export const usePostDelete = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (id: number) => postApi.deletePost(id),
+ // 낙관적 업데이트: 서버 응답 전에 UI 업데이트
+ onMutate: async (id) => {
+ // 진행 중인 쿼리 취소
+ await queryClient.cancelQueries({ queryKey: postKeys.all() })
+
+ // 이전 데이터 백업
+ const previousQueries = queryClient.getQueriesData({ queryKey: postKeys.all() })
+
+ // 낙관적 업데이트: 목록에서 즉시 제거
+ queryClient.setQueriesData<{ posts: Post[]; total: number }>(
+ { queryKey: postKeys.all() },
+ (old) => {
+ if (!old) return old
+ return {
+ ...old,
+ posts: old.posts.filter((p) => p.id !== id),
+ total: old.total - 1,
+ }
+ }
+ )
+
+ return { previousQueries }
+ },
+ // 에러 발생 시 롤백
+ onError: (error, _id, context) => {
+ if (context?.previousQueries) {
+ context.previousQueries.forEach(([queryKey, data]) => {
+ queryClient.setQueryData(queryKey, data)
+ })
+ }
+ console.error('게시물 삭제 오류:', error)
+ },
+ // 성공 시 쿼리 무효화
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ },
+ })
+}
diff --git a/src/features/post/model/usePostDetailModal.ts b/src/features/post/model/usePostDetailModal.ts
new file mode 100644
index 000000000..63cf04795
--- /dev/null
+++ b/src/features/post/model/usePostDetailModal.ts
@@ -0,0 +1,30 @@
+import { useState } from 'react'
+import type { Post } from '../../../entities/post'
+
+/**
+ * 게시물 상세 모달 기능
+ * 게시물 상세 모달의 상태 관리를 담당합니다.
+ */
+export const usePostDetailModal = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const [selectedPost, setSelectedPost] = useState(null)
+
+ const openModal = (post: Post) => {
+ setSelectedPost(post)
+ setIsOpen(true)
+ }
+
+ const closeModal = () => {
+ setIsOpen(false)
+ setSelectedPost(null)
+ }
+
+ return {
+ isOpen,
+ selectedPost,
+ openModal,
+ closeModal,
+ setSelectedPost,
+ }
+}
+
diff --git a/src/features/post/model/usePostEditModal.ts b/src/features/post/model/usePostEditModal.ts
new file mode 100644
index 000000000..fbc884e5c
--- /dev/null
+++ b/src/features/post/model/usePostEditModal.ts
@@ -0,0 +1,57 @@
+import { useState } from 'react'
+import type { Post, UpdatePostDto } from '../../../entities/post'
+
+/**
+ * 게시물 수정 모달 기능
+ * 게시물 수정 모달의 상태 및 폼 관리를 담당합니다.
+ */
+export const usePostEditModal = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const [editingPost, setEditingPost] = useState(null)
+ const [formData, setFormData] = useState({
+ title: "",
+ body: "",
+ })
+
+ const openModal = (post: Post) => {
+ setEditingPost(post)
+ setFormData({
+ title: post.title,
+ body: post.body,
+ })
+ setIsOpen(true)
+ }
+
+ const closeModal = () => {
+ setIsOpen(false)
+ setEditingPost(null)
+ resetForm()
+ }
+
+ const resetForm = () => {
+ setFormData({
+ title: "",
+ body: "",
+ })
+ }
+
+ const updateTitle = (title: string) => {
+ setFormData((prev) => ({ ...prev, title }))
+ }
+
+ const updateBody = (body: string) => {
+ setFormData((prev) => ({ ...prev, body }))
+ }
+
+ return {
+ isOpen,
+ editingPost,
+ formData,
+ openModal,
+ closeModal,
+ resetForm,
+ updateTitle,
+ updateBody,
+ }
+}
+
diff --git a/src/features/post/model/usePostFilters.ts b/src/features/post/model/usePostFilters.ts
new file mode 100644
index 000000000..aadd74aa2
--- /dev/null
+++ b/src/features/post/model/usePostFilters.ts
@@ -0,0 +1,93 @@
+import { useEffect, useState } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+import { getNumberParam, getStringParam, updateURL } from '../../../shared/lib/urlUtils'
+import { PAGINATION, SORT } from '../../../shared/config/constants'
+import { usePostSort } from './usePostSort'
+
+/**
+ * 게시물 필터링 기능
+ * URL 파라미터와 동기화된 필터 상태를 관리합니다.
+ * usePostSort를 내부적으로 사용하여 정렬 상태도 함께 관리합니다.
+ */
+export const usePostFilters = () => {
+ const navigate = useNavigate()
+ const location = useLocation()
+
+ // 정렬 기능 사용
+ const sort = usePostSort()
+
+ // URL 파라미터에서 초기값 가져오기
+ const [skip, setSkip] = useState(() => getNumberParam(location.search, "skip", PAGINATION.DEFAULT_SKIP))
+ const [limit, setLimit] = useState(() => getNumberParam(location.search, "limit", PAGINATION.DEFAULT_LIMIT))
+ const [searchQuery, setSearchQuery] = useState(() => getStringParam(location.search, "search"))
+ const [selectedTag, setSelectedTag] = useState(() => getStringParam(location.search, "tag"))
+
+ // URL 파라미터 동기화
+ const syncURL = (overrideParams?: {
+ skip?: number
+ limit?: number
+ search?: string | null
+ tag?: string | null
+ }) => {
+ const finalSkip = overrideParams?.skip ?? skip
+ const finalLimit = overrideParams?.limit ?? limit
+ const finalSearch = overrideParams?.search !== undefined ? overrideParams.search : (searchQuery || null)
+ const finalTag = overrideParams?.tag !== undefined ? overrideParams.tag : (selectedTag || null)
+
+ updateURL(navigate, location.pathname, {
+ skip: finalSkip,
+ limit: finalLimit,
+ search: finalSearch,
+ sortBy: sort.sortBy !== "none" ? sort.sortBy : null,
+ sortOrder: sort.sortOrder !== "asc" ? sort.sortOrder : null,
+ tag: finalTag,
+ })
+ }
+
+ // URL 파라미터 변경 감지
+ useEffect(() => {
+ const skipValue = getNumberParam(location.search, "skip", PAGINATION.DEFAULT_SKIP)
+ const limitValue = getNumberParam(location.search, "limit", PAGINATION.DEFAULT_LIMIT)
+ const searchValue = getStringParam(location.search, "search")
+ const urlSortBy = getStringParam(location.search, "sortBy", SORT.DEFAULT_SORT_BY)
+ const urlSortOrder = getStringParam(location.search, "sortOrder", SORT.DEFAULT_SORT_ORDER)
+ const tagValue = getStringParam(location.search, "tag")
+
+ // 상태가 실제로 변경된 경우에만 업데이트 (불필요한 리렌더링 방지)
+ if (skipValue !== skip) setSkip(skipValue)
+ if (limitValue !== limit) setLimit(limitValue)
+ if (searchValue !== searchQuery) setSearchQuery(searchValue)
+ if (tagValue !== selectedTag) setSelectedTag(tagValue)
+
+ if (urlSortBy && urlSortBy !== sort.sortBy) {
+ sort.setSortBy(urlSortBy as typeof sort.sortBy)
+ }
+ if (urlSortOrder && urlSortOrder !== sort.sortOrder) {
+ sort.setSortOrder(urlSortOrder as typeof sort.sortOrder)
+ }
+ }, [location.search]) // sort 객체를 의존성에서 제거하여 무한 루프 방지
+
+ return {
+ // 상태
+ skip,
+ limit,
+ searchQuery,
+ selectedTag,
+ sortBy: sort.sortBy,
+ sortOrder: sort.sortOrder,
+
+ // 상태 변경 함수
+ setSkip,
+ setLimit,
+ setSearchQuery,
+ setSelectedTag,
+ setSortBy: sort.setSortBy,
+ setSortOrder: sort.setSortOrder,
+
+ // 정렬 기능
+ sortPosts: sort.sortPosts,
+
+ // URL 동기화
+ syncURL,
+ }
+}
diff --git a/src/features/post/model/usePostList.ts b/src/features/post/model/usePostList.ts
new file mode 100644
index 000000000..51e9fe7bb
--- /dev/null
+++ b/src/features/post/model/usePostList.ts
@@ -0,0 +1,54 @@
+import { useQuery } from '@tanstack/react-query'
+import { postApi } from '../../../entities/post/api'
+import { userApi } from '../../../entities/user/api'
+import { postKeys, userKeys } from '../../../shared/lib'
+import type { Post, PostAuthor } from '../../../entities/post'
+
+/**
+ * 게시물 목록 조회 기능
+ * 게시물과 사용자 정보를 병렬로 가져와서 author 정보를 매핑합니다.
+ * (기획 변경에 영향받는 로직이므로 Features에 위치)
+ */
+export const usePostList = (limit: number, skip: number) => {
+ // 게시물 목록 조회
+ const postsQuery = useQuery({
+ queryKey: postKeys.list(limit, skip),
+ queryFn: () => postApi.getPosts(limit, skip),
+ refetchOnMount: true, // 컴포넌트 마운트 시 항상 리프레시
+ })
+
+ // 사용자 정보 조회 (author 매핑용)
+ const usersQuery = useQuery({
+ queryKey: userKeys.list({ limit: 0, select: 'username,image' }),
+ queryFn: () => userApi.getUsers({ limit: 0, select: 'username,image' }),
+ enabled: postsQuery.isSuccess, // 게시물 조회 성공 시에만 실행
+ })
+
+ // author 정보 매핑 (비즈니스 로직)
+ const postsWithAuthors: Post[] | undefined = postsQuery.data && usersQuery.data
+ ? postsQuery.data.posts.map((post) => {
+ const user = usersQuery.data.users.find((u) => u.id === post.userId)
+ const author: PostAuthor | undefined = user
+ ? {
+ id: user.id,
+ username: user.username,
+ image: user.image,
+ }
+ : undefined
+
+ return {
+ ...post,
+ author,
+ }
+ })
+ : undefined
+
+ return {
+ posts: postsWithAuthors ?? [],
+ total: postsQuery.data?.total ?? 0,
+ isLoading: postsQuery.isLoading || usersQuery.isLoading,
+ isError: postsQuery.isError || usersQuery.isError,
+ error: postsQuery.error || usersQuery.error,
+ refetch: postsQuery.refetch,
+ }
+}
diff --git a/src/features/post/model/usePostPagination.ts b/src/features/post/model/usePostPagination.ts
new file mode 100644
index 000000000..a2b6e9d8c
--- /dev/null
+++ b/src/features/post/model/usePostPagination.ts
@@ -0,0 +1,48 @@
+import { PAGINATION } from '../../../shared/config/constants'
+
+/**
+ * 게시물 페이지네이션 기능
+ * 페이지네이션 관련 로직을 관리합니다.
+ */
+export const usePostPagination = (
+ skip: number,
+ limit: number,
+ total: number,
+ setSkip: (skip: number) => void,
+ setLimit: (limit: number) => void,
+ syncURL: (params?: { skip?: number; limit?: number }) => void
+) => {
+ const handleLimitChange = (newLimit: number) => {
+ setLimit(newLimit)
+ setSkip(0)
+ // 최신 값을 직접 전달하여 URL 동기화
+ syncURL({ skip: 0, limit: newLimit })
+ }
+
+ const handlePrevPage = () => {
+ const newSkip = Math.max(0, skip - limit)
+ setSkip(newSkip)
+ // 최신 값을 직접 전달하여 URL 동기화
+ syncURL({ skip: newSkip })
+ }
+
+ const handleNextPage = () => {
+ const newSkip = skip + limit
+ setSkip(newSkip)
+ // 최신 값을 직접 전달하여 URL 동기화
+ syncURL({ skip: newSkip })
+ }
+
+ const canGoPrev = skip > 0
+ const canGoNext = skip + limit < total
+
+ return {
+ handleLimitChange,
+ handlePrevPage,
+ handleNextPage,
+ canGoPrev,
+ canGoNext,
+ limitOptions: PAGINATION.LIMIT_OPTIONS,
+ }
+}
+
diff --git a/src/features/post/model/usePostSearch.ts b/src/features/post/model/usePostSearch.ts
new file mode 100644
index 000000000..decf24f88
--- /dev/null
+++ b/src/features/post/model/usePostSearch.ts
@@ -0,0 +1,58 @@
+import { useQuery } from '@tanstack/react-query'
+import { postApi } from '../../../entities/post/api'
+import { userApi } from '../../../entities/user/api'
+import { postKeys, userKeys } from '../../../shared/lib'
+import type { Post, PostAuthor } from '../../../entities/post'
+
+/**
+ * 게시물 검색 기능
+ * 게시물과 사용자 정보를 병렬로 가져와서 author 정보를 매핑합니다.
+ */
+export const usePostSearch = (query: string | undefined) => {
+ // 게시물 검색
+ const postsQuery = useQuery({
+ queryKey: postKeys.search(query || ''),
+ queryFn: () => postApi.searchPosts(query || ''),
+ enabled: !!query && query.trim().length > 0, // 검색어가 있을 때만 실행
+ refetchOnMount: true, // 컴포넌트 마운트 시 항상 리프레시
+ })
+
+ // 사용자 정보 조회 (author 매핑용)
+ const usersQuery = useQuery({
+ queryKey: userKeys.list({ limit: 0, select: 'username,image' }),
+ queryFn: () => userApi.getUsers({ limit: 0, select: 'username,image' }),
+ enabled: postsQuery.isSuccess, // 게시물 조회 성공 시에만 실행
+ })
+
+ // author 정보 매핑 (비즈니스 로직)
+ const postsWithAuthors: Post[] | undefined = postsQuery.data && usersQuery.data
+ ? postsQuery.data.posts.map((post) => {
+ const user = usersQuery.data.users.find((u) => u.id === post.userId)
+ const author: PostAuthor | undefined = user
+ ? {
+ id: user.id,
+ username: user.username,
+ image: user.image,
+ }
+ : undefined
+
+ return {
+ ...post,
+ author,
+ }
+ })
+ : undefined
+
+ return {
+ data: postsWithAuthors
+ ? {
+ ...postsQuery.data,
+ posts: postsWithAuthors,
+ }
+ : undefined,
+ isLoading: postsQuery.isLoading || usersQuery.isLoading,
+ isError: postsQuery.isError || usersQuery.isError,
+ error: postsQuery.error || usersQuery.error,
+ refetch: postsQuery.refetch,
+ }
+}
diff --git a/src/features/post/model/usePostSort.ts b/src/features/post/model/usePostSort.ts
new file mode 100644
index 000000000..b22b610f7
--- /dev/null
+++ b/src/features/post/model/usePostSort.ts
@@ -0,0 +1,66 @@
+import { useState } from 'react'
+import type { Post } from '../../../entities/post'
+
+export type SortBy = 'none' | 'id' | 'title' | 'reactions'
+export type SortOrder = 'asc' | 'desc'
+
+/**
+ * 게시물 정렬 기능
+ * 정렬 상태 관리 및 정렬 실행 로직을 제공합니다.
+ */
+export const usePostSort = () => {
+ const [sortBy, setSortBy] = useState('none')
+ const [sortOrder, setSortOrder] = useState('asc')
+
+ const handleSortByChange = (value: SortBy) => {
+ setSortBy(value)
+ }
+
+ const handleSortOrderChange = (value: SortOrder) => {
+ setSortOrder(value)
+ }
+
+ const resetSort = () => {
+ setSortBy('none')
+ setSortOrder('asc')
+ }
+
+ /**
+ * 게시물 목록을 정렬합니다.
+ */
+ const sortPosts = (posts: Post[]): Post[] => {
+ if (sortBy === "none") return posts
+
+ const sorted = [...posts].sort((a, b) => {
+ let comparison = 0
+ switch (sortBy) {
+ case "id": {
+ comparison = a.id - b.id
+ break
+ }
+ case "title": {
+ comparison = a.title.localeCompare(b.title)
+ break
+ }
+ case "reactions": {
+ const aReactions = (a.reactions?.likes || 0) - (a.reactions?.dislikes || 0)
+ const bReactions = (b.reactions?.likes || 0) - (b.reactions?.dislikes || 0)
+ comparison = aReactions - bReactions
+ break
+ }
+ }
+ return sortOrder === "asc" ? comparison : -comparison
+ })
+
+ return sorted
+ }
+
+ return {
+ sortBy,
+ sortOrder,
+ setSortBy: handleSortByChange,
+ setSortOrder: handleSortOrderChange,
+ resetSort,
+ sortPosts,
+ }
+}
diff --git a/src/features/post/model/usePostUpdate.ts b/src/features/post/model/usePostUpdate.ts
new file mode 100644
index 000000000..c50c6146b
--- /dev/null
+++ b/src/features/post/model/usePostUpdate.ts
@@ -0,0 +1,64 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { postApi, type UpdatePostDto, type Post } from '../../../entities/post/api'
+import { postKeys } from '../../../shared/lib'
+
+/**
+ * 게시물 수정 기능
+ * 낙관적 업데이트를 적용합니다.
+ */
+export const usePostUpdate = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ id, post }: { id: number; post: UpdatePostDto }) =>
+ postApi.updatePost(id, post),
+ // 낙관적 업데이트: 서버 응답 전에 UI 업데이트
+ onMutate: async ({ id, post }) => {
+ // 진행 중인 쿼리 취소
+ await queryClient.cancelQueries({ queryKey: postKeys.detail(id) })
+ await queryClient.cancelQueries({ queryKey: postKeys.all() })
+
+ // 이전 데이터 백업
+ const previousPost = queryClient.getQueryData(postKeys.detail(id))
+ const previousPosts = queryClient.getQueriesData({ queryKey: postKeys.all() })
+
+ // 낙관적 업데이트: 즉시 UI 업데이트
+ if (previousPost) {
+ queryClient.setQueryData(postKeys.detail(id), {
+ ...previousPost,
+ ...post,
+ })
+ }
+
+ // 목록도 업데이트
+ queryClient.setQueriesData<{ posts: Post[] }>(
+ { queryKey: postKeys.all() },
+ (old) => {
+ if (!old) return old
+ return {
+ ...old,
+ posts: old.posts.map((p) => (p.id === id ? { ...p, ...post } : p)),
+ }
+ }
+ )
+
+ return { previousPost, previousPosts }
+ },
+ // 에러 발생 시 롤백
+ onError: (error, variables, context) => {
+ if (context?.previousPost) {
+ queryClient.setQueryData(postKeys.detail(variables.id), context.previousPost)
+ }
+ if (context?.previousPosts) {
+ context.previousPosts.forEach(([queryKey, data]) => {
+ queryClient.setQueryData(queryKey, data)
+ })
+ }
+ console.error('게시물 수정 오류:', error)
+ },
+ // 성공 시 쿼리 무효화
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ },
+ })
+}
diff --git a/src/features/post/model/useTagFilter.ts b/src/features/post/model/useTagFilter.ts
new file mode 100644
index 000000000..4b3be7f64
--- /dev/null
+++ b/src/features/post/model/useTagFilter.ts
@@ -0,0 +1,64 @@
+import { useQuery } from '@tanstack/react-query'
+import { postApi } from '../../../entities/post/api'
+import { userApi } from '../../../entities/user/api'
+import { postKeys, userKeys } from '../../../shared/lib'
+import type { Post, PostAuthor } from '../../../entities/post'
+
+/**
+ * 태그 필터 기능
+ */
+export const useTags = () => {
+ return useQuery({
+ queryKey: postKeys.tags(),
+ queryFn: () => postApi.getTags(),
+ })
+}
+
+export const usePostsByTag = (tag: string) => {
+ // 태그별 게시물 조회
+ const postsQuery = useQuery({
+ queryKey: postKeys.byTag(tag),
+ queryFn: () => postApi.getPostsByTag(tag),
+ enabled: tag !== 'all' && tag.length > 0, // 'all'이 아니고 태그가 있을 때만 실행
+ refetchOnMount: true, // 컴포넌트 마운트 시 항상 리프레시
+ })
+
+ // 사용자 정보 조회 (author 매핑용)
+ const usersQuery = useQuery({
+ queryKey: userKeys.list({ limit: 0, select: 'username,image' }),
+ queryFn: () => userApi.getUsers({ limit: 0, select: 'username,image' }),
+ enabled: postsQuery.isSuccess, // 게시물 조회 성공 시에만 실행
+ })
+
+ // author 정보 매핑 (비즈니스 로직)
+ const postsWithAuthors: Post[] | undefined = postsQuery.data && usersQuery.data
+ ? postsQuery.data.posts.map((post) => {
+ const user = usersQuery.data.users.find((u) => u.id === post.userId)
+ const author: PostAuthor | undefined = user
+ ? {
+ id: user.id,
+ username: user.username,
+ image: user.image,
+ }
+ : undefined
+
+ return {
+ ...post,
+ author,
+ }
+ })
+ : undefined
+
+ return {
+ data: postsWithAuthors
+ ? {
+ ...postsQuery.data,
+ posts: postsWithAuthors,
+ }
+ : undefined,
+ isLoading: postsQuery.isLoading || usersQuery.isLoading,
+ isError: postsQuery.isError || usersQuery.isError,
+ error: postsQuery.error || usersQuery.error,
+ refetch: postsQuery.refetch,
+ }
+}
diff --git a/src/features/post/ui/PostPagination.tsx b/src/features/post/ui/PostPagination.tsx
new file mode 100644
index 000000000..2756862e2
--- /dev/null
+++ b/src/features/post/ui/PostPagination.tsx
@@ -0,0 +1,51 @@
+import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../shared/ui'
+import { usePostStore } from '../../../entities/post'
+import { usePostFilters } from '../model/usePostFilters'
+import { usePostPagination } from '../model/usePostPagination'
+
+/**
+ * 게시물 페이지네이션 컴포넌트
+ * 페이지 이동과 항목 수 변경을 처리합니다.
+ */
+export const PostPagination = () => {
+ const { total } = usePostStore()
+ const { skip, limit, setSkip, setLimit, syncURL } = usePostFilters()
+ const { handleLimitChange, handlePrevPage, handleNextPage, canGoPrev, canGoNext, limitOptions } = usePostPagination(
+ skip,
+ limit,
+ total,
+ setSkip,
+ setLimit,
+ syncURL
+ )
+
+ return (
+
+
+ 표시
+ handleLimitChange(Number(value))}>
+
+
+
+
+ {limitOptions.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ 항목
+
+
+
+ 이전
+
+
+ 다음
+
+
+
+ )
+}
+
diff --git a/src/features/post/ui/PostSearchInput.tsx b/src/features/post/ui/PostSearchInput.tsx
new file mode 100644
index 000000000..f3848b268
--- /dev/null
+++ b/src/features/post/ui/PostSearchInput.tsx
@@ -0,0 +1,59 @@
+import { useState, useEffect } from "react"
+import { useQueryClient } from "@tanstack/react-query"
+import { Search } from "lucide-react"
+import { Input } from '../../../shared/ui'
+import { usePostFilters } from '../model/usePostFilters'
+import { postKeys } from '../../../shared/lib'
+
+/**
+ * 게시물 검색 입력 컴포넌트
+ * 검색어 입력과 검색 실행을 처리합니다.
+ */
+export const PostSearchInput = () => {
+ const { searchQuery, setSearchQuery, syncURL } = usePostFilters()
+ const queryClient = useQueryClient()
+ // 로컬 상태로 입력값 관리 (URL과 분리)
+ const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery || '')
+
+ // URL의 searchQuery가 변경되면 로컬 상태도 업데이트
+ useEffect(() => {
+ setLocalSearchQuery(searchQuery || '')
+ }, [searchQuery])
+
+ const handleSearch = () => {
+ // 검색어를 필터 상태에 반영하고 URL 동기화
+ setSearchQuery(localSearchQuery)
+ // 최신 검색어를 직접 전달하여 URL 동기화
+ syncURL({ search: localSearchQuery.trim() || null })
+
+ // 검색어가 변경되면 관련 쿼리 무효화하여 새로고침
+ if (localSearchQuery.trim()) {
+ // 검색 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: postKeys.search(localSearchQuery.trim()) })
+ } else {
+ // 검색어가 비어있으면 일반 목록 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ }
+ }
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ handleSearch()
+ }
+ }
+
+ return (
+
+
+
+ setLocalSearchQuery(e.target.value)}
+ onKeyPress={handleKeyPress}
+ />
+
+
+ )
+}
diff --git a/src/features/post/ui/PostSortControls.tsx b/src/features/post/ui/PostSortControls.tsx
new file mode 100644
index 000000000..bdde76d37
--- /dev/null
+++ b/src/features/post/ui/PostSortControls.tsx
@@ -0,0 +1,54 @@
+import { useQueryClient } from "@tanstack/react-query"
+import type { SortBy, SortOrder } from '../model/usePostSort'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../shared/ui'
+import { usePostFilters } from '../model/usePostFilters'
+import { postKeys } from '../../../shared/lib'
+
+/**
+ * 게시물 정렬 컨트롤 컴포넌트
+ * 정렬 기준과 정렬 순서를 선택합니다.
+ */
+export const PostSortControls = () => {
+ const { sortBy, sortOrder, setSortBy, setSortOrder, syncURL } = usePostFilters()
+ const queryClient = useQueryClient()
+
+ const handleSortByChange = (value: SortBy) => {
+ setSortBy(value)
+ syncURL() // URL 동기화
+ // 정렬 변경 시 관련 쿼리 무효화하여 새로고침
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ }
+
+ const handleSortOrderChange = (value: SortOrder) => {
+ setSortOrder(value)
+ syncURL() // URL 동기화
+ // 정렬 변경 시 관련 쿼리 무효화하여 새로고침
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ }
+
+ return (
+ <>
+
+
+
+
+
+ 없음
+ ID
+ 제목
+ 반응
+
+
+
+
+
+
+
+ 오름차순
+ 내림차순
+
+
+ >
+ )
+}
+
diff --git a/src/features/post/ui/PostTagFilter.tsx b/src/features/post/ui/PostTagFilter.tsx
new file mode 100644
index 000000000..5a557cffc
--- /dev/null
+++ b/src/features/post/ui/PostTagFilter.tsx
@@ -0,0 +1,50 @@
+import { useQueryClient } from "@tanstack/react-query"
+import type { Tag } from '../../../entities/post'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../shared/ui'
+import { usePostFilters } from '../model/usePostFilters'
+import { postKeys } from '../../../shared/lib'
+
+interface PostTagFilterProps {
+ tags: Tag[]
+}
+
+/**
+ * 게시물 태그 필터 컴포넌트
+ * 태그를 선택하여 게시물을 필터링합니다.
+ */
+export const PostTagFilter = ({ tags }: PostTagFilterProps) => {
+ const { selectedTag, setSelectedTag, syncURL } = usePostFilters()
+ const queryClient = useQueryClient()
+
+ const handleTagChange = (tag: string) => {
+ setSelectedTag(tag)
+ // 최신 태그를 직접 전달하여 URL 동기화
+ syncURL({ tag: tag === 'all' ? null : tag })
+
+ // 태그가 변경되면 관련 쿼리 무효화하여 새로고침
+ if (tag !== 'all') {
+ // 태그별 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: postKeys.byTag(tag) })
+ } else {
+ // 'all'로 변경되면 일반 목록 쿼리 무효화
+ queryClient.invalidateQueries({ queryKey: postKeys.all() })
+ }
+ }
+
+ return (
+
+
+
+
+
+ 모든 태그
+ {tags.map((tag) => (
+
+ {tag.slug}
+
+ ))}
+
+
+ )
+}
+
diff --git a/src/features/post/ui/index.ts b/src/features/post/ui/index.ts
new file mode 100644
index 000000000..15dd44205
--- /dev/null
+++ b/src/features/post/ui/index.ts
@@ -0,0 +1,5 @@
+export { PostSearchInput } from './PostSearchInput'
+export { PostTagFilter } from './PostTagFilter'
+export { PostSortControls } from './PostSortControls'
+export { PostPagination } from './PostPagination'
+
diff --git a/src/features/user/index.ts b/src/features/user/index.ts
new file mode 100644
index 000000000..2ac7d5ef3
--- /dev/null
+++ b/src/features/user/index.ts
@@ -0,0 +1,3 @@
+export { useUserDetail } from './model/useUserDetail'
+export { useUserModal } from './model/useUserModal'
+
diff --git a/src/features/user/model/useUserDetail.ts b/src/features/user/model/useUserDetail.ts
new file mode 100644
index 000000000..42b63dcda
--- /dev/null
+++ b/src/features/user/model/useUserDetail.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query'
+import { userApi } from '../../../entities/user/api'
+import { userKeys } from '../../../shared/lib'
+
+/**
+ * 사용자 상세 정보 조회 기능
+ */
+export const useUserDetail = (id: number) => {
+ return useQuery({
+ queryKey: userKeys.detail(id),
+ queryFn: () => userApi.getUser(id),
+ enabled: id > 0, // id가 유효할 때만 실행
+ })
+}
diff --git a/src/features/user/model/useUserModal.ts b/src/features/user/model/useUserModal.ts
new file mode 100644
index 000000000..1f83d0439
--- /dev/null
+++ b/src/features/user/model/useUserModal.ts
@@ -0,0 +1,24 @@
+import { useState } from 'react'
+
+/**
+ * 사용자 모달 기능
+ * 사용자 모달의 상태 관리를 담당합니다.
+ */
+export const useUserModal = () => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ const openModal = () => {
+ setIsOpen(true)
+ }
+
+ const closeModal = () => {
+ setIsOpen(false)
+ }
+
+ return {
+ isOpen,
+ openModal,
+ closeModal,
+ }
+}
+
diff --git a/src/pages/PostsManagerPage.tsx b/src/pages/PostsManagerPage.tsx
index f80eb91ef..5a3580c77 100644
--- a/src/pages/PostsManagerPage.tsx
+++ b/src/pages/PostsManagerPage.tsx
@@ -1,480 +1,236 @@
-import { useEffect, useState } from "react"
-import { Edit2, MessageSquare, Plus, Search, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react"
-import { useLocation, useNavigate } from "react-router-dom"
+import { useState } from "react"
+import { Plus } from "lucide-react"
+import { Button, Card, CardContent, CardHeader, CardTitle } from "../shared/ui"
+
+// Features
import {
- Button,
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- Textarea,
-} from "../components"
-
-const PostsManager = () => {
- const navigate = useNavigate()
- const location = useLocation()
- const queryParams = new URLSearchParams(location.search)
-
- // 상태 관리
- const [posts, setPosts] = useState([])
- const [total, setTotal] = useState(0)
- const [skip, setSkip] = useState(parseInt(queryParams.get("skip") || "0"))
- const [limit, setLimit] = useState(parseInt(queryParams.get("limit") || "10"))
- const [searchQuery, setSearchQuery] = useState(queryParams.get("search") || "")
- const [selectedPost, setSelectedPost] = useState(null)
- const [sortBy, setSortBy] = useState(queryParams.get("sortBy") || "")
- const [sortOrder, setSortOrder] = useState(queryParams.get("sortOrder") || "asc")
- const [showAddDialog, setShowAddDialog] = useState(false)
- const [showEditDialog, setShowEditDialog] = useState(false)
- const [newPost, setNewPost] = useState({ title: "", body: "", userId: 1 })
- const [loading, setLoading] = useState(false)
- const [tags, setTags] = useState([])
- const [selectedTag, setSelectedTag] = useState(queryParams.get("tag") || "")
- const [comments, setComments] = useState({})
- const [selectedComment, setSelectedComment] = useState(null)
- const [newComment, setNewComment] = useState({ body: "", postId: null, userId: 1 })
- const [showAddCommentDialog, setShowAddCommentDialog] = useState(false)
- const [showEditCommentDialog, setShowEditCommentDialog] = useState(false)
- const [showPostDetailDialog, setShowPostDetailDialog] = useState(false)
- const [showUserModal, setShowUserModal] = useState(false)
- const [selectedUser, setSelectedUser] = useState(null)
-
- // URL 업데이트 함수
- const updateURL = () => {
- const params = new URLSearchParams()
- if (skip) params.set("skip", skip.toString())
- if (limit) params.set("limit", limit.toString())
- if (searchQuery) params.set("search", searchQuery)
- if (sortBy) params.set("sortBy", sortBy)
- if (sortOrder) params.set("sortOrder", sortOrder)
- if (selectedTag) params.set("tag", selectedTag)
- navigate(`?${params.toString()}`)
- }
+ usePostList,
+ useTags,
+ usePostsByTag,
+ usePostSearch,
+ usePostCreate,
+ usePostUpdate,
+ usePostDelete,
+ usePostFilters,
+ usePostCreateModal,
+ usePostEditModal,
+ usePostDetailModal,
+} from "../features/post"
+import {
+ useCommentList,
+ useCommentCreate,
+ useCommentUpdate,
+ useCommentDelete,
+ useCommentLike,
+} from "../features/comment"
+import { useUserDetail, useUserModal } from "../features/user"
+
+// Widgets
+import { PostList } from "../widgets/post-list"
+import { PostDetail } from "../widgets/post-detail"
+import { PostCreateModal } from "../widgets/post-create-modal"
+import { PostEditModal } from "../widgets/post-edit-modal"
+import { UserModal } from "../widgets/user-modal"
+
+// Types
+import type { Comment } from "../entities/comment"
+import type { Post } from "../entities/post"
+
+const PostsManagerPage = () => {
+ // 필터
+ const filters = usePostFilters()
+
+ // 모달 관리
+ const postCreateModal = usePostCreateModal()
+ const postEditModal = usePostEditModal()
+ const postDetailModal = usePostDetailModal()
+ const userModal = useUserModal()
+
+ // 선택된 사용자 ID (모달이 열릴 때 설정)
+ const [selectedUserId, setSelectedUserId] = useState(null)
+
+ // Query hooks - 선언적 방식
+ // 검색 쿼리 (검색어가 있을 때)
+ const searchQuery = usePostSearch(filters.searchQuery)
+
+ // 게시물 목록 (태그가 'all'이거나 없을 때, 검색어도 없을 때)
+ const postListQuery = usePostList(
+ filters.limit,
+ filters.skip
+ )
- // 게시물 가져오기
- const fetchPosts = () => {
- setLoading(true)
- let postsData
- let usersData
-
- fetch(`/api/posts?limit=${limit}&skip=${skip}`)
- .then((response) => response.json())
- .then((data) => {
- postsData = data
- return fetch("/api/users?limit=0&select=username,image")
- })
- .then((response) => response.json())
- .then((users) => {
- usersData = users.users
- const postsWithUsers = postsData.posts.map((post) => ({
- ...post,
- author: usersData.find((user) => user.id === post.userId),
- }))
- setPosts(postsWithUsers)
- setTotal(postsData.total)
- })
- .catch((error) => {
- console.error("게시물 가져오기 오류:", error)
- })
- .finally(() => {
- setLoading(false)
- })
- }
+ // 태그별 게시물 (태그가 선택되었을 때)
+ const postsByTagQuery = usePostsByTag(filters.selectedTag || '')
- // 태그 가져오기
- const fetchTags = async () => {
- try {
- const response = await fetch("/api/posts/tags")
- const data = await response.json()
- setTags(data)
- } catch (error) {
- console.error("태그 가져오기 오류:", error)
- }
- }
+ // 태그 목록
+ const tagsQuery = useTags()
- // 게시물 검색
- const searchPosts = async () => {
- if (!searchQuery) {
- fetchPosts()
- return
- }
- setLoading(true)
- try {
- const response = await fetch(`/api/posts/search?q=${searchQuery}`)
- const data = await response.json()
- setPosts(data.posts)
- setTotal(data.total)
- } catch (error) {
- console.error("게시물 검색 오류:", error)
- }
- setLoading(false)
- }
+ // 댓글 목록 (게시물 상세 모달이 열렸을 때)
+ const commentListQuery = useCommentList(
+ postDetailModal.selectedPost?.id || 0
+ )
- // 태그별 게시물 가져오기
- const fetchPostsByTag = async (tag) => {
- if (!tag || tag === "all") {
- fetchPosts()
- return
- }
- setLoading(true)
- try {
- const [postsResponse, usersResponse] = await Promise.all([
- fetch(`/api/posts/tag/${tag}`),
- fetch("/api/users?limit=0&select=username,image"),
- ])
- const postsData = await postsResponse.json()
- const usersData = await usersResponse.json()
-
- const postsWithUsers = postsData.posts.map((post) => ({
- ...post,
- author: usersData.users.find((user) => user.id === post.userId),
- }))
-
- setPosts(postsWithUsers)
- setTotal(postsData.total)
- } catch (error) {
- console.error("태그별 게시물 가져오기 오류:", error)
- }
- setLoading(false)
+ // 사용자 상세 (사용자 모달이 열렸을 때)
+ const userDetailQuery = useUserDetail(selectedUserId || 0)
+
+ // 현재 표시할 게시물 데이터 결정
+ // 우선순위: 검색어 > 태그 필터 > 일반 목록
+ const currentPosts = filters.searchQuery?.trim()
+ ? searchQuery.data?.posts ?? []
+ : filters.selectedTag && filters.selectedTag !== 'all'
+ ? postsByTagQuery.data?.posts ?? []
+ : postListQuery.posts
+
+ const isLoadingPosts = filters.searchQuery?.trim()
+ ? searchQuery.isLoading
+ : filters.selectedTag && filters.selectedTag !== 'all'
+ ? postsByTagQuery.isLoading
+ : postListQuery.isLoading
+
+ // Mutation hooks
+ const createPostMutation = usePostCreate()
+ const updatePostMutation = usePostUpdate()
+ const deletePostMutation = usePostDelete()
+ const createCommentMutation = useCommentCreate()
+ const updateCommentMutation = useCommentUpdate()
+ const deleteCommentMutation = useCommentDelete()
+ const likeCommentMutation = useCommentLike()
+
+ // 태그 변경
+ const handleTagChange = (tag: string) => {
+ filters.setSelectedTag(tag)
+ filters.syncURL()
}
- // 게시물 추가
- const addPost = async () => {
- try {
- const response = await fetch("/api/posts/add", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newPost),
- })
- const data = await response.json()
- setPosts([data, ...posts])
- setShowAddDialog(false)
- setNewPost({ title: "", body: "", userId: 1 })
- } catch (error) {
- console.error("게시물 추가 오류:", error)
- }
+ // 게시물 생성
+ const handleCreatePost = () => {
+ createPostMutation.mutate(postCreateModal.formData, {
+ onSuccess: () => {
+ postCreateModal.closeModal()
+ },
+ onError: (error) => {
+ console.error("게시물 생성 오류:", error)
+ },
+ })
}
- // 게시물 업데이트
- const updatePost = async () => {
- try {
- const response = await fetch(`/api/posts/${selectedPost.id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(selectedPost),
- })
- const data = await response.json()
- setPosts(posts.map((post) => (post.id === data.id ? data : post)))
- setShowEditDialog(false)
- } catch (error) {
- console.error("게시물 업데이트 오류:", error)
- }
+ // 게시물 수정
+ const handleUpdatePost = () => {
+ if (!postEditModal.editingPost) return
+
+ updatePostMutation.mutate(
+ {
+ id: postEditModal.editingPost.id,
+ post: postEditModal.formData,
+ },
+ {
+ onSuccess: () => {
+ postEditModal.closeModal()
+ },
+ onError: (error) => {
+ console.error("게시물 수정 오류:", error)
+ },
+ }
+ )
}
// 게시물 삭제
- const deletePost = async (id) => {
- try {
- await fetch(`/api/posts/${id}`, {
- method: "DELETE",
- })
- setPosts(posts.filter((post) => post.id !== id))
- } catch (error) {
- console.error("게시물 삭제 오류:", error)
- }
+ const handleDeletePost = (postId: number) => {
+ deletePostMutation.mutate(postId, {
+ onError: (error) => {
+ console.error("게시물 삭제 오류:", error)
+ },
+ })
+ }
+
+ // 게시물 상세 보기
+ const handlePostDetail = (post: Post) => {
+ postDetailModal.openModal(post)
}
- // 댓글 가져오기
- const fetchComments = async (postId) => {
- if (comments[postId]) return // 이미 불러온 댓글이 있으면 다시 불러오지 않음
- try {
- const response = await fetch(`/api/comments/post/${postId}`)
- const data = await response.json()
- setComments((prev) => ({ ...prev, [postId]: data.comments }))
- } catch (error) {
- console.error("댓글 가져오기 오류:", error)
- }
+ // 게시물 수정 모달 열기
+ const handleEditPost = (post: Post) => {
+ postEditModal.openModal(post)
}
// 댓글 추가
- const addComment = async () => {
- try {
- const response = await fetch("/api/comments/add", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newComment),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [data.postId]: [...(prev[data.postId] || []), data],
- }))
- setShowAddCommentDialog(false)
- setNewComment({ body: "", postId: null, userId: 1 })
- } catch (error) {
- console.error("댓글 추가 오류:", error)
- }
+ const handleAddComment = () => {
+ if (!postDetailModal.selectedPost) return
+
+ createCommentMutation.mutate(
+ {
+ body: "",
+ postId: postDetailModal.selectedPost.id,
+ userId: 1,
+ },
+ {
+ onError: (error) => {
+ console.error("댓글 추가 오류:", error)
+ },
+ }
+ )
}
- // 댓글 업데이트
- const updateComment = async () => {
- try {
- const response = await fetch(`/api/comments/${selectedComment.id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ body: selectedComment.body }),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [data.postId]: prev[data.postId].map((comment) => (comment.id === data.id ? data : comment)),
- }))
- setShowEditCommentDialog(false)
- } catch (error) {
- console.error("댓글 업데이트 오류:", error)
- }
+ // 댓글 수정
+ const handleEditComment = (comment: Comment) => {
+ updateCommentMutation.mutate(
+ {
+ id: comment.id,
+ comment: { body: comment.body },
+ },
+ {
+ onError: (error) => {
+ console.error("댓글 수정 오류:", error)
+ },
+ }
+ )
}
// 댓글 삭제
- const deleteComment = async (id, postId) => {
- try {
- await fetch(`/api/comments/${id}`, {
- method: "DELETE",
- })
- setComments((prev) => ({
- ...prev,
- [postId]: prev[postId].filter((comment) => comment.id !== id),
- }))
- } catch (error) {
- console.error("댓글 삭제 오류:", error)
- }
+ const handleDeleteComment = (commentId: number, postId: number) => {
+ deleteCommentMutation.mutate(
+ { id: commentId, postId },
+ {
+ onError: (error) => {
+ console.error("댓글 삭제 오류:", error)
+ },
+ }
+ )
}
// 댓글 좋아요
- const likeComment = async (id, postId) => {
- try {
-
- const response = await fetch(`/api/comments/${id}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ likes: comments[postId].find((c) => c.id === id).likes + 1 }),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [postId]: prev[postId].map((comment) => (comment.id === data.id ? {...data, likes: comment.likes + 1} : comment)),
- }))
- } catch (error) {
- console.error("댓글 좋아요 오류:", error)
- }
- }
-
- // 게시물 상세 보기
- const openPostDetail = (post) => {
- setSelectedPost(post)
- fetchComments(post.id)
- setShowPostDetailDialog(true)
+ const handleLikeComment = (commentId: number, postId: number) => {
+ // 현재 댓글 찾기
+ const comment = commentListQuery.data?.comments.find((c) => c.id === commentId)
+ if (!comment) return
+
+ likeCommentMutation.mutate(
+ {
+ id: commentId,
+ likes: comment.likes,
+ postId,
+ },
+ {
+ onError: (error) => {
+ console.error("댓글 좋아요 오류:", error)
+ },
+ }
+ )
}
// 사용자 모달 열기
- const openUserModal = async (user) => {
- try {
- const response = await fetch(`/api/users/${user.id}`)
- const userData = await response.json()
- setSelectedUser(userData)
- setShowUserModal(true)
- } catch (error) {
- console.error("사용자 정보 가져오기 오류:", error)
- }
- }
-
- useEffect(() => {
- fetchTags()
- }, [])
-
- useEffect(() => {
- if (selectedTag) {
- fetchPostsByTag(selectedTag)
- } else {
- fetchPosts()
- }
- updateURL()
- }, [skip, limit, sortBy, sortOrder, selectedTag])
-
- useEffect(() => {
- const params = new URLSearchParams(location.search)
- setSkip(parseInt(params.get("skip") || "0"))
- setLimit(parseInt(params.get("limit") || "10"))
- setSearchQuery(params.get("search") || "")
- setSortBy(params.get("sortBy") || "")
- setSortOrder(params.get("sortOrder") || "asc")
- setSelectedTag(params.get("tag") || "")
- }, [location.search])
-
- // 하이라이트 함수 추가
- const highlightText = (text: string, highlight: string) => {
- if (!text) return null
- if (!highlight.trim()) {
- return {text}
- }
- const regex = new RegExp(`(${highlight})`, "gi")
- const parts = text.split(regex)
- return (
-
- {parts.map((part, i) => (regex.test(part) ? {part} : {part} ))}
-
- )
+ const handleUserClick = (author: Post["author"]) => {
+ if (!author) return
+ setSelectedUserId(author.id)
+ userModal.openModal()
}
- // 게시물 테이블 렌더링
- const renderPostTable = () => (
-
-
-
- ID
- 제목
- 작성자
- 반응
- 작업
-
-
-
- {posts.map((post) => (
-
- {post.id}
-
-
-
{highlightText(post.title, searchQuery)}
-
-
- {post.tags?.map((tag) => (
- {
- setSelectedTag(tag)
- updateURL()
- }}
- >
- {tag}
-
- ))}
-
-
-
-
- openUserModal(post.author)}>
-
-
{post.author?.username}
-
-
-
-
-
- {post.reactions?.likes || 0}
-
- {post.reactions?.dislikes || 0}
-
-
-
-
- openPostDetail(post)}>
-
-
- {
- setSelectedPost(post)
- setShowEditDialog(true)
- }}
- >
-
-
- deletePost(post.id)}>
-
-
-
-
-
- ))}
-
-
- )
-
- // 댓글 렌더링
- const renderComments = (postId) => (
-
-
-
댓글
-
{
- setNewComment((prev) => ({ ...prev, postId }))
- setShowAddCommentDialog(true)
- }}
- >
-
- 댓글 추가
-
-
-
- {comments[postId]?.map((comment) => (
-
-
- {comment.user.username}:
- {highlightText(comment.body, searchQuery)}
-
-
- likeComment(comment.id, postId)}>
-
- {comment.likes}
-
- {
- setSelectedComment(comment)
- setShowEditCommentDialog(true)
- }}
- >
-
-
- deleteComment(comment.id, postId)}>
-
-
-
-
- ))}
-
-
- )
-
return (
게시물 관리자
- setShowAddDialog(true)}>
+
게시물 추가
@@ -482,227 +238,71 @@ const PostsManager = () => {
- {/* 검색 및 필터 컨트롤 */}
-
-
-
-
- setSearchQuery(e.target.value)}
- onKeyPress={(e) => e.key === "Enter" && searchPosts()}
- />
-
+ {/* 게시물 목록 위젯 */}
+
+
+ {/* 에러 표시 */}
+ {(postListQuery.isError || postsByTagQuery.isError || searchQuery.isError) && (
+
+ {searchQuery.error?.message || postListQuery.error?.message || postsByTagQuery.error?.message || '게시물을 불러오는데 실패했습니다'}
-
{
- setSelectedTag(value)
- fetchPostsByTag(value)
- updateURL()
- }}
- >
-
-
-
-
- 모든 태그
- {tags.map((tag) => (
-
- {tag.slug}
-
- ))}
-
-
-
-
-
-
-
- 없음
- ID
- 제목
- 반응
-
-
-
-
-
-
-
- 오름차순
- 내림차순
-
-
-
-
- {/* 게시물 테이블 */}
- {loading ?
로딩 중...
: renderPostTable()}
-
- {/* 페이지네이션 */}
-
-
- 표시
- setLimit(Number(value))}>
-
-
-
-
- 10
- 20
- 30
-
-
- 항목
-
-
- setSkip(Math.max(0, skip - limit))}>
- 이전
-
- = total} onClick={() => setSkip(skip + limit)}>
- 다음
-
-
-
+ )}
- {/* 게시물 추가 대화상자 */}
-
-
-
- 새 게시물 추가
-
-
- setNewPost({ ...newPost, title: e.target.value })}
- />
-
-
-
-
- {/* 게시물 수정 대화상자 */}
-
-
-
- 게시물 수정
-
-
- setSelectedPost({ ...selectedPost, title: e.target.value })}
- />
-
-
-
-
- {/* 댓글 추가 대화상자 */}
-
-
-
- 새 댓글 추가
-
-
-
-
-
-
- {/* 댓글 수정 대화상자 */}
-
-
-
- 댓글 수정
-
-
-
-
-
-
- {/* 게시물 상세 보기 대화상자 */}
-
-
-
- {highlightText(selectedPost?.title, searchQuery)}
-
-
-
{highlightText(selectedPost?.body, searchQuery)}
- {renderComments(selectedPost?.id)}
-
-
-
+ {/* 게시물 생성 모달 */}
+
+
+ {/* 게시물 수정 모달 */}
+
+
+ {/* 게시물 상세 모달 */}
+
{/* 사용자 모달 */}
-
-
-
- 사용자 정보
-
-
-
-
{selectedUser?.username}
-
-
- 이름: {selectedUser?.firstName} {selectedUser?.lastName}
-
-
- 나이: {selectedUser?.age}
-
-
- 이메일: {selectedUser?.email}
-
-
- 전화번호: {selectedUser?.phone}
-
-
- 주소: {selectedUser?.address?.address}, {selectedUser?.address?.city},{" "}
- {selectedUser?.address?.state}
-
-
- 직장: {selectedUser?.company?.name} - {selectedUser?.company?.title}
-
-
-
-
-
+
)
}
-export default PostsManager
+export default PostsManagerPage
diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts
new file mode 100644
index 000000000..833145c5b
--- /dev/null
+++ b/src/shared/api/client.ts
@@ -0,0 +1,63 @@
+/**
+ * 공통 API 클라이언트 설정
+ * 필요시 fetch 래퍼, 에러 처리, 인터셉터 등을 추가할 수 있습니다.
+ */
+
+/**
+ * 기본 fetch 래퍼 (필요시 확장)
+ */
+export const apiClient = {
+ get: async
(url: string): Promise => {
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch: ${url}`)
+ }
+ return response.json()
+ },
+
+ post: async (url: string, data: unknown): Promise => {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to post: ${url}`)
+ }
+ return response.json()
+ },
+
+ put: async (url: string, data: unknown): Promise => {
+ const response = await fetch(url, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to put: ${url}`)
+ }
+ return response.json()
+ },
+
+ patch: async (url: string, data: unknown): Promise => {
+ const response = await fetch(url, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to patch: ${url}`)
+ }
+ return response.json()
+ },
+
+ delete: async (url: string): Promise => {
+ const response = await fetch(url, {
+ method: "DELETE",
+ })
+ if (!response.ok) {
+ throw new Error(`Failed to delete: ${url}`)
+ }
+ },
+}
+
diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts
new file mode 100644
index 000000000..8f9f15013
--- /dev/null
+++ b/src/shared/api/index.ts
@@ -0,0 +1,2 @@
+export { apiClient } from './client'
+
diff --git a/src/shared/config/constants.ts b/src/shared/config/constants.ts
new file mode 100644
index 000000000..342f97fce
--- /dev/null
+++ b/src/shared/config/constants.ts
@@ -0,0 +1,21 @@
+/**
+ * 공통 상수 정의
+ */
+
+export const PAGINATION = {
+ DEFAULT_LIMIT: 10,
+ DEFAULT_SKIP: 0,
+ LIMIT_OPTIONS: [10, 20, 30] as const,
+} as const
+
+export const SORT = {
+ DEFAULT_SORT_BY: 'none' as const,
+ DEFAULT_SORT_ORDER: 'asc' as const,
+} as const
+
+/**
+ * API Base URL
+ * 개발 환경에서는 Vite 프록시를 사용하고, 프로덕션에서는 직접 API를 호출합니다.
+ */
+export const API_BASE_URL = import.meta.env.DEV ? '/api' : 'https://dummyjson.com'
+
diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts
new file mode 100644
index 000000000..f6a221c34
--- /dev/null
+++ b/src/shared/config/index.ts
@@ -0,0 +1,2 @@
+export { PAGINATION, SORT, API_BASE_URL } from './constants'
+
diff --git a/src/shared/index.ts b/src/shared/index.ts
new file mode 100644
index 000000000..830852fd5
--- /dev/null
+++ b/src/shared/index.ts
@@ -0,0 +1,12 @@
+// UI
+export * from './ui'
+
+// Lib
+export * from './lib'
+
+// API
+export * from './api'
+
+// Config
+export * from './config'
+
diff --git a/src/shared/lib/dateUtils.ts b/src/shared/lib/dateUtils.ts
new file mode 100644
index 000000000..82cc78b45
--- /dev/null
+++ b/src/shared/lib/dateUtils.ts
@@ -0,0 +1,79 @@
+/**
+ * 날짜 포맷팅 유틸리티 함수
+ */
+
+/**
+ * 날짜를 지정된 형식으로 포맷팅
+ * @param date - 포맷팅할 날짜 (Date 객체 또는 문자열)
+ * @param format - 포맷 형식 (기본값: 'YYYY-MM-DD')
+ * @returns 포맷팅된 날짜 문자열
+ */
+export const formatDate = (date: Date | string, format: string = 'YYYY-MM-DD'): string => {
+ const d = typeof date === 'string' ? new Date(date) : date
+
+ if (isNaN(d.getTime())) {
+ return ''
+ }
+
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ const hours = String(d.getHours()).padStart(2, '0')
+ const minutes = String(d.getMinutes()).padStart(2, '0')
+ const seconds = String(d.getSeconds()).padStart(2, '0')
+
+ return format
+ .replace('YYYY', String(year))
+ .replace('MM', month)
+ .replace('DD', day)
+ .replace('HH', hours)
+ .replace('mm', minutes)
+ .replace('ss', seconds)
+}
+
+/**
+ * 날짜를 상대 시간으로 표시 (예: "2시간 전", "3일 전")
+ * @param date - 날짜 (Date 객체 또는 문자열)
+ * @returns 상대 시간 문자열
+ */
+export const formatRelativeTime = (date: Date | string): string => {
+ const d = typeof date === 'string' ? new Date(date) : date
+ const now = new Date()
+ const diffMs = now.getTime() - d.getTime()
+ const diffSec = Math.floor(diffMs / 1000)
+ const diffMin = Math.floor(diffSec / 60)
+ const diffHour = Math.floor(diffMin / 60)
+ const diffDay = Math.floor(diffHour / 24)
+
+ if (diffSec < 60) {
+ return '방금 전'
+ } else if (diffMin < 60) {
+ return `${diffMin}분 전`
+ } else if (diffHour < 24) {
+ return `${diffHour}시간 전`
+ } else if (diffDay < 7) {
+ return `${diffDay}일 전`
+ } else {
+ return formatDate(d, 'YYYY-MM-DD')
+ }
+}
+
+/**
+ * 날짜를 한국어 형식으로 포맷팅 (예: "2024년 1월 1일")
+ * @param date - 날짜 (Date 객체 또는 문자열)
+ * @returns 한국어 형식의 날짜 문자열
+ */
+export const formatDateKorean = (date: Date | string): string => {
+ const d = typeof date === 'string' ? new Date(date) : date
+
+ if (isNaN(d.getTime())) {
+ return ''
+ }
+
+ const year = d.getFullYear()
+ const month = d.getMonth() + 1
+ const day = d.getDate()
+
+ return `${year}년 ${month}월 ${day}일`
+}
+
diff --git a/src/shared/lib/highlightText.tsx b/src/shared/lib/highlightText.tsx
new file mode 100644
index 000000000..c8ce155c8
--- /dev/null
+++ b/src/shared/lib/highlightText.tsx
@@ -0,0 +1,24 @@
+import { ReactNode } from 'react'
+
+/**
+ * 텍스트에서 검색어를 하이라이트하는 유틸리티 함수
+ * @param text - 하이라이트할 텍스트
+ * @param highlight - 검색어
+ * @returns 하이라이트된 ReactNode
+ */
+export const highlightText = (text: string, highlight: string): ReactNode => {
+ if (!text) return null
+ if (!highlight.trim()) {
+ return {text}
+ }
+ const regex = new RegExp(`(${highlight})`, "gi")
+ const parts = text.split(regex)
+ return (
+
+ {parts.map((part, i) =>
+ regex.test(part) ? {part} : {part}
+ )}
+
+ )
+}
+
diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts
new file mode 100644
index 000000000..86190093c
--- /dev/null
+++ b/src/shared/lib/index.ts
@@ -0,0 +1,6 @@
+export { highlightText } from './highlightText'
+export { getUrlParams, updateURL, getNumberParam, getStringParam } from './urlUtils'
+export { formatDate, formatRelativeTime, formatDateKorean } from './dateUtils'
+export { queryClient } from './queryClient'
+export { postKeys, commentKeys, userKeys } from './queryKeys'
+
diff --git a/src/shared/lib/queryClient.ts b/src/shared/lib/queryClient.ts
new file mode 100644
index 000000000..148de0155
--- /dev/null
+++ b/src/shared/lib/queryClient.ts
@@ -0,0 +1,27 @@
+import { QueryClient } from '@tanstack/react-query'
+
+/**
+ * TanStack Query Client 설정
+ * 전역 옵션 및 에러 핸들링 설정
+ */
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ // 데이터가 fresh로 유지되는 시간 (밀리초)
+ staleTime: 1000 * 30, // 30초
+ // 캐시에 유지되는 시간 (밀리초)
+ gcTime: 1000 * 60 * 5, // 5분 (이전 cacheTime)
+ // 자동 리프레시 설정
+ refetchOnWindowFocus: true,
+ refetchOnReconnect: true,
+ refetchOnMount: true,
+ // 재시도 설정
+ retry: 1,
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
+ },
+ mutations: {
+ // Mutation 재시도 설정
+ retry: 0,
+ },
+ },
+})
diff --git a/src/shared/lib/queryKeys.ts b/src/shared/lib/queryKeys.ts
new file mode 100644
index 000000000..07083bf87
--- /dev/null
+++ b/src/shared/lib/queryKeys.ts
@@ -0,0 +1,38 @@
+/**
+ * 쿼리 키 팩토리
+ * 모든 쿼리 키를 중앙에서 관리하여 타입 안정성과 일관성 확보
+ */
+
+export const postKeys = {
+ // 게시물 목록
+ list: (limit?: number, skip?: number) => ['posts', 'list', { limit, skip }] as const,
+ // 게시물 단건
+ detail: (id: number) => ['posts', id] as const,
+ // 게시물 검색
+ search: (query: string) => ['posts', 'search', { query }] as const,
+ // 태그별 게시물
+ byTag: (tag: string) => ['posts', 'tag', { tag }] as const,
+ // 태그 목록
+ tags: () => ['posts', 'tags'] as const,
+ // 모든 게시물 관련 쿼리
+ all: () => ['posts'] as const,
+}
+
+export const commentKeys = {
+ // 게시물별 댓글 목록
+ list: (postId: number) => ['comments', 'post', postId] as const,
+ // 댓글 단건
+ detail: (id: number) => ['comments', id] as const,
+ // 모든 댓글 관련 쿼리
+ all: () => ['comments'] as const,
+}
+
+export const userKeys = {
+ // 사용자 목록
+ list: (params?: { limit?: number; skip?: number; select?: string }) =>
+ ['users', 'list', params] as const,
+ // 사용자 단건
+ detail: (id: number) => ['users', id] as const,
+ // 모든 사용자 관련 쿼리
+ all: () => ['users'] as const,
+}
diff --git a/src/shared/lib/urlUtils.ts b/src/shared/lib/urlUtils.ts
new file mode 100644
index 000000000..c5b2c6181
--- /dev/null
+++ b/src/shared/lib/urlUtils.ts
@@ -0,0 +1,54 @@
+/**
+ * URL 파라미터 관리 유틸리티
+ */
+
+/**
+ * URLSearchParams를 객체로 변환
+ */
+export const getUrlParams = (search: string): Record => {
+ const params = new URLSearchParams(search)
+ const result: Record = {}
+ params.forEach((value, key) => {
+ result[key] = value
+ })
+ return result
+}
+
+/**
+ * 객체를 URLSearchParams로 변환하여 URL 업데이트
+ */
+export const updateURL = (
+ navigate: (path: string) => void,
+ pathname: string,
+ params: Record
+) => {
+ const searchParams = new URLSearchParams()
+
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== null && value !== undefined && value !== '') {
+ searchParams.set(key, String(value))
+ }
+ })
+
+ const queryString = searchParams.toString()
+ const newPath = queryString ? `${pathname}?${queryString}` : pathname
+ navigate(newPath)
+}
+
+/**
+ * URL 파라미터에서 숫자 값 가져오기
+ */
+export const getNumberParam = (search: string, key: string, defaultValue: number): number => {
+ const params = new URLSearchParams(search)
+ const value = params.get(key)
+ return value ? parseInt(value, 10) : defaultValue
+}
+
+/**
+ * URL 파라미터에서 문자열 값 가져오기
+ */
+export const getStringParam = (search: string, key: string, defaultValue: string = ''): string => {
+ const params = new URLSearchParams(search)
+ return params.get(key) || defaultValue
+}
+
diff --git a/src/shared/ui/Footer.tsx b/src/shared/ui/Footer.tsx
new file mode 100644
index 000000000..4b46fb86f
--- /dev/null
+++ b/src/shared/ui/Footer.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const Footer: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Footer;
+
diff --git a/src/shared/ui/Header.tsx b/src/shared/ui/Header.tsx
new file mode 100644
index 000000000..63ecec168
--- /dev/null
+++ b/src/shared/ui/Header.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { MessageSquare } from 'lucide-react';
+
+const Header: React.FC = () => {
+ return (
+
+
+
+
+
게시물 관리 시스템
+
+
+
+
+
+
+ );
+};
+
+export default Header;
+
diff --git a/src/shared/ui/avatar/Avatar.tsx b/src/shared/ui/avatar/Avatar.tsx
new file mode 100644
index 000000000..8686e04e8
--- /dev/null
+++ b/src/shared/ui/avatar/Avatar.tsx
@@ -0,0 +1,44 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import { cva, VariantProps } from "class-variance-authority"
+
+const avatarVariants = cva(
+ "inline-flex items-center justify-center rounded-full bg-gray-200 text-gray-700 overflow-hidden",
+ {
+ variants: {
+ size: {
+ sm: "h-6 w-6 text-xs",
+ default: "h-8 w-8 text-sm",
+ lg: "h-12 w-12 text-base",
+ xl: "h-16 w-16 text-lg",
+ },
+ },
+ defaultVariants: {
+ size: "default",
+ },
+ },
+)
+
+export interface AvatarProps extends React.HTMLAttributes, VariantProps {
+ src?: string
+ alt?: string
+ fallback?: string
+}
+
+export const Avatar = forwardRef(
+ ({ className, size, src, alt, fallback, children, ...props }, ref) => {
+ return (
+
+ {src ? (
+
+ ) : (
+
+ {fallback || children}
+
+ )}
+
+ )
+ }
+)
+Avatar.displayName = "Avatar"
+
diff --git a/src/shared/ui/avatar/index.ts b/src/shared/ui/avatar/index.ts
new file mode 100644
index 000000000..f30513ee3
--- /dev/null
+++ b/src/shared/ui/avatar/index.ts
@@ -0,0 +1,3 @@
+export { Avatar } from './Avatar'
+export type { AvatarProps } from './Avatar'
+
diff --git a/src/shared/ui/badge/Badge.tsx b/src/shared/ui/badge/Badge.tsx
new file mode 100644
index 000000000..5d837e10d
--- /dev/null
+++ b/src/shared/ui/badge/Badge.tsx
@@ -0,0 +1,32 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import { cva, VariantProps } from "class-variance-authority"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default: "bg-blue-100 text-blue-800 hover:bg-blue-200",
+ secondary: "bg-gray-100 text-gray-800 hover:bg-gray-200",
+ success: "bg-green-100 text-green-800 hover:bg-green-200",
+ warning: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
+ destructive: "bg-red-100 text-red-800 hover:bg-red-200",
+ outline: "border border-gray-300 text-gray-700",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+export const Badge = forwardRef(({ className, variant, ...props }, ref) => {
+ return
+})
+Badge.displayName = "Badge"
+
diff --git a/src/shared/ui/badge/index.ts b/src/shared/ui/badge/index.ts
new file mode 100644
index 000000000..70864d4f6
--- /dev/null
+++ b/src/shared/ui/badge/index.ts
@@ -0,0 +1,3 @@
+export { Badge } from './Badge'
+export type { BadgeProps } from './Badge'
+
diff --git a/src/shared/ui/button/Button.tsx b/src/shared/ui/button/Button.tsx
new file mode 100644
index 000000000..b98278e38
--- /dev/null
+++ b/src/shared/ui/button/Button.tsx
@@ -0,0 +1,40 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import { cva, VariantProps } from "class-variance-authority"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default: "bg-blue-500 text-white hover:bg-blue-600",
+ destructive: "bg-red-500 text-white hover:bg-red-600",
+ outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100",
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
+ link: "underline-offset-4 hover:underline text-blue-500",
+ },
+ size: {
+ default: "h-10 py-2 px-4",
+ sm: "h-8 px-3 rounded-md text-xs",
+ lg: "h-11 px-8 rounded-md",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+
+export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {
+ className?: string
+}
+
+export const Button = forwardRef(({ className, variant, size, ...props }, ref) => {
+ return
+})
+
+Button.displayName = "Button"
+
diff --git a/src/shared/ui/button/index.ts b/src/shared/ui/button/index.ts
new file mode 100644
index 000000000..b63243862
--- /dev/null
+++ b/src/shared/ui/button/index.ts
@@ -0,0 +1,3 @@
+export { Button } from './Button'
+export type { ButtonProps } from './Button'
+
diff --git a/src/shared/ui/card/Card.tsx b/src/shared/ui/card/Card.tsx
new file mode 100644
index 000000000..e92e747c1
--- /dev/null
+++ b/src/shared/ui/card/Card.tsx
@@ -0,0 +1,31 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Card = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+Card.displayName = "Card"
+
+export const CardHeader = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+CardHeader.displayName = "CardHeader"
+
+export const CardTitle = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+CardTitle.displayName = "CardTitle"
+
+export const CardContent = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+CardContent.displayName = "CardContent"
+
diff --git a/src/shared/ui/card/index.ts b/src/shared/ui/card/index.ts
new file mode 100644
index 000000000..3a0668aa1
--- /dev/null
+++ b/src/shared/ui/card/index.ts
@@ -0,0 +1,2 @@
+export { Card, CardHeader, CardTitle, CardContent } from './Card'
+
diff --git a/src/shared/ui/dialog/Dialog.tsx b/src/shared/ui/dialog/Dialog.tsx
new file mode 100644
index 000000000..d024e2f4f
--- /dev/null
+++ b/src/shared/ui/dialog/Dialog.tsx
@@ -0,0 +1,48 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+export const Dialog = DialogPrimitive.Root
+export const DialogTrigger = DialogPrimitive.Trigger
+export const DialogPortal = DialogPrimitive.Portal
+export const DialogOverlay = DialogPrimitive.Overlay
+
+export const DialogContent = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ 닫기
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+export const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+export const DialogTitle = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
diff --git a/src/shared/ui/dialog/index.ts b/src/shared/ui/dialog/index.ts
new file mode 100644
index 000000000..e0ac9fc4f
--- /dev/null
+++ b/src/shared/ui/dialog/index.ts
@@ -0,0 +1,10 @@
+export {
+ Dialog,
+ DialogTrigger,
+ DialogPortal,
+ DialogOverlay,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from './Dialog'
+
diff --git a/src/shared/ui/index.tsx b/src/shared/ui/index.tsx
new file mode 100644
index 000000000..25a3a9f78
--- /dev/null
+++ b/src/shared/ui/index.tsx
@@ -0,0 +1,33 @@
+// Button
+export * from './button'
+
+// Input
+export * from './input'
+
+// Card
+export * from './card'
+
+// Textarea
+export * from './textarea'
+
+// Select
+export * from './select'
+
+// Dialog
+export * from './dialog'
+
+// Table
+export * from './table'
+
+// Avatar
+export * from './avatar'
+
+// Badge
+export * from './badge'
+
+// Text
+export * from './text'
+
+// Header & Footer
+export { default as Header } from './Header'
+export { default as Footer } from './Footer'
diff --git a/src/shared/ui/input/Input.tsx b/src/shared/ui/input/Input.tsx
new file mode 100644
index 000000000..3fc30921c
--- /dev/null
+++ b/src/shared/ui/input/Input.tsx
@@ -0,0 +1,17 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Input = forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
diff --git a/src/shared/ui/input/index.ts b/src/shared/ui/input/index.ts
new file mode 100644
index 000000000..4c0a80ff0
--- /dev/null
+++ b/src/shared/ui/input/index.ts
@@ -0,0 +1,2 @@
+export { Input } from './Input'
+
diff --git a/src/shared/ui/select/Select.tsx b/src/shared/ui/select/Select.tsx
new file mode 100644
index 000000000..d245846cf
--- /dev/null
+++ b/src/shared/ui/select/Select.tsx
@@ -0,0 +1,60 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown } from "lucide-react"
+
+export const Select = SelectPrimitive.Root
+export const SelectGroup = SelectPrimitive.Group
+export const SelectValue = SelectPrimitive.Value
+
+export const SelectTrigger = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+export const SelectContent = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+ {children}
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+export const SelectItem = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
diff --git a/src/shared/ui/select/index.ts b/src/shared/ui/select/index.ts
new file mode 100644
index 000000000..ba2821559
--- /dev/null
+++ b/src/shared/ui/select/index.ts
@@ -0,0 +1,9 @@
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+} from './Select'
+
diff --git a/src/shared/ui/table/Table.tsx b/src/shared/ui/table/Table.tsx
new file mode 100644
index 000000000..24f6d2f4a
--- /dev/null
+++ b/src/shared/ui/table/Table.tsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Table = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+Table.displayName = "Table"
+
+export const TableHeader = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+TableHeader.displayName = "TableHeader"
+
+export const TableBody = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+TableBody.displayName = "TableBody"
+
+export const TableRow = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+TableRow.displayName = "TableRow"
+
+export const TableHead = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+TableHead.displayName = "TableHead"
+
+export const TableCell = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
+TableCell.displayName = "TableCell"
+
diff --git a/src/shared/ui/table/index.ts b/src/shared/ui/table/index.ts
new file mode 100644
index 000000000..715ae4fa3
--- /dev/null
+++ b/src/shared/ui/table/index.ts
@@ -0,0 +1,9 @@
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableRow,
+ TableHead,
+ TableCell,
+} from './Table'
+
diff --git a/src/shared/ui/text/Text.tsx b/src/shared/ui/text/Text.tsx
new file mode 100644
index 000000000..eb2d127b1
--- /dev/null
+++ b/src/shared/ui/text/Text.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import { cva, VariantProps } from "class-variance-authority"
+
+const textVariants = cva("", {
+ variants: {
+ variant: {
+ default: "text-gray-900",
+ muted: "text-gray-500",
+ primary: "text-blue-600",
+ secondary: "text-gray-600",
+ success: "text-green-600",
+ warning: "text-yellow-600",
+ destructive: "text-red-600",
+ },
+ size: {
+ xs: "text-xs",
+ sm: "text-sm",
+ default: "text-base",
+ lg: "text-lg",
+ xl: "text-xl",
+ "2xl": "text-2xl",
+ },
+ weight: {
+ normal: "font-normal",
+ medium: "font-medium",
+ semibold: "font-semibold",
+ bold: "font-bold",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ weight: "normal",
+ },
+})
+
+export interface TextProps
+ extends React.HTMLAttributes,
+ VariantProps {
+ as?: "span" | "p" | "div"
+}
+
+export const Text = forwardRef(
+ ({ className, variant, size, weight, as = "span", children, ...props }, ref) => {
+ const Component = as
+ // ref는 as prop에 따라 타입이 달라질 수 있으므로 any로 처리
+ return (
+
+ {children}
+
+ )
+ }
+)
+Text.displayName = "Text"
+
diff --git a/src/shared/ui/text/index.ts b/src/shared/ui/text/index.ts
new file mode 100644
index 000000000..ac263e65d
--- /dev/null
+++ b/src/shared/ui/text/index.ts
@@ -0,0 +1,3 @@
+export { Text } from './Text'
+export type { TextProps } from './Text'
+
diff --git a/src/shared/ui/textarea/Textarea.tsx b/src/shared/ui/textarea/Textarea.tsx
new file mode 100644
index 000000000..75374ed4a
--- /dev/null
+++ b/src/shared/ui/textarea/Textarea.tsx
@@ -0,0 +1,16 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Textarea = forwardRef>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
diff --git a/src/shared/ui/textarea/index.ts b/src/shared/ui/textarea/index.ts
new file mode 100644
index 000000000..a92a824bf
--- /dev/null
+++ b/src/shared/ui/textarea/index.ts
@@ -0,0 +1,2 @@
+export { Textarea } from './Textarea'
+
diff --git a/src/widgets/comment-list/index.ts b/src/widgets/comment-list/index.ts
new file mode 100644
index 000000000..6afab078b
--- /dev/null
+++ b/src/widgets/comment-list/index.ts
@@ -0,0 +1,2 @@
+export { CommentList } from './ui/CommentList'
+
diff --git a/src/widgets/comment-list/ui/CommentList.tsx b/src/widgets/comment-list/ui/CommentList.tsx
new file mode 100644
index 000000000..137f822d2
--- /dev/null
+++ b/src/widgets/comment-list/ui/CommentList.tsx
@@ -0,0 +1,54 @@
+import { Plus } from "lucide-react"
+import type { Comment } from '../../../entities/comment'
+import { CommentItem } from '../../../entities/comment/ui'
+import { Button } from "../../../shared/ui"
+
+interface CommentListProps {
+ comments: Comment[]
+ postId: number
+ searchQuery?: string
+ onAddComment: () => void
+ onLikeComment: (commentId: number, postId: number) => void
+ onEditComment: (comment: Comment) => void
+ onDeleteComment: (commentId: number, postId: number) => void
+}
+
+/**
+ * 댓글 목록 위젯
+ * 게시물의 댓글을 표시하고 관리합니다.
+ * Entities UI 컴포넌트를 사용하여 구조를 개선했습니다.
+ */
+export const CommentList = ({
+ comments,
+ postId,
+ searchQuery = "",
+ onAddComment,
+ onLikeComment,
+ onEditComment,
+ onDeleteComment,
+}: CommentListProps) => {
+ return (
+
+
+
+ {comments.map((comment) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/widgets/post-create-modal/index.ts b/src/widgets/post-create-modal/index.ts
new file mode 100644
index 000000000..19113cad0
--- /dev/null
+++ b/src/widgets/post-create-modal/index.ts
@@ -0,0 +1,2 @@
+export { PostCreateModal } from './ui/PostCreateModal'
+
diff --git a/src/widgets/post-create-modal/ui/PostCreateModal.tsx b/src/widgets/post-create-modal/ui/PostCreateModal.tsx
new file mode 100644
index 000000000..8b19e4c43
--- /dev/null
+++ b/src/widgets/post-create-modal/ui/PostCreateModal.tsx
@@ -0,0 +1,59 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle, Input, Textarea, Button } from "../../../shared/ui"
+
+interface PostCreateModalProps {
+ isOpen: boolean
+ onClose: () => void
+ title: string
+ body: string
+ userId: number
+ onTitleChange: (title: string) => void
+ onBodyChange: (body: string) => void
+ onUserIdChange: (userId: number) => void
+ onSubmit: () => void
+}
+
+/**
+ * 게시물 생성 모달 위젯
+ */
+export const PostCreateModal = ({
+ isOpen,
+ onClose,
+ title,
+ body,
+ userId,
+ onTitleChange,
+ onBodyChange,
+ onUserIdChange,
+ onSubmit,
+}: PostCreateModalProps) => {
+ return (
+
+
+
+ 새 게시물 추가
+
+
+ onTitleChange(e.target.value)}
+ />
+
+
+
+ )
+}
+
diff --git a/src/widgets/post-detail/index.ts b/src/widgets/post-detail/index.ts
new file mode 100644
index 000000000..b9a506bef
--- /dev/null
+++ b/src/widgets/post-detail/index.ts
@@ -0,0 +1,2 @@
+export { PostDetail } from './ui/PostDetail'
+
diff --git a/src/widgets/post-detail/ui/PostDetail.tsx b/src/widgets/post-detail/ui/PostDetail.tsx
new file mode 100644
index 000000000..cdecf9e9a
--- /dev/null
+++ b/src/widgets/post-detail/ui/PostDetail.tsx
@@ -0,0 +1,58 @@
+import type { Post } from '../../../entities/post'
+import type { Comment } from '../../../entities/comment'
+import { CommentList } from '../../comment-list'
+import { highlightText } from '../../../shared/lib'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../../shared/ui"
+
+interface PostDetailProps {
+ post: Post | null
+ comments: Comment[]
+ searchQuery?: string
+ isOpen: boolean
+ onClose: () => void
+ onAddComment: () => void
+ onLikeComment: (commentId: number, postId: number) => void
+ onEditComment: (comment: Comment) => void
+ onDeleteComment: (commentId: number, postId: number) => void
+}
+
+/**
+ * 게시물 상세 위젯
+ * 게시물 상세 정보와 댓글 목록을 표시합니다.
+ */
+export const PostDetail = ({
+ post,
+ comments,
+ searchQuery = "",
+ isOpen,
+ onClose,
+ onAddComment,
+ onLikeComment,
+ onEditComment,
+ onDeleteComment,
+}: PostDetailProps) => {
+ if (!post) return null
+
+ return (
+
+
+
+ {highlightText(post.title, searchQuery)}
+
+
+
{highlightText(post.body, searchQuery)}
+
+
+
+
+ )
+}
+
diff --git a/src/widgets/post-edit-modal/index.ts b/src/widgets/post-edit-modal/index.ts
new file mode 100644
index 000000000..71a5610d1
--- /dev/null
+++ b/src/widgets/post-edit-modal/index.ts
@@ -0,0 +1,2 @@
+export { PostEditModal } from './ui/PostEditModal'
+
diff --git a/src/widgets/post-edit-modal/ui/PostEditModal.tsx b/src/widgets/post-edit-modal/ui/PostEditModal.tsx
new file mode 100644
index 000000000..9e1924a54
--- /dev/null
+++ b/src/widgets/post-edit-modal/ui/PostEditModal.tsx
@@ -0,0 +1,50 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle, Input, Textarea, Button } from "../../../shared/ui"
+import type { Post } from '../../../entities/post'
+
+interface PostEditModalProps {
+ isOpen: boolean
+ onClose: () => void
+ post: Post | null
+ onTitleChange: (title: string) => void
+ onBodyChange: (body: string) => void
+ onSubmit: () => void
+}
+
+/**
+ * 게시물 수정 모달 위젯
+ */
+export const PostEditModal = ({
+ isOpen,
+ onClose,
+ post,
+ onTitleChange,
+ onBodyChange,
+ onSubmit,
+}: PostEditModalProps) => {
+ if (!post) return null
+
+ return (
+
+
+
+ 게시물 수정
+
+
+ onTitleChange(e.target.value)}
+ />
+
+
+
+ )
+}
+
diff --git a/src/widgets/post-list/index.ts b/src/widgets/post-list/index.ts
new file mode 100644
index 000000000..f88fdb0fb
--- /dev/null
+++ b/src/widgets/post-list/index.ts
@@ -0,0 +1 @@
+export { PostList } from './ui/PostList'
diff --git a/src/widgets/post-list/ui/PostList.tsx b/src/widgets/post-list/ui/PostList.tsx
new file mode 100644
index 000000000..275df2ad8
--- /dev/null
+++ b/src/widgets/post-list/ui/PostList.tsx
@@ -0,0 +1,83 @@
+import { useMemo } from 'react'
+import type { Post } from '../../../entities/post'
+import { usePostFilters } from '../../../features/post'
+import { PostRow } from '../../../entities/post/ui'
+import { PostSearchInput, PostTagFilter, PostSortControls, PostPagination } from '../../../features/post/ui'
+import { Table, TableBody, TableHead, TableHeader, TableRow } from "../../../shared/ui"
+
+interface PostListProps {
+ tags: Array<{ url: string; slug: string }>
+ posts: Post[]
+ isLoading?: boolean
+ onPostDetail: (post: Post) => void
+ onPostEdit: (post: Post) => void
+ onPostDelete: (postId: number) => void
+ onUserClick: (author: Post['author']) => void
+ onTagChange: (tag: string) => void
+}
+
+/**
+ * 게시물 목록 위젯
+ * 검색/필터 컨트롤과 게시물 테이블을 포함합니다.
+ * Features UI 컴포넌트와 Entities UI 컴포넌트를 사용하여 Props Drilling을 최소화했습니다.
+ */
+export const PostList = ({
+ tags,
+ posts,
+ isLoading = false,
+ onPostDetail,
+ onPostEdit,
+ onPostDelete,
+ onUserClick,
+ onTagChange,
+}: PostListProps) => {
+ const { sortPosts, searchQuery, selectedTag, sortBy, sortOrder } = usePostFilters()
+ // sortBy나 sortOrder가 변경되면 정렬이 다시 실행되도록 useMemo 사용
+ const sortedPosts = useMemo(() => sortPosts(posts), [posts, sortPosts, sortBy, sortOrder])
+
+ return (
+
+ {/* 검색 및 필터 컨트롤 */}
+
+
+ {/* 게시물 테이블 */}
+ {isLoading ? (
+
로딩 중...
+ ) : (
+
+
+
+ ID
+ 제목
+ 작성자
+ 반응
+ 작업
+
+
+
+ {sortedPosts.map((post) => (
+
+ ))}
+
+
+ )}
+
+ {/* 페이지네이션 */}
+
+
+ )
+}
diff --git a/src/widgets/user-modal/index.ts b/src/widgets/user-modal/index.ts
new file mode 100644
index 000000000..b3eed39fc
--- /dev/null
+++ b/src/widgets/user-modal/index.ts
@@ -0,0 +1,2 @@
+export { UserModal } from './ui/UserModal'
+
diff --git a/src/widgets/user-modal/ui/UserModal.tsx b/src/widgets/user-modal/ui/UserModal.tsx
new file mode 100644
index 000000000..d90fc3164
--- /dev/null
+++ b/src/widgets/user-modal/ui/UserModal.tsx
@@ -0,0 +1,51 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../../shared/ui"
+import type { User } from '../../../entities/user'
+
+interface UserModalProps {
+ isOpen: boolean
+ onClose: () => void
+ user: User | null
+}
+
+/**
+ * 사용자 모달 위젯
+ */
+export const UserModal = ({ isOpen, onClose, user }: UserModalProps) => {
+ if (!user) return null
+
+ return (
+
+
+
+ 사용자 정보
+
+
+
+
{user.username}
+
+
+ 이름: {user.firstName} {user.lastName}
+
+
+ 나이: {user.age}
+
+
+ 이메일: {user.email}
+
+
+ 전화번호: {user.phone}
+
+
+ 주소: {user.address?.address}, {user.address?.city},{" "}
+ {user.address?.state}
+
+
+ 직장: {user.company?.name} - {user.company?.title}
+
+
+
+
+
+ )
+}
+
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 5a2def4b7..1381ace91 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -21,5 +21,5 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"]
+ "include": ["src", "vite-env.d.ts"]
}
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
new file mode 100644
index 000000000..61018ee76
--- /dev/null
+++ b/tsconfig.app.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.tsx","./src/index.tsx","./src/main.tsx","./src/components/footer.tsx","./src/components/header.tsx","./src/components/index.tsx","./src/entities/comment/index.ts","./src/entities/comment/api/commentapi.ts","./src/entities/comment/api/index.ts","./src/entities/comment/model/store.ts","./src/entities/comment/model/types.ts","./src/entities/comment/ui/commentitem.tsx","./src/entities/comment/ui/index.ts","./src/entities/post/index.ts","./src/entities/post/api/index.ts","./src/entities/post/api/postapi.ts","./src/entities/post/model/store.ts","./src/entities/post/model/types.ts","./src/entities/post/ui/postauthor.tsx","./src/entities/post/ui/postrow.tsx","./src/entities/post/ui/posttag.tsx","./src/entities/post/ui/index.ts","./src/entities/user/index.ts","./src/entities/user/api/index.ts","./src/entities/user/api/userapi.ts","./src/entities/user/model/store.ts","./src/entities/user/model/types.ts","./src/entities/user/ui/index.ts","./src/features/comment/index.ts","./src/features/comment/model/usecommentcreate.ts","./src/features/comment/model/usecommentdelete.ts","./src/features/comment/model/usecommentlike.ts","./src/features/comment/model/usecommentlist.ts","./src/features/comment/model/usecommentupdate.ts","./src/features/post/index.ts","./src/features/post/model/usepostcreate.ts","./src/features/post/model/usepostcreatemodal.ts","./src/features/post/model/usepostdelete.ts","./src/features/post/model/usepostdetailmodal.ts","./src/features/post/model/useposteditmodal.ts","./src/features/post/model/usepostfilters.ts","./src/features/post/model/usepostlist.ts","./src/features/post/model/usepostpagination.ts","./src/features/post/model/usepostsearch.ts","./src/features/post/model/usepostsort.ts","./src/features/post/model/usepostupdate.ts","./src/features/post/model/usetagfilter.ts","./src/features/post/ui/postpagination.tsx","./src/features/post/ui/postsearchinput.tsx","./src/features/post/ui/postsortcontrols.tsx","./src/features/post/ui/posttagfilter.tsx","./src/features/post/ui/index.ts","./src/features/user/index.ts","./src/features/user/model/useuserdetail.ts","./src/features/user/model/useusermodal.ts","./src/pages/postsmanagerpage.tsx","./src/shared/index.ts","./src/shared/api/client.ts","./src/shared/api/index.ts","./src/shared/config/constants.ts","./src/shared/config/index.ts","./src/shared/lib/dateutils.ts","./src/shared/lib/highlighttext.tsx","./src/shared/lib/index.ts","./src/shared/lib/queryclient.ts","./src/shared/lib/querykeys.ts","./src/shared/lib/urlutils.ts","./src/shared/ui/footer.tsx","./src/shared/ui/header.tsx","./src/shared/ui/index.tsx","./src/shared/ui/avatar/avatar.tsx","./src/shared/ui/avatar/index.ts","./src/shared/ui/badge/badge.tsx","./src/shared/ui/badge/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/card/card.tsx","./src/shared/ui/card/index.ts","./src/shared/ui/dialog/dialog.tsx","./src/shared/ui/dialog/index.ts","./src/shared/ui/input/input.tsx","./src/shared/ui/input/index.ts","./src/shared/ui/select/select.tsx","./src/shared/ui/select/index.ts","./src/shared/ui/table/table.tsx","./src/shared/ui/table/index.ts","./src/shared/ui/text/text.tsx","./src/shared/ui/text/index.ts","./src/shared/ui/textarea/textarea.tsx","./src/shared/ui/textarea/index.ts","./src/widgets/comment-list/index.ts","./src/widgets/comment-list/ui/commentlist.tsx","./src/widgets/post-create-modal/index.ts","./src/widgets/post-create-modal/ui/postcreatemodal.tsx","./src/widgets/post-detail/index.ts","./src/widgets/post-detail/ui/postdetail.tsx","./src/widgets/post-edit-modal/index.ts","./src/widgets/post-edit-modal/ui/posteditmodal.tsx","./src/widgets/post-list/index.ts","./src/widgets/post-list/ui/postlist.tsx","./src/widgets/user-modal/index.ts","./src/widgets/user-modal/ui/usermodal.tsx","./vite-env.d.ts"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo
new file mode 100644
index 000000000..62c7bf924
--- /dev/null
+++ b/tsconfig.node.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index be7b7a3d4..a65703714 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,6 +3,7 @@ import react from "@vitejs/plugin-react"
// https://vite.dev/config/
export default defineConfig({
+ base: "/front_7th_chapter3-3/",
plugins: [react()],
server: {
proxy: {