diff --git a/.claude/agent-memory/code-reviewer/MEMORY.md b/.claude/agent-memory/code-reviewer/MEMORY.md new file mode 100644 index 0000000..d528afa --- /dev/null +++ b/.claude/agent-memory/code-reviewer/MEMORY.md @@ -0,0 +1,22 @@ +# code-reviewer 메모리 + +code-reviewer 에이전트가 이 레포를 리뷰하며 축적하는 지속 메모. 리뷰에서 반복되는 패턴·합의된 판단·자주 놓치는 지점을 기록한다. 한 줄에 하나씩, 근거와 함께. + +## 프로젝트 고정 사실 + +- 스택: React 19, TypeScript(strict 지향), Vite 6, Tailwind CSS 4, pnpm 11.9.0. 테스트 러너 미도입. +- 구조: Feature-Sliced Design. 의존 방향 `app → pages → widgets → features → entities → shared` (역방향 import 금지). +- 검증 명령: `corepack pnpm run lint`(= `biome check .` + `tsc --noEmit`), `corepack pnpm run build`. 포맷·lint는 Biome, 타입 검증은 `tsc`가 담당(별도 `typecheck` 스크립트도 있음). +- 디자인 토큰은 `src/index.css`의 `@theme`/`:root`에 정의. 임의 hex/radius/shadow 금지. +- 에러 처리: HTTP status가 아니라 응답 body의 `code` 기준으로 사용자 메시지를 매핑한다. + +## 반복 지적 패턴 + + + + + +## 합의된 판단 / 예외 + + + diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..5118fc5 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,54 @@ +--- +name: code-reviewer +description: 프론트엔드(React/TS/FSD) 변경사항을 리뷰 전용으로 점검한다. 코드를 수정하지 않고 발견과 권장만 보고한다. PR 전이나 변경 검토가 필요할 때 사용한다. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +너는 이 레포 전용 프론트엔드 코드 리뷰어다. **코드를 수정하지 않는다.** 발견 사항과 권장안만 보고한다. 판단 기준은 [`AGENTS.md`](../../AGENTS.md)와 정본 상세 규칙 [`rules.md`](../rules.md)다. + +## 진행 방식 + +1. `git diff origin/dev...HEAD`(없으면 워킹/스테이징 diff)로 변경 범위를 확인한다. +2. 변경 파일과 그 주변 맥락(같은 slice의 public API `index.ts`와 관련 세그먼트: feature는 `api/hooks/components`, entity는 `api/model/ui`)을 읽고 diff만 보고 판단하지 않는다. +3. 아래 관점으로 점검하고, 각 발견을 `파일:라인 · 무엇이 문제 · 왜 · 권장 수정`으로 남긴다. + +## 리뷰 관점 + +**FSD 구조** +- 의존 방향 위반(하위 레이어가 상위 import)이 없는가? +- slice 내부 파일을 public API(`index.ts`) 우회해 깊게 import하지 않는가? +- 코드가 책임에 맞는 레이어에 있는가? + +**Container / Presenter** +- Presenter가 API 호출·localStorage·복잡한 상태 전이를 직접 하지 않는가? +- 기능 로직이 feature의 `hooks`에 있는 `use*` custom hook으로 분리됐는가? hook 이름은 `use`로 시작하는가? + +**TypeScript** +- `any`, 불필요한 `as`, `@ts-ignore`가 있는가? (`unknown` + type guard 권장) +- API DTO와 UI model이 분리되고 매퍼가 올바른 위치(공유 도메인은 `entities/*/model`, feature 전용은 feature `api` 근처)에 있는가? +- nullable 처리와 union/discriminated union 사용이 안전한가? + +**React** +- 함수 컴포넌트, PascalCase, `interface {Name}Props`, list stable key(index key 금지)를 지키는가? +- `useEffect`가 외부 동기화에만 쓰이고 cleanup이 있는가? 파생값을 effect로 계산하지 않는가? +- event handler `handleX` / props `onX` 컨벤션을 지키는가? + +**디자인 시스템** +- `src/index.css` 토큰을 쓰는가? 임의 hex/radius/shadow/spacing을 도입하지 않는가? +- 같은 의미의 버튼/카드 스타일을 중복 생성하지 않는가? + +**상태/에러/접근성** +- loading/error/empty/fallback 상태를 처리하는가? +- API 에러를 `code` 기반으로 매핑하고 raw message/stack을 노출하지 않는가? +- semantic HTML, `button type`, `aria-label`, 포커스, `target="_blank"`의 `rel="noopener noreferrer"`를 확인했는가? + +**보안** +- 시크릿 번들 노출, `dangerouslySetInnerHTML`, 저장소 민감정보가 없는가? + +## 보고 형식 + +- 심각도 순(Blocking / High / Medium / Nit)으로 정렬한다. +- 확신이 낮은 항목은 추정임을 명시하고, 불필요한 지적으로 노이즈를 늘리지 않는다. +- 잘된 점도 한두 줄 짚는다. +- 코드를 직접 고치지 말고, 필요한 변경을 구체적으로 제안한다. diff --git a/.claude/commands/fix-issue.md b/.claude/commands/fix-issue.md new file mode 100644 index 0000000..263e027 --- /dev/null +++ b/.claude/commands/fix-issue.md @@ -0,0 +1,48 @@ +--- +description: GitHub Issue 번호를 받아 이슈 우선 워크플로우로 구현한다. +argument-hint: +allowed-tools: Bash(gh issue *), Bash(gh pr *), Bash(git *), Bash(corepack pnpm *), Read, Edit, Write, Grep, Glob +--- + +# /fix-issue + +이 레포의 이슈 우선 워크플로우로 이슈 하나를 끝까지 처리한다. 반드시 [`AGENTS.md`](../../AGENTS.md)와 정본 상세 규칙 [`rules.md`](../rules.md)를 먼저 따른다. + +## 대상 이슈 + +`#$1` + +## 절차 + +1. **이슈 파악**: `gh issue view $1`로 배경/작업 범위/검증 항목을 읽는다. 요구가 모호하면 진행 전에 질문한다. +2. **브랜치 생성**: 기본 브랜치 `dev` 최신화 후 이슈 번호 브랜치를 만든다. 타입은 작업 성격에 맞춘다. + ```bash + git switch dev && git pull + git switch -c feat/$1- # 브랜치 타입: feat|fix|docs|refac + ``` +3. **탐색**: 관련 slice와 FSD 레이어를 파악한다. 기존 패턴/디자인 토큰/유사 구현을 먼저 읽는다. +4. **구현**: + - FSD 의존 방향(`app → pages → widgets → features → entities → shared`)을 지킨다. + - 새/큰 변경 기능은 Container/Presenter로 분리하고 로직은 `hooks`의 `use*` custom hook에 둔다. + - API는 DTO ↔ UI model 분리 + 매퍼(공유 도메인은 `entities/*/model`, feature 전용은 feature `api` 근처). 에러는 `code` 기반 매핑. + - 디자인은 `src/index.css` 토큰과 기존 UI 패턴을 따른다. 임의 색/여백/radius 금지. + - `any`, 불필요한 `as`, `@ts-ignore` 금지. + - loading/error/empty/fallback 상태와 접근성(semantic HTML, button type, aria, 외부 링크 rel)을 처리한다. +5. **검증** (실제 실행하고 결과를 기록): + ```bash + corepack pnpm run lint # biome check . && tsc --noEmit + corepack pnpm run typecheck # tsc --noEmit (타입만 별도로 볼 때) + corepack pnpm run build # vite build + ``` +6. **커밋**: `type: subject` 형식. 관련 없는 변경을 섞지 않는다. +7. **PR**: `.github/PULL_REQUEST_TEMPLATE.md`를 채워 연다. 변경 요약 · 실제 검증 결과 · (UI면) 스크린샷 · `Closes #$1`을 포함한다. + ```bash + gh pr create --fill --base dev + ``` +8. **보고**: 무엇을 바꿨고, 검증 결과가 무엇이며, 남은 확인 사항이 있으면 명시한다. + +## 하지 말 것 + +- 이슈/브랜치 없이 바로 구현하지 않는다. +- 검증을 실행하지 않고 "완료"라고 하지 않는다. +- 요청 범위를 넘는 대규모 리팩터링을 임의로 하지 않는다. diff --git a/.claude/hooks/block-secrets.sh b/.claude/hooks/block-secrets.sh new file mode 100755 index 0000000..16ab869 --- /dev/null +++ b/.claude/hooks/block-secrets.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# PreToolUse(Bash) 훅: 파괴적 삭제와 시크릿 파일 노출 명령을 차단한다. +# exit 2 => 명령 차단 후 stderr 메시지를 에이전트에 전달. +set -euo pipefail + +input=$(cat) + +cmd=$(printf '%s' "$input" | node -e ' +let d=""; +process.stdin.on("data", c => d += c); +process.stdin.on("end", () => { + try { process.stdout.write(JSON.parse(d).tool_input?.command || ""); } + catch { process.stdout.write(""); } +}); +' 2>/dev/null || true) + +[ -z "$cmd" ] && exit 0 + +block() { + echo "🚫 차단됨: $1" >&2 + echo "이 명령은 안전 훅(block-secrets.sh)에 의해 막혔습니다. 의도한 작업이면 사람이 직접 실행하세요." >&2 + exit 2 +} + +# 1) 루트/홈 대상 파괴적 삭제 (short/long 옵션 모두) +if printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+((-[a-zA-Z]+|--(recursive|force|no-preserve-root))[[:space:]]+)*(-[a-zA-Z]*[rRfF][a-zA-Z]*|--(recursive|force))[[:space:]]+(/|~|\$HOME|/\*|\.\*)([[:space:]]|$)'; then + block "루트/홈 대상 파괴적 삭제(rm -rf)" +fi + +# 2) .env 등 시크릿 파일 내용 출력 (읽기 명령 또는 입력 리다이렉션) +if printf '%s' "$cmd" | grep -Eq '((cat|less|more|head|tail|xxd|od|base64|strings|grep|egrep|sed|awk|nl|tac)[[:space:]]+[^|;&]*|<[[:space:]]*)(\.env($|[^.a-zA-Z0-9])|\.env\.[a-z]+|id_rsa|id_ed25519|\.pem($|[^a-zA-Z]))'; then + block "시크릿/자격증명 파일 내용 출력 시도" +fi + +# 3) .env를 git에 강제 추가 +if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+add[[:space:]]+[^|;&]*(-f|--force)[^|;&]*\.env'; then + block ".env 파일 강제 git add" +fi + +# 4) 원격으로 시크릿 유출 (curl/wget에 .env 첨부) +if printf '%s' "$cmd" | grep -Eq '(curl|wget)[[:space:]]+[^|;&]*\.env'; then + block ".env 파일을 원격으로 전송 시도" +fi + +exit 0 diff --git a/.claude/hooks/format-on-save.sh b/.claude/hooks/format-on-save.sh new file mode 100755 index 0000000..e4c41bb --- /dev/null +++ b/.claude/hooks/format-on-save.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# PostToolUse(Edit|Write) 훅: 방금 저장한 파일을 Biome로 포맷 + 정리한다. +# Biome가 설치돼 있을 때만 동작하고, 없으면 조용히 통과한다. +set -euo pipefail + +input=$(cat) + +# stdin JSON에서 tool_input.file_path 추출 (node는 이 레포에 항상 존재) +file_path=$(printf '%s' "$input" | node -e ' +let d=""; +process.stdin.on("data", c => d += c); +process.stdin.on("end", () => { + try { process.stdout.write(JSON.parse(d).tool_input?.file_path || ""); } + catch { process.stdout.write(""); } +}); +' 2>/dev/null || true) + +[ -z "$file_path" ] && exit 0 + +# Biome가 지원하는 확장자만 (md/yaml 등은 제외) +case "$file_path" in + *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.css|*.json|*.jsonc) ;; + *) exit 0 ;; +esac + +[ -f "$file_path" ] || exit 0 + +cd "${CLAUDE_PROJECT_DIR:-.}" + +# 로컬에 설치된 Biome 바이너리가 있을 때만 실행 (없으면 no-op). +# node_modules의 바이너리를 직접 호출한다(pnpm exec의 워크스페이스 파일 생성 부작용 회피). +# check --write: 포맷 + import 정리 + 안전한 lint 자동수정. 저장 파일 하나만 대상으로 한다. +if [ -x "node_modules/.bin/biome" ]; then + node_modules/.bin/biome check --write --no-errors-on-unmatched "$file_path" >/dev/null 2>&1 || true +fi + +exit 0 diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..ed39591 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "corepack", + "runtimeArgs": ["pnpm", "run", "dev"], + "port": 5173, + "autoPort": true + } + ] +} diff --git a/.claude/output-styles/teaching.md b/.claude/output-styles/teaching.md new file mode 100644 index 0000000..952da88 --- /dev/null +++ b/.claude/output-styles/teaching.md @@ -0,0 +1,21 @@ +--- +name: teaching +description: 작업을 수행하면서 왜 그렇게 했는지 짧게 설명을 덧붙이는 말투. +--- + +너는 이 레포에서 작업하는 코딩 에이전트다. 평소처럼 정확하게 작업하되, 프론트엔드(React/TS/FSD)를 배우는 팀원에게 설명하듯 맥락을 덧붙인다. + +## 말투 규칙 + +- 결정을 내릴 때, **무엇을** 했는지뿐 아니라 **왜** 그 선택이 이 레포 규칙(FSD, Container/Presenter, 디자인 토큰, 에러코드 매핑 등)에 맞는지 한 줄로 짚는다. +- 새 패턴/개념(custom hook 분리, DTO↔model 매퍼, discriminated union 등)이 나오면 2~3문장으로 짧게 설명한다. +- 트레이드오프가 있으면 대안과 선택 이유를 간단히 밝힌다. +- 설명은 **간결하게**. 장황한 강의가 아니라 코드 옆의 주석 같은 밀도로 유지한다. +- 코드/명령/검증 결과 등 실질 작업은 절대 생략하지 않는다. 설명은 작업을 대체하지 않고 보완한다. +- 한국어로 설명한다. 코드/식별자/명령은 원문 그대로 둔다. + +## 하지 말 것 + +- 이미 아는 기본 개념을 매번 장황하게 반복하지 않는다. +- 설명하느라 실제 구현·검증을 미루지 않는다. +- 추측을 사실처럼 설명하지 않는다. 불확실하면 불확실하다고 말한다. diff --git a/.claude/rules.md b/.claude/rules.md new file mode 100644 index 0000000..5aa54ca --- /dev/null +++ b/.claude/rules.md @@ -0,0 +1,551 @@ +# BOP Front-end Agent Rules + +이 문서는 이 레포에서 작업하는 모든 에이전트와 개발자가 따라야 하는 프론트엔드 구현 규칙이다. 새 기능을 만들거나 기존 코드를 수정하기 전에 반드시 이 파일을 먼저 읽고, 이 규칙과 충돌하는 구현은 하지 않는다. + +핵심 목표는 사용자가 현재 상태를 명확하게 이해할 수 있고, API 장애 상황에서도 서비스 흐름이 끊기지 않는 UI를 만드는 것이다. + +## 1. 기술 스택 + +- Core: React 19, TypeScript, Vite +- Routing: React Router +- Server State: TanStack Query +- Client State: Zustand +- API Client: Axios +- Styling: Tailwind CSS 4, shadcn/ui +- Form: React Hook Form, Zod +- Notification: Sonner +- Code Quality: Biome +- Package Manager: pnpm + +프론트는 Vite 정적 SPA로 빌드하고, production smoke는 Vite preview 서버(`pnpm start`)로 확인한다. 별도 Node 서버를 추가하지 않는다. Docker 이미지(`Dockerfile`)는 빌드 산출물을 nginx로 정적 서빙하며, `back-end/docker-compose.yml`의 `frontend` 서비스가 이를 빌드/기동한다. + +## 2. 작업 흐름 + +- 모든 작업은 GitHub Issue를 먼저 만들고, issue 번호가 포함된 브랜치에서 진행한다. +- 브랜치 형식은 `feat/{issue-number}-{short-name}`, `fix/{issue-number}-{short-name}`, `docs/{issue-number}-{short-name}`, `refac/{issue-number}-{short-name}`를 따른다. +- 커밋 형식은 `type: subject`이며, 허용 type은 `feat`, `fix`, `docs`, `refac`, `test`, `chore`이다. +- PR 본문에는 변경 요약, 테스트 결과, 필요한 경우 스크린샷/로그, 그리고 `Closes #issue-number`로 연결한 이슈를 포함한다. +- 구현 후에는 검증 명령을 직접 실행하고 PR에 실제 결과를 적는다. +- 단순 설명이나 스텁으로 끝내지 않는다. 동작하는 코드와 검증 결과를 남긴다. +- 백엔드 API/기획/API 명세와 연결되는 변경은 docs 레포의 기획/API 문서를 먼저 확인하고 필요하면 문서도 갱신한다. + +## 3. FSD 폴더 구조 + +패키지 구조는 Feature-Sliced Design(FSD) 아키텍처를 따른다. `app`, `pages`, `widgets`, `features`, `entities`, `shared` 6개 레이어로 구성하고, 새 코드는 FSD 레이어 책임에 맞는 위치에 둔다. + +```text +src +├── app +│ ├── router +│ ├── providers +│ └── store +├── pages +├── widgets +├── features +│ └── {feature} +│ ├── api # HTTP 요청 함수, API DTO, endpoint 호출 +│ ├── hooks # custom hook (use 접두) +│ ├── components # Container / Presenter component +│ ├── store # feature 전용 Zustand store (필요할 때만) +│ ├── types # feature 타입 +│ └── index.ts # public API export +├── entities +│ └── {domain} # 여러 feature가 공유하는 도메인 (policy, user 등) +│ ├── api # 도메인 DTO, 조회 함수 +│ ├── model # 도메인 타입, mapper, 공용 hook/store +│ ├── ui # 도메인 표현 컴포넌트 (PolicyCard 등) +│ └── index.ts # public API export +├── shared +│ ├── api # axios instance, queryClient +│ ├── ui # 공통 UI (Button, Modal, Input, Skeleton, EmptyState) +│ ├── hooks # 공통 hook (useDebounce 등) +│ ├── utils # date, format, storage 등 순수 함수 +│ ├── constants +│ └── types +└── assets +``` + +레이어 의존 방향은 아래를 따르며, 하위 레이어가 상위 레이어를 import하지 않는다. 같은 레이어 안의 slice끼리도 서로 import하지 않는다(특히 feature → feature 금지). + +```text +app → pages → widgets → features → entities → shared +``` + +각 레이어 책임: + +- `app`: 앱 초기화, 전역 provider, 라우팅, 전역 store, 전역 스타일 연결 등 앱 조립 책임 +- `pages`: 라우트/page 단위 화면 조립 책임. 화면 조립만 수행한다. +- `widgets`: 여러 feature/entity를 조합한 독립 UI 블록 (Header, Footer, PolicyList, SearchFilter 등) +- `features`: 사용자가 수행하는 행위, use case 단위 비즈니스 기능. API, hook, component를 관리한다. +- `entities`: 여러 feature가 공유하는 비즈니스 도메인의 타입, API, model, 표현 UI +- `shared`: 도메인에 의존하지 않는 공통 UI, hook, util, api, config, types + +slice 내부 세그먼트는 팀 컨벤션을 따른다. `features`는 `api / hooks / components / store / types`, `entities`는 FSD 관례대로 `api / model / ui`를 사용한다. + +### entities 승격 규칙 (실용 적용) + +빈 entity slice를 미리 만들지 않는다. 도메인 코드는 다음 기준으로 배치한다. + +- 도메인 모델(타입 · 표현 UI · mapper)이 **한 feature 안에서만** 쓰이면 그 feature 안에 둔다. +- 동일 도메인 모델을 **2개 이상 feature가 공유하게 되면** `entities/{domain}`으로 승격한다. +- 이 프로젝트에서 `Policy`는 `policy-search`, `policy-read`, `compare`, `bookmark`, `recommendation`, `admin`이 공유하므로 `entities/policy`에 둔다. `User`, `BoardPost` 등도 교차 사용되면 같은 기준으로 승격한다. +- feature가 다른 feature의 코드를 직접 import해야 할 상황이 오면, 그건 해당 코드를 `entities`(또는 `shared`)로 올리라는 신호다. feature → feature import로 회피하지 않는다. + +외부 slice에서 내부 파일을 깊게 import하지 않는다. 가능한 slice의 public API(`index.ts`)를 통해 import한다. + +금지 예시: + +```ts +import { FilterBar } from "@features/policy-filter/components/FilterBar"; +``` + +권장 예시: + +```ts +import { FilterBar } from "@features/policy-filter"; +``` + +## 4. 컴포넌트 작성 원칙 + +### 페이지는 조립만 담당 + +```tsx +function SearchPage() { + return ( + <> + + + + ); +} +``` + +### 비즈니스 로직은 feature 내부 + +기능 로직은 `features/*/hooks`의 custom hook에 둔다. + +```text +features/policy-search/hooks/usePolicySearch.ts +``` + +### 공통 UI는 shared/ui + +`Button`, `Modal`, `Input`, `EmptyState`, `Skeleton` 등 도메인 비의존 UI는 `shared/ui`에 둔다. feature 내부에서만 쓰는 UI는 해당 feature의 `components`에 둔다. + +### Props 이름은 역할이 드러나게 작성 + +```tsx +// 좋은 예 + + +// 나쁜 예 + +``` + +## 5. Container / Presenter 패턴 + +화면 기능은 Container와 Presenter를 분리한다. + +- Container는 상태, side effect, API 호출, event handler 조립을 담당한다. +- Presenter는 props를 받아 렌더링만 담당한다. +- Presenter 내부에서 API 호출, localStorage 접근, 복잡한 상태 전이를 직접 하지 않는다. +- 큰 컴포넌트 하나에 상태와 UI를 모두 넣지 않는다. 기존 큰 파일은 기능 변경 시 점진적으로 hook과 Presenter로 분리한다. + +권장 구조: + +```text +features/policy-search +├── hooks +│ └── usePolicySearch.ts +├── components +│ ├── PolicySearchContainer.tsx +│ └── PolicySearchPresenter.tsx +└── index.ts +``` + +Container 예시: + +```tsx +export function PolicySearchContainer() { + const searchState = usePolicySearch(); + + return ; +} +``` + +Presenter 예시: + +```tsx +interface PolicySearchPresenterProps { + isLoading: boolean; + onSubmit: () => void; +} + +export function PolicySearchPresenter({ isLoading, onSubmit }: PolicySearchPresenterProps) { + return ( +
+ +
+ ); +} +``` + +### 컴포넌트 조직 규칙 + +`components` 안을 뎁스 상관없이 flat하게 다 넣지 않는다. **폴더는 렌더 깊이가 아니라 "재사용 범위(누가 쓰는가)"로 나눈다.** 하나의 큰 flat을 만드는 대신, 경계로 쪼갠 작은 flat 여러 개로 구성한다. + +배치 규칙 (재사용 범위 순): + +- **한 부모에서만 쓰는 자식**은 그 부모와 같은 블록 폴더에 colocate한다. +- **같은 feature 내 여러 컴포넌트가 공유**하면 `components/common`에 둔다. +- **여러 feature가 공유**하면 feature 밖으로 승격한다. 도메인 표현 UI는 `entities/{domain}/ui`, 여러 feature를 조합한 큰 블록은 `widgets`, 도메인 무관 범용 UI는 `shared/ui`로 올린다. "다른 feature의 컴포넌트를 import하고 싶다"는 순간이 곧 승격 신호다. + +폴더/파일 판단 기준: + +- 자식 컴포넌트가 있는 **블록만 폴더**로 묶는다. 자식 없는 단일 컴포넌트는 `.tsx` 파일 하나로 둔다. +- 폴더 이름은 대표(조립) 컴포넌트 이름과 같게 하고, 폴더 안의 대표 컴포넌트가 자식들을 조립한다. +- 처음부터 폴더를 만들지 않는다. 자식이 분리되어 관련 컴포넌트가 생기는 순간 파일 → 폴더로 승격한다. +- 블록 폴더 안은 flat을 유지하고, 대개 **한 단계**만 내려간다. 자식이 또 여러 자식을 가지면 그 자식이 자기 블록 폴더가 되는 것이지, 폴더를 깊게 중첩하지 않는다. +- Container는 feature 최상위에만 둔다. 중첩 자식은 모두 props를 받는 Presenter로 작성한다. +- 각 블록 폴더는 `index.ts`로 대표 컴포넌트만 노출하고, 외부에서 폴더 내부 파일을 깊게 import하지 않는다. + +권장 구조: + +```text +features/policy-search/components +├── PolicySearchContainer.tsx # 진입점 Container (feature당 1~소수) +├── PolicySearchPresenter.tsx # 최상위 Presenter (하위 블록 조립) +├── PolicyFilter/ # 자식이 있는 블록 → 폴더 +│ ├── PolicyFilter.tsx # 대표(조립) 컴포넌트 +│ ├── PolicyFilterChip.tsx # PolicyFilter 전용 자식 +│ └── index.ts # export { PolicyFilter } +├── PolicyResultList/ +│ ├── PolicyResultList.tsx +│ ├── PolicyResultItem.tsx +│ └── index.ts +└── common/ # feature 내 2곳 이상 공유하는 단일 조각 + └── EmptyResult.tsx +``` + +## 6. Custom Hook 규칙 + +- hook 이름은 반드시 `use`로 시작한다. +- hook은 feature의 `hooks` 디렉터리에 둔다. +- hook은 화면 상태, derived state, event handler, API loading/error 상태를 캡슐화한다. +- hook 내부 effect는 cleanup이 필요한지 확인하고 명확히 작성한다. +- hook return 값은 Presenter가 바로 사용할 수 있는 형태로 구성한다. +- 비동기 요청 hook은 `isLoading`, `errorMessage`, `isFallback`처럼 UI가 상태를 명확히 표현할 수 있는 값을 제공한다. + +금지: + +```tsx +// Presenter에서 직접 fetch/localStorage/window side effect 수행 금지 +function PolicyCard() { + useEffect(() => { + axios.get("/api/policies"); + }, []); +} +``` + +권장: + +```tsx +function PolicyListContainer() { + const { policies, isLoading, errorMessage, reload } = usePolicies(); + return ; +} +``` + +## 7. 상태 관리 규칙 + +서버 상태는 TanStack Query, 클라이언트 상태는 Zustand로 관리한다. + +### TanStack Query (서버 상태) + +정책 검색, 정책 상세, 게시판, 댓글, 관리자 데이터, 추천 정책, 챗봇 응답, 읽음 처리 등 **서버에서 오는 모든 데이터**는 TanStack Query를 우선 사용한다. + +```tsx +const { data, isLoading, isError } = useQuery(...); +const mutation = useMutation(...); +``` + +### Zustand (클라이언트 상태) + +Zustand는 아래 용도에만 사용한다. + +- 인증 상태: `user`, `isAuthenticated`, `role` +- 사용자 선호 정보: `categories`, `region`, `employmentStatus` +- 정책 비교함: `policyIds` +- UI 상태: `theme`, `sidebarOpen`, `modalOpen` + +다음은 **Zustand에 저장 금지**한다 (서버 데이터이므로 TanStack Query로 관리): +`policyList`, `policyDetail`, `boardList`, `commentList`, `searchResult`, 관리자 `users`/`logs`/`policies`. + +### 기타 + +- 컴포넌트 내부에서만 쓰는 UI 상태는 `useState`로 관리한다. +- feature use case 상태는 custom hook으로 분리한다. +- derived state는 원본 state에서 계산한다. 중복 state로 저장하지 않는다. +- localStorage 접근은 `shared/utils/storage` 또는 hook 내부로 캡슐화한다. + +## 8. API 연동 규칙 + +- API 호출 함수는 `shared/api` 또는 feature의 `api` 디렉터리에만 둔다. 컴포넌트에서 직접 axios를 호출하지 않는다. +- API 응답 DTO는 UI model과 분리한다. +- DTO → UI model 변환은 mapper 함수로 처리한다. 공유 도메인이면 `entities/{domain}/model`, feature 전용이면 feature `api` 근처에 둔다. +- API 실패, timeout, 서버 장애, 빈 응답 상태를 UI가 표현할 수 있게 hook에서 상태를 제공한다. +- mock fallback이 필요한 기능은 fallback 여부를 `isFallback` 등으로 노출한다. +- API key 같은 secret은 클라이언트 번들에 직접 노출하지 않는다. +- 브라우저 저장소에는 민감정보를 저장하지 않는다. + +나쁜 예: + +```tsx +useEffect(() => { + axios.get(...); +}, []); +``` + +좋은 예: + +```tsx +const policies = await getPolicies(); +``` + +## 9. HTTP 상태 코드 & 에러코드 처리 + +HTTP status는 **화면 흐름 분기**에 사용하고, 사용자에게 보여줄 **메시지**는 응답 body의 `code`를 기준으로 매핑한다. status만 보고 모든 오류를 같은 메시지로 처리하지 않는다. + +### 9.1 HTTP status 기준 흐름 처리 + +| 상태 코드 | 의미 | 프론트 처리 | +| --- | --- | --- | +| 401 | 인증 만료 / 비로그인 | 로그인 페이지 이동 또는 로그인 모달 표시 | +| 403 | 권한 없음 | 접근 불가 화면 표시 | +| 404 | 데이터 없음 | Empty UI 또는 "존재하지 않는 데이터" 안내 | +| 409 | 데이터 충돌 | 최신 데이터 재조회 | +| 429 | 요청 과다 | 잠시 후 다시 시도 안내 | +| 500 | 서버 오류 | 에러 페이지 또는 재시도 버튼 표시 | + +### 9.2 에러코드(body `code`) 기준 메시지 처리 + +백엔드는 검증/비즈니스 오류를 `C001` 같은 `ErrorCode`로 내려준다. 프론트는 이 코드를 사용자 메시지로 매핑한다. + +표준 에러 응답: + +```ts +interface ApiErrorResponse { + code: string; + message: string; + details?: Array<{ + field?: string; + value?: unknown; + reason?: string; + }>; +} +``` + +처리 규칙: + +- API client는 실패 응답에서 `code`, `message`, `details`를 파싱해 caller가 사용할 수 있게 보존한다. +- UI 문구는 서버 raw message를 그대로 뿌리지 말고 `code` → 사용자 친화 메시지 매핑을 우선 사용한다. +- 검증 오류 `C001`은 가능하면 `details[].reason`을 함께 보여주되, 여러 field 오류는 대표 메시지 + 세부 항목 형태로 정리한다. +- 알 수 없는 `code`는 공통 fallback 메시지로 처리한다. +- HTTP status, exception text, stack trace, SQL, 내부 endpoint, secret 값은 사용자에게 직접 노출하지 않는다. +- 새 백엔드 `ErrorCode`가 추가되면 프론트의 code-message map 갱신 여부를 확인한다. + +권장 예시: + +```ts +const ERROR_MESSAGE_BY_CODE: Record = { + C001: "입력값을 다시 확인해 주세요.", + A001: "로그인이 필요한 기능입니다.", + P001: "정책 정보를 찾을 수 없습니다.", +}; + +function getUserMessage(error: ApiErrorResponse): string { + return ERROR_MESSAGE_BY_CODE[error.code] + ?? "요청 처리 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."; +} +``` + +금지 예시: + +```ts +// 서버 내부 message/exception을 그대로 노출하지 않는다. +showToast(error.message); + +// status만 보고 모든 400을 같은 메시지로 처리하지 않는다. +if (response.status === 400) showToast("잘못된 요청입니다."); +``` + +## 10. 화면 상태 규칙 + +모든 목록 및 상세 화면은 아래 상태를 반드시 고려한다. + +| 상태 | 설명 | UI 예시 | +| --- | --- | --- | +| Loading | 데이터 조회 중 | Skeleton, Spinner | +| Empty | 데이터 없음 | "조회된 정책이 없습니다." | +| Error | 복구 불가 오류 | 에러 메시지 + 재시도 버튼 | +| Degraded | 일부 기능 장애 | 추천 비활성화, 기본 검색 결과 제공 | +| Stale | 최신 상태 재확인 필요 | 새로고침 배너, 데이터 갱신 안내 | + +체크 포인트: + +- 목록 화면: Loading / Empty / Error 필수 +- 상세 화면: Loading / Error 필수 +- 챗봇: Degraded 고려 (챗봇 응답 실패 → 정책 검색 기능만 제공) +- 추천 정책: Degraded 고려 (추천 엔진 장애 → 최신 정책 목록 제공) +- 관리자 페이지: Stale 고려 (정책 데이터 갱신됨 → 새로고침 안내) + +## 11. 코드 포맷팅 규칙 + +포맷은 사람이 손으로 맞추지 않고 Biome(`biome.json`)가 강제한다. 아래 값이 이 프로젝트의 포맷 컨벤션이며, 임의로 다른 스타일(다른 들여쓰기 폭, 다른 quote 등)을 섞지 않는다. + +| 항목 | 값 | +| --- | --- | +| 들여쓰기 | space, 2칸 | +| 한 줄 길이 | 100자 | +| Quote (JS/TS) | single(`'`) | +| Quote (JSX) | double(`"`) | +| 세미콜론 | 항상 사용 | +| Trailing comma | 항상 사용(`all`) | +| 중괄호 안 공백 | `{ a }`처럼 공백 포함 | +| import 정렬 | 저장 시 자동 정리(`organizeImports`) | + +- `.claude/hooks/format-on-save.sh`가 Edit/Write 직후 변경 파일을 `biome check --write`로 자동 포맷·import 정리한다. 사람이 직접 들여쓰기/quote/세미콜론을 맞출 필요가 없다. +- `pnpm run lint`(= `biome check . && tsc --noEmit`)가 CI(`ci.yml`)에서 포맷·lint·타입을 강제한다. 로컬에서 포맷이 어긋나면 `pnpm run format` 또는 `pnpm run check`로 맞춘다. +- 포맷 규칙 자체를 바꾸고 싶으면 `biome.json`을 수정하고, 이 표도 함께 갱신한다. 개별 파일에서 Biome 규칙과 다른 스타일을 임의로 쓰지 않는다. +- `.css` 파일과 `.claude/workflows`는 Biome 대상에서 제외된다(`biome.json`의 `files.includes` 참고). + +## 12. React 코딩 컨벤션 + +- 함수 컴포넌트를 사용한다. +- component 이름은 PascalCase, hook 이름은 `use` + camelCase를 사용한다. +- props type은 component 근처에 `interface {ComponentName}Props`로 둔다. +- props는 명시적으로 선언하고, `React.FC`는 기본으로 사용하지 않는다. +- list key는 stable id를 사용한다. 동적 list에서 index key는 금지한다. +- event handler 이름은 `handleX`, props callback 이름은 `onX`를 사용한다. +- 조건부 렌더링은 early return 또는 명확한 분기로 작성한다. +- `useEffect`는 외부 시스템 동기화에만 사용한다. 단순 계산은 render 또는 `useMemo`로 처리한다. +- `useMemo`, `useCallback`은 성능 문제가 있거나 memoized child에 전달할 때만 사용한다. +- 접근성을 위해 semantic HTML, `button type="button"`, `aria-label`, focus 가능 상태를 고려한다. +- `target="_blank"` 링크에는 `rel="noopener noreferrer"`를 붙인다. + +## 13. TypeScript 코딩 컨벤션 + +- `any`는 금지한다. 알 수 없는 값은 `unknown`을 사용하고 type guard로 좁힌다. +- `@ts-ignore`는 금지한다. 정말 필요하면 `@ts-expect-error`와 이유 주석을 남긴다. +- API 응답 DTO와 UI model을 분리하고, 변환은 mapper 함수로 처리한다. +- type-only import는 `import type`을 사용한다. +- union state는 string union 또는 discriminated union을 사용한다. +- nullable 값은 명시적으로 처리한다. +- public API로 노출되는 함수/컴포넌트 props는 타입을 명확히 선언한다. +- 타입 단언 `as`는 런타임 검증이 있거나 라이브러리 경계에서 불가피할 때만 사용한다. + +권장: + +```ts +type AsyncState = + | { status: "idle" } + | { status: "loading" } + | { status: "success"; data: T } + | { status: "error"; message: string }; +``` + +금지: + +```ts +const data = response as Policy[]; +function parse(value: any) {} +``` + +## 14. 디자인 시스템 규칙 + +디자인은 반드시 현재 디자인 시스템을 따른다. 임의 색상, 임의 radius, 임의 그림자, 임의 폰트를 남발하지 않는다. 공통 UI는 shadcn/ui와 `shared/ui`를 우선 활용한다. + +현재 디자인 토큰은 `src/index.css`에 정의되어 있다. + +- Tailwind `@theme`: `--font-sans`, `--font-display`, `--color-primary`, `--color-primary-foreground`, `--color-primary-light`, `--color-brand-secondary`, `--color-secondary`, `--color-accent`, `--color-success`, `--color-border` +- CSS variables: `--background`, `--surface`, `--foreground`, `--card`, `--muted`, `--destructive`, `--warning`, `--ring`, `--radius` + +우선 사용 기준: + +- 브랜드 CTA: `bg-primary` + `text-primary-foreground`(대비 확보), `from-primary to-brand-secondary`. `text-primary`는 밝은 배경 위 텍스트 색으로만 쓴다. +- 카드/패널: `rounded-2xl` 또는 `rounded-3xl`, `bg-white`, `border border-slate-100`, `shadow-sm` +- 보조 배경: `bg-slate-50`, `bg-primary/5`, `bg-primary-light` +- 상태 색상: success/warning/destructive 계열 토큰을 일관되게 사용 +- 폰트: `font-sans`, 필요한 display 영역만 `font-display` + +금지: + +- 기존 토큰과 무관한 hex 색상 직접 추가 +- 같은 의미의 버튼/카드 스타일을 매번 새로 만드는 것 +- contrast 확인 없이 흐린 텍스트/CTA 색상 추가 +- 디자인 시스템과 다른 radius/shadow/spacing을 임의 도입 + +## 15. 네이밍 규칙 + +| 종류 | 형식 | 예시 | +| --- | --- | --- | +| Component | PascalCase.tsx, `[도메인][역할]` | `PolicyCard.tsx`, `PolicyList.tsx`, `CommentForm.tsx` | +| Hook | camelCase.ts, `use` + 기능명 | `usePolicySearch.ts`, `useBookmark.ts` | +| Store | camelCase.ts, `[기능]Store` | `authStore.ts`, `compareStore.ts` | +| API | camelCase.ts, `[기능]Api` | `policyApi.ts`, `boardApi.ts` | +| Mapper | camelCase.ts, `[기능]Mapper` | `policyMapper.ts` | +| Utils | camelCase.ts | `formatDate.ts` | +| Types | camelCase.ts, `[기능].types` | `policy.types.ts` | +| Constants | camelCase.ts (값은 UPPER_SNAKE_CASE) | `MAX_COMPARE_COUNT`, `DEFAULT_PAGE_SIZE` | + +- Type / Interface 이름은 PascalCase 명사: `type Policy`, `interface BoardPost` +- Zustand store type은 PascalCase `[기능]Store`: `type AuthStore`, `type CompareStore` +- Enum은 PascalCase: `enum UserRole`, `enum PolicyCategory` +- barrel export는 각 slice의 `index.ts`에서 public API만 노출한다. + +금지: `card.tsx`, `list.tsx`, `data.ts`, `temp.ts`, `util.ts`, `test2.ts` + +## 16. 반응형 기준 + +필수 확인 해상도: `390px`, `768px`, `1440px`. 최소 390px 모바일 환경에서 깨지지 않아야 한다. + +## 17. 테스트 / 검증 규칙 + +기본 검증 명령: + +```bash +corepack prepare pnpm@11.9.0 --activate +corepack pnpm install --frozen-lockfile +corepack pnpm run lint +corepack pnpm run build +``` + +작업 유형별 추가 확인: + +- UI 변경: 주요 화면 수동 확인 또는 screenshot 첨부 +- API 연동: 성공 / 빈 상태 / 오류 상태 확인 +- 상태 로직 변경: hook 또는 순수 함수 단위 테스트 추가 우선 고려 +- Docker/Compose 관련 변경: `docker compose config`, build, runtime smoke를 로컬 가능 범위로 검증. Docker가 없으면 PR 검증 결과에 명시한다. + +## 18. PR 전 체크리스트 + +- [ ] GitHub Issue를 만들고 issue 번호 브랜치에서 작업했다. +- [ ] 레이어 의존 방향(`app → pages → widgets → features → entities → shared`)을 지켰고, feature → feature import를 만들지 않았다. +- [ ] 2개 이상 feature가 공유하는 도메인 모델은 `entities`로 승격했다. +- [ ] Container/Presenter 역할을 분리했다. +- [ ] 기능 로직을 custom hook으로 분리했다. +- [ ] 서버 데이터는 TanStack Query, 클라이언트 상태만 Zustand로 관리했다. +- [ ] API 호출을 컴포넌트 내부에서 직접 수행하지 않았다. +- [ ] API DTO와 UI model을 분리하고 mapper로 변환했다. +- [ ] API 오류는 body `code` 기준으로 사용자 메시지를 매핑하고, raw exception/message를 그대로 노출하지 않았다. +- [ ] 새 백엔드 에러코드가 있다면 프론트 code-message map 갱신 필요 여부를 확인했다. +- [ ] Loading / Empty / Error 상태를 구현했다. (챗봇·추천은 Degraded, 관리자는 Stale 고려) +- [ ] 디자인 시스템 토큰과 기존 UI 패턴을 따랐다. +- [ ] 모바일 390px에서 확인했다. +- [ ] 비로그인 사용자의 흐름을 확인했다. +- [ ] `any`, 불필요한 `as`, `@ts-ignore`를 추가하지 않았다. +- [ ] 접근성 기본 요소(semantic HTML, button type, aria-label, focus, 안전한 외부 링크)를 확인했다. +- [ ] `pnpm run lint`를 실행했다. +- [ ] `pnpm run build`를 실행했다. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..af6d8d4 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(corepack pnpm run lint)", + "Bash(corepack pnpm run build)", + "Bash(corepack pnpm install*)", + "Bash(corepack prepare *)", + "Bash(git status)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git switch*)", + "Bash(git add*)", + "Bash(gh issue *)", + "Bash(gh pr *)", + "Bash(gh repo *)" + ], + "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./**/*.pem)"] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.sh\"" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-on-save.sh\"" + } + ] + } + ] + } +} diff --git a/.claude/skills/security-review/SKILL.md b/.claude/skills/security-review/SKILL.md new file mode 100644 index 0000000..d34a772 --- /dev/null +++ b/.claude/skills/security-review/SKILL.md @@ -0,0 +1,33 @@ +--- +name: security-review +description: 프론트엔드(React/TS) 변경사항의 보안 점검. XSS, 번들 내 시크릿 노출, dangerouslySetInnerHTML, 브라우저 저장소 민감정보, 외부 링크, 의존성 위험을 점검할 때 사용한다. +--- + +# Front-end Security Review + +이 스킬은 이 레포(React 19 + TypeScript + Vite)에서 프론트 변경사항의 보안 위험을 점검한다. 상세 항목은 [`checklist.md`](./checklist.md)를 사용한다. + +## 사용 시점 + +- PR 전 변경 파일에 대한 보안 점검을 요청받았을 때 +- 폼 입력, 외부 데이터 렌더, 인증/토큰 처리, 링크, 파일 업로드 등을 건드렸을 때 +- 새 의존성을 추가했을 때 + +## 절차 + +1. **범위 파악**: `git diff origin/dev...HEAD --name-only`로 변경 파일을 추린다. 없으면 스테이징/워킹 변경을 본다. +2. **정적 점검**: 아래 패턴을 변경 파일 중심으로 검색한다. + - `dangerouslySetInnerHTML`, `innerHTML`, `eval(`, `new Function(` + - `import.meta.env`, `process.env`, 하드코딩된 키/토큰(`sk-`, `AIza`, `Bearer `, `api_key` 등) + - `localStorage`, `sessionStorage`, `document.cookie` + - `target="_blank"` 후 `rel` 누락 + - `http://` 평문 엔드포인트, CORS/credentials 설정 +3. **체크리스트 대조**: [`checklist.md`](./checklist.md)의 각 항목을 변경사항에 대입한다. +4. **보고**: 발견마다 `파일:라인 · 위험 · 근거 · 권장 수정`을 심각도(Critical/High/Medium/Low) 순으로 정리한다. 확실치 않은 것은 추정임을 명시한다. + +## 원칙 + +- 이 레포는 클라이언트 정적 SPA다. 브라우저 번들에 담기는 값은 모두 공개된다고 가정한다. +- 서버 raw 에러/스택/내부 endpoint를 사용자에게 노출하는 코드는 보안+UX 이슈로 함께 지적한다. +- 수정 자체보다 "왜 위험한지"와 "구체적 대안"을 명확히 전달한다. 코드를 임의로 대규모 리팩터링하지 않는다. +- 오탐을 줄인다: 실제 익스플로잇 경로가 그려지지 않으면 확신 수준을 낮춰 보고한다. diff --git a/.claude/skills/security-review/checklist.md b/.claude/skills/security-review/checklist.md new file mode 100644 index 0000000..ea4da98 --- /dev/null +++ b/.claude/skills/security-review/checklist.md @@ -0,0 +1,60 @@ +# Front-end 보안 점검 체크리스트 + +React/TS SPA에 특화된 항목이다. 각 항목을 변경사항에 대입한다. + +## 1. XSS / 렌더 안전성 + +- [ ] `dangerouslySetInnerHTML`을 외부/사용자 입력에 사용하지 않는가? 불가피하면 sanitize(DOMPurify 등)를 거치는가? +- [ ] `innerHTML`, `document.write`, `eval`, `new Function`을 사용하지 않는가? +- [ ] URL을 렌더/이동에 쓸 때 `javascript:` 스킴을 차단하는가? (`href`, `src`, 리다이렉트) +- [ ] 마크다운/리치텍스트 렌더러에 raw HTML 허용 옵션이 켜져 있지 않은가? + +## 2. 시크릿 / 환경변수 + +- [ ] API key, 토큰, 비밀번호가 소스에 하드코딩돼 있지 않은가? +- [ ] `import.meta.env` / `process.env`로 번들에 들어가는 값 중 브라우저에 노출되면 안 되는 것이 없는가? +- [ ] `.env`가 커밋되지 않고 `.gitignore`에 있는가? (`.env.example`만 커밋) +- [ ] 서버 전용 시크릿(Gemini API key 등)을 클라이언트에서 직접 호출하지 않는가? (프록시 필요 여부 확인) + +## 3. 브라우저 저장소 / 인증 + +- [ ] localStorage/sessionStorage/cookie에 토큰·개인정보 등 민감정보를 저장하지 않는가? +- [ ] 인증 토큰을 저장한다면 XSS 노출·만료·초기화 정책이 있는가? +- [ ] 쿠키 사용 시 `Secure`, `HttpOnly`(서버 설정), `SameSite`를 고려했는가? + +## 4. 네트워크 / API + +- [ ] 엔드포인트가 `https`인가? 평문 `http://`가 없는가? +- [ ] 에러 응답의 raw `message`/stack/내부 endpoint를 사용자에게 그대로 노출하지 않는가? (`code` 기반 매핑 사용) +- [ ] 사용자 입력이 쿼리스트링/경로에 들어갈 때 인코딩·검증하는가? +- [ ] `fetch`에 불필요하게 `credentials: "include"`를 넣지 않았는가? + +## 5. 외부 링크 / 리소스 + +- [ ] `target="_blank"` 링크에 `rel="noopener noreferrer"`가 있는가? +- [ ] 외부에서 받은 URL로 이동/렌더할 때 신뢰 도메인을 검증하는가? +- [ ] 서드파티 스크립트/iframe을 무분별하게 삽입하지 않는가? + +## 6. 입력 / 폼 + +- [ ] 사용자 입력을 신뢰하지 않고, 클라이언트 검증은 UX용일 뿐 서버 검증을 대체하지 않음을 전제하는가? +- [ ] 파일 업로드 시 타입/크기 제한을 확인하는가? +- [ ] 정규식에 ReDoS 위험(중첩 수량자)이 없는가? + +## 7. 의존성 / 빌드 + +- [ ] 새로 추가한 패키지의 신뢰성·필요성을 확인했는가? +- [ ] `pnpm audit` 수준의 알려진 취약점을 확인했는가? +- [ ] source map이 프로덕션에 시크릿을 노출하지 않는가? + +## 8. 정보 노출 + +- [ ] `console.log`에 토큰·개인정보·응답 전체를 남기지 않는가? +- [ ] 에러 바운더리/화면에 스택트레이스를 그대로 보여주지 않는가? + +## 심각도 기준 + +- **Critical**: 시크릿 노출, 저장형 XSS, 인증 우회. +- **High**: 반사형 XSS, 민감정보 저장, 평문 전송. +- **Medium**: `rel` 누락, 과도한 로깅, 약한 입력 검증. +- **Low**: 방어적 개선 권고. diff --git a/.claude/workflows/pr-review.js b/.claude/workflows/pr-review.js new file mode 100644 index 0000000..a5c225b --- /dev/null +++ b/.claude/workflows/pr-review.js @@ -0,0 +1,135 @@ +// 이 파일은 Claude Code의 Workflow 런타임 스크립트다. 본문은 async 함수 컨텍스트로 +// 감싸져 실행되므로 top-level await와 마지막 줄의 `return`(워크플로 결과 반환)이 유효하다. +// 일반 ESM 모듈로 파싱하는 정적 분석기는 top-level return을 오탐할 수 있다(Biome 대상에서도 제외됨). +export const meta = { + name: 'pr-review', + description: + '현재 브랜치 변경사항을 관점별로 병렬 리뷰하고, 각 발견을 적대적으로 검증한 뒤 종합한다.', + whenToUse: + 'PR 전 또는 변경 검토 시. dev 대비 diff를 FSD/Container-Presenter/TS/디자인/접근성/보안 관점으로 나눠 리뷰한다.', + phases: [ + { title: 'Review', detail: '관점별 병렬 리뷰' }, + { title: 'Verify', detail: '발견마다 적대적 검증' }, + { title: 'Synthesize', detail: '심각도순 종합 보고' }, + ], +}; + +// 리뷰 관점 (.claude/rules.md 기반) +const DIMENSIONS = [ + { + key: 'fsd', + prompt: + 'FSD 구조 관점에서만 리뷰하라. 의존 방향(app→pages→widgets→features→entities→shared) 위반, public API(index.ts) 우회한 깊은 import, 레이어 오배치를 찾아라.', + }, + { + key: 'container-presenter', + prompt: + 'Container/Presenter 관점에서만 리뷰하라. Presenter가 API 호출/localStorage/복잡한 상태전이를 직접 하는지, 기능 로직이 hooks의 use* custom hook으로 분리됐는지 확인하라.', + }, + { + key: 'typescript', + prompt: + 'TypeScript 관점에서만 리뷰하라. any / 불필요한 as / @ts-ignore, API DTO와 UI model 분리 및 lib 매퍼 여부, nullable·union 처리 안전성을 확인하라.', + }, + { + key: 'react', + prompt: + 'React 관점에서만 리뷰하라. 함수형 컴포넌트/PascalCase/Props 인터페이스, list의 index key 사용, useEffect 오남용과 cleanup 누락, handleX/onX 컨벤션을 확인하라.', + }, + { + key: 'design-system', + prompt: + '디자인 시스템 관점에서만 리뷰하라. src/index.css 토큰 대신 임의 hex/radius/shadow/spacing을 도입했는지, 같은 의미의 버튼/카드 스타일을 중복 생성했는지 확인하라.', + }, + { + key: 'a11y-error', + prompt: + '접근성/상태처리/에러 관점에서만 리뷰하라. loading/error/empty/fallback 처리, semantic HTML/button type/aria/포커스, target=_blank의 rel, 에러의 code 기반 매핑(raw message 노출 금지)을 확인하라.', + }, + { + key: 'security', + prompt: + '보안 관점에서만 리뷰하라. 번들 내 시크릿 노출, dangerouslySetInnerHTML/innerHTML/eval, localStorage 민감정보, 평문 http, 정보 과다 로깅을 확인하라.', + }, +]; + +const FINDINGS_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['findings'], + properties: { + findings: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['file', 'line', 'severity', 'summary', 'suggestion'], + properties: { + file: { type: 'string' }, + line: { type: 'integer' }, + severity: { type: 'string', enum: ['Blocking', 'High', 'Medium', 'Nit'] }, + summary: { type: 'string' }, + suggestion: { type: 'string' }, + }, + }, + }, + }, +}; + +const VERDICT_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['isReal', 'reason'], + properties: { + isReal: { type: 'boolean' }, + reason: { type: 'string' }, + }, +}; + +const DIFF_CMD = 'git diff origin/dev...HEAD 2>/dev/null || git diff'; + +log('dev 대비 변경사항을 관점별로 리뷰합니다.'); + +const reviewed = await pipeline( + DIMENSIONS, + (d) => + agent( + `이 레포(React19+TS+Vite+Tailwind, FSD)의 변경사항을 리뷰한다. 기준은 AGENTS.md와 정본 규칙 .claude/rules.md다.\n` + + `먼저 \`${DIFF_CMD}\`로 변경 파일과 diff를 확보하고, 필요하면 주변 파일도 읽어라.\n` + + `아래 관점만 담당한다. 다른 관점은 무시한다.\n관점: ${d.prompt}\n` + + `확신이 낮은 항목은 넣지 말고, 실제 문제만 findings로 반환하라. 없으면 빈 배열.`, + { label: `review:${d.key}`, phase: 'Review', schema: FINDINGS_SCHEMA }, + ), + (review, d) => + parallel( + (review?.findings ?? []).map( + (f) => () => + agent( + `다음 리뷰 발견이 실제 문제인지 적대적으로 검증하라. 기본값은 "반증"이며, 근거가 확실할 때만 isReal=true.\n` + + `파일: ${f.file}:${f.line}\n요약: ${f.summary}\n제안: ${f.suggestion}\n` + + `해당 파일을 직접 읽고 .claude/rules.md 기준으로 판단하라.`, + { label: `verify:${d.key}:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA }, + ).then((v) => ({ ...f, dimension: d.key, verdict: v })), + ), + ), +); + +const confirmed = reviewed + .flat() + .filter(Boolean) + .filter((f) => f.verdict?.isReal); + +const order = { Blocking: 0, High: 1, Medium: 2, Nit: 3 }; +confirmed.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9)); + +phase('Synthesize'); +log(`검증 통과 ${confirmed.length}건. 종합합니다.`); + +const report = await agent( + `아래는 관점별 리뷰 후 적대적 검증을 통과한 발견 목록(JSON)이다. 이 레포 규칙(AGENTS.md · .claude/rules.md) 기준으로 한국어 리뷰 리포트를 작성하라.\n` + + `- 심각도(Blocking/High/Medium/Nit)순으로 정리\n- 각 항목: 파일:라인 · 문제 · 왜 · 권장 수정\n- 중복은 병합\n- 마지막에 잘된 점 1~2줄과 머지 가능 여부 의견\n\n` + + JSON.stringify(confirmed, null, 2), + { phase: 'Synthesize' }, +); + +return report diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..61cd2c2 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,71 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit 설정 파일 +# 참고: https://docs.coderabbit.ai/getting-started/configure-coderabbit + +language: "ko-KR" + +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false + base_branches: + - "dev" + labeling_instructions: + - label: "feat" + instructions: "새로운 기능 추가 PR에 적용" + - label: "fix" + instructions: "버그 수정 PR에 적용" + - label: "docs" + instructions: "문서 변경 PR에 적용" + - label: "refac" + instructions: "리팩토링 PR에 적용" + path_instructions: + - path: "src/**" + instructions: | + 이 프로젝트는 .claude/rules.md의 규칙을 따른다. 리뷰 시 아래 사항을 중점적으로 확인한다. + + FSD(Feature-Sliced Design) 구조: + - 레이어 의존 방향은 app → pages → widgets → features → entities → shared 만 허용된다. + 하위 레이어가 상위 레이어를 import하면 지적한다. (예: entities가 features를 import 금지) + - 외부 slice의 내부 파일을 깊게 import하지 않고 slice의 index.ts(public API)를 통해 import해야 한다. + + Container/Presenter 패턴: + - Container는 상태, side effect, API 호출을 담당하고, Presenter는 props 기반 렌더링만 한다. + - Presenter 내부에서 fetch, localStorage 접근, 복잡한 상태 전이가 있으면 지적한다. + - 기능 로직은 hooks 디렉터리의 custom hook(use 접두사)으로 분리되어야 한다. + + TypeScript: + - any 사용 금지. unknown + type guard를 권장한다. + - @ts-ignore 금지. 불가피하면 @ts-expect-error와 이유 주석이 있어야 한다. + - 런타임 검증 없는 타입 단언(as)은 지적한다. + - API 응답 DTO와 UI model이 분리되어 있는지, DTO → model 변환이 매퍼 함수(예: entities/{domain}/model의 *Mapper.ts)로 처리되는지 확인한다. + - type-only import는 import type을 사용해야 한다. + + React: + - 동적 list에서 index를 key로 사용하면 지적한다. + - useEffect는 외부 시스템 동기화에만 사용하고, cleanup 누락을 확인한다. + - event handler는 handleX, props callback은 onX 네이밍을 따른다. + - 접근성: semantic HTML, button type="button", aria-label, target="_blank" 링크의 rel="noopener noreferrer"를 확인한다. + + API/상태 처리: + - API 오류는 HTTP status가 아닌 응답 body의 code 기준으로 사용자 메시지를 매핑해야 한다. + - 서버 raw message, exception, stack trace를 사용자에게 그대로 노출하면 지적한다. + - loading / error / empty / fallback 상태 처리가 누락되면 지적한다. + - API key 등 secret이 클라이언트 번들에 노출되는지 확인한다. + + 디자인 시스템: + - src/index.css의 디자인 토큰(--color-primary 등)과 무관한 임의 hex 색상 추가를 지적한다. + - 기존 UI 패턴과 다른 radius, shadow, spacing을 임의로 도입하면 지적한다. + - path: "**/*.css" + instructions: | + 디자인 토큰은 src/index.css에서 관리한다. 새로운 색상/폰트/radius 변수를 추가할 때 + 기존 토큰과 중복되거나 일관성이 깨지지 않는지 확인한다. + +chat: + auto_reply: true diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a52cbbd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.github +.vscode +*.md +.playwright-mcp +.env +.env copy.example diff --git a/.env.example b/.env.example deleted file mode 100755 index 7a550fe..0000000 --- a/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# GEMINI_API_KEY: Required for Gemini AI API calls. -# AI Studio automatically injects this at runtime from user secrets. -# Users configure this via the Secrets panel in the AI Studio UI. -GEMINI_API_KEY="MY_GEMINI_API_KEY" - -# APP_URL: The URL where this applet is hosted. -# AI Studio automatically injects this at runtime with the Cloud Run service URL. -# Used for self-referential links, OAuth callbacks, and API endpoints. -APP_URL="MY_APP_URL" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..ac0a961 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,23 @@ +--- +name: 작업 이슈 +about: 구현, 수정, 리팩토링, 테스트 작업을 간단히 정리합니다. +title: "" +labels: [] +assignees: [] +--- + +## 배경 + + + +## 작업 범위 + + + +- + +## 검증 + +- [ ] 작은 범위 테스트 실행 +- [ ] `corepack pnpm run lint` 실행 +- [ ] `corepack pnpm run build` 실행 \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..7b0eaeb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## 변경 내용 + + + +- + +## 검증 + + + +- + +## 연결 이슈 + + + +Closes # \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a1c0195 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +# 동일 브랜치/PR에서 새 커밋이 푸시되면 진행 중인 실행을 취소한다. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# 검증 전용 잡이므로 읽기 권한만 부여한다. +permissions: + contents: read + +jobs: + verify: + name: Lint & Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # 검증만 하는 잡이라 push 권한이 필요 없다. + # 자격증명을 워크스페이스에 남기지 않아 서드파티 postinstall의 토큰 탈취를 막는다. + persist-credentials: false + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint & typecheck + run: pnpm run lint + + - name: Build + run: pnpm run build diff --git a/.gitignore b/.gitignore index 5a86d2a..589b75c 100755 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ coverage/ *.log .env* !.env.example + +# 개인/로컬 전용 (커밋 금지) +CLAUDE.local.md +.claude/settings.local.json + +# IntelliJ IDEA +.idea/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..9bf2699 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json.schemastore.org/mcp.json", + "mcpServers": {} +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7e6882b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "files.eol": "\n" +} diff --git a/.worktreeinclude b/.worktreeinclude new file mode 100644 index 0000000..24b2fc2 --- /dev/null +++ b/.worktreeinclude @@ -0,0 +1,6 @@ +# git worktree 생성 시 새 워크트리로 복사할 gitignore 대상 파일 목록. +# (커밋되지 않지만 로컬 작업에 필요한 파일들) +.env +.env.example +.claude/settings.local.json +CLAUDE.local.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0e5e715 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# BOP Front-end — Agent Guide + +모든 코딩 에이전트(그리고 새로 합류한 개발자)의 공통 진입점이다. **상세 규칙의 정본은 [`.claude/rules.md`](./.claude/rules.md)** (18개 섹션)이며, 코드를 만들거나 수정하기 전에 반드시 먼저 읽고 그대로 따른다. Claude Code는 `CLAUDE.md`의 `@AGENTS.md` · `@.claude/rules.md` import로 이 문서와 상세 규칙을 함께 자동 로드한다. AGENTS.md를 직접 읽는 다른 도구는 이 링크를 따라가 `.claude/rules.md`도 함께 읽는다. + +## 프로젝트 개요 + +- 청년 정책 추천 서비스 **BOP** 프론트엔드 SPA +- Core: React 19 · TypeScript · Vite 6 · Tailwind CSS 4 · pnpm 11.9.0 +- Routing: React Router · Server state: TanStack Query · Client state: Zustand · API: Axios +- Code Quality: **Biome** (`biome.json`) — 포맷·import 정리·lint. 타입 검증은 `tsc --noEmit`. +- 구조: Feature-Sliced Design(FSD) — `app / pages / widgets / features / entities / shared` +- 빌드: Vite 정적 SPA. production smoke는 `pnpm build` 후 `pnpm start`(= `vite preview`)로 확인한다. 별도 Node 서버를 추가하지 않는다. +- Docker: `Dockerfile`은 `pnpm build` 산출물(`dist`)을 nginx(`nginx.conf`)로 정적 서빙한다(Node 런타임 아님). 루트의 `back-end/docker-compose.yml`이 `frontend` 서비스로 빌드/기동한다. `nginx.conf`가 `/api`를 `backend` 컨테이너로 리버스 프록시해 프론트/백엔드가 브라우저 기준 같은 origin이 되므로 배포 도메인이 바뀌어도 CORS 걱정 없이 그대로 쓸 수 있다. 이를 위해 Docker 빌드는 `VITE_API_BASE_URL=/api`(상대경로)로 고정하며, 로컬 `.env`의 절대경로 값은 무시된다. +- 백엔드 API 연동: 에러는 HTTP status가 아니라 응답 body의 `code` 기반으로 사용자 메시지를 매핑한다. +- 디자인 토큰은 `src/index.css`에 정의된 것만 사용한다. + +## 검증 명령 + +완료를 주장하기 전에 실행해 통과를 확인하고, PR에 실제 결과를 적는다. + +```bash +corepack prepare pnpm@11.9.0 --activate +corepack pnpm install --frozen-lockfile +corepack pnpm run format # biome format --write . (자동 정리, 선택) +corepack pnpm run lint # biome check . && tsc --noEmit +corepack pnpm run build # vite build +``` + +## Non-negotiable rules (요약) + +> 세부·근거는 [`.claude/rules.md`](./.claude/rules.md)를 본다. 아래는 최소 요약이다. + +- 기본 브랜치는 `dev`다(레포 default·통합 브랜치, PR base도 `dev`). `main`은 현재 미사용. `dev` 직접 커밋 금지, 이슈 없이 임의 브랜치 작업 금지. 브랜치는 `feat|fix|docs|refac/{issue-number}-{short-name}`. +- 들여쓰기 2칸 space · quote(JS single/JSX double) · 세미콜론 · trailing comma는 손으로 맞추지 않고 Biome(`biome.json`)가 강제한다. 저장 시 `format-on-save` 훅과 `pnpm run lint`(CI)가 자동 적용·검증한다. +- FSD 레이어 의존 방향을 지킨다: `app → pages → widgets → features → entities → shared`. 하위→상위 및 feature→feature import 금지. slice 외부에서는 `index.ts`(public API)로만 import한다. +- 새/큰 변경 기능은 Container/Presenter로 분리하고, 기능 로직은 `hooks`의 `use*` custom hook에 둔다. Presenter는 props로 렌더만 한다. +- 서버 상태는 TanStack Query, 클라이언트/UI/auth 상태는 Zustand로 관리한다. domain model은 2개 이상 feature가 공유할 때 `entities/{domain}`으로 승격한다. +- API 응답 DTO와 UI model을 분리하고 매퍼로 변환한다. 비즈니스 에러는 백엔드 `code` 기준으로 사용자 메시지를 매핑하고 raw exception/message를 노출하지 않는다. +- 디자인은 `src/index.css` 토큰과 기존 UI 패턴을 따른다. 임의 색/spacing/radius/shadow/폰트 도입 금지. +- `any`, 불필요한 `as`, `@ts-ignore` 금지. +- loading/error/empty/fallback 상태와 접근성(semantic HTML, `button type`, aria, 외부 링크 `rel`)을 처리한다. +- secret / API key / token을 코드·로그·응답·브라우저 저장소에 노출하지 않는다. `.env`는 커밋하지 않는다. +- 완료 전 검증(lint·build)을 실행하고 결과를 남긴다. + +## 에이전트 하네스 (.claude/) + +- **[`.claude/rules.md`](./.claude/rules.md)** — 정본 상세 규칙 (18개 섹션). 기술 스택 · 작업 흐름 · FSD 폴더 구조 · 컴포넌트/Container·Presenter · Custom Hook · 상태 관리 · API 연동 · HTTP 상태·에러코드 · 화면 상태 · 코드 포맷팅 · React/TypeScript 컨벤션 · 디자인 시스템 · 네이밍 · 반응형 · 테스트/검증 · PR 체크리스트. `CLAUDE.md`의 `@.claude/rules.md` import로 Claude Code 세션에 자동 로드된다. +- `.claude/commands/fix-issue.md` — `/fix-issue <이슈번호>`. 이슈 우선 워크플로우로 구현→검증→PR. +- `.claude/agents/code-reviewer.md` — 수정하지 않고 리뷰만 하는 전용 에이전트. +- `.claude/skills/security-review/` — 프론트 보안 점검 스킬. +- `.claude/workflows/pr-review.js` — 관점별 병렬 리뷰 → 적대적 검증 → 종합 워크플로우. +- `.claude/hooks/` — `block-secrets.sh`(파괴적 삭제·시크릿 노출 차단), `format-on-save.sh`(Biome로 저장 파일 포맷·정리). `settings.json`에 연결됨. +- `.claude/output-styles/teaching.md` — 설명을 덧붙이는 말투. +- `CLAUDE.local.md`, `.claude/settings.local.json` — 개인/로컬 전용(gitignore). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d4f5558 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md + +이 레포의 에이전트 지침은 도구 무관 정본인 [`AGENTS.md`](./AGENTS.md)와 Claude Code 전용 상세 규칙 [`.claude/rules.md`](./.claude/rules.md)로 구성된다. 아래 import로 둘 다 그대로 불러온다. 지침을 수정할 때는 해당 원본 파일을 편집한다. + +@AGENTS.md +@.claude/rules.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7920b71 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# ---- Build stage ---- +FROM node:22-alpine AS build +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@11.9.0 --activate + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . + +# 배포 도메인이 무엇이든(로컬/스테이징/프로덕션) 항상 "같은 origin"의 /api로 호출하게 상대경로로 고정한다. +# nginx.conf가 /api를 backend 컨테이너로 프록시하므로 별도 도메인 하드코딩이 필요 없다. +# (front-end/.env의 절대경로 값은 로컬 vite dev 전용 — 여기선 무시하고 빌드타임에 고정값으로 덮어쓴다.) +ENV VITE_API_BASE_URL=/api +RUN pnpm run build + +# ---- Runtime stage ---- +FROM nginx:1.27-alpine AS runtime +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/README.md b/README.md index de807c3..69a130e 100755 --- a/README.md +++ b/README.md @@ -1,20 +1,135 @@ -
-GHBanner -
+# BOP (YouthPick) Front-end -# Run and deploy your AI Studio app +청년을 위한 정책 추천 서비스 **BOP**의 프론트엔드 SPA다. 정책 검색/추천, 신청 관리(트래커), 커뮤니티, 마이페이지, 관리자 화면을 제공한다. -This contains everything you need to run your app locally. +[![CI](https://github.com/YouthPick/front-end/actions/workflows/ci.yml/badge.svg)](https://github.com/YouthPick/front-end/actions/workflows/ci.yml) +![React](https://img.shields.io/badge/React-19-61DAFB) +![TypeScript](https://img.shields.io/badge/TypeScript-5.8-3178C6) +![Vite](https://img.shields.io/badge/Vite-6-646CFF) -View your app in AI Studio: https://ai.studio/apps/48b34dc8-cc78-4bf4-9f91-ebaf7854c195 +## 기술 스택 -## Run Locally +| 영역 | 기술 | +| --- | --- | +| Core | React 19, TypeScript, Vite 6, pnpm 11.9.0 | +| Routing | React Router | +| Server State | TanStack Query | +| Client State | Zustand | +| API Client | Axios, STOMP(WebSocket, 실시간 챗봇/알림용) | +| Styling | Tailwind CSS 4 (`@tailwindcss/vite`) | +| 에디터 | Tiptap (커뮤니티 글쓰기) | +| Code Quality | Biome (format/lint), `tsc --noEmit` (타입 검증) | +| Test | Vitest, Testing Library | -**Prerequisites:** Node.js +## 구현 특징 +- **정책 비교는 상태를 저장하지 않는다.** 비교 대상 정책 ID를 쿼리스트링에 실어 단일 `GET` 요청으로 처리해, 요청 URL 자체가 그대로 공유 가능한 링크가 되도록 설계했다. +- **맞춤 정책 추천 사유를 사람이 읽을 수 있는 문장으로 변환한다.** 서버가 내려주는 매칭 축(나이·지역·취업상태·학력 등)을 프론트에서 자연어 추천 사유로 매핑하고, 점수 구간에 따라 신뢰도(HIGH/MEDIUM/LOW) 배지를 표시한다. +- **정책 챗봇은 STOMP(WebSocket)로 실시간 통신한다.** 로컬 개발 시에도 재연결이 끊기지 않도록 Vite dev 서버 프록시에 `ws: true`를 명시적으로 설정했다. +- **화면 상태를 Loading/Empty/Error 외에 Degraded/Stale까지 구분한다.** 예를 들어 추천 엔진이 장애를 일으켜도 최신 정책 목록으로 폴백해 서비스 흐름이 끊기지 않게 했다(추천 위젯: `useRecommendations`의 `isError`가 프로필 조회 실패까지 함께 반영하도록 처리). +- **에러 메시지는 HTTP status가 아니라 응답 body의 `code`로 매핑한다.** status는 401→로그인 이동, 404→Empty UI 같은 화면 흐름 분기에만 쓰고, 사용자에게 보여줄 문구는 백엔드 `ErrorCode` 기준 매핑 테이블을 거친다. +- **실사용 중 발견한 버그를 구조 개선으로 해결한 사례.** 신청 관리(트래커)가 "최신 정책 100건 join" 방식이라 100건 밖의 오래된 신청 건이 조용히 목록에서 빠지는 문제를 발견했고, 트래커에 등록된 정책만 개별 상세 API로 조회하도록 바꿔 해결했다. 상세 모달과 조회 캐시를 공유해 중복 요청도 피한다. +- **FSD 레이어 의존 방향을 Biome/리뷰로 강제한다.** `app → pages → widgets → features → entities → shared` 역방향 import와 feature 간 직접 import를 금지하고, 2개 이상 feature가 공유하는 도메인 모델만 `entities`로 승격하는 기준을 문서화해 임의 승격을 막는다. -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` +## 프로젝트 구조 + +Feature-Sliced Design(FSD) 아키텍처를 따른다. 레이어 의존 방향은 `app → pages → widgets → features → entities → shared`이며, 하위 레이어가 상위 레이어를 import하지 않는다. + +```text +src +├── app # 앱 초기화, 전역 provider, 라우팅, 전역 store +├── pages # 라우트 단위 화면 조립 (home, search, recommend, tracker, community, my, admin* 등) +├── widgets # 여러 feature/entity를 조합한 독립 UI 블록 (header, footer, policy-card-grid 등) +├── features # 사용자 행위 단위 비즈니스 기능 (api / hooks / components) +├── entities # feature 간 공유 도메인 모델 (policy, user, region 등) +└── shared # 도메인 비의존 공통 UI/hook/util/api +``` + +상세 컨벤션(Container/Presenter, custom hook, 상태 관리, API 연동, 디자인 시스템 등)은 [`AGENTS.md`](./AGENTS.md)와 [`.claude/rules.md`](./.claude/rules.md)를 참고한다. + +## 시작하기 + +### 사전 요구사항 + +- Node.js 22 (`.nvmrc` 기준) +- pnpm 11.9.0 (Corepack으로 활성화) + +```bash +corepack prepare pnpm@11.9.0 --activate +``` + +### 1. 의존성 설치 + +```bash +corepack pnpm install --frozen-lockfile +``` + +### 2. 환경변수 설정 + +`.env`에 아래 값을 설정한다. (백엔드 API 주소) + +```bash +VITE_API_BASE_URL=http://localhost:8080 +``` + +개발 서버(`vite dev`)는 `/api` 요청을 `http://localhost:8080`으로 프록시하므로(WebSocket 포함), 백엔드가 기본 포트(8080)로 떠 있으면 이 값이 없어도 대부분 동작한다. + +### 3. 개발 서버 실행 + +```bash +corepack pnpm run dev +``` + +기본 포트는 `5173`이다. (Vite 기본값) + +### 4. 빌드 / 프리뷰 + +```bash +corepack pnpm run build # vite build → dist/ +corepack pnpm run start # vite preview (production build 로컬 확인) +``` + +## 스크립트 + +| 명령 | 설명 | +| --- | --- | +| `pnpm run dev` | 개발 서버 실행 | +| `pnpm run build` | 프로덕션 빌드 (`dist/`) | +| `pnpm run start` | 빌드 결과 프리뷰 (`vite preview`) | +| `pnpm run lint` | `biome check .` + `tsc --noEmit` | +| `pnpm run format` | Biome로 포맷 자동 적용 | +| `pnpm run check` | Biome check + 자동 수정 | +| `pnpm run typecheck` | `tsc --noEmit`만 실행 | +| `pnpm run test` | Vitest 실행 | +| `pnpm run clean` | `dist/` 삭제 | + +## 완료 전 검증 + +```bash +corepack pnpm run lint +corepack pnpm run build +``` + +UI 변경은 주요 화면을 수동으로 확인하거나 스크린샷을 남긴다. 상세 체크리스트는 [`AGENTS.md`의 PR 체크리스트](./AGENTS.md)를 참고한다. + +## 문서 + +| 문서 | 내용 | +| --- | --- | +| [`AGENTS.md`](./AGENTS.md) | 모든 에이전트/개발자 공통 진입점 | +| [`.claude/rules.md`](./.claude/rules.md) | 상세 규칙 정본(18개 섹션) — FSD, 상태 관리, API 연동, 디자인 시스템, PR 체크리스트 등 | + +## 작업 흐름 + +```text +GitHub Issue 생성 → 브랜치 생성 → 작업/커밋 → PR 생성 → 리뷰/검증 확인 → merge +``` + +- 기본 브랜치는 `dev`다. `main`은 현재 미사용. +- 브랜치: `feat|fix|docs|refac/{issue-number}-{short-name}` +- 커밋: `type: subject` (`feat`/`fix`/`docs`/`refac`/`test`/`chore`) +- `dev` 직접 커밋 금지, 이슈 없이 임의 브랜치 작업 금지. + +## 관련 저장소 + +- Back-end: [YouthPick/back-end](https://github.com/YouthPick/back-end) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..394872b --- /dev/null +++ b/biome.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!**/dist", "!**/node_modules", "!**/*.css", "!.claude/workflows"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all", + "bracketSpacing": true + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/index.html b/index.html index 21dfe69..becae66 100755 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ - My Google AI Studio App + + YouthPick
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..3b8ef23 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,62 @@ +# /api 로 오는 요청을 같은 컨테이너 네트워크의 backend 서비스로 프록시한다. +# 프론트/백엔드가 브라우저 기준 같은 origin(도메인)이 되어 CORS 자체가 발생하지 않는다. +# STOMP(WebSocket, /api/ws)도 같은 prefix라 이 location 하나로 함께 처리된다. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_http_version 1.1; + + # WebSocket(STOMP) upgrade 지원 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + # 유휴 커넥션이 끊기지 않도록 heartbeat 간격(WebSocketConfig, 10s)보다 여유 있게 잡는다. + proxy_read_timeout 3600s; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # springdoc-openapi(Swagger) 문서. backend가 직접 서빙하므로 /api 프리픽스 없이 그대로 넘긴다. + location /swagger-ui/ { + proxy_pass http://backend:8080/swagger-ui/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /swagger-ui.html { + proxy_pass http://backend:8080/swagger-ui.html; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 정확히 일치(=)로 매칭하면 swagger-ui가 실제 스펙 위치를 알아내려고 추가로 요청하는 + # /v3/api-docs/swagger-config 가 이 location을 안 타고 SPA fallback으로 떨어져 + # swagger-ui가 기본 데모 스펙(petstore)으로 폴백된다. prefix로 하위 경로까지 잡는다. + location /v3/api-docs { + proxy_pass http://backend:8080/v3/api-docs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package.json b/package.json index 55128b8..7c1ff6d 100755 --- a/package.json +++ b/package.json @@ -1,35 +1,51 @@ { - "name": "react-example", + "name": "bop-front-end", "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@11.9.0", "scripts": { - "dev": "tsx server.ts", - "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs", - "start": "node dist/server.cjs", - "clean": "rm -rf dist server.js", - "lint": "tsc --noEmit" + "preinstall": "node -e \"if (!process.env.npm_config_user_agent?.includes('pnpm')) { console.error('Use pnpm instead of npm or yarn.'); process.exit(1); }\"", + "dev": "vite", + "build": "vite build", + "start": "vite preview", + "test": "vitest", + "clean": "rm -rf dist", + "format": "biome format --write .", + "check": "biome check --write .", + "lint": "biome check . && tsc --noEmit", + "typecheck": "tsc --noEmit" }, "dependencies": { - "@google/genai": "^2.4.0", - "@tailwindcss/vite": "^4.1.14", - "@vitejs/plugin-react": "^5.0.4", + "@stomp/stompjs": "^7.3.0", + "@tanstack/react-query": "^5.101.2", + "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-image": "^3.28.0", + "@tiptap/react": "^3.28.0", + "@tiptap/starter-kit": "^3.28.0", + "axios": "^1.18.1", + "dompurify": "^3.4.12", "lucide-react": "^0.546.0", + "motion": "^12.23.24", "react": "^19.0.1", "react-dom": "^19.0.1", - "vite": "^6.2.3", - "express": "^4.21.2", - "dotenv": "^17.2.3", - "motion": "^12.23.24" + "react-router": "^8.1.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { + "@biomejs/biome": "^2.5.3", + "@tailwindcss/vite": "^4.1.14", + "@testing-library/react": "^16.3.0", "@types/node": "^22.14.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.21", - "esbuild": "^0.25.0", + "jsdom": "^27.0.0", "tailwindcss": "^4.1.14", - "tsx": "^4.21.0", "typescript": "~5.8.2", "vite": "^6.2.3", - "@types/express": "^4.17.21" + "vitest": "^4.1.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf71d62..1f6b649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,21 +8,30 @@ importers: .: dependencies: - '@google/genai': - specifier: ^2.4.0 - version: 2.10.0 - '@tailwindcss/vite': - specifier: ^4.1.14 - version: 4.3.1(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) - '@vitejs/plugin-react': - specifier: ^5.0.4 - version: 5.2.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) - dotenv: - specifier: ^17.2.3 - version: 17.4.2 - express: - specifier: ^4.21.2 - version: 4.22.2 + '@stomp/stompjs': + specifier: ^7.3.0 + version: 7.3.0 + '@tanstack/react-query': + specifier: ^5.101.2 + version: 5.101.2(react@19.2.7) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tiptap/extension-image': + specifier: ^3.28.0 + version: 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/react': + specifier: ^3.28.0 + version: 3.28.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tiptap/starter-kit': + specifier: ^3.28.0 + version: 3.28.0 + axios: + specifier: ^1.18.1 + version: 1.18.1 + dompurify: + specifier: ^3.4.12 + version: 3.4.12 lucide-react: specifier: ^0.546.0 version: 0.546.0(react@19.2.7) @@ -35,34 +44,70 @@ importers: react-dom: specifier: ^19.0.1 version: 19.2.7(react@19.2.7) - vite: - specifier: ^6.2.3 - version: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + react-router: + specifier: ^8.1.0 + version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + zod: + specifier: ^4.4.3 + version: 4.4.3 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: - '@types/express': - specifier: ^4.17.21 - version: 4.17.25 + '@biomejs/biome': + specifier: ^2.5.3 + version: 2.5.3 + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.3.1(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/node': specifier: ^22.14.0 version: 22.20.0 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^5.0.4 + version: 5.2.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) autoprefixer: specifier: ^10.4.21 version: 10.5.2(postcss@8.5.15) - esbuild: - specifier: ^0.25.0 - version: 0.25.12 + jsdom: + specifier: ^27.0.0 + version: 27.4.0 tailwindcss: specifier: ^4.1.14 version: 4.3.1 - tsx: - specifier: ^4.21.0 - version: 4.22.4 typescript: specifier: ~5.8.2 version: 5.8.3 + vite: + specifier: ^6.2.3 + version: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + vitest: + specifier: ^4.1.6 + version: 4.1.10(@types/node@22.20.0)(jsdom@27.4.0)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -134,6 +179,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -146,6 +195,99 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.5.3': + resolution: {integrity: sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.3': + resolution: {integrity: sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.3': + resolution: {integrity: sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.3': + resolution: {integrity: sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.3': + resolution: {integrity: sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.3': + resolution: {integrity: sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.3': + resolution: {integrity: sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.3': + resolution: {integrity: sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.3': + resolution: {integrity: sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -458,15 +600,24 @@ packages: cpu: [x64] os: [win32] - '@google/genai@2.10.0': - resolution: {integrity: sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==} - engines: {node: '>=20.0.0'} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 + '@noble/hashes': ^1.8.0 || ^2.0.0 peerDependenciesMeta: - '@modelcontextprotocol/sdk': + '@noble/hashes': optional: true + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -483,33 +634,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -651,6 +775,12 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stomp/stompjs@7.3.0': + resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==} + '@tailwindcss/node@4.3.1': resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} @@ -745,6 +875,202 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tiptap/core@3.28.0': + resolution: {integrity: sha512-gUuD5WAYfbDxNSSJya/emh2KSzXZXLUYKW4fEnc1AQ5FE2twzh4LJ9UlKFIawigrUCAksWI5Fy1hRbv+5m4ZdQ==} + peerDependencies: + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-blockquote@3.28.0': + resolution: {integrity: sha512-y8Xi6Z3AQLXedz0J8Z3kDsu7SKBmL50gKVXx3RK1Oo1cuCGhNChm3syChkAz3PLAbrPUM5rvJDSZdF2jUrDnng==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bold@3.28.0': + resolution: {integrity: sha512-JhZQmr0AU741bOwjVMfuwJdK4g0TQwwPbeca9aqKHv5zvZw4i4G9G6fESVyMFc3Yag1ffpnq5EtNldHKTnMhlw==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-bubble-menu@3.28.0': + resolution: {integrity: sha512-7AUNoHj2K4XLKCgW4uspeD/ENejPu2BeHvLTsiOoIO+XXDNSi2j0bbaIS8f2s/qnFirscwp/sbznp1qph/76qg==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bullet-list@3.28.0': + resolution: {integrity: sha512-dSVuNiH7PFMmWGkNER9vfUb5bXUHDARvHbJ3l+APEb+ZiusVN1KFdM3nd/RsW/Rt5Gd3XwlIEU/c2Uf7cxYDcw==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-code-block@3.28.0': + resolution: {integrity: sha512-bHITDzT0umTLh+SiEVQzIXkCMt/TzbPM1+HQy9ZxgQDHAJj/vdmfNAnP3RCOrz4lETXyhNQ2b6kxeu3lWckxyA==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-code@3.28.0': + resolution: {integrity: sha512-AUw2Acof3CQE6Q6Y22saK4lyg0nkeJ4XXniU65TdAi/9TE66TGiO4NQJJNNPgu8+VMEwl5j8VsIFK8sfTcNYtg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-document@3.28.0': + resolution: {integrity: sha512-5SAKtlB1Tr/eZFhISL86HqmUup6rItvPtuPyRjmZo/VgbMwXEwiyAh1sMgFp5VNaeSMM14vJSyQpjhSaOmaBqg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-dropcursor@3.28.0': + resolution: {integrity: sha512-JA+dXQjjfJBpD0nVsZeddGVXRsmanESM9+8CtM35hI9wJnYMXay/B+5GUhswO20zhT3zhB2ZOTGe9knu6A8nIQ==} + peerDependencies: + '@tiptap/extensions': 3.28.0 + + '@tiptap/extension-floating-menu@3.28.0': + resolution: {integrity: sha512-59SECvJq3pQfeJBuydEdhQqrpl0oDlk8N1Ovs1Si3/fJa6rEQAJB2zIvcpBHky9Z+5JodI2eueDnv8eimiyGbg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-gapcursor@3.28.0': + resolution: {integrity: sha512-Wo73kM4q8z4Va9i4+0n1cO0UEMVUVBHPqM6cAqEYXjB4T48LQkKjRsiQydCezm185rQAsmm1dyi72gtvVQfdMg==} + peerDependencies: + '@tiptap/extensions': 3.28.0 + + '@tiptap/extension-hard-break@3.28.0': + resolution: {integrity: sha512-JHIFjX1luJgQ4VFEkIeXZpbc54Qiw+69P8FmlazYTh7AIJ6iLCpMFgQFELnivEAZ+/vS0lMPQubg0rCeM3UrHw==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-heading@3.28.0': + resolution: {integrity: sha512-rKjGG/Ik2lNaXBNVmONEDm5DBfGDLM1NWgSf5RRfBISrH8Lm1xqFruOB9tIaB0YUoSV3SBOiwTF9svmzvKSoRA==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-horizontal-rule@3.28.0': + resolution: {integrity: sha512-neBCMWprkppQUoLWyeJZDHqBw+xKX2oNhLD9MbFlhKIr092zgfP4mpCvQnSkn1OHl156KzZMp070+NyLBjgQ7A==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-image@3.28.0': + resolution: {integrity: sha512-aKGCdEjyujUcQ6tEtq4EPbKxEC16QsKapv3DeZs1c/UHFiEUalbbPHBnxjGnupiLz75IR+07GYqBoZ81kSCxiA==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-italic@3.28.0': + resolution: {integrity: sha512-Yur/ELz6dNVKQC7m8wGjtoLFxamGjJmA0rUEEOacTH6J39aFmcSxYkEGNDkYHbZFyHFYqbej6eI3VE0h08O0mg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-link@3.28.0': + resolution: {integrity: sha512-hy/PqSeTyl317yseFcHGE+3XVfQKQYaL4uZuj27xlrcO7MZ15drt1h9sUZy1FTBF3mr8676pQu7aoEm/Ww4ZRA==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-list-item@3.28.0': + resolution: {integrity: sha512-GbXrnMac6knSetZY33NHwOrgXFPqH28uCklQqmOZQlsrdGqezDKk31qoEMr4GsKW9n/lViAeuc07oIzwLwCMng==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-list-keymap@3.28.0': + resolution: {integrity: sha512-u9Wks8c/eQAv0RkULNggOyTKDD69WVbcV9H5cbasPDUbq5XbEFVa9ajxhEGfMmQ5et3EX0QL8qBi50K0GqbIjA==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-list@3.28.0': + resolution: {integrity: sha512-zQ66i5DuhVOndmZ0d7H475Y5Yb+BMx+zfCjFkCzEc6WVef5MumtxBVTkGLQ0ibxUaD71s4QQZbjHWagpaTg0eQ==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-ordered-list@3.28.0': + resolution: {integrity: sha512-OZNYrKKL9D01ySOujlNbKlLS9/3wGgKNYeoHZUg0e+Ta8WPVvL3W1zH6TgiU5sF2ZSGUBoq3w7mdaO/iROlUkw==} + peerDependencies: + '@tiptap/extension-list': 3.28.0 + + '@tiptap/extension-paragraph@3.28.0': + resolution: {integrity: sha512-8UMlQIjzqf6r2hSJ/VeJ00RqaOrOB1J5rTk02Vcw2pYeHkOqp+B0ff0HFu4abT9x1q4oXrBAgMsxRtgh/kR14Q==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-strike@3.28.0': + resolution: {integrity: sha512-VVj2ZZU9QYqiHLcjqMqRYvuHSsokj/AgUl+6TzLrKjlWwyZ18D4H2vkyI0g70iVTKnUpZ3sEy8WA/rqjgBjqBg==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-text@3.28.0': + resolution: {integrity: sha512-Jqp1LgfY1mnp4TQHoy+vnHyfn6qnnAM6nHgGwbY5zA8b/Xf1Etll6pziTx9p1J1qcfAgSrnjOyAmA++H7VQqww==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extension-underline@3.28.0': + resolution: {integrity: sha512-rwxCS6vTh2DJkNIYQX7JSrrRSmm76e2y48aZeuZO5ShkvLWMuJ3/zqDHRxAQVDiJhuE2Cp8NrTmPYlUc9YrT0A==} + peerDependencies: + '@tiptap/core': 3.28.0 + + '@tiptap/extensions@3.28.0': + resolution: {integrity: sha512-DJT1khCK+O/pT1gQlAnoKAx6zwDkgv7GtnhSfkqm1/4KHC3x+SSJnBIgBl9oGHymA+DxWbW1EULU4V3d/kT2uQ==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + + '@tiptap/pm@3.28.0': + resolution: {integrity: sha512-ALcpwZMUdat9gjJKlpscpoqXStoLhU246LPEVBDvJdIsoUKvUu3MrzfXik2Y8mtSGfhjtm9O2TRkWxQiFVMwsQ==} + + '@tiptap/react@3.28.0': + resolution: {integrity: sha512-BxzSAqaDEldQ97K/v6+GizU1/Dxx9VWkRh7PLQql6bXlAMiz6T1G3dGt25vvv1hQ1FoMt5ExWY5NkrPmR8G8ew==} + peerDependencies: + '@tiptap/core': 3.28.0 + '@tiptap/pm': 3.28.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.28.0': + resolution: {integrity: sha512-pqvFpValV+ONtlu3l4s73ROm/tEjoGMmt6uHmSJaLbqTcMHkdWuR3flJ/5brr4IDHe1foxmNicMbRxlje7yBYw==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -757,47 +1083,31 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} - '@types/qs@6.15.1': - resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} @@ -805,16 +1115,60 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} autoprefixer@10.5.2: resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} @@ -823,73 +1177,57 @@ packages: peerDependencies: postcss: ^8.1.0 - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} baseline-browser-mapping@2.10.40: resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} hasBin: true - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} browserslist@4.28.4: resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -900,43 +1238,42 @@ packages: supports-color: optional: true - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.379: resolution: {integrity: sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -945,10 +1282,17 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -963,19 +1307,16 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - express@4.22.2: - resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} - engines: {node: '>= 0.10.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-equals@5.4.1: + resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} + engines: {node: '>=6.0.0'} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -986,21 +1327,18 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -1019,10 +1357,6 @@ packages: react-dom: optional: true - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1031,14 +1365,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gaxios@7.1.5: - resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} - engines: {node: '>=18'} - - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1051,14 +1377,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - google-auth-library@10.9.0: - resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} - engines: {node: '>=18'} - - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1070,28 +1388,32 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} @@ -1100,25 +1422,25 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1193,8 +1515,12 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1204,6 +1530,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1211,16 +1541,8 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -1230,11 +1552,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - motion-dom@12.42.0: resolution: {integrity: sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==} @@ -1255,9 +1572,6 @@ packages: react-dom: optional: true - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1266,41 +1580,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.50: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1316,53 +1611,98 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - protobufjs@7.6.4: - resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} - engines: {node: '>=12.0.0'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} - engines: {node: '>=0.6'} + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.1: + resolution: {integrity: sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: react: ^19.2.7 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react-router@8.1.0: + resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==} + engines: {node: '>=22.22.0'} + peerDependencies: + react: '>=19.2.7' + react-dom: '>=19.2.7' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1371,40 +1711,21 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} tailwindcss@4.3.1: resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} @@ -1413,13 +1734,35 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1429,10 +1772,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -1441,23 +1780,16 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 vite@6.4.3: resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} @@ -1499,12 +1831,77 @@ packages: yaml: optional: true - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1515,11 +1912,59 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + 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: + '@acemir/cssom@0.9.31': {} + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -1609,6 +2054,8 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 + '@babel/runtime@7.29.7': {} + '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -1632,6 +2079,65 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@biomejs/biome@2.5.3': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.3 + '@biomejs/cli-darwin-x64': 2.5.3 + '@biomejs/cli-linux-arm64': 2.5.3 + '@biomejs/cli-linux-arm64-musl': 2.5.3 + '@biomejs/cli-linux-x64': 2.5.3 + '@biomejs/cli-linux-x64-musl': 2.5.3 + '@biomejs/cli-win32-arm64': 2.5.3 + '@biomejs/cli-win32-x64': 2.5.3 + + '@biomejs/cli-darwin-arm64@2.5.3': + optional: true + + '@biomejs/cli-darwin-x64@2.5.3': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.3': + optional: true + + '@biomejs/cli-linux-arm64@2.5.3': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.3': + optional: true + + '@biomejs/cli-linux-x64@2.5.3': + optional: true + + '@biomejs/cli-win32-arm64@2.5.3': + optional: true + + '@biomejs/cli-win32-x64@2.5.3': + optional: true + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -1788,16 +2294,21 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@google/genai@2.10.0': + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.8.0': dependencies: - google-auth-library: 10.9.0 - p-retry: 4.6.2 - protobufjs: 7.6.4 - ws: 8.21.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + '@floating-ui/utils': 0.2.12 + optional: true + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + optional: true + + '@floating-ui/utils@0.2.12': + optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -1818,26 +2329,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.1': {} - '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.62.2': @@ -1915,6 +2406,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@standard-schema/spec@1.1.0': {} + + '@stomp/stompjs@7.3.0': {} + '@tailwindcss/node@4.3.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -1983,6 +2478,221 @@ snapshots: tailwindcss: 4.3.1 vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + '@tanstack/query-core@5.101.2': {} + + '@tanstack/react-query@5.101.2(react@19.2.7)': + dependencies: + '@tanstack/query-core': 5.101.2 + react: 19.2.7 + + '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/table-core@8.21.3': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@tiptap/core@3.28.0(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-blockquote@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-bold@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-bubble-menu@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + optional: true + + '@tiptap/extension-bullet-list@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-code-block@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-code@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-document@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-dropcursor@3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-floating-menu@3.28.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + optional: true + + '@tiptap/extension-gapcursor@3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-hard-break@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-heading@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-horizontal-rule@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-image@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-italic@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-link@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-list-keymap@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/extension-ordered-list@3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + + '@tiptap/extension-paragraph@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-strike@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-text@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extension-underline@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + + '@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@tiptap/pm@3.28.0': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + '@tiptap/react@3.28.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-floating-menu': 3.28.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.28.0': + dependencies: + '@tiptap/core': 3.28.0(@tiptap/pm@3.28.0) + '@tiptap/extension-blockquote': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-bold': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-bullet-list': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-code': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-code-block': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-document': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-dropcursor': 3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-gapcursor': 3.28.0(@tiptap/extensions@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-hard-break': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-heading': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-horizontal-rule': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-italic': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-link': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-list': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/extension-list-item': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-list-keymap': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-ordered-list': 3.28.0(@tiptap/extension-list@3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0)) + '@tiptap/extension-paragraph': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-strike': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-text': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extension-underline': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0)) + '@tiptap/extensions': 3.28.0(@tiptap/core@3.28.0(@tiptap/pm@3.28.0))(@tiptap/pm@3.28.0) + '@tiptap/pm': 3.28.0 + + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -2004,59 +2714,31 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@types/body-parser@1.19.6': + '@types/chai@5.2.3': dependencies: - '@types/connect': 3.4.38 - '@types/node': 22.20.0 + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.20.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 22.20.0 - '@types/qs': 6.15.1 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.15.1 - '@types/serve-static': 1.15.10 - - '@types/http-errors@2.0.5': {} - - '@types/mime@1.3.5': {} - '@types/node@22.20.0': dependencies: undici-types: 6.21.0 - '@types/qs@6.15.1': {} - - '@types/range-parser@1.2.7': {} - - '@types/retry@0.12.0': {} - - '@types/send@0.17.6': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.20.0 + '@types/react': 19.2.17 - '@types/send@1.2.1': + '@types/react@19.2.17': dependencies: - '@types/node': 22.20.0 + csstype: 3.2.3 - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 22.20.0 - '@types/send': 0.17.6 + '@types/trusted-types@2.0.7': + optional: true + + '@types/use-sync-external-store@0.0.6': {} '@vitejs/plugin-react@5.2.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))': dependencies: @@ -2070,14 +2752,66 @@ snapshots: transitivePeerDependencies: - supports-color - accepts@1.3.8: + '@vitest/expect@4.1.10': dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color agent-base@7.1.4: {} - array-flatten@1.1.1: {} + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} autoprefixer@10.5.2(postcss@8.5.15): dependencies: @@ -2088,28 +2822,21 @@ snapshots: postcss: 8.5.15 postcss-value-parser: 4.2.0 - base64-js@1.5.1: {} + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color baseline-browser-mapping@2.10.40: {} - bignumber.js@9.3.1: {} - - body-parser@1.20.5: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 browserslist@4.28.4: dependencies: @@ -2119,51 +2846,59 @@ snapshots: node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) - buffer-equal-constant-time@1.0.1: {} - - bytes@3.1.2: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - caniuse-lite@1.0.30001799: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 + chai@6.2.2: {} - content-type@1.0.5: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 convert-source-map@2.0.0: {} - cookie-signature@1.0.7: {} + cookie-es@3.1.1: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 - cookie@0.7.2: {} + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.2 - data-uri-to-buffer@4.0.1: {} + csstype@3.2.3: {} - debug@2.6.9: + data-urls@6.0.1: dependencies: - ms: 2.0.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 debug@4.4.3: dependencies: ms: 2.1.3 - depd@2.0.0: {} + decimal.js@10.6.0: {} + + delayed-stream@1.0.0: {} - destroy@1.2.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} - dotenv@17.4.2: {} + dom-accessibility-api@0.5.16: {} + + dompurify@3.4.12: + optionalDependencies: + '@types/trusted-types': 2.0.7 dunder-proto@1.0.1: dependencies: @@ -2171,29 +2906,32 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - - ee-first@1.1.1: {} - electron-to-chromium@1.5.379: {} - encodeurl@2.0.0: {} - enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@8.0.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -2251,77 +2989,31 @@ snapshots: '@esbuild/win32-arm64': 0.28.1 '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + optional: true escalade@3.2.0: {} - escape-html@1.0.3: {} - - etag@1.8.1: {} - - express@4.22.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.5 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.13 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} - extend@3.0.2: {} + fast-equals@5.4.1: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - - finalhandler@1.3.2: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + follow-redirects@1.16.0: {} - formdata-polyfill@4.0.10: + form-data@4.0.6: dependencies: - fetch-blob: 3.2.0 - - forwarded@0.2.0: {} + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 fraction.js@5.3.4: {} @@ -2334,29 +3026,11 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - fresh@0.5.2: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - gaxios@7.1.5: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.1.5 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - gensync@1.0.0-beta.2: {} get-intrinsic@1.3.0: @@ -2377,74 +3051,84 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 - google-auth-library@10.9.0: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.5 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - - google-logging-utils@1.1.3: {} - gopd@1.2.0: {} graceful-fs@4.2.11: {} has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.4: dependencies: function-bind: 1.1.2 - http-errors@2.0.1: + html-encoding-sniffer@6.0.0: dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' - https-proxy-agent@7.0.6: + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 debug: 4.4.3 transitivePeerDependencies: - supports-color - iconv-lite@0.4.24: + https-proxy-agent@5.0.1: dependencies: - safer-buffer: 2.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color - inherits@2.0.4: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color - ipaddr.js@1.9.1: {} + is-potential-custom-element-name@1.0.1: {} jiti@2.7.0: {} js-tokens@4.0.0: {} - jsesc@3.1.0: {} - - json-bigint@1.0.0: + jsdom@27.4.0: dependencies: - bignumber.js: 9.3.1 - - json5@2.2.3: {} + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 + jsesc@3.1.0: {} - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 + json5@2.2.3: {} lightningcss-android-arm64@1.32.0: optional: true @@ -2495,7 +3179,9 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - long@5.3.2: {} + linkifyjs@4.3.3: {} + + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -2505,17 +3191,15 @@ snapshots: dependencies: react: 19.2.7 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} - - merge-descriptors@1.0.3: {} - - methods@1.1.2: {} + mdn-data@2.27.1: {} mime-db@1.52.0: {} @@ -2523,8 +3207,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} - motion-dom@12.42.0: dependencies: motion-utils: 12.39.0 @@ -2539,38 +3221,21 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - ms@2.0.0: {} - ms@2.1.3: {} nanoid@3.3.15: {} - negotiator@0.6.3: {} - - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-releases@2.0.50: {} - object-inspect@1.13.4: {} + obug@2.1.4: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 + orderedmap@2.1.1: {} - p-retry@4.6.2: + parse5@8.0.1: dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - parseurl@1.3.3: {} + entities: 8.0.0 - path-to-regexp@0.1.13: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -2584,49 +3249,109 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - protobufjs@7.6.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 22.20.0 - long: 5.3.2 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 - proxy-addr@2.0.7: + prosemirror-changeset@2.4.1: dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 + prosemirror-transform: 1.12.0 - qs@6.15.3: + prosemirror-commands@1.7.1: dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 - range-parser@1.2.1: {} + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.1 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 - raw-body@2.5.3: + prosemirror-tables@1.8.5: dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.1 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 scheduler: 0.27.0 + react-is@17.0.2: {} + react-refresh@0.18.0: {} + react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + cookie-es: 3.1.1 + react: 19.2.7 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + react@19.2.7: {} - retry@0.13.1: {} + require-from-string@2.0.2: {} rollup@4.62.2: dependencies: @@ -2659,85 +3384,54 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 - safe-buffer@5.2.1: {} + rope-sequence@1.3.4: {} - safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 scheduler@0.27.0: {} semver@6.3.1: {} - send@0.19.2: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.1 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} + siginfo@2.0.0: {} - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 + source-map-js@1.2.1: {} - side-channel@1.1.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 + stackback@0.0.2: {} - source-map-js@1.2.1: {} + std-env@4.2.0: {} - statuses@2.0.2: {} + symbol-tree@3.2.4: {} tailwindcss@4.3.1: {} tapable@2.3.3: {} + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - toidentifier@1.0.1: {} + tinyrainbow@3.1.0: {} + + tldts-core@7.4.9: {} + + tldts@7.4.9: + dependencies: + tldts-core: 7.4.9 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.9 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 tslib@2.8.1: {} @@ -2746,27 +3440,21 @@ snapshots: esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 + optional: true typescript@5.8.3: {} undici-types@6.21.0: {} - unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 - utils-merge@1.0.1: {} - - vary@1.1.2: {} + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4): dependencies: @@ -2783,8 +3471,68 @@ snapshots: lightningcss: 1.32.0 tsx: 4.22.4 - web-streams-polyfill@3.3.3: {} + vitest@4.1.10(@types/node@22.20.0)(jsdom@27.4.0)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.0 + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} - ws@8.21.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.1: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} yallist@3.1.1: {} + + zod@4.4.3: {} + + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): + optionalDependencies: + '@types/react': 19.2.17 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..4993edc Binary files /dev/null and b/public/favicon.png differ diff --git a/server.ts b/server.ts deleted file mode 100755 index 6fbe47f..0000000 --- a/server.ts +++ /dev/null @@ -1,141 +0,0 @@ -import express from "express"; -import path from "path"; -import { createServer as createViteServer } from "vite"; -import { GoogleGenAI } from "@google/genai"; -import dotenv from "dotenv"; - -dotenv.config(); - -const app = express(); -const PORT = 4000; - -// Initialize Gemini client safely -const apiKey = process.env.GEMINI_API_KEY; -let ai: GoogleGenAI | null = null; - -if (apiKey) { - ai = new GoogleGenAI({ - apiKey: apiKey, - httpOptions: { - headers: { - "User-Agent": "aistudio-build", - }, - }, - }); -} else { - console.warn( - "GEMINI_API_KEY environment variable is not defined. Chatbot will run in fallback mock mode.", - ); -} - -app.use(express.json()); - -// API route for chatbot helper -app.post("/api/chat", async (req, res) => { - try { - const { message, history } = req.body; - - if (!message) { - return res.status(400).json({ error: "Message is required." }); - } - - if (!ai) { - // Return a simulated high-quality response if Gemini is not initialized - return res.json({ - text: `[알림] API 키가 설정되지 않아 데모 모드로 응답합니다.\n\n질문하신 "${message}"에 대한 맞춤 답변입니다:\n청년정책 서비스 **YouthPick**에서는 일자리, 주거, 교육, 복지 등의 카테고리별로 정책을 맞춤 설계하고 있습니다. 대표적으로 서울시 '청년 월세 한시 특별지원'(월 최대 20만원 지원) 및 '청년 디지털 일자리 지원 사업'을 통해 일과 주거 안정을 도모하실 수 있습니다. 상세 상담을 위해 API 키를 Secrets에 입력해 주세요!`, - }); - } - - // System instruction detailing the brand and specific mock policies - const systemInstruction = ` -You are the expert Youth Policy AI Assistant named "정책 탐색 도우미" for the website "YouthPick" (청년정책 맞춤형 서비스). -Your goal is to help Korean youth (ages 19-34) find policies that suit their needs. - -Here is the knowledge base of policies on YouthPick: -1. 일자리 (Job): - - 청년 디지털 일자리 지원 사업: IT 분야 실무 경험과 교육을 제공하고 취업 연계를 지원하는 사업. 대상: 만 19세 ~ 34세 미취업 청년. - - 청년내일채움공제: 중소기업 취업 청년의 자산 형성을 지원하는 제도. 2년간 300만원을 적립하면 정부와 기업이 공동 적립하여 1,200만원으로 환급. - - 국민취업지원제도: 구직자에게 취업지원서비스를 종합적으로 제공하고 저소득 구직자에게는 생계안정을 위한 구직촉진수당(월 50만원씩 6개월) 지급. - -2. 주거 (Housing): - - 청년 월세 한시 특별지원: 청년들의 주거비 부담 경감을 위해 월세를 실지급액 기준 최대 20만원까지 최대 12개월 동안 지원하는 사업. 대상: 만 19세~34세 무주택 청년 (부모와 별도 거주). - - 청년 주거급여 분리지급: 주거급여 수급가구 내 20대 미혼 자녀가 학업이나 구직 등을 이유로 부모와 다른 시·군에서 거주하는 경우 별도로 주거급여를 지급하는 제도. - - 청년 버팀목 전세자금대출: 청년들을 위한 저금리(연 1.5%~2.1%) 전세자금 대출 지원 사업. - -3. 교육 (Education): - - K-디지털 트레이닝 (국비지원): 디지털, 신기술 분야 실무 중심 교육을 통해 취업 역량을 높여주는 과정. 교육비 전액 정부 지원. - - 국민내일배움카드: 스스로 역량 개발을 할 수 있도록 훈련비를 지원하는 카드 (1인당 300만~500만원 지원). - - 서울시 청년수당: 서울에 거주하는 미취업 청년(만 19~34세)에게 월 50만원씩 최대 6개월간 활동 지원금을 지급하는 사업. - -4. 복지·문화 (Welfare & Culture): - - 청년내일저축계좌: 일하는 저소득층 청년의 자산형성을 지원하는 계좌. 본인 저축액(10만원) 대비 정부지원금을 10만~30만원 매칭 적립. - - 청년문화예술패스: 성년이 되는 청년(만 19세)에게 문화예술 관람비(연 최대 15만원) 지원. - -5. 참여·권리 (Participation & Rights): - - 청년보좌역 및 청년인턴: 정부 부처의 정책 결정 과정에 청년의 목소리를 반영하기 위한 참여 기회 제공. - -Guidelines: -- Answer in Korean in a polite, friendly, and enthusiastic tone ("~해요", "~드립니다"). -- If the user asks for specific policies, explain them clearly based on the knowledge base above. -- Recommend corresponding policies based on their status or region (primarily Seoul/서울특별시). -- Organize the output with clear bullet points, bold text, and neat sections. -- Keep responses relatively concise but informative. Do not use markdown features not supported. -`; - - // Format chat history for Gemini API - const contents = []; - if (history && Array.isArray(history)) { - for (const h of history) { - contents.push({ - role: h.role === "user" ? "user" : "model", - parts: [{ text: h.text }], - }); - } - } - contents.push({ - role: "user", - parts: [{ text: message }], - }); - - const response = await ai.models.generateContent({ - model: "gemini-3.5-flash", - contents: contents, - config: { - systemInstruction: systemInstruction, - temperature: 0.7, - }, - }); - - const replyText = - response.text || "죄송합니다. 답변을 생성하지 못했습니다."; - res.json({ text: replyText }); - } catch (error: any) { - console.error("Error communicating with Gemini API:", error); - res - .status(500) - .json({ error: "서버 오류가 발생하여 챗봇 응답에 실패했습니다." }); - } -}); - -// Configure Vite or serve static assets -async function startServer() { - if (process.env.NODE_ENV !== "production") { - const vite = await createViteServer({ - server: { middlewareMode: true }, - appType: "spa", - }); - app.use(vite.middlewares); - } else { - const distPath = path.join(process.cwd(), "dist"); - app.use(express.static(distPath)); - app.get("*", (req, res) => { - res.sendFile(path.join(distPath, "index.html")); - }); - } - - app.listen(PORT, "0.0.0.0", () => { - console.log(`[YouthPick] Server running on port ${PORT}`); - }); -} - -startServer(); diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100755 index 615fafc..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,2094 +0,0 @@ -import { useState, useEffect, useMemo } from "react"; -import { - Briefcase, - Home as HomeIcon, - GraduationCap, - Heart, - Sparkles, - Info, - Facebook, - Instagram, - Youtube, - ArrowRight, - Hand, - Compass, - Search, - Calendar, - User, - Loader2, - Check, - AlertTriangle, - Plus, - Trash2, - Edit3, - CheckCircle2, - CheckSquare, - Square, - ChevronRight, - RefreshCw, - LogOut, - ShieldAlert, - FileText, - LayoutDashboard, - ArrowLeft, - X -} from "lucide-react"; -import { motion, AnimatePresence } from "motion/react"; - -import { Policy, FilterState, UserProfile, TrackerItem, TrackerChecklistItem } from "./types"; -import { MOCK_POLICIES } from "./data"; - -import Header from "./components/Header"; -import HeroBanner from "./components/HeroBanner"; -import FilterBar from "./components/FilterBar"; -import PolicyCard from "./components/PolicyCard"; -import CompareModule from "./components/CompareModule"; -import RecentlyViewed from "./components/RecentlyViewed"; -import ChatbotAssistant from "./components/ChatbotAssistant"; -import DetailModal from "./components/DetailModal"; - -// Toast Interface -interface Toast { - id: string; - message: string; - type: "success" | "info" | "warning"; -} - -export default function App() { - // Views: 'home' | 'search' | 'recommend' | 'tracker' | 'mypage' | 'admin' | 'login' | 'profile_setup' - const [activeView, setActiveView] = useState("home"); - const [previousView, setPreviousView] = useState("home"); - - // Authentication State - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [userName, setUserName] = useState("민지"); - - // Custom User Profile State (PROFILE-01 ~ 03) - const [userProfile, setUserProfile] = useState({ - birthYear: 1998, - region: "서울특별시", - subRegion: "마포구", - employmentStatus: "미취업·구직", - educationStatus: "대학 졸업", - interests: ["일자리", "교육"], - keywords: ["직무교육", "면접비"] - }); - - // Profile setup wizard steps (1 | 2 | 3) - const [profileSetupStep, setProfileSetupStep] = useState(1); - const [wizardProfile, setWizardProfile] = useState({ - birthYear: 1998, - region: "서울특별시", - subRegion: "마포구", - employmentStatus: "미취업·구직", - educationStatus: "대학 졸업", - interests: ["일자리", "교육"], - keywords: ["직무교육", "면접비"] - }); - const [newKeywordInput, setNewKeywordInput] = useState(""); - - // Application Tracker State (TRACKER-01 ~ 02) - const [trackers, setTrackers] = useState([ - { - policyId: "p1", - status: "준비중", - targetDate: "2026-06-25", - checklist: [ - { id: "c1", text: "지원서 작성", completed: true }, - { id: "c2", text: "자기소개서 초안 작성", completed: true }, - { id: "c3", text: "구직상태 확인서 발급", completed: false }, - { id: "c4", text: "최종 제출 완료", completed: false } - ], - memo: "교육 일정과 면접 일정이 겹치는지 확인할 것" - }, - { - policyId: "p2", - status: "관심", - targetDate: "2026-07-10", - checklist: [ - { id: "c5", text: "주민등록등본 발급", completed: false }, - { id: "c6", text: "임대차계약서 사본 준비", completed: false } - ], - memo: "월세 이체 내역 3개월분 미리 출력해두기" - }, - { - policyId: "p5", - status: "결과대기", - targetDate: "2026-06-20", - checklist: [ - { id: "c7", text: "서류 업로드", completed: true }, - { id: "c8", text: "심사 대기 확인", completed: true } - ], - memo: "결과 발표는 공식 홈페이지 고시 예정" - } - ]); - - // Selected tracker policy ID for TRACKER-02 detail view - const [selectedTrackerPolicyId, setSelectedTrackerPolicyId] = useState(null); - - // Search & Filter State (POLICY-01) - const [searchQuery, setSearchQuery] = useState(""); - const [filters, setFilters] = useState({ - region: "전체", - status: "전체", - category: "전체", - age: "전체" - }); - - // Saved bookmarks (Favorite policies) - const [savedPolicyIds, setSavedPolicyIds] = useState(["p1", "p2", "p5", "p_missing"]); - - // Comparing policies - const [comparingPolicies, setComparingPolicies] = useState([]); - - // Selected policy in detail modal - const [selectedPolicy, setSelectedPolicy] = useState(null); - - // Tracker interactive states - const [trackerTab, setTrackerTab] = useState("전체"); - const [showAddChecklistItem, setShowAddChecklistItem] = useState(false); - const [newChecklistItemText, setNewChecklistItemText] = useState(""); - const [tempMemoText, setTempMemoText] = useState(""); - - // Confirmation Modals - const [showDeleteTrackerConfirm, setShowDeleteTrackerConfirm] = useState(null); - const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false); - - // Admin sync states - const [isAdminSyncing, setIsAdminSyncing] = useState(false); - const [syncHistory, setSyncHistory] = useState([ - { date: "06.24 06:00", status: "SUCCESS", newCount: 18, editCount: 44, missingCount: 2, errorCount: 0 }, - { date: "06.23 06:00", status: "PARTIAL", newCount: 12, editCount: 31, missingCount: 4, errorCount: 2 } - ]); - - // Custom Toast state - const [toasts, setToasts] = useState([]); - - const showToast = (message: string, type: "success" | "info" | "warning" = "success") => { - const id = Date.now().toString(); - setToasts((prev) => [...prev, { id, message, type }]); - setTimeout(() => { - setToasts((prev) => prev.filter((t) => t.id !== id)); - }, 3000); - }; - - // View navigation router - const navigateTo = (view: string) => { - // Check if member view is requested but user is logged out - if ((view === "recommend" || view === "tracker" || view === "mypage") && !isLoggedIn) { - setPreviousView(view); - setActiveView("login"); - showToast("이 서비스는 로그인이 필요합니다. 회원 화면으로 안내합니다.", "info"); - return; - } - setPreviousView(activeView); - setActiveView(view); - // Auto sync scroll - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - - // Social Login Execution - const handleSocialLogin = (provider: string) => { - setIsLoggedIn(true); - setUserName("민지"); - showToast(`🎉 ${provider} 계정으로 환영합니다! 맞춤 청년 정책 매칭이 활성화되었습니다.`, "success"); - - // First-time simulation: if user has no interests, direct to profile setup - if (userProfile.interests.length === 0) { - setProfileSetupStep(1); - setWizardProfile({ ...userProfile }); - setActiveView("profile_setup"); - } else { - // Go back to previous or targeted page - if (previousView === "recommend") { - setActiveView("recommend"); - } else if (previousView === "tracker") { - setActiveView("tracker"); - } else if (previousView === "mypage") { - setActiveView("mypage"); - } else { - setActiveView("home"); - } - } - }; - - const handleLogout = () => { - setIsLoggedIn(false); - setActiveView("home"); - showToast("안전하게 로그아웃 되었습니다.", "info"); - }; - - const handleDeleteAccount = () => { - setIsLoggedIn(false); - setShowDeleteAccountConfirm(false); - setActiveView("home"); - showToast("회원 탈퇴가 안전하게 처리되었습니다. 이용해주셔서 감사합니다.", "warning"); - }; - - // Toggle Save bookmark (favorite) - const handleToggleSave = (id: string) => { - if (savedPolicyIds.includes(id)) { - setSavedPolicyIds((prev) => prev.filter((pId) => pId !== id)); - showToast("관심 정책 목록에서 해제되었습니다.", "info"); - } else { - setSavedPolicyIds((prev) => [...prev, id]); - showToast("관심 정책으로 보관되었습니다! [신청관리]에서 일정을 추가해 보세요.", "success"); - } - }; - - // Compare Policy Controls - const handleToggleCompare = (policy: Policy) => { - if (comparingPolicies.some((p) => p.id === policy.id)) { - setComparingPolicies((prev) => prev.filter((p) => p.id !== policy.id)); - } else { - if (comparingPolicies.length >= 2) { - showToast("비교 분석은 한 번에 최대 2개의 정책만 가능합니다.", "warning"); - return; - } - setComparingPolicies((prev) => [...prev, policy]); - showToast(`${policy.title}이 비교 슬롯에 등록되었습니다.`, "success"); - } - }; - - const handleClearCompare = () => { - setComparingPolicies([]); - }; - - const handleRemoveCompare = (policy: Policy) => { - setComparingPolicies((prev) => prev.filter((p) => p.id !== policy.id)); - }; - - // Profile setup wizard buttons - const handleWizardNext = () => { - if (profileSetupStep < 3) { - setProfileSetupStep((prev) => prev + 1); - } else { - // Step 3 finished - setUserProfile({ ...wizardProfile }); - setActiveView("recommend"); - showToast("✨ 맞춤 프로필 설정 완료! 실시간 추천 결과 28건이 연계되었습니다.", "success"); - } - }; - - const handleWizardPrev = () => { - if (profileSetupStep > 1) { - setProfileSetupStep((prev) => prev - 1); - } - }; - - const handleAddKeyword = () => { - if (newKeywordInput.trim() === "") return; - if (wizardProfile.keywords.length >= 5) { - showToast("관심 키워드는 최대 5개까지 추가할 수 있습니다.", "warning"); - return; - } - setWizardProfile((prev) => ({ - ...prev, - keywords: [...prev.keywords, newKeywordInput.trim()] - })); - setNewKeywordInput(""); - }; - - const handleRemoveKeyword = (kw: string) => { - setWizardProfile((prev) => ({ - ...prev, - keywords: prev.keywords.filter((k) => k !== kw) - })); - }; - - const handleToggleInterest = (interest: string) => { - if (wizardProfile.interests.includes(interest)) { - setWizardProfile((prev) => ({ - ...prev, - interests: prev.interests.filter((i) => i !== interest) - })); - } else { - if (wizardProfile.interests.length >= 3) { - showToast("관심 분야는 최대 3개까지 선택할 수 있습니다.", "warning"); - return; - } - setWizardProfile((prev) => ({ - ...prev, - interests: [...prev.interests, interest] - })); - } - }; - - // Policy Search Listings Filtered - const filteredPolicies = useMemo(() => { - return MOCK_POLICIES.filter((policy) => { - // Search Query - if (searchQuery.trim() !== "") { - const query = searchQuery.toLowerCase(); - const matchesQuery = - policy.title.toLowerCase().includes(query) || - policy.category.toLowerCase().includes(query) || - policy.description.toLowerCase().includes(query); - if (!matchesQuery) return false; - } - - // Region Filter - if (filters.region !== "전체" && policy.region !== "전국") { - if (policy.region !== filters.region) return false; - } - - // Category Filter - if (filters.category !== "전체" && policy.category !== filters.category) { - if (filters.category === "참여・권리" && policy.category === "참여·권리") { - // ignore slightly different characters - } else { - return false; - } - } - - // Status/Age Filter matching - if (filters.age !== "전체" && policy.target !== "전체" && policy.target !== filters.age) { - return false; - } - - return true; - }); - }, [filters, searchQuery]); - - // Recommended Matching Engine based on Active Profile - const recommendedPolicies = useMemo(() => { - return MOCK_POLICIES.map((p, index) => { - // Calculate a highly realistic match score based on user Profile - let score = 70; - if (p.region === userProfile.region || p.region === "전국") score += 15; - if (userProfile.interests.includes(p.category)) score += 10; - if (p.target === "전체" || p.target === "만 19~34세") score += 5; - // Add small mock variance - score += (index % 3) * 4; - return { - policy: p, - score: Math.min(score, 98), - reliability: "MEDIUM" as const, - reasons: [ - `거주지역이 ${userProfile.region} 조건과 일치합니다.`, - `관심 분야인 ${p.category} 카테고리에 속합니다.`, - `취업상태 조건이 정책 자격에 근접합니다.` - ] - }; - }).sort((a, b) => b.score - a.score); - }, [userProfile]); - - // Start Tracker Control - const handleStartTracker = (policy: Policy) => { - if (!isLoggedIn) { - setPreviousView("tracker"); - setActiveView("login"); - showToast("신청관리 일정 추가를 위해 먼저 로그인해 주세요.", "info"); - return; - } - - const exists = trackers.find((t) => t.policyId === policy.id); - if (exists) { - setSelectedTrackerPolicyId(policy.id); - setActiveView("tracker"); - showToast("이미 등록된 신청 정보가 존재하여, 관리 상세로 연결되었습니다.", "info"); - } else { - const newTracker: TrackerItem = { - policyId: policy.id, - status: "준비중", - targetDate: policy.deadline !== "원본확인불가" && policy.deadline !== "상시모집" ? policy.deadline : "2026-06-30", - checklist: [ - { id: Math.random().toString(), text: "기본 제출 서류 취합", completed: false }, - { id: Math.random().toString(), text: "공고 상세 자격요건 검증", completed: false }, - { id: Math.random().toString(), text: "지원서 및 온라인 제출 완료", completed: false } - ], - memo: "" - }; - setTrackers((prev) => [...prev, newTracker]); - setSelectedTrackerPolicyId(policy.id); - setActiveView("tracker"); - showToast(`🎉 '${policy.title}' 신청관리 상태가 새로 시작되었습니다!`, "success"); - } - }; - - // Selected tracker item for detail - const activeTrackerItem = useMemo(() => { - return trackers.find((t) => t.policyId === selectedTrackerPolicyId) || null; - }, [trackers, selectedTrackerPolicyId]); - - // Selected tracker policy reference - const activeTrackerPolicy = useMemo(() => { - if (!activeTrackerItem) return null; - return MOCK_POLICIES.find((p) => p.id === activeTrackerItem.policyId) || null; - }, [activeTrackerItem]); - - // Memo temp editor synchronization - useEffect(() => { - if (activeTrackerItem) { - setTempMemoText(activeTrackerItem.memo); - } - }, [selectedTrackerPolicyId, activeTrackerItem]); - - const handleSaveMemo = () => { - if (!selectedTrackerPolicyId) return; - setTrackers((prev) => prev.map((t) => { - if (t.policyId === selectedTrackerPolicyId) { - return { ...t, memo: tempMemoText }; - } - return t; - })); - showToast("📝 개인 기록 메모가 성공적으로 저장되었습니다.", "success"); - }; - - const handleAddChecklistItemAction = () => { - if (newChecklistItemText.trim() === "" || !selectedTrackerPolicyId) return; - setTrackers((prev) => prev.map((t) => { - if (t.policyId === selectedTrackerPolicyId) { - return { - ...t, - checklist: [ - ...t.checklist, - { id: Math.random().toString(), text: newChecklistItemText.trim(), completed: false } - ] - }; - } - return t; - })); - setNewChecklistItemText(""); - setShowAddChecklistItem(false); - showToast("체크리스트 준비 일감이 추가되었습니다.", "success"); - }; - - const handleToggleChecklistItem = (policyId: string, itemId: string) => { - setTrackers((prev) => prev.map((t) => { - if (t.policyId === policyId) { - return { - ...t, - checklist: t.checklist.map((item) => { - if (item.id === itemId) { - return { ...item, completed: !item.completed }; - } - return item; - }) - }; - } - return t; - })); - }; - - const handleDeleteChecklistItem = (policyId: string, itemId: string) => { - setTrackers((prev) => prev.map((t) => { - if (t.policyId === policyId) { - return { - ...t, - checklist: t.checklist.filter((item) => item.id !== itemId) - }; - } - return t; - })); - showToast("준비할 일감이 삭제되었습니다.", "info"); - }; - - const handleUpdateTrackerStatus = (policyId: string, newStatus: TrackerItem["status"]) => { - setTrackers((prev) => prev.map((t) => { - if (t.policyId === policyId) { - return { ...t, status: newStatus }; - } - return t; - })); - showToast(`신청관리 상태가 [${newStatus}]로 갱신되었습니다.`, "success"); - }; - - const handleUpdateTrackerDate = (policyId: string, date: string) => { - setTrackers((prev) => prev.map((t) => { - if (t.policyId === policyId) { - return { ...t, targetDate: date }; - } - return t; - })); - showToast(`제출 마감일정이 변경되었습니다: ${date}`, "info"); - }; - - const handleDeleteTracker = (policyId: string) => { - setTrackers((prev) => prev.filter((t) => t.policyId !== policyId)); - setSelectedTrackerPolicyId(null); - setShowDeleteTrackerConfirm(null); - showToast("신청관리 목록에서 안전하게 삭제되었습니다.", "warning"); - }; - - // Run admin database synchronizer - const runAdminSync = () => { - setIsAdminSyncing(true); - setTimeout(() => { - setIsAdminSyncing(false); - const newLog = { - date: "오늘 " + new Date().toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }), - status: "SUCCESS", - newCount: Math.floor(Math.random() * 20) + 5, - editCount: Math.floor(Math.random() * 30) + 10, - missingCount: Math.floor(Math.random() * 3), - errorCount: 0 - }; - setSyncHistory((prev) => [newLog, ...prev]); - showToast("공공 API 연동 및 청년정책 정보 수동 동기화가 완료되었습니다!", "success"); - }, 2000); - }; - - // Category selection handler from home - const handleCategoryCardClick = (catKey: string) => { - setFilters((prev) => ({ - ...prev, - category: catKey - })); - setActiveView("search"); - }; - - return ( -
- - {/* Toast Notification Container */} -
- - {toasts.map((toast) => ( - - {toast.type === "success" && } - {toast.type === "warning" && } - {toast.type === "info" && } - {toast.message} - - ))} - -
- - {/* Header component */} -
navigateTo("login")} - userName={userName} - /> - - {/* Main Content Router */} -
- - {/* VIEW 1: HOME PAGE (Guest HOME-01 / Member HOME-02) */} - {activeView === "home" && ( -
- {/* Custom Hero Banner */} - navigateTo("search")} - /> - - {/* Category Quicklinks Section */} -
-
-

청년정책 분야 바로가기

-

원하시는 주요 분야별 정책 카테고리를 눌러 빠르게 상세 필터 탐색을 시작해 보세요.

-
-
- {[ - { key: "일자리", title: "일자리", subtitle: "취업・창업 지원", bg: "bg-primary/10 text-primary hover:bg-primary/20", icon: Briefcase }, - { key: "주거", title: "주거", subtitle: "주거・금융 지원", bg: "bg-blue-50 text-blue-600 hover:bg-blue-100/50", icon: HomeIcon }, - { key: "교육", title: "교육", subtitle: "역량・자기개발", bg: "bg-indigo-50 text-indigo-600 hover:bg-indigo-100/50", icon: GraduationCap }, - { key: "복지·문화", title: "복지・문화", subtitle: "생활・건강・문화", bg: "bg-rose-50 text-rose-600 hover:bg-rose-100/50", icon: Heart }, - { key: "참여·권리", title: "참여・권리", subtitle: "참여・권익 보호", bg: "bg-amber-50 text-amber-600 hover:bg-amber-100/50", icon: Hand } - ].map((item) => { - const IconComp = item.icon; - return ( - - ); - })} -
-
- - {/* Profile configuration invite / Recommendation Overview Widget */} - {!isLoggedIn ? ( -
-
- - 스마트 맞춤 추천 서비스 - -

- 프로필을 딱 한 번만 입력하고, 나에게 꼭 맞는 청년 정책을 받아보세요! -

-

- 거주지역, 연령뿐만 아니라 상세한 취업상태, 학력, 관심 키워드 조건을 대조하여 복잡한 수령자격을 자동으로 비교 분석해 드립니다. -

-
-
- - -
-
- ) : ( - /* MEMBER HOME-02: LOGGED-IN CUSTOM OVERVIEW */ -
-
-
-

- - {userName}님과 일치 확률이 가장 높은 추천 정책 -

-

- 현재 프로필: {userProfile.region} {userProfile.subRegion} · {userProfile.employmentStatus} · {userProfile.educationStatus} -

-
- -
- -
- {recommendedPolicies.slice(0, 3).map(({ policy, score }) => ( -
-
-
- - 유사도 {score}점 - - {policy.region} -
-

{policy.title}

-

{policy.description}

-
-
- 마감 {policy.deadline} - -
-
- ))} -
- -
- -
-
- )} - - {/* Recent & Grid Policies Split */} -
-
-
-
-

최근 화제인 신규 정책 소식

-

전국 및 서울 등 청년들이 가장 집중해서 조회하는 핵심 소식입니다.

-
- -
- -
- {MOCK_POLICIES.slice(0, 4).map((policy) => ( - p.id === policy.id)} - onToggleCompare={handleToggleCompare} - /> - ))} -
-
- - {/* Sidebar items */} -
- - -
-
-
- )} - - {/* VIEW 2: POLICY SEARCH (POLICY-01) */} - {activeView === "search" && ( -
-
- YouthPick > 통합 검색 -

실시간 정책 검색기

-

조건 필터와 지역 정보를 조합하여 나에게 딱 맞는 혜택 사업을 세밀하게 타겟팅해 보세요.

-
- - {/* Big Search Input */} -
-
- setSearchQuery(e.target.value)} - className="w-full rounded-2xl border border-slate-200 bg-white py-3 pl-5 pr-12 text-xs transition-colors focus:border-primary focus:outline-none shadow-sm" - id="search-main-input" - /> - -
- -
- - {/* Interactive Filters */} - - - {/* Results row info */} -
-
- - 총 {filteredPolicies.length}건의 청년 정책이 정밀 필터링되었습니다. - -
-
- 보기 구분: - -
-
- - {/* Core Card Grid or Empty State */} - {filteredPolicies.length > 0 ? ( -
-
-
- {filteredPolicies.map((policy) => ( - p.id === policy.id)} - onToggleCompare={handleToggleCompare} - /> - ))} -
-
- -
- - -
-
- ) : ( - /* EMPTY STATE VARIANT (5.4) */ -
- 🔍 -

일치하는 검색 결과가 전혀 없습니다

-

- 선택한 시·도 필터 조건이 맞지 않거나, 특정 카테고리에 너무 협소한 연령 조건이 지정되었을 수 있습니다. -

-
- - -
-
- )} -
- )} - - {/* VIEW 3: USER CUSTOM RECOMMENDATIONS (REC-01) */} - {activeView === "recommend" && ( -
-
-
- AI 종합 분석 스마트 팩트 매칭 -

나만을 위한 1:1 맞춤 청년정책 추천

-

회원님의 맞춤 프로필 가중치를 기반으로 상위 일치하는 자격을 자동 순위화했습니다.

-
- -
- - {/* Profile Briefing Plate */} -
-
- 현재 대조 프로필 조건 -

- {userProfile.region} {userProfile.subRegion} · {userProfile.birthYear}년생 · {userProfile.employmentStatus} · {userProfile.educationStatus} -

-
- {userProfile.interests.map((it) => ( - - #{it} - - ))} - {userProfile.keywords.map((it) => ( - - #{it} - - ))} -
-
-
- 통합 매칭 수 - 총 28건 추천 -
-
- - {/* Recommendation Cards Flow (REC-01 / Section 7) */} -
-
- {recommendedPolicies.slice(0, 8).map(({ policy, score, reliability, reasons }) => ( -
- {/* Corner ribbon matching score */} -
- 유사도 {score}점 -
- -
-
- - {policy.category} - - {policy.region} -
-

{policy.title}

-
- -

{policy.description}

- - {/* Reasons and Warning Section (REC-01 Section 7 requirement) */} -
-
- 추천 및 우대 조건 매칭 -
-

{reasons[0]} (+25)

-

{reasons[1]} (+20)

-

{reasons[2]} (+15)

-
-
- -
- 자가진단 및 공식공고 대조 필요 -

- - 세부 건강보험 소득요건 및 기업 규모별 제한사항은 공식 페이지 상세조회가 필요합니다. -

-
-
- - {/* Card Footer Actions */} -
- - - -
-
- ))} -
- -
- - -
-
-
- )} - - {/* VIEW 4: APPLICATION TRACKER (TRACKER-01 & TRACKER-02) */} - {activeView === "tracker" && ( -
- - {/* Split layout between List and Details */} -
- - {/* TRACKER-01: LEFT COLUMN (Tracker list with Tabs) */} -
-
- YouthPick > 나의 보관함 -

신청 준비 일정 관리

-

관심 등록한 정책의 제출 기한, 구비 서류 체크리스트를 놓치지 않게 체계적으로 추적합니다.

-
- - {/* Tracker Status filter buttons (TRACKER-01 layout) */} -
- {["전체", "관심", "준비중", "신청완료", "결과대기", "종료"].map((tab) => { - const count = tab === "전체" - ? trackers.length - : trackers.filter((t) => t.status === tab).length; - return ( - - ); - })} -
- - {/* Listing Tracker Cards */} -
- {trackers - .filter((t) => trackerTab === "전체" || t.status === trackerTab) - .map((track) => { - const policyRef = MOCK_POLICIES.find((p) => p.id === track.policyId); - if (!policyRef) return null; - - // Calculate completion percentage - const totalItems = track.checklist.length; - const completedItems = track.checklist.filter((c) => c.completed).length; - const pct = totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0; - - const isSelected = selectedTrackerPolicyId === track.policyId; - - return ( -
setSelectedTrackerPolicyId(track.policyId)} - className={`rounded-2xl border p-4 transition-all cursor-pointer text-left ${ - isSelected - ? "border-primary bg-primary/[0.01] shadow-sm ring-2 ring-primary/10" - : "border-slate-100 bg-white hover:border-slate-200" - }`} - > -
- - {track.status} - - 목표: {track.targetDate} -
- -

{policyRef.title}

- - {/* Progress bar */} -
-
- 체크리스트 {completedItems}/{totalItems} 완료 - {pct}% -
-
-
-
-
- -
- 수정기록: 2026.06.23 - - 관리하기 - - -
-
- ); - })} - - {trackers.filter((t) => trackerTab === "전체" || t.status === trackerTab).length === 0 && ( -
- 이 카테고리 상태에 배정된 신청 일감이 없습니다. -
- )} -
-
- - {/* TRACKER-02: RIGHT COLUMN (Selected Tracker detailed checks & memos) */} -
- {activeTrackerItem && activeTrackerPolicy ? ( -
- {/* Header bar */} -
-
- - {activeTrackerPolicy.category} - -

{activeTrackerPolicy.title}

-
- - 공식 공고 ↗ - -
- - {/* Interactive inputs: Status, Target Date */} -
-
- - -
- -
- - handleUpdateTrackerDate(activeTrackerItem.policyId, e.target.value)} - className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-bold text-slate-700 outline-none focus:bg-white focus:border-primary" - /> -
-
- - {/* Required documents brief (From wireframe Section 10) */} -
-

정책 제출 서류 기본 안내

-

- • 신청서 및 자기소개서, 구직상태 자격 소명 확인서 -

-

- * 주관 부처 마감 이전에 제출 서류 목록이 변동되었는지 공식 안내 고시를 반드시 교차 검토하세요. -

-
- - {/* Interactive checklist area with deletion option */} -
-
-

- - 준비 작업 체크리스트 -

- -
- - {/* Checklist overlay modal */} - {showAddChecklistItem && ( -
- setNewChecklistItemText(e.target.value)} - className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs focus:outline-none focus:border-primary" - /> -
- - -
-
- )} - -
- {activeTrackerItem.checklist.map((item) => ( -
- - -
- ))} -
-
- - {/* Memo textarea (Character monitoring limit 2000 chars) */} -
- -