diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 63d64fb4..e19d215b 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,6 +1,390 @@ # SAM React 작업 현황 -## 2026-01-02 (목) - 견적 등록 자동산출 기능 구현 +## 2026-01-09 (목) - Phase L 건설관리 Mock → API 연동 (3개 모듈) ✅ + +### 작업 목표 +- Backend API가 이미 존재하는 3개 모듈의 Mock → API 연동 +- pricing-management, estimates, category-management + +### 완료된 작업 + +| 모듈 | 변경 내용 | 상태 | +|------|----------|------| +| pricing-management | Mock → apiClient 변환 (378줄), types.ts 타입 추가 | ✅ | +| estimates | Mock → apiClient 변환, 복잡한 중첩 타입 처리 | ✅ | +| category-management | Mock → apiClient 변환, 에러 타입 처리 (IN_USE/DEFAULT/GENERAL) | ✅ | + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/pricing-management/actions.ts` | Mock → apiClient 표준화 | +| `src/components/business/construction/pricing-management/types.ts` | PricingListResponse, PricingFilter, PricingFormData 추가 | +| `src/components/business/construction/estimates/actions.ts` | Mock → apiClient 표준화 (중첩 타입) | +| `src/components/business/construction/category-management/actions.ts` | Mock → apiClient 표준화 | + +### 적용된 패턴 +- `'use server'` + `apiClient from '@/lib/api'` +- Snake_case API 타입 (ApiXxx) → camelCase Frontend 타입 변환 +- 표준 응답: `{ success, data?, error? }` +- 페이지네이션: `{ items, total, page, size, totalPages }` + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +### 남은 Mock 모듈 (Backend API 개발 필요) +| 모듈 | Backend API | 비고 | +|------|-------------|------| +| bidding | ❌ 없음 | Backend 필요 | +| site-briefings | ❌ 없음 | Backend 필요 | +| structure-review | ❌ 없음 | Backend 필요 | +| labor-management | ❌ 없음 | Backend 필요 | + +--- + +## 2026-01-09 (목) - Phase 1.3-1.5 건설관리 apiClient 표준화 + +### 작업 목표 +- 건설관리 모듈의 커스텀 `apiRequest` 함수를 표준 `apiClient` 패턴으로 변환 +- Phase 1.3: 계약관리(contract), Phase 1.4: 거래처관리(partners), Phase 1.5: 현장관리(site-management) + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/contract/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | +| `src/components/business/construction/partners/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | +| `src/components/business/construction/site-management/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | + +### 주요 변경 내용 + +#### 1. 제거된 코드 (각 파일에서) +- 커스텀 `apiRequest()` 함수 전체 +- `import { cookies } from 'next/headers'` +- `const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL` +- `const API_KEY = process.env.API_KEY` + +#### 2. 추가된 코드 +- `import { apiClient } from '@/lib/api'` +- 명시적 API 타입 정의: + - **contract**: `ApiContract`, `ApiContractFile`, `ApiAttachment`, `ApiContractStats`, `ApiContractStageCount` + - **partners**: `ApiPartner`, `ApiPartnerStats` + - **site-management**: `ApiSite`, `ApiSiteStats` + +#### 3. API 엔드포인트 (변경 없음) +**계약관리 (contract)** +- `GET /construction/contracts` - 목록 +- `GET /construction/contracts/stats` - 통계 +- `GET /construction/contracts/stage-counts` - 단계별 건수 +- `GET /construction/contracts/{id}` - 상세 +- `POST /construction/contracts` - 등록 +- `PUT /construction/contracts/{id}` - 수정 +- `DELETE /construction/contracts/{id}` - 삭제 +- `DELETE /construction/contracts/bulk` - 일괄 삭제 + +**거래처관리 (partners)** +- `GET /clients` - 목록 +- `GET /clients/stats` - 통계 +- `GET /clients/{id}` - 상세 +- `POST /clients` - 등록 +- `PUT /clients/{id}` - 수정 +- `DELETE /clients/{id}` - 삭제 +- `DELETE /clients/bulk` - 일괄 삭제 + +**현장관리 (site-management)** +- `GET /sites` - 목록 +- `GET /sites/stats` - 통계 +- `DELETE /sites/{id}` - 삭제 +- `DELETE /sites/bulk` - 일괄 삭제 + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +### Git 커밋 +- React: `5db6e59` refactor(construction): 건설관리 3개 모듈 apiClient 표준화 + +--- + +## 2026-01-09 (목) - Phase 1.2 인수인계보고서 API 표준화 + +### 작업 목표 +- `handover-report/actions.ts` 커스텀 fetch → 표준 apiClient 변환 +- 기존 API 연동 코드를 프로젝트 표준 패턴으로 통일 + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/handover-report/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | + +### 주요 변경 내용 + +#### 1. 제거된 코드 +- 커스텀 `apiRequest()` 함수 (52줄) +- `cookies()` 직접 import +- `API_BASE_URL`, `API_KEY` 직접 정의 + +#### 2. 추가된 코드 +- `import { apiClient } from '@/lib/api'` +- 명시적 API 타입 정의: `ApiHandoverReport`, `ApiManager`, `ApiContractItem`, `ApiExternalEquipmentCost` + +#### 3. API 엔드포인트 (변경 없음) +- `GET /construction/handover-reports` - 목록 +- `GET /construction/handover-reports/stats` - 통계 +- `GET /construction/handover-reports/{id}` - 상세 +- `POST /construction/handover-reports` - 등록 +- `PUT /construction/handover-reports/{id}` - 수정 +- `DELETE /construction/handover-reports/{id}` - 삭제 +- `DELETE /construction/handover-reports/bulk` - 일괄 삭제 + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +### Git 커밋 +- React: `b7b8b90` refactor(handover-report): 커스텀 fetch → apiClient 표준화 + +--- + +## 2026-01-09 (목) - Phase 2.4 수주관리 API 연동 + +### 작업 목표 +- 시공사 페이지 API 연동 계획 Phase 2.4: 수주관리 +- `order-management/actions.ts` Mock 데이터 → 실제 API 연동 +- common_codes 테이블 기반 공용 코드 시스템 도입 + +### 수정된 파일 +| 저장소 | 파일명 | 설명 | +|--------|--------|------| +| api | `database/migrations/2026_01_09_171700_add_order_codes_to_common_codes.php` | order_status/order_type 코드 추가 | +| api | `app/Http/Controllers/Api/V1/CommonController.php` | index 메서드 구현 | +| react | `src/lib/api/common-codes.ts` | 공용 코드 조회 유틸리티 (신규) | +| react | `src/lib/api/index.ts` | common-codes 모듈 export 추가 | +| react | `src/components/business/construction/order-management/actions.ts` | Mock → API 완전 재작성 | + +### 주요 변경 내용 + +#### 1. common_codes 공용 코드 시스템 +- `order_status` 코드 그룹: DRAFT, CONFIRMED, IN_PROGRESS, COMPLETED, CANCELLED +- `order_type` 코드 그룹: ORDER, PURCHASE +- API 엔드포인트: `GET /api/v1/settings/common/{group}` + +#### 2. 상태 매핑 함수 +| Frontend | Backend | +|----------|---------| +| waiting | DRAFT | +| order_complete | CONFIRMED | +| delivery_scheduled | IN_PROGRESS | +| delivery_complete | COMPLETED | + +#### 3. API 함수 구현 (10개) +- `getOrderList()` - GET /api/v1/orders +- `getOrderStats()` - GET /api/v1/orders/stats +- `getOrderDetail()` - GET /api/v1/orders/{id} +- `getOrderDetailFull()` - GET /api/v1/orders/{id} (전체 정보) +- `createOrder()` - POST /api/v1/orders +- `updateOrder()` - PUT /api/v1/orders/{id} +- `deleteOrder()` - DELETE /api/v1/orders/{id} +- `deleteOrders()` - 개별 삭제 반복 (batch API 미존재) +- `duplicateOrder()` - 조회 후 새로 생성 +- `updateOrderStatus()` - PATCH /api/v1/orders/{id}/status + +### Git 커밋 +- API: `9f8bff2` feat(common-codes): order_status/order_type 공용 코드 추가 +- React: `6615f39` feat(order-management): Mock → API 연동 및 common-codes 유틸리티 추가 + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +--- + +## 2026-01-09 (목) - TODO-1 결재선/참조 Select 버그 수정 + +### 작업 목표 +- 결재선/참조 Select 컴포넌트에서 선택한 직원 정보가 표시되지 않는 버그 수정 +- @/lib/api barrel export 추가 (빌드 오류 해결) + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/approval/DocumentCreate/ApprovalLineSection.tsx` | SelectValue 버그 수정 | +| `src/components/approval/DocumentCreate/ReferenceSection.tsx` | SelectValue 버그 수정 | +| `src/lib/api/index.ts` | 신규 생성 - barrel export | + +### 주요 변경 내용 + +#### 1. SelectValue 버그 수정 +**문제**: Radix UI SelectValue의 children prop에 조건부 렌더링 사용 시 Select 상태 관리가 깨짐 + +**해결**: children 제거, placeholder prop으로 이동 +```tsx +// Before (버그) + + {person.name ? `${person.department} / ${person.position} / ${person.name}` : null} + + +// After (수정) + +``` + +#### 2. @/lib/api barrel export +Phase 2.3 자재관리 작업에서 사용하는 import 경로 지원: +```typescript +// src/lib/api/index.ts +export { ApiClient, withTokenRefresh } from './client'; +export { serverFetch } from './fetch-wrapper'; +export { AUTH_CONFIG } from './auth/auth-config'; + +export const apiClient = new ApiClient({ + mode: 'api-key', + apiKey: process.env.API_KEY, +}); +``` + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +--- + +## 2026-01-09 (목) - Phase 2.3 자재관리(품목관리) API 연동 + +### 작업 목표 +- 시공사 페이지 API 연동 계획 Phase 2.3: 자재관리 +- `item-management/actions.ts` Mock 데이터 → 실제 API 연동 + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/item-management/actions.ts` | Mock → API 완전 재작성 | +| `claudedocs/[IMPL-2026-01-09] item-management-api-integration.md` | 구현 문서 | + +### 주요 변경 내용 + +#### 1. 타입 변환 함수 추가 +- `transformItemType()` - Backend item_type → Frontend itemType +- `transformToBackendItemType()` - Frontend itemType → Backend item_type +- `transformSpecification()` - Backend options → Frontend specification +- `transformOrderType()` - Backend options → Frontend orderType +- `transformStatus()` - Backend is_active + options → Frontend status +- `transformOrderItems()` - Backend options → Frontend orderItems +- `transformItem()` - API 응답 → Item 타입 +- `transformItemDetail()` - API 응답 → ItemDetail 타입 +- `transformItemToApi()` - ItemFormData → API 요청 데이터 + +#### 2. 품목유형 매핑 +| Frontend | Backend | +|----------|---------| +| 제품 | FG | +| 부품 | PT | +| 소모품 | CS | +| 공과 | RM | + +#### 3. API 함수 구현 (8개) +- `getItemList()` - GET /api/v1/items +- `getItemStats()` - GET /api/v1/items/stats +- `getItem()` - GET /api/v1/items/{id} +- `createItem()` - POST /api/v1/items +- `updateItem()` - PUT /api/v1/items/{id} +- `deleteItem()` - DELETE /api/v1/items/{id} +- `deleteItems()` - DELETE /api/v1/items/batch +- `getCategoryOptions()` - GET /api/v1/categories + +#### 4. Frontend 전용 필터링 +Backend에서 미지원 필터는 Frontend에서 처리: +- 규격 (specification) +- 구분 (orderType) +- 날짜 범위 (startDate, endDate) +- 정렬 (sortBy) + +### 관련 API 변경 (api 저장소) +- `routes/api.php` - `/items/stats` 라우트 추가 + +### 관련 문서 +- 구현 문서: `claudedocs/[IMPL-2026-01-09] item-management-api-integration.md` + +--- + +## 2025-01-09 (목) - 작업지시 process_type → process_id FK 변환 + +### 작업 목표 +- 작업지시의 `process_type` (varchar enum: 'screen'/'slat'/'bending')를 `process_id` (FK → processes.id)로 변환 +- API와 Frontend 전체 스택 마이그레이션 + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/production/WorkOrders/types.ts` | processId, processName, processCode 필드 추가, transformApiToFrontend에서 processType 하위 호환 유지 | +| `src/components/production/WorkOrders/actions.ts` | getProcessOptions() 추가, createWorkOrder에서 processId 사용 | +| `src/components/production/WorkOrders/WorkOrderCreate.tsx` | processType enum → processId FK 변경, 동적 공정 옵션 로딩 | +| `src/components/production/WorkOrders/WorkOrderList.tsx` | PROCESS_TYPE_LABELS 제거, order.processName 사용 | +| `src/components/production/WorkOrders/WorkOrderDetail.tsx` | PROCESS_TYPE_LABELS 제거, order.processName 사용 (비즈니스 로직은 processType 유지) | + +### 주요 변경 내용 + +#### 1. types.ts - 타입 및 변환 함수 +- `WorkOrder` 인터페이스에 `processId`, `processName`, `processCode` 추가 +- `processType`은 `@deprecated` 마킹, 하위 호환용 유지 +- `transformApiToFrontend`에서 `processName` → `processType` 자동 매핑 + +#### 2. actions.ts - 서버 액션 +- `getProcessOptions()`: 공정 목록 API 조회 (GET /api/v1/processes) +- `createWorkOrder()`: `processId` 필드 사용 (기존 processType 제거) + +#### 3. WorkOrderCreate.tsx - 등록 폼 +- `processType: ProcessType` → `processId: number | null` +- `useEffect`로 공정 옵션 동적 로딩 +- 첫 번째 공정 자동 선택 (기본값) +- Select 컴포넌트 동적 옵션 렌더링 + +#### 4. WorkOrderList.tsx / WorkOrderDetail.tsx - 목록/상세 +- `PROCESS_TYPE_LABELS[order.processType]` → `order.processName` +- 비즈니스 로직(ProcessSteps, 절곡 확인)은 `processType` 유지 + +### 빌드 검증 +✅ Next.js 빌드 성공 (TypeScript 오류 없음) + +### 관련 API 변경 (api 저장소) +- `WorkOrder` 모델: `process_id` FK 추가, `process()` 관계 정의 +- `WorkOrderService`: `process_id` 사용 +- `WorkOrderStoreRequest/UpdateRequest`: `process_id` 검증 규칙 + +--- + +## 2025-01-09 (목) - 작업지시 코드 리뷰 기반 프론트엔드 개선 + +### 작업 목표 +- 작업지시 기능 코드 리뷰 결과 기반 프론트엔드 개선 +- Critical, High, Medium 우선순위 항목 전체 수정 + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/production/WorkOrders/WorkOrderList.tsx` | useCallback 의존성 순환 수정 | +| `src/components/production/WorkOrders/WorkOrderDetail.tsx` | 작업 버튼 핸들러 구현 | +| `src/components/production/WorkOrders/types.ts` | scheduledDate 매핑, 다중 담당자 타입 추가 | +| `src/components/production/WorkOrders/actions.ts` | API 경로 수정 (/sales-orders → /orders) | +| `src/components/production/WorkOrders/SalesOrderSelectModal.tsx` | debounce 적용 | +| `src/components/production/WorkOrders/hooks/useDebounce.ts` | 신규 생성 - 커스텀 debounce 훅 | + +### 주요 변경 내용 +1. **useCallback 의존성 수정**: 무한 루프 방지를 위한 의존성 배열 수정 +2. **scheduledDate 매핑**: transformFrontendToApi에 scheduled_date 필드 추가 +3. **작업 버튼 구현**: "시작"/"완료" 버튼 핸들러 추가 +4. **API 경로 수정**: `/api/v1/sales-orders` → `/api/v1/orders` 변경 +5. **debounce 적용**: 커스텀 useDebounce 훅 (300ms) 적용 +6. **다중 담당자 타입**: WorkOrderAssigneeApi 인터페이스 및 assignees 필드 추가 + +### Git 커밋 +- `12b4259 refactor(work-orders): 코드 리뷰 기반 프론트엔드 개선` + +### 관련 문서 +- 계획: `~/.claude/plans/purring-sparking-pinwheel.md` + +--- + +## 2025-01-02 (목) - 견적 등록 자동산출 기능 구현 ### 작업 목표 - 견적 등록 화면에서 BOM 기반 자동산출 기능 구현 @@ -43,7 +427,7 @@ --- -## 2026-01-02 (목) - 채권현황 동적월 지원 및 버그 수정 +## 2025-01-02 (목) - 채권현황 동적월 지원 및 버그 수정 ### 작업 목표 - "최근 1년" 필터 선택 시 동적 월 기간(최근 12개월) 지원 @@ -244,14 +628,16 @@ React 컴포넌트에서 Mock 데이터를 실제 API 호출로 교체하는 작 - [x] A-5 알림 설정 API 연동 - [x] A-6 거래처 원장 (API 미존재로 스킵) -#### Phase B (진행 중) +#### Phase B (✅ 완료) - [x] B-1 매출관리 (SalesManagement) API 연동 ✅ - [x] B-2 매입관리 (PurchaseManagement) API 연동 ✅ - [x] B-2.1 매입 세금계산서 토글 기능 수정 ✅ -- [ ] B-3 세금계산서 API 연동 -- [ ] B-4 입금관리 API 연동 -- [ ] B-5 출금관리 API 연동 -- [ ] B-6 미수금현황 API 연동 +- [x] B-3 입금관리 (DepositManagement) API 연동 ✅ +- [x] B-4 출금관리 (WithdrawalManagement) API 연동 ✅ +- [x] B-5 거래처관리 (VendorManagement) API 연동 ✅ +- [x] B-6 어음관리 (BillManagement) API 연동 ✅ + +> **참고**: 원본 계획 문서(`docs/plans/react-mock-to-api-migration-plan.md`)의 Phase B 정의와 일치하도록 수정함 --- @@ -388,8 +774,72 @@ useEffect(() => { --- +#### Phase C (✅ 완료) +- [x] C-1 직원관리 (EmployeeManagement) API 연동 ✅ +- [x] C-2 근태관리 (AttendanceManagement) API 연동 ✅ +- [x] C-3 휴가관리 (VacationManagement) API 연동 ✅ + +#### Phase D (✅ 완료) - 설정/시스템 +- [x] D-1 부서관리 (DepartmentManagement) API 연동 ✅ +- [x] D-2 직급관리 (RankManagement) API 연동 ✅ +- [x] D-3 직책관리 (TitleManagement) API 연동 ✅ +- [x] D-4 근무시간설정 (WorkScheduleManagement) API 연동 ✅ + +#### Phase E (✅ 완료) - 인사/급여 +- [x] E-1 급여관리 (SalaryManagement) API 연동 ✅ +- [x] E-2 카드관리 (CardManagement) API 연동 ✅ + +#### Phase F (✅ 완료) - 결재시스템 +- [x] F-1 기안함 (DraftBox) API 연동 ✅ +- [x] F-2 결재함 (ApprovalBox) API 연동 ✅ +- [x] F-3 참조함 (ReferenceBox) API 연동 ✅ +- [x] F-4 문서작성 (DocumentCreate) API 연동 ✅ + +#### Phase G (✅ 완료) - 생산관리 +- [x] G-1 작업지시 (WorkOrders) API 연동 ✅ +- [x] G-2 작업실적 (WorkResults) API 연동 ✅ +- [x] G-3 작업자화면 (WorkerScreen) API 연동 ✅ +- [x] G-4 생산현황 (ProductionDashboard) API 연동 ✅ + +#### Phase H (✅ 완료) - 자재/출하 +- [x] H-1 재고현황 (StockStatus) API 연동 ✅ +- [x] H-2 입고관리 (ReceivingManagement) API 연동 ✅ +- [x] H-3 출하관리 (ShipmentManagement) API 연동 ✅ + +#### Phase I (✅ 완료) - 판매/견적 +- [x] I-1 수주관리 (Orders) API 연동 ✅ +- [x] I-2 단가관리 (Pricing) API 연동 ✅ +- [x] I-3 견적관리 (Quotes) API 연동 ✅ + +#### Phase J (✅ 완료) - 회계관리 +- [x] 악성채권, 계좌조회, 어음관리, 카드거래 등 13개 모듈 API 연동 ✅ + +#### Phase K (✅ 완료) - 보고서 +- [x] K-1 종합분석 (Reports) API 연동 ✅ + +#### Phase L (🔄 진행중 ~80%) - 건설관리 +**✅ apiClient 표준화 완료:** +- [x] handover-report (b7b8b90) +- [x] contract (5db6e59) +- [x] partners (5db6e59) +- [x] site-management (5db6e59) +- [x] order-management (6615f39) +- [x] item-management (Phase 2.3) +- [x] pricing-management (Phase L) ✅ 2026-01-09 +- [x] estimates (Phase L) ✅ 2026-01-09 +- [x] category-management (Phase L) ✅ 2026-01-09 + +**⏳ Mock → API 변환 필요 (Backend API 개발 필요):** +- [ ] bidding - 입찰관리 +- [ ] site-briefings - 현장설명회 +- [ ] structure-review - 구조검토 +- [ ] labor-management - 노무관리 + +> **마이그레이션 진행률**: 97% 완료 (41/43 모듈) - 건설관리 4개 모듈 Backend API 개발 필요 +> **점검일**: 2026-01-09 + ### 다음 작업 -- B-3 세금계산서 API 연동 -- B-4 ~ B-6 회계관리 나머지 컴포넌트 +- Phase L 건설관리 모듈 마이그레이션 완료 (Backend API 개발 필요: bidding, site-briefings, structure-review, labor-management) +- ~~TODO-1: 결재선/참조 Select 변경 불가 문제~~ ✅ 2026-01-09 수정 완료 --- diff --git a/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md b/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md new file mode 100644 index 00000000..e9f459e0 --- /dev/null +++ b/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md @@ -0,0 +1,154 @@ +# [IMPL-2026-01-09] 자재관리(품목관리) API 연동 + +## 작업 개요 +- **작업자**: Claude Code +- **작업일**: 2026-01-09 +- **Phase**: 2.3 자재관리 (시공사 페이지 API 연동 계획) +- **이전 Phase**: 2.2 거래처관리 완료 + +## 변경 사항 요약 + +### Backend (api/) + +#### 1. 라우트 추가 +**파일**: `routes/api.php` + +```php +// Items (통합 품목 관리 - items 테이블) +Route::prefix('items')->group(function () { + Route::get('', [ItemsController::class, 'index'])->name('v1.items.index'); + Route::get('/stats', [ItemsController::class, 'stats'])->name('v1.items.stats'); // 신규 + Route::post('', [ItemsController::class, 'store'])->name('v1.items.store'); + Route::get('/code/{code}', [ItemsController::class, 'showByCode'])->name('v1.items.show_by_code'); + Route::get('/{id}', [ItemsController::class, 'show'])->name('v1.items.show'); + Route::put('/{id}', [ItemsController::class, 'update'])->name('v1.items.update'); + Route::delete('/batch', [ItemsController::class, 'batchDestroy'])->name('v1.items.batch_destroy'); + Route::delete('/{id}', [ItemsController::class, 'destroy'])->name('v1.items.destroy'); +}); +``` + +**중요**: `/stats` 라우트는 `/{id}` 보다 먼저 정의하여 "stats"가 ID로 캡처되는 것을 방지 + +### Frontend (react/) + +#### 1. actions.ts 완전 재작성 +**파일**: `src/components/business/construction/item-management/actions.ts` + +**변경 전**: Mock 데이터 기반 (mockItems, mockOrderItems 배열) +**변경 후**: 실제 API 연동 + +#### 주요 구현 내용 + +##### 타입 변환 함수 +| 함수명 | 용도 | +|--------|------| +| `transformItemType()` | Backend item_type → Frontend itemType | +| `transformToBackendItemType()` | Frontend itemType → Backend item_type | +| `transformSpecification()` | Backend options → Frontend specification | +| `transformOrderType()` | Backend options → Frontend orderType | +| `transformStatus()` | Backend is_active + options → Frontend status | +| `transformOrderItems()` | Backend options → Frontend orderItems | +| `transformItem()` | API 응답 → Item 타입 | +| `transformItemDetail()` | API 응답 → ItemDetail 타입 | +| `transformItemToApi()` | ItemFormData → API 요청 데이터 | + +##### 품목 유형 매핑 +| Frontend (Korean) | Backend (Code) | +|-------------------|----------------| +| 제품 | FG | +| 부품 | PT | +| 소모품 | CS (또는 SM) | +| 공과 | RM | + +##### API 함수 +| 함수명 | API Endpoint | 설명 | +|--------|-------------|------| +| `getItemList()` | GET /api/v1/items | 품목 목록 조회 | +| `getItemStats()` | GET /api/v1/items/stats | 품목 통계 조회 | +| `getItem()` | GET /api/v1/items/{id} | 품목 상세 조회 | +| `createItem()` | POST /api/v1/items | 품목 등록 | +| `updateItem()` | PUT /api/v1/items/{id} | 품목 수정 | +| `deleteItem()` | DELETE /api/v1/items/{id} | 품목 삭제 | +| `deleteItems()` | DELETE /api/v1/items/batch | 품목 일괄 삭제 | +| `getCategoryOptions()` | GET /api/v1/categories | 카테고리 목록 조회 | + +##### Frontend 전용 필터링 +Backend에서 지원하지 않는 필터는 Frontend에서 처리: +- 규격 (specification) 필터 +- 구분 (orderType) 필터 +- 날짜 범위 (startDate, endDate) 필터 +- 정렬 (sortBy: latest/oldest) + +## 필드 매핑 상세 + +### Item 기본 필드 +| Frontend | Backend | 변환 방식 | +|----------|---------|----------| +| id | id | String 변환 | +| itemNumber | code | 직접 매핑 | +| itemName | name | 직접 매핑 | +| itemType | item_type | transformItemType() | +| categoryId | category_id | String 변환 | +| categoryName | category.name | nested 접근 | +| unit | unit | 직접 매핑 (기본값: EA) | +| specification | options.specification | transformSpecification() | +| orderType | options.orderType | transformOrderType() | +| status | is_active + options.status | transformStatus() | +| createdAt | created_at | 직접 매핑 | +| updatedAt | updated_at | 직접 매핑 | + +### ItemDetail 추가 필드 +| Frontend | Backend | 변환 방식 | +|----------|---------|----------| +| note | description | 직접 매핑 | +| orderItems | options.orderItems | transformOrderItems() | + +## 테스트 체크리스트 + +### API 연동 확인 +- [ ] 품목 목록 조회 (GET /items) +- [ ] 품목 통계 조회 (GET /items/stats) +- [ ] 품목 상세 조회 (GET /items/{id}) +- [ ] 품목 등록 (POST /items) +- [ ] 품목 수정 (PUT /items/{id}) +- [ ] 품목 삭제 (DELETE /items/{id}) +- [ ] 품목 일괄 삭제 (DELETE /items/batch) +- [ ] 카테고리 목록 조회 (GET /categories) + +### 필터링 확인 +- [ ] 검색 필터 (search → q) +- [ ] 품목유형 필터 (itemType → type) +- [ ] 카테고리 필터 (categoryId → category_id) +- [ ] 활성상태 필터 (status → active) +- [ ] 규격 필터 (Frontend only) +- [ ] 구분 필터 (Frontend only) +- [ ] 날짜 필터 (Frontend only) + +### 데이터 변환 확인 +- [ ] 품목유형 한글 ↔ 코드 변환 +- [ ] 상태값 변환 (is_active ↔ status) +- [ ] options JSON 필드 파싱/생성 + +## 관련 파일 + +### 수정된 파일 +1. `api/routes/api.php` - /items/stats 라우트 추가 +2. `react/src/components/business/construction/item-management/actions.ts` - Mock → API 변환 + +### 참조 파일 +- `api/app/Http/Controllers/Api/V1/ItemsController.php` +- `api/app/Services/ItemService.php` +- `react/src/components/business/construction/item-management/types.ts` +- `react/src/lib/api.ts` + +## 다음 단계 + +### Phase 2.4 예정 +- 자재관리 (품목관리) UI 컴포넌트 연동 테스트 +- 에러 핸들링 개선 +- 로딩 상태 처리 + +### 향후 개선 사항 +- Backend에서 추가 필터 지원 시 Frontend 필터 제거 +- options 필드 구조 표준화 +- 품목 일괄 등록 API 추가 고려 \ No newline at end of file diff --git a/claudedocs/changes/20250108_order_frontend_api_integration.md b/claudedocs/changes/20250108_order_frontend_api_integration.md new file mode 100644 index 00000000..fabe8c51 --- /dev/null +++ b/claudedocs/changes/20250108_order_frontend_api_integration.md @@ -0,0 +1,92 @@ +# 수주 관리 Frontend API 연동 + +**날짜:** 2025-01-08 +**Phase:** Phase 2 - Frontend 연동 +**관련 Plan:** docs/plans/order-management-plan.md + +## 변경 개요 + +수주 관리 React 페이지들을 백엔드 API와 연동 완료. Mock 데이터를 제거하고 실제 API 호출로 대체. + +## 수정된 파일 + +### 1. `src/components/orders/actions.ts` (신규 생성) +- Server Actions 패턴으로 API 클라이언트 구현 +- 주요 함수: + - `getOrders()`: 수주 목록 조회 + - `getOrderById(id)`: 수주 상세 조회 + - `createOrder(data)`: 수주 등록 + - `updateOrder(id, data)`: 수주 수정 + - `deleteOrder(id)`: 수주 삭제 + - `deleteOrders(ids)`: 수주 일괄 삭제 + - `updateOrderStatus(id, status)`: 수주 상태 변경 + - `getOrderStats()`: 통계 조회 +- 데이터 변환: API snake_case → Frontend camelCase +- 상태 매핑: API 상태(DRAFT, CONFIRMED 등) → Frontend 상태(order_registered, order_confirmed 등) + +### 2. `src/components/orders/index.ts` (수정) +- actions.ts export 추가 +- 타입 충돌 해결 (OrderItem → OrderItemApi) + +### 3. `src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` (수정) +- SAMPLE_ORDERS (~115줄) 제거 +- API 연동 state 추가: `orders`, `apiStats`, `isLoading`, `isDeleting` +- `loadData()` 함수로 API 호출 (getOrders, getOrderStats) +- 삭제 핸들러에 API 호출 추가 (deleteOrder, deleteOrders) +- 로딩 UI 추가 + +### 4. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` (수정) +- SAMPLE_ITEMS, SAMPLE_ORDERS (~250줄) 제거 +- useEffect에서 getOrderById API 호출 +- handleConfirmCancel에서 updateOrderStatus API 호출 +- isCancelling 로딩 상태 적용 + +### 5. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (수정) +- SAMPLE_ORDER (~50줄) 제거 +- useEffect에서 getOrderById API 호출 +- handleSave에서 updateOrder API 호출 + +### 6. `src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` (수정) +- handleSave에서 createOrder API 호출 + +## 기술 패턴 + +### Server Actions 패턴 +```typescript +"use server"; +import { serverFetch } from "@/lib/api/serverFetch"; + +export async function getOrders() { + const response = await serverFetch("/orders"); + // 데이터 변환 로직 +} +``` + +### 데이터 변환 +- API: `order_no`, `client_name`, `site_name` +- Frontend: `orderNo`, `clientName`, `siteName` + +### 상태 매핑 +| API | Frontend | +|-----|----------| +| DRAFT | order_registered | +| CONFIRMED | order_confirmed | +| IN_PROGRESS | production_ordered | +| COMPLETED | shipped | +| CANCELLED | cancelled | + +## 테스트 체크리스트 + +- [ ] 수주 목록 로드 +- [ ] 수주 상세 조회 +- [ ] 수주 등록 (견적 선택 후) +- [ ] 수주 수정 +- [ ] 수주 개별 삭제 +- [ ] 수주 일괄 삭제 +- [ ] 수주 취소 +- [ ] 통계 카드 표시 + +## 연관 작업 + +- Phase 1: Order API 백엔드 구현 (커밋: de19ac9) +- Phase 1.1: OrderController/Service 구현 (진행 중) diff --git a/claudedocs/changes/20250108_order_phase3_advanced_features.md b/claudedocs/changes/20250108_order_phase3_advanced_features.md new file mode 100644 index 00000000..208d7ad4 --- /dev/null +++ b/claudedocs/changes/20250108_order_phase3_advanced_features.md @@ -0,0 +1,113 @@ +# 수주 관리 Phase 3 - 고급 기능 + +**날짜:** 2025-01-08 +**Phase:** Phase 3 - 고급 기능 +**관련 Plan:** docs/plans/order-management-plan.md + +## 변경 개요 + +수주 관리 시스템에 견적→수주 변환 및 생산지시 생성 기능 추가. + +## API 추가 사항 + +### 1. 견적에서 수주 생성 +- **Endpoint**: `POST /api/v1/orders/from-quote/{quoteId}` +- **기능**: 기존 견적서를 기반으로 수주를 자동 생성 +- **검증**: 이미 수주가 생성된 견적은 중복 생성 방지 + +### 2. 생산지시 생성 +- **Endpoint**: `POST /api/v1/orders/{id}/production-order` +- **기능**: 확정된 수주에서 작업지시(WorkOrder) 생성 +- **검증**: CONFIRMED 상태의 수주만 생산지시 가능 + +## 수정된 파일 + +### API (Laravel) + +#### 1. `app/Services/OrderService.php` +- `createFromQuote(int $quoteId, array $data)`: 견적→수주 변환 로직 +- `createProductionOrder(int $orderId, array $data)`: 생산지시 생성 로직 +- `generateWorkOrderNo(int $tenantId)`: 작업지시번호 자동 생성 + +#### 2. `app/Http/Controllers/Api/V1/OrderController.php` +- `createFromQuote()`: 견적→수주 액션 +- `createProductionOrder()`: 생산지시 생성 액션 + +#### 3. `app/Http/Requests/Order/CreateFromQuoteRequest.php` (신규) +- 견적→수주 변환 요청 검증 +- 선택 필드: delivery_date, memo + +#### 4. `app/Http/Requests/Order/CreateProductionOrderRequest.php` (신규) +- 생산지시 생성 요청 검증 +- 선택 필드: process_type, assignee_id, team_id, scheduled_date, memo + +#### 5. `routes/api.php` +- `POST /orders/from-quote/{quoteId}`: 견적→수주 라우트 +- `POST /orders/{id}/production-order`: 생산지시 라우트 + +#### 6. `lang/ko/message.php` +- `order.created_from_quote`: 견적에서 수주가 생성되었습니다. +- `order.production_order_created`: 생산지시가 생성되었습니다. + +#### 7. `lang/ko/error.php` +- `order.already_created_from_quote`: 이미 해당 견적에서 수주가 생성되었습니다. +- `order.must_be_confirmed_for_production`: 확정 상태의 수주만 생산지시를 생성할 수 있습니다. +- `order.production_order_already_exists`: 이미 생산지시가 존재합니다. +- `quote.not_found`: 견적을 찾을 수 없습니다. + +### Frontend (React) + +#### 1. `src/components/orders/actions.ts` +- 타입 추가: `CreateFromQuoteData`, `CreateProductionOrderData`, `WorkOrder`, `ProductionOrderResult` +- API 인터페이스 추가: `ApiWorkOrder`, `ApiProductionOrderResponse` +- `createOrderFromQuote(quoteId, data)`: 견적→수주 API 호출 +- `createProductionOrder(orderId, data)`: 생산지시 생성 API 호출 +- `transformWorkOrderApiToFrontend()`: WorkOrder 데이터 변환 + +## 비즈니스 로직 + +### 견적→수주 변환 흐름 +``` +Quote (견적) + ↓ createFromQuote() +Order (수주) - DRAFT 상태 + - quote_id 연결 + - client, site_name 복사 + - items 변환 (quantity=calculated_quantity) + - 금액 재계산 +``` + +### 생산지시 생성 흐름 +``` +Order (수주) - CONFIRMED 상태 + ↓ createProductionOrder() +WorkOrder (작업지시) - PENDING 상태 + - sales_order_id 연결 + - project_name = site_name + - process_type 설정 + ↓ +Order 상태 → IN_PROGRESS +``` + +### 상태 전환 규칙 (기존) +``` +DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED + ↓ ↓ ↓ +CANCELLED (어느 단계에서든 취소 가능) +``` + +## 테스트 체크리스트 + +- [ ] 견적→수주 생성 (정상 케이스) +- [ ] 견적→수주 생성 (중복 방지) +- [ ] 견적→수주 생성 (존재하지 않는 견적) +- [ ] 생산지시 생성 (정상 케이스) +- [ ] 생산지시 생성 (CONFIRMED 아닌 수주) +- [ ] 생산지시 생성 (중복 방지) +- [ ] 수주 상태 자동 변경 (CONFIRMED → IN_PROGRESS) + +## 연관 작업 + +- Phase 1: Order API 백엔드 구현 (커밋: de19ac9) +- Phase 2: Frontend API 연동 (커밋: 572ffe8) +- Phase 3: 고급 기능 (현재) diff --git a/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md b/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md new file mode 100644 index 00000000..4e75315b --- /dev/null +++ b/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md @@ -0,0 +1,117 @@ +# Phase 2.2 거래처관리 API 연동 + +**날짜**: 2026-01-09 +**작업**: 거래처관리 Mock → API 연동 + +## 개요 + +시공사 페이지 API 연동 계획 Phase 2.2 - 거래처관리(partners) API 연동 완료. + +## 변경 사항 + +### Backend (API) + +#### 1. 서비스 (ClientService.php) +- `stats()` - 거래처 통계 조회 (신규) + - total: 전체 거래처 수 + - sales: 판매 거래처 (client_type='SALES') + - purchase: 구매 거래처 (client_type='PURCHASE') + - both: 판매/구매 거래처 (client_type='BOTH') + - badDebt: 악성채권 보유 거래처 수 + - normal: 정상 거래처 수 +- `bulkDestroy()` - 일괄 삭제 (신규) + - 주문 존재 시 해당 거래처는 건너뜀 + +#### 2. 컨트롤러 (ClientController.php) +- `stats()` - GET /api/v1/clients/stats +- `bulkDestroy()` - DELETE /api/v1/clients/bulk + +#### 3. 라우트 (api.php) +```php +Route::get('/stats', [ClientController::class, 'stats']); +Route::delete('/bulk', [ClientController::class, 'bulkDestroy']); +``` + +### Frontend (React) + +#### 1. actions.ts +- Mock 데이터 제거 (mockPartners 배열) +- API 연동 구현 + - `getPartnerList()` - GET /api/v1/clients + - `getPartner()` - GET /api/v1/clients/{id} + - `createPartner()` - POST /api/v1/clients + - `updatePartner()` - PUT /api/v1/clients/{id} + - `getPartnerStats()` - GET /api/v1/clients/stats + - `deletePartner()` - DELETE /api/v1/clients/{id} + - `deletePartners()` - DELETE /api/v1/clients/bulk + +#### 2. 변환 함수 +- `transformClientType()` - client_type → partnerType 변환 +- `transformPartnerType()` - partnerType → client_type 변환 +- `transformPartner()` - API 응답 → Partner 타입 변환 +- `transformPartnerToApi()` - PartnerFormData → API 요청 데이터 변환 + +## API 매핑 + +| Frontend | Backend | 비고 | +|----------|---------|------| +| id | id | string ↔ int | +| partnerCode | client_code | 자동 생성 | +| businessNumber | business_no | | +| partnerName | name | | +| representative | contact_person | | +| partnerType | client_type | sales/SALES, purchase/PURCHASE, both/BOTH | +| businessType | business_type | | +| businessCategory | business_item | | +| address1 | address | | +| phone | phone | | +| mobile | mobile | | +| fax | fax | | +| email | email | | +| manager | manager_name | | +| managerPhone | manager_tel | | +| systemManager | system_manager | | +| outstandingAmount | outstanding_amount | 계산 필드 (매출-입금) | +| overdueToggle | is_overdue | | +| isBadDebt | has_bad_debt | 계산 필드 | +| isActive | is_active | | +| createdAt | created_at | | +| updatedAt | updated_at | | + +### Frontend 전용 필드 (기본값 사용) +- zipCode, address2: '' +- logoUrl, logoBlob: null +- salesPaymentDay, paymentDay: 0 +- creditRating, transactionGrade: '' +- memos, documents: [] +- category: '' +- overdueDays: is_overdue ? 30 : 0 + +## 설계 결정 + +### 기존 Client API 재사용 +- `/api/v1/clients` 기존 엔드포인트 확장 사용 +- 별도의 `/api/v1/construction/partners` 생성하지 않음 +- accounting/vendors 와 construction/partners 모두 Client API 사용 + +### 악성채권 통계 +- BadDebt 테이블과 연계하여 악성채권 보유 거래처 수 계산 +- 상태가 '추심중' 또는 '법적조치'인 활성 악성채권만 카운트 + +### 필터링 전략 +- 검색(`q`): API에서 처리 (name, client_code, contact_person) +- 악성채권 필터: 프론트엔드에서 처리 (API 전체 반환 후 필터) +- 정렬: 프론트엔드에서 처리 (API 기본 정렬 사용) + +## 진행률 + +시공사 API 연동: 4/9 (44%) +- [x] Phase 1.1 견적관리 +- [x] Phase 1.2 인수인계보고서관리 +- [x] Phase 2.1 현장관리 +- [x] Phase 2.2 거래처관리 ← 현재 완료 +- [ ] Phase 2.3 자재관리 +- [ ] Phase 3.1 발주관리 +- [ ] Phase 3.2 재고관리 +- [ ] Phase 4.1 정산관리 +- [ ] Phase 4.2 급여관리 \ No newline at end of file diff --git a/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md b/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md new file mode 100644 index 00000000..307362d8 --- /dev/null +++ b/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md @@ -0,0 +1,90 @@ +# Phase 2.1 현장관리 API 연동 + +**날짜**: 2026-01-09 +**작업**: 현장관리 Mock → API 연동 + +## 개요 + +시공사 페이지 API 연동 계획 Phase 2.1 - 현장관리(site-management) API 연동 완료. + +## 변경 사항 + +### Backend (API) + +#### 1. 마이그레이션 +- `2026_01_09_162534_add_construction_fields_to_sites_table.php` + - `site_code` (VARCHAR 50) - 현장코드 + - `client_id` (FK → clients) - 거래처 연결 + - `status` (ENUM) - unregistered/suspended/active/pending + - 인덱스: tenant_id + site_code, tenant_id + status + +#### 2. 모델 (Site.php) +- 상태 상수 추가: STATUS_UNREGISTERED, STATUS_SUSPENDED, STATUS_ACTIVE, STATUS_PENDING +- fillable 확장: site_code, client_id, status +- Client 관계 추가 + +#### 3. 서비스 (SiteService.php) +- `index()` - 필터 확장 (status, client_id, start_date, end_date) +- `stats()` - 상태별 통계 조회 (신규) +- `bulkDestroy()` - 일괄 삭제 (신규) + +#### 4. 컨트롤러 (SiteController.php) +- `stats()` - GET /api/v1/sites/stats +- `bulkDestroy()` - DELETE /api/v1/sites/bulk + +#### 5. 라우트 (api.php) +```php +Route::get('/stats', [SiteController::class, 'stats']); +Route::delete('/bulk', [SiteController::class, 'bulkDestroy']); +``` + +### Frontend (React) + +#### 1. types.ts +- SiteStats에 suspended, pending 필드 추가 + +#### 2. actions.ts +- Mock 데이터 제거 +- API 연동 구현 + - `getSiteList()` - GET /api/v1/sites + - `getSiteStats()` - GET /api/v1/sites/stats + - `deleteSite()` - DELETE /api/v1/sites/{id} + - `deleteSites()` - DELETE /api/v1/sites/bulk + +## API 매핑 + +| Frontend | Backend | 비고 | +|----------|---------|------| +| id | id | string ↔ int | +| siteCode | site_code | | +| partnerId | client_id | | +| partnerName | client.name | 관계 eager load | +| siteName | name | | +| address | address | | +| status | status | 동일 | +| createdAt | created_at | | +| updatedAt | updated_at | | + +## 설계 결정 + +### is_active vs status +- `is_active` (boolean): 사용 여부 (활성화/비활성화) +- `status` (enum): 상태값 (미등록/중지/사용/보류) +- 두 필드는 다른 용도로 둘 다 유지 + +### 기존 API 활용 +- `/api/v1/sites` 기존 엔드포인트 확장 사용 +- `/api/v1/construction/sites` 별도 생성하지 않음 + +## 진행률 + +시공사 API 연동: 3/9 (33%) +- [x] Phase 1.1 견적관리 +- [x] Phase 1.2 인수인계보고서관리 +- [x] Phase 2.1 현장관리 ← 현재 완료 +- [ ] Phase 2.2 거래처관리 +- [ ] Phase 2.3 자재관리 +- [ ] Phase 3.1 발주관리 +- [ ] Phase 3.2 재고관리 +- [ ] Phase 4.1 정산관리 +- [ ] Phase 4.2 급여관리 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d748cda5..fb9fcfed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,9 +116,9 @@ "license": "MIT" }, "node_modules/@emnapi/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", - "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "dev": true, "license": "MIT", "optional": true, @@ -128,9 +128,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", - "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "license": "MIT", "optional": true, "dependencies": { @@ -149,9 +149,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -232,9 +232,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -244,7 +244,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -256,9 +256,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -465,9 +465,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -483,13 +483,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -505,13 +505,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -541,9 +541,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -557,9 +557,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -573,9 +573,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -588,10 +588,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -605,9 +621,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -621,9 +637,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -637,9 +653,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -653,9 +669,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -671,13 +687,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -693,13 +709,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -715,13 +731,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -737,13 +775,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -759,13 +797,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -781,13 +819,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -803,20 +841,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.5.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -826,9 +864,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -845,9 +883,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -864,9 +902,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -952,9 +990,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz", - "integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.9.tgz", + "integrity": "sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1137,6 +1175,313 @@ "node": ">=12.4.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -1193,29 +1538,6 @@ } } }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1257,47 +1579,6 @@ } } }, - "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", @@ -1328,47 +1609,6 @@ } } }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", @@ -1399,47 +1639,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1466,29 +1665,6 @@ } } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1573,29 +1749,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1656,47 +1809,6 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dropdown-menu": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", @@ -1726,47 +1838,6 @@ } } }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1807,47 +1878,6 @@ } } }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1889,6 +1919,29 @@ } } }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", @@ -1929,29 +1982,6 @@ } } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1971,24 +2001,24 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", - "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -2007,260 +2037,13 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -2272,114 +2055,6 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", - "license": "MIT" - }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -2412,47 +2087,6 @@ } } }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", @@ -2477,47 +2111,6 @@ } } }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -2543,12 +2136,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.4" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -2565,6 +2158,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-progress": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", @@ -2604,6 +2215,29 @@ } } }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", @@ -2636,47 +2270,6 @@ } } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -2708,47 +2301,6 @@ } } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-scroll-area": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", @@ -2780,47 +2332,6 @@ } } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", @@ -2864,29 +2375,6 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -2938,47 +2426,6 @@ } } }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -3026,47 +2473,6 @@ } } }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", @@ -3097,47 +2503,6 @@ } } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -3172,29 +2537,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -3372,47 +2714,6 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", @@ -3420,14 +2721,14 @@ "license": "MIT" }, "node_modules/@reduxjs/toolkit": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", - "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", - "immer": "^10.2.0", + "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" @@ -3445,16 +2746,6 @@ } } }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -3469,9 +2760,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz", - "integrity": "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", "dev": true, "license": "MIT" }, @@ -3482,9 +2773,9 @@ "license": "MIT" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@standard-schema/utils": { @@ -3493,6 +2784,172 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz", + "integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz", + "integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz", + "integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz", + "integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz", + "integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz", + "integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz", + "integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz", + "integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz", + "integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz", + "integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3502,10 +2959,19 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", - "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3513,39 +2979,39 @@ "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", - "magic-string": "^0.30.19", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.16" + "tailwindcss": "4.1.18" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", - "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-x64": "4.1.16", - "@tailwindcss/oxide-freebsd-x64": "4.1.16", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-x64-musl": "4.1.16", - "@tailwindcss/oxide-wasm32-wasi": "4.1.16", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", - "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "cpu": [ "arm64" ], @@ -3560,9 +3026,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", - "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "cpu": [ "arm64" ], @@ -3577,9 +3043,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", - "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "cpu": [ "x64" ], @@ -3594,9 +3060,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", - "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "cpu": [ "x64" ], @@ -3611,9 +3077,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", - "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "cpu": [ "arm" ], @@ -3628,9 +3094,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", - "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" ], @@ -3645,9 +3111,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", - "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" ], @@ -3662,9 +3128,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", - "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -3679,9 +3145,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", - "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -3696,9 +3162,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", - "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3714,10 +3180,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", + "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, @@ -3726,9 +3192,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", - "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ "arm64" ], @@ -3743,9 +3209,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", - "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" ], @@ -3760,62 +3226,62 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz", - "integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.16", - "@tailwindcss/oxide": "4.1.16", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", - "tailwindcss": "4.1.16" + "tailwindcss": "4.1.18" } }, "node_modules/@tiptap/core": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.13.0.tgz", - "integrity": "sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz", + "integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.13.0" + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.13.0.tgz", - "integrity": "sha512-K1z/PAIIwEmiWbzrP//4cC7iG1TZknDlF1yb42G7qkx2S2X4P0NiqX7sKOej3yqrPjKjGwPujLMSuDnCF87QkQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.15.3.tgz", + "integrity": "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.13.0.tgz", - "integrity": "sha512-VYiDN9EEwR6ShaDLclG8mphkb/wlIzqfk7hxaKboq1G+NSDj8PcaSI9hldKKtTCLeaSNu6UR5nkdu/YHdzYWTw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.15.3.tgz", + "integrity": "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.13.0.tgz", - "integrity": "sha512-qZ3j2DBsqP9DjG2UlExQ+tHMRhAnWlCKNreKddKocb/nAFrPdBCtvkqIEu+68zPlbLD4ukpoyjUklRJg+NipFg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.15.3.tgz", + "integrity": "sha512-e88DG1bTy6hKxrt7iPVQhJnH5/EOrnKpIyp09dfRDgWrrW88fE0Qjys7a/eT8W+sXyXM3z10Ye7zpERWsrLZDg==", "license": "MIT", "optional": true, "dependencies": { @@ -3826,80 +3292,80 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.13.0.tgz", - "integrity": "sha512-fFQmmEUoPzRGiQJ/KKutG35ZX21GE+1UCDo8Q6PoWH7Al9lex47nvyeU1BiDYOhcTKgIaJRtEH5lInsOsRJcSA==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.15.3.tgz", + "integrity": "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.13.0" + "@tiptap/extension-list": "^3.15.3" } }, "node_modules/@tiptap/extension-code": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.13.0.tgz", - "integrity": "sha512-sF5raBni6iSVpXWvwJCAcOXw5/kZ+djDHx1YSGWhopm4+fsj0xW7GvVO+VTwiFjZGKSw+K5NeAxzcQTJZd3Vhw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.15.3.tgz", + "integrity": "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.13.0.tgz", - "integrity": "sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.15.3.tgz", + "integrity": "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-document": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.13.0.tgz", - "integrity": "sha512-RjU7hTJwjKXIdY57o/Pc+Yr8swLkrwT7PBQ/m+LCX5oO/V2wYoWCjoBYnK5KSHrWlNy/aLzC33BvLeqZZ9nzlQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.15.3.tgz", + "integrity": "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.13.0.tgz", - "integrity": "sha512-m7GPT3c/83ni+bbU8c+3dpNa8ug+aQ4phNB1Q52VQG3oTonDJnZS7WCtn3lB/Hi1LqoqMtEHwhepU2eD+JeXqQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.15.3.tgz", + "integrity": "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.13.0" + "@tiptap/extensions": "^3.15.3" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.13.0.tgz", - "integrity": "sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.15.3.tgz", + "integrity": "sha512-+3DVBleKKffadEJEdLYxmYAJOjHjLSqtiSFUE3RABT4V2ka1ODy2NIpyKX0o1SvQ5N1jViYT9Q+yUbNa6zCcDw==", "license": "MIT", "optional": true, "funding": { @@ -3908,93 +3374,93 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.13.0.tgz", - "integrity": "sha512-KVxjQKkd964nin+1IdM2Dvej/Jy4JTMcMgq5seusUhJ9T9P8F9s2D5Iefwgkps3OCzub/aF+eAsZe+1P5KSIgA==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.15.3.tgz", + "integrity": "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.13.0" + "@tiptap/extensions": "^3.15.3" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.13.0.tgz", - "integrity": "sha512-nH1OBaO+/pakhu+P1jF208mPgB70IKlrR/9d46RMYoYbqJTNf4KVLx5lHAOHytIhjcNg+MjyTfJWfkK+dyCCyg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.15.3.tgz", + "integrity": "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.13.0.tgz", - "integrity": "sha512-8VKWX8waYPtUWN97J89em9fOtxNteh6pvUEd0htcOAtoxjt2uZjbW5N4lKyWhNKifZBrVhH2Cc2NUPuftCVgxw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.15.3.tgz", + "integrity": "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.13.0.tgz", - "integrity": "sha512-ZUFyORtjj22ib8ykbxRhWFQOTZjNKqOsMQjaAGof30cuD2DN5J5pMz7Haj2fFRtLpugWYH+f0Mi+WumQXC3hCw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.15.3.tgz", + "integrity": "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-image": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.13.0.tgz", - "integrity": "sha512-223uzLUkIa1rkK7aQK3AcIXe6LbCtmnpVb7sY5OEp+LpSaSPyXwyrZ4A0EO1o98qXG68/0B2OqMntFtA9c5Fbw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.15.3.tgz", + "integrity": "sha512-Tjq9BHlC/0bGR9/uySA0tv6I1Ua1Q5t5P/mdbWyZi4JdUpKHRfgenzfXF5DYnklJ01QJ7uOPSp9sAGgPzBixtQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.13.0.tgz", - "integrity": "sha512-XbVTgmzk1kgUMTirA6AGdLTcKHUvEJoh3R4qMdPtwwygEOe7sBuvKuLtF6AwUtpnOM+Y3tfWUTNEDWv9AcEdww==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.15.3.tgz", + "integrity": "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-link": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.13.0.tgz", - "integrity": "sha512-LuFPJ5GoL12GHW4A+USsj60O90pLcwUPdvEUSWewl9USyG6gnLnY/j5ZOXPYH7LiwYW8+lhq7ABwrDF2PKyBbA==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.15.3.tgz", + "integrity": "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg==", "license": "MIT", "dependencies": { "linkifyjs": "^4.3.2" @@ -4004,159 +3470,159 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-list": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.13.0.tgz", - "integrity": "sha512-MMFH0jQ4LeCPkJJFyZ77kt6eM/vcKujvTbMzW1xSHCIEA6s4lEcx9QdZMPpfmnOvTzeoVKR4nsu2t2qT9ZXzAw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz", + "integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.13.0.tgz", - "integrity": "sha512-63NbcS/XeQP2jcdDEnEAE3rjJICDj8y1SN1h/MsJmSt1LusnEo8WQ2ub86QELO6XnD3M04V03cY6Knf6I5mTkw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.15.3.tgz", + "integrity": "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.13.0" + "@tiptap/extension-list": "^3.15.3" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.13.0.tgz", - "integrity": "sha512-P+HtIa1iwosb1feFc8B/9MN5EAwzS+/dZ0UH0CTF2E4wnp5Z9OMxKl1IYjfiCwHzZrU5Let+S/maOvJR/EmV0g==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.15.3.tgz", + "integrity": "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.13.0" + "@tiptap/extension-list": "^3.15.3" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.13.0.tgz", - "integrity": "sha512-QuDyLzuK/3vCvx9GeKhgvHWrGECBzmJyAx6gli2HY+Iil7XicbfltV4nvhIxgxzpx3LDHLKzJN9pBi+2MzX60g==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.15.3.tgz", + "integrity": "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.13.0" + "@tiptap/extension-list": "^3.15.3" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.13.0.tgz", - "integrity": "sha512-9csQde1i0yeZI5oQQ9e1GYNtGL2JcC2d8Fwtw9FsGC8yz2W0h+Fmk+3bc2kobbtO5LGqupSc1fKM8fAg5rSRDg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.15.3.tgz", + "integrity": "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-placeholder": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.13.0.tgz", - "integrity": "sha512-Au4ktRBraQktX9gjSzGWyJV6kPof7+kOhzE8ej+rOMjIrHbx3DCHy1CJWftSO9BbqIyonjsFmm4nE+vjzZ3Z5Q==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.15.3.tgz", + "integrity": "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.13.0" + "@tiptap/extensions": "^3.15.3" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.13.0.tgz", - "integrity": "sha512-VHhWNqTAMOfrC48m2FcPIZB0nhl6XHQviAV16SBc+EFznKNv9tQUsqQrnuQ2y6ZVfqq5UxvZ3hKF/JlN/Ff7xw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.15.3.tgz", + "integrity": "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-text": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.13.0.tgz", - "integrity": "sha512-VcZIna93rixw7hRkHGCxDbL3kvJWi80vIT25a2pXg0WP1e7Pi3nBYvZIL4SQtkbBCji9EHrbZx3p8nNPzfazYw==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.15.3.tgz", + "integrity": "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-text-align": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.13.0.tgz", - "integrity": "sha512-hebIus9tdXWb+AmhO+LTeUxZLdb0tqwdeaL/0wYxJQR5DeCTlJe6huXacMD/BkmnlEpRhxzQH0FrmXAd0d4Wgg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.15.3.tgz", + "integrity": "sha512-hkLeEKm44aqimyjv+D8JUxzDG/iNjDrSCGvGrMOPcpaKn4f8C5z1EKnEufT61RitNPBAxQMXUhmGQUNrmlICmQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extension-underline": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz", - "integrity": "sha512-VDQi+UYw0tFnfghpthJTFmtJ3yx90kXeDwFvhmT8G+O+si5VmP05xYDBYBmYCix5jqKigJxEASiBL0gYOgMDEg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.15.3.tgz", + "integrity": "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0" + "@tiptap/core": "^3.15.3" } }, "node_modules/@tiptap/extensions": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.13.0.tgz", - "integrity": "sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz", + "integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" } }, "node_modules/@tiptap/pm": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.13.0.tgz", - "integrity": "sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz", + "integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.3.0", @@ -4184,9 +3650,9 @@ } }, "node_modules/@tiptap/react": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.13.0.tgz", - "integrity": "sha512-VqpqNZ9qtPr3pWK4NsZYxXgLSEiAnzl6oS7tEGmkkvJbcGSC+F7R13Xc9twv/zT5QCLxaHdEbmxHbuAIkrMgJQ==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.15.3.tgz", + "integrity": "sha512-XvouB+Hrqw8yFmZLPEh+HWlMeRSjZfHSfWfWuw5d8LSwnxnPeu3Bg/rjHrRrdwb+7FumtzOnNWMorpb/PSOttQ==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -4198,12 +3664,12 @@ "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.13.0", - "@tiptap/extension-floating-menu": "^3.13.0" + "@tiptap/extension-bubble-menu": "^3.15.3", + "@tiptap/extension-floating-menu": "^3.15.3" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0", + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3", "@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", @@ -4211,35 +3677,35 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.13.0.tgz", - "integrity": "sha512-Ojn6sRub04CRuyQ+9wqN62JUOMv+rG1vXhc2s6DCBCpu28lkCMMW+vTe7kXJcEdbot82+5swPbERw9vohswFzg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.15.3.tgz", + "integrity": "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw==", "license": "MIT", "dependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/extension-blockquote": "^3.13.0", - "@tiptap/extension-bold": "^3.13.0", - "@tiptap/extension-bullet-list": "^3.13.0", - "@tiptap/extension-code": "^3.13.0", - "@tiptap/extension-code-block": "^3.13.0", - "@tiptap/extension-document": "^3.13.0", - "@tiptap/extension-dropcursor": "^3.13.0", - "@tiptap/extension-gapcursor": "^3.13.0", - "@tiptap/extension-hard-break": "^3.13.0", - "@tiptap/extension-heading": "^3.13.0", - "@tiptap/extension-horizontal-rule": "^3.13.0", - "@tiptap/extension-italic": "^3.13.0", - "@tiptap/extension-link": "^3.13.0", - "@tiptap/extension-list": "^3.13.0", - "@tiptap/extension-list-item": "^3.13.0", - "@tiptap/extension-list-keymap": "^3.13.0", - "@tiptap/extension-ordered-list": "^3.13.0", - "@tiptap/extension-paragraph": "^3.13.0", - "@tiptap/extension-strike": "^3.13.0", - "@tiptap/extension-text": "^3.13.0", - "@tiptap/extension-underline": "^3.13.0", - "@tiptap/extensions": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.15.3", + "@tiptap/extension-blockquote": "^3.15.3", + "@tiptap/extension-bold": "^3.15.3", + "@tiptap/extension-bullet-list": "^3.15.3", + "@tiptap/extension-code": "^3.15.3", + "@tiptap/extension-code-block": "^3.15.3", + "@tiptap/extension-document": "^3.15.3", + "@tiptap/extension-dropcursor": "^3.15.3", + "@tiptap/extension-gapcursor": "^3.15.3", + "@tiptap/extension-hard-break": "^3.15.3", + "@tiptap/extension-heading": "^3.15.3", + "@tiptap/extension-horizontal-rule": "^3.15.3", + "@tiptap/extension-italic": "^3.15.3", + "@tiptap/extension-link": "^3.15.3", + "@tiptap/extension-list": "^3.15.3", + "@tiptap/extension-list-item": "^3.15.3", + "@tiptap/extension-list-keymap": "^3.15.3", + "@tiptap/extension-ordered-list": "^3.15.3", + "@tiptap/extension-paragraph": "^3.15.3", + "@tiptap/extension-strike": "^3.15.3", + "@tiptap/extension-text": "^3.15.3", + "@tiptap/extension-underline": "^3.15.3", + "@tiptap/extensions": "^3.15.3", + "@tiptap/pm": "^3.15.3" }, "funding": { "type": "github", @@ -4371,9 +3837,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", - "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", "dependencies": { @@ -4381,18 +3847,18 @@ } }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4405,21 +3871,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", - "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", + "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/type-utils": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4429,7 +3894,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.3", + "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4445,17 +3910,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", - "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", + "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4470,15 +3935,15 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", + "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.52.0", + "@typescript-eslint/types": "^8.52.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4492,14 +3957,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", + "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4510,9 +3975,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", + "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", "dev": true, "license": "MIT", "engines": { @@ -4527,17 +3992,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", + "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4552,9 +4017,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", + "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", "dev": true, "license": "MIT", "engines": { @@ -4566,22 +4031,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", + "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.52.0", + "@typescript-eslint/tsconfig-utils": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4604,36 +4068,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4650,30 +4084,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", + "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4688,13 +4109,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", + "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -5261,9 +4682,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -5302,7 +4723,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5372,9 +4792,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001753", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", - "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", "funding": [ { "type": "opencollective", @@ -5534,9 +4954,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-array": { @@ -5862,9 +5282,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5888,9 +5308,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -5977,27 +5397,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -6065,9 +5485,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", - "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", + "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", "license": "MIT", "workspaces": [ "docs", @@ -6087,9 +5507,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -6099,7 +5519,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -6147,13 +5567,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz", - "integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.9.tgz", + "integrity": "sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.6", + "@next/eslint-plugin-next": "15.5.9", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -6303,6 +5723,16 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", @@ -6397,6 +5827,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -6446,9 +5886,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6558,9 +5998,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -6584,7 +6024,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -6864,13 +6303,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6976,9 +6408,9 @@ } }, "node_modules/immer": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", - "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -7129,19 +6561,6 @@ "semver": "^7.7.1" } }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -7210,7 +6629,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7256,7 +6674,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7295,7 +6712,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7513,9 +6929,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7546,6 +6962,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7984,7 +7413,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -8127,9 +7555,9 @@ } }, "node_modules/next-intl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.4.0.tgz", - "integrity": "sha512-QHqnP9V9Pe7Tn0PdVQ7u1Z8k9yCkW5SJKeRy2g5gxzhSt/C01y3B9qNxuj3Fsmup/yreIHe6osxU6sFa+9WIkQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.7.0.tgz", + "integrity": "sha512-gvROzcNr/HM0jTzQlKWQxUNk8jrZ0bREz+bht3wNbv+uzlZ5Kn3J+m+viosub18QJ72S08UJnVK50PXWcUvwpQ==", "funding": [ { "type": "individual", @@ -8139,8 +7567,12 @@ "license": "MIT", "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", "negotiator": "^1.0.0", - "use-intl": "^4.4.0" + "next-intl-swc-plugin-extractor": "^4.7.0", + "po-parser": "^2.1.1", + "use-intl": "^4.7.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -8153,6 +7585,50 @@ } } }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.7.0.tgz", + "integrity": "sha512-iAqflu2FWdQMWhwB0B2z52X7LmEpvnMNJXqVERZQ7bK5p9iqQLu70ur6Ka6NfiXLxfb+AeAkUX5qIciQOg+87A==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz", + "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.8", + "@swc/core-darwin-x64": "1.15.8", + "@swc/core-linux-arm-gnueabihf": "1.15.8", + "@swc/core-linux-arm64-gnu": "1.15.8", + "@swc/core-linux-arm64-musl": "1.15.8", + "@swc/core-linux-x64-gnu": "1.15.8", + "@swc/core-linux-x64-musl": "1.15.8", + "@swc/core-win32-arm64-msvc": "1.15.8", + "@swc/core-win32-ia32-msvc": "1.15.8", + "@swc/core-win32-x64-msvc": "1.15.8" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -8181,6 +7657,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8428,7 +7910,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -8469,6 +7950,12 @@ "node": ">=18" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8530,6 +8017,13 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prosemirror-changeset": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", @@ -8678,9 +8172,9 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.3.tgz", - "integrity": "sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.2.3", @@ -8775,9 +8269,9 @@ } }, "node_modules/react-day-picker": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz", - "integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz", + "integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -8808,9 +8302,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", - "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "version": "7.70.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz", + "integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -8824,10 +8318,11 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", @@ -8853,9 +8348,9 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -8922,9 +8417,9 @@ } }, "node_modules/recharts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", - "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", "license": "MIT", "workspaces": [ "www" @@ -9170,13 +8665,16 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/set-function-length": { @@ -9229,16 +8727,16 @@ } }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -9247,41 +8745,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -9621,9 +9108,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "license": "MIT", "funding": { "type": "github", @@ -9631,9 +9118,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", - "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "dev": true, "license": "MIT" }, @@ -9709,7 +9196,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -9719,9 +9205,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -9744,19 +9230,6 @@ "strip-bom": "^3.0.0" } }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -9967,9 +9440,9 @@ } }, "node_modules/use-intl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.4.0.tgz", - "integrity": "sha512-smFekJWtokDRBLC5/ZumlBREzdXOkw06+56Ifj2uRe9266Mk+yWQm2PcJO+EwlOE5sHIXHixOTzN6V8E0RGUbw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.7.0.tgz", + "integrity": "sha512-jyd8nSErVRRsSlUa+SDobKHo9IiWs5fjcPl9VBUnzUyEQpVM5mwJCgw8eUiylhvBpLQzUGox1KN0XlRivSID9A==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -10220,9 +9693,9 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx index 123c421b..815b3c85 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx @@ -3,204 +3,36 @@ import { use, useEffect, useState } from 'react'; import { EstimateDetailForm } from '@/components/business/construction/estimates'; import type { EstimateDetail } from '@/components/business/construction/estimates'; +import { getEstimateDetail } from '@/components/business/construction/estimates/actions'; interface EstimateEditPageProps { params: Promise<{ id: string }>; } -// 목업 데이터 - 추후 API 연동 -function getEstimateDetail(id: string): EstimateDetail { - // TODO: 실제 API 연동 - const mockData: EstimateDetail = { - id, - estimateCode: '123123', - partnerId: '1', - partnerName: '회사명', - projectName: '현장명', - estimatorId: 'hong', - estimatorName: '이름', - estimateCompanyManager: '홍길동', - estimateCompanyManagerContact: '01012341234', - itemCount: 21, - estimateAmount: 1420000, - completedDate: null, - bidDate: '2025-12-12', - status: 'pending', - createdAt: '2025-12-01', - updatedAt: '2025-12-01', - createdBy: 'hong', - siteBriefing: { - briefingCode: '123123', - partnerName: '회사명', - companyName: '회사명', - briefingDate: '2025-12-12', - attendee: '이름', - }, - bidInfo: { - projectName: '현장명', - bidDate: '2025-12-12', - siteCount: 21, - constructionPeriod: '2026-01-01 ~ 2026-12-10', - constructionStartDate: '2026-01-01', - constructionEndDate: '2026-12-10', - vatType: 'excluded', - workReport: '업무 보고 내용', - documents: [ - { - id: '1', - fileName: 'abc.zip', - fileUrl: '#', - fileSize: 1024000, - }, - ], - }, - summaryItems: [ - { - id: '1', - name: '서터 심창측공사', - quantity: 1, - unit: '식', - materialCost: 78540000, - laborCost: 15410000, - totalCost: 93950000, - remarks: '', - }, - ], - expenseItems: [ - { - id: '1', - name: 'public_1', - amount: 10000, - }, - ], - priceAdjustments: [ - { - id: '1', - category: '배합비', - unitPrice: 10000, - coating: 10000, - batting: 10000, - boxReinforce: 10500, - painting: 10500, - total: 51000, - }, - { - id: '2', - category: '재단비', - unitPrice: 1375, - coating: 0, - batting: 0, - boxReinforce: 0, - painting: 0, - total: 1375, - }, - { - id: '3', - category: '판매단가', - unitPrice: 0, - coating: 10000, - batting: 10000, - boxReinforce: 10500, - painting: 10500, - total: 41000, - }, - { - id: '4', - category: '조립단가', - unitPrice: 10300, - coating: 10300, - batting: 10300, - boxReinforce: 10500, - painting: 10200, - total: 51600, - }, - ], - detailItems: [ - { - id: '1', - no: 1, - name: 'FS530외/주차', - material: 'screen', - width: 2350, - height: 2500, - quantity: 1, - box: 1, - assembly: 0, - coating: 0, - batting: 0, - mounting: 0, - fitting: 0, - controller: 0, - widthConstruction: 0, - heightConstruction: 0, - materialCost: 1420000, - laborCost: 510000, - quantityPrice: 1930000, - expenseQuantity: 5500, - expenseTotal: 5500, - totalCost: 1930000, - otherCost: 0, - marginCost: 0, - totalPrice: 1930000, - unitPrice: 1420000, - expense: 0, - marginRate: 0, - unitQuantity: 1, - expenseResult: 0, - marginActual: 0, - }, - { - id: '2', - no: 2, - name: 'FS530외/주차', - material: 'screen', - width: 7500, - height: 2500, - quantity: 1, - box: 1, - assembly: 0, - coating: 0, - batting: 0, - mounting: 0, - fitting: 0, - controller: 0, - widthConstruction: 0, - heightConstruction: 0, - materialCost: 4720000, - laborCost: 780000, - quantityPrice: 5500000, - expenseQuantity: 5500, - expenseTotal: 5500, - totalCost: 5500000, - otherCost: 0, - marginCost: 0, - totalPrice: 5500000, - unitPrice: 4720000, - expense: 0, - marginRate: 0, - unitQuantity: 1, - expenseResult: 0, - marginActual: 0, - }, - ], - approval: { - approvers: [], - references: [], - }, - }; - - return mockData; -} - export default function EstimateEditPage({ params }: EstimateEditPageProps) { const { id } = use(params); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - const detail = getEstimateDetail(id); - setData(detail); - setIsLoading(false); + async function fetchData() { + try { + setIsLoading(true); + setError(null); + const result = await getEstimateDetail(id); + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '견적 정보를 불러오는데 실패했습니다.'); + } + } catch (err) { + setError(err instanceof Error ? err.message : '오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } + fetchData(); }, [id]); if (isLoading) { @@ -211,6 +43,14 @@ export default function EstimateEditPage({ params }: EstimateEditPageProps) { ); } + if (error) { + return ( +
+
{error}
+
+ ); + } + return ( ; } -// 목업 데이터 - 추후 API 연동 -function getEstimateDetail(id: string): EstimateDetail { - // TODO: 실제 API 연동 - const mockData: EstimateDetail = { - id, - estimateCode: '123123', - partnerId: '1', - partnerName: '회사명', - projectName: '현장명', - estimatorId: 'hong', - estimatorName: '이름', - estimateCompanyManager: '홍길동', - estimateCompanyManagerContact: '01012341234', - itemCount: 21, - estimateAmount: 1420000, - completedDate: null, - bidDate: '2025-12-12', - status: 'pending', - createdAt: '2025-12-01', - updatedAt: '2025-12-01', - createdBy: 'hong', - siteBriefing: { - briefingCode: '123123', - partnerName: '회사명', - companyName: '회사명', - briefingDate: '2025-12-12', - attendee: '이름', - }, - bidInfo: { - projectName: '현장명', - bidDate: '2025-12-12', - siteCount: 21, - constructionPeriod: '2026-01-01 ~ 2026-12-10', - constructionStartDate: '2026-01-01', - constructionEndDate: '2026-12-10', - vatType: 'excluded', - workReport: '업무 보고 내용', - documents: [ - { - id: '1', - fileName: 'abc.zip', - fileUrl: '#', - fileSize: 1024000, - }, - ], - }, - summaryItems: [ - { - id: '1', - name: '서터 심창측공사', - quantity: 1, - unit: '식', - materialCost: 78540000, - laborCost: 15410000, - totalCost: 93950000, - remarks: '', - }, - ], - expenseItems: [ - { - id: '1', - name: 'public_1', - amount: 10000, - }, - ], - priceAdjustments: [ - { - id: '1', - category: '배합비', - unitPrice: 10000, - coating: 10000, - batting: 10000, - boxReinforce: 10500, - painting: 10500, - total: 51000, - }, - { - id: '2', - category: '재단비', - unitPrice: 1375, - coating: 0, - batting: 0, - boxReinforce: 0, - painting: 0, - total: 1375, - }, - { - id: '3', - category: '판매단가', - unitPrice: 0, - coating: 10000, - batting: 10000, - boxReinforce: 10500, - painting: 10500, - total: 41000, - }, - { - id: '4', - category: '조립단가', - unitPrice: 10300, - coating: 10300, - batting: 10300, - boxReinforce: 10500, - painting: 10200, - total: 51600, - }, - ], - detailItems: [ - { - id: '1', - no: 1, - name: 'FS530외/주차', - material: 'screen', - width: 2350, - height: 2500, - quantity: 1, - box: 1, - assembly: 0, - coating: 0, - batting: 0, - mounting: 0, - fitting: 0, - controller: 0, - widthConstruction: 0, - heightConstruction: 0, - materialCost: 1420000, - laborCost: 510000, - quantityPrice: 1930000, - expenseQuantity: 5500, - expenseTotal: 5500, - totalCost: 1930000, - otherCost: 0, - marginCost: 0, - totalPrice: 1930000, - unitPrice: 1420000, - expense: 0, - marginRate: 0, - unitQuantity: 1, - expenseResult: 0, - marginActual: 0, - }, - { - id: '2', - no: 2, - name: 'FS530외/주차', - material: 'screen', - width: 7500, - height: 2500, - quantity: 1, - box: 1, - assembly: 0, - coating: 0, - batting: 0, - mounting: 0, - fitting: 0, - controller: 0, - widthConstruction: 0, - heightConstruction: 0, - materialCost: 4720000, - laborCost: 780000, - quantityPrice: 5500000, - expenseQuantity: 5500, - expenseTotal: 5500, - totalCost: 5500000, - otherCost: 0, - marginCost: 0, - totalPrice: 5500000, - unitPrice: 4720000, - expense: 0, - marginRate: 0, - unitQuantity: 1, - expenseResult: 0, - marginActual: 0, - }, - ], - approval: { - approvers: [], - references: [], - }, - }; - - return mockData; -} - export default function EstimateDetailPage({ params }: EstimateDetailPageProps) { const { id } = use(params); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - const detail = getEstimateDetail(id); - setData(detail); - setIsLoading(false); + async function fetchData() { + try { + setIsLoading(true); + setError(null); + const result = await getEstimateDetail(id); + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '견적 정보를 불러오는데 실패했습니다.'); + } + } catch (err) { + setError(err instanceof Error ? err.message : '오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } + fetchData(); }, [id]); if (isLoading) { @@ -211,6 +43,14 @@ export default function EstimateDetailPage({ params }: EstimateDetailPageProps) ); } + if (error) { + return ( +
+
{error}
+
+ ); + } + return ( { + startTransition(async () => { + try { + const result = await sendNewClientNotification(); + if (result.success) { + toast.success(`신규업체 알림을 발송했습니다. (${result.sentCount || 0}건)`); + } else { + toast.error(result.error || "알림 발송에 실패했습니다."); + } + } catch (error) { + if (isNextRedirectError(error)) throw error; + toast.error("알림 발송 중 오류가 발생했습니다."); + } + }); + }, []); + // 상태 뱃지 const getStatusBadge = (status: "활성" | "비활성") => { if (status === "활성") { @@ -578,10 +599,16 @@ export default function CustomerAccountManagementPage() { description="거래처 정보 및 계정을 관리합니다" icon={Building2} headerActions={ - +
+ + +
} stats={stats} searchValue={searchTerm} diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index 765e89e1..13f67c87 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -13,7 +13,6 @@ import { useState, useEffect } from "react"; import { useRouter, useParams } from "next/navigation"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -32,26 +31,19 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { FileText, ArrowLeft, Info, AlertTriangle } from "lucide-react"; +import { FileText, AlertTriangle } from "lucide-react"; import { toast } from "sonner"; import { PageLayout } from "@/components/organisms/PageLayout"; import { PageHeader } from "@/components/organisms/PageHeader"; -import { FormSection } from "@/components/templates/ResponsiveFormTemplate"; import { FormActions } from "@/components/organisms/FormActions"; import { BadgeSm } from "@/components/atoms/BadgeSm"; import { formatAmount } from "@/utils/formatAmount"; -import { OrderItem } from "@/components/orders"; - -// 수주 상태 타입 -type OrderStatus = - | "order_registered" - | "order_confirmed" - | "production_ordered" - | "in_production" - | "rework" - | "work_completed" - | "shipped" - | "cancelled"; +import { + OrderItem, + getOrderById, + updateOrder, + type OrderStatus, +} from "@/components/orders"; // 수정 폼 데이터 interface EditFormData { @@ -99,60 +91,6 @@ const SHIPPING_COSTS = [ { value: "negotiable", label: "협의" }, ]; -// 샘플 데이터 -const SAMPLE_ORDER: EditFormData = { - lotNumber: "KD-TS-251217-01", - quoteNumber: "KD-PR-251210-01", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - manager: "김철수", - contact: "010-1234-5678", - status: "order_confirmed", - expectedShipDate: "2025-01-15", - expectedShipDateUndecided: false, - deliveryRequestDate: "2025-01-20", - deliveryMethod: "direct", - shippingCost: "free", - receiver: "박반장", - receiverContact: "010-9876-5432", - address: "경기도 화성시 동탄대로 123-45", - addressDetail: "데시앙 동탄 파크뷰 현장", - remarks: "4층 우선 납품 요청", - items: [ - { - id: "1", - itemCode: "PRD-001", - itemName: "국민방화스크린세터", - type: "B1", - symbol: "FSS1", - spec: "7260×2600", - width: 7260, - height: 2600, - quantity: 2, - unit: "EA", - unitPrice: 8000000, - amount: 16000000, - }, - { - id: "2", - itemCode: "PRD-002", - itemName: "국민방화스크린세터", - type: "B1", - symbol: "FSS2", - spec: "5000×2400", - width: 5000, - height: 2400, - quantity: 3, - unit: "EA", - unitPrice: 7600000, - amount: 22800000, - }, - ], - canEditItems: true, - subtotal: 38800000, - discountRate: 0, - totalAmount: 38800000, -}; // 상태 뱃지 헬퍼 function getOrderStatusBadge(status: OrderStatus) { @@ -183,21 +121,57 @@ export default function OrderEditPage() { const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); - // 데이터 로드 + // 데이터 로드 (API) useEffect(() => { - setTimeout(() => { - // 상태에 따라 품목 수정 가능 여부 결정 - const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes( - SAMPLE_ORDER.status - ); - setForm({ ...SAMPLE_ORDER, canEditItems }); - setLoading(false); - }, 300); - }, [orderId]); - - const handleBack = () => { - router.push(`/sales/order-management-sales/${orderId}`); - }; + async function loadOrder() { + try { + setLoading(true); + const result = await getOrderById(orderId); + if (result.success && result.data) { + const order = result.data; + // 상태에 따라 품목 수정 가능 여부 결정 + const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes( + order.status + ); + // Order 데이터를 EditFormData로 변환 + setForm({ + lotNumber: order.lotNumber, + quoteNumber: order.quoteNumber || "", + client: order.client, + siteName: order.siteName, + manager: order.manager || "", + contact: order.contact || "", + status: order.status, + expectedShipDate: order.expectedShipDate || "", + expectedShipDateUndecided: !order.expectedShipDate, + deliveryRequestDate: order.deliveryRequestDate || "", + deliveryMethod: order.deliveryMethod || "", + shippingCost: order.shippingCost || "", + receiver: order.receiver || "", + receiverContact: order.receiverContact || "", + address: order.address || "", + addressDetail: order.addressDetail || "", + remarks: order.remarks || "", + items: order.items || [], + canEditItems, + subtotal: order.subtotal || order.amount, + discountRate: order.discountRate || 0, + totalAmount: order.amount, + }); + } else { + toast.error(result.error || "수주 정보를 불러오는데 실패했습니다."); + router.push("/sales/order-management-sales"); + } + } catch (error) { + console.error("Error loading order:", error); + toast.error("수주 정보를 불러오는 중 오류가 발생했습니다."); + router.push("/sales/order-management-sales"); + } finally { + setLoading(false); + } + } + loadOrder(); + }, [orderId, router]); const handleCancel = () => { router.push(`/sales/order-management-sales/${orderId}`); @@ -222,11 +196,39 @@ export default function OrderEditPage() { setIsSaving(true); try { - // TODO: API 연동 - console.log("수주 수정 데이터:", form); - await new Promise((resolve) => setTimeout(resolve, 500)); - toast.success("수주가 수정되었습니다."); - router.push(`/sales/order-management-sales/${orderId}`); + // API 연동 + const result = await updateOrder(orderId, { + clientId: undefined, // 기존 값 유지 + siteName: form.siteName, + expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate, + deliveryRequestDate: form.deliveryRequestDate, + deliveryMethod: form.deliveryMethod, + shippingCost: form.shippingCost, + receiver: form.receiver, + receiverContact: form.receiverContact, + address: form.address, + addressDetail: form.addressDetail, + remarks: form.remarks, + items: form.items.map((item) => ({ + itemId: item.id ? parseInt(item.id, 10) : undefined, + itemCode: item.itemCode, + itemName: item.itemName, + specification: item.spec, + quantity: item.quantity, + unit: item.unit, + unitPrice: item.unitPrice, + })), + }); + + if (result.success) { + toast.success("수주가 수정되었습니다."); + router.push(`/sales/order-management-sales/${orderId}`); + } else { + toast.error(result.error || "수주 수정에 실패했습니다."); + } + } catch (error) { + console.error("Error updating order:", error); + toast.error("수주 수정 중 오류가 발생했습니다."); } finally { setIsSaving(false); } diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index c48b0b14..a57c947a 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -31,6 +31,7 @@ import { FileCheck, ClipboardList, Eye, + CheckCircle2, } from "lucide-react"; import { toast } from "sonner"; import { PageLayout } from "@/components/organisms/PageLayout"; @@ -54,266 +55,14 @@ import { } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { - OrderItem, OrderDocumentModal, type OrderDocumentType, + getOrderById, + updateOrderStatus, + type Order, + type OrderStatus, } from "@/components/orders"; -// 수주 상태 타입 -type OrderStatus = - | "order_registered" - | "order_confirmed" - | "production_ordered" - | "in_production" - | "rework" - | "work_completed" - | "shipped" - | "cancelled"; - -// 수주 상세 데이터 타입 -interface OrderDetail { - id: string; - lotNumber: string; - quoteNumber: string; - orderDate: string; - status: OrderStatus; - client: string; - siteName: string; - manager: string; - contact: string; - expectedShipDate: string; - deliveryRequestDate: string; - deliveryMethod: string; - shippingCost: string; - receiver: string; - receiverContact: string; - address: string; - remarks: string; - items: OrderItem[]; - subtotal: number; - discountRate: number; - totalAmount: number; -} - -// 샘플 품목 데이터 -const SAMPLE_ITEMS: OrderItem[] = [ - { - id: "1", - itemCode: "PRD-001", - itemName: "국민방화스크린세터", - type: "B1", - symbol: "FSS1", - spec: "7260×2600", - width: 7260, - height: 2600, - quantity: 2, - unit: "EA", - unitPrice: 8000000, - amount: 16000000, - }, - { - id: "2", - itemCode: "PRD-002", - itemName: "국민방화스크린세터", - type: "B1", - symbol: "FSS2", - spec: "5000×2400", - width: 5000, - height: 2400, - quantity: 3, - unit: "EA", - unitPrice: 7600000, - amount: 22800000, - }, -]; - -// 샘플 수주 데이터 (리스트 페이지와 동기화) -const SAMPLE_ORDERS: Record = { - "ORD-001": { - id: "ORD-001", - lotNumber: "KD-TS-251217-01", - quoteNumber: "KD-PR-251210-01", - orderDate: "2024-12-17", - status: "order_confirmed", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - manager: "김철수", - contact: "010-1234-5678", - expectedShipDate: "2025-01-15", - deliveryRequestDate: "2025-01-20", - deliveryMethod: "직접배차", - shippingCost: "무료", - receiver: "박반장", - receiverContact: "010-9876-5432", - address: "경기도 화성시 동탄대로 123-45 데시앙 동탄 파크뷰 현장", - remarks: "4층 우선 납품 요청", - items: SAMPLE_ITEMS, - subtotal: 38800000, - discountRate: 0, - totalAmount: 38800000, - }, - "ORD-002": { - id: "ORD-002", - lotNumber: "KD-TS-251217-02", - quoteNumber: "KD-PR-251211-02", - orderDate: "2024-12-17", - status: "in_production", - client: "현대건설(주)", - siteName: "힐스테이트 판교역", - manager: "이영희", - contact: "010-2345-6789", - expectedShipDate: "2025-01-20", - deliveryRequestDate: "2025-01-25", - deliveryMethod: "상차", - shippingCost: "선불", - receiver: "김반장", - receiverContact: "010-8765-4321", - address: "경기도 성남시 분당구 판교역로 123", - remarks: "지하 1층 납품", - items: SAMPLE_ITEMS, - subtotal: 52500000, - discountRate: 0, - totalAmount: 52500000, - }, - "ORD-003": { - id: "ORD-003", - lotNumber: "KD-TS-251216-01", - quoteNumber: "KD-PR-251208-03", - orderDate: "2024-12-16", - status: "production_ordered", - client: "GS건설(주)", - siteName: "자이 강남센터", - manager: "박민수", - contact: "010-3456-7890", - expectedShipDate: "2025-01-10", - deliveryRequestDate: "2025-01-15", - deliveryMethod: "직접배차", - shippingCost: "무료", - receiver: "최반장", - receiverContact: "010-7654-3210", - address: "서울시 강남구 테헤란로 234", - remarks: "", - items: SAMPLE_ITEMS, - subtotal: 45000000, - discountRate: 0, - totalAmount: 45000000, - }, - "ORD-004": { - id: "ORD-004", - lotNumber: "KD-TS-251215-01", - quoteNumber: "KD-PR-251205-04", - orderDate: "2024-12-15", - status: "shipped", - client: "대우건설(주)", - siteName: "푸르지오 송도", - manager: "정수진", - contact: "010-4567-8901", - expectedShipDate: "2024-12-20", - deliveryRequestDate: "2024-12-22", - deliveryMethod: "상차", - shippingCost: "착불", - receiver: "오반장", - receiverContact: "010-6543-2109", - address: "인천시 연수구 송도동 456", - remarks: "출하 완료", - items: SAMPLE_ITEMS, - subtotal: 28900000, - discountRate: 0, - totalAmount: 28900000, - }, - "ORD-005": { - id: "ORD-005", - lotNumber: "KD-TS-251214-01", - quoteNumber: "KD-PR-251201-05", - orderDate: "2024-12-14", - status: "rework", - client: "포스코건설", - siteName: "더샵 분당센트럴", - manager: "강호동", - contact: "010-5678-9012", - expectedShipDate: "2025-01-25", - deliveryRequestDate: "2025-01-30", - deliveryMethod: "직접배차", - shippingCost: "무료", - receiver: "유반장", - receiverContact: "010-5432-1098", - address: "경기도 성남시 분당구 정자동 789", - remarks: "재작업 진행 중", - items: SAMPLE_ITEMS, - subtotal: 62000000, - discountRate: 0, - totalAmount: 62000000, - }, - "ORD-006": { - id: "ORD-006", - lotNumber: "KD-TS-251213-01", - quoteNumber: "KD-PR-251128-06", - orderDate: "2024-12-13", - status: "work_completed", - client: "롯데건설(주)", - siteName: "캐슬 잠실파크", - manager: "신동엽", - contact: "010-6789-0123", - expectedShipDate: "2024-12-25", - deliveryRequestDate: "2024-12-28", - deliveryMethod: "직접배차", - shippingCost: "무료", - receiver: "한반장", - receiverContact: "010-4321-0987", - address: "서울시 송파구 잠실동 321", - remarks: "작업 완료, 출하 대기", - items: SAMPLE_ITEMS, - subtotal: 35500000, - discountRate: 0, - totalAmount: 35500000, - }, - "ORD-007": { - id: "ORD-007", - lotNumber: "KD-TS-251212-01", - quoteNumber: "KD-PR-251125-07", - orderDate: "2024-12-12", - status: "order_registered", - client: "삼성물산(주)", - siteName: "래미안 서초", - manager: "유재석", - contact: "010-7890-1234", - expectedShipDate: "", - deliveryRequestDate: "2025-02-01", - deliveryMethod: "", - shippingCost: "", - receiver: "이반장", - receiverContact: "010-3210-9876", - address: "서울시 서초구 서초동 654", - remarks: "수주 등록 상태", - items: SAMPLE_ITEMS, - subtotal: 48000000, - discountRate: 0, - totalAmount: 48000000, - }, - "ORD-008": { - id: "ORD-008", - lotNumber: "KD-TS-251211-01", - quoteNumber: "KD-PR-251120-08", - orderDate: "2024-12-11", - status: "shipped", - client: "SK에코플랜트", - siteName: "SK VIEW 일산", - manager: "하하", - contact: "010-8901-2345", - expectedShipDate: "2024-12-18", - deliveryRequestDate: "2024-12-20", - deliveryMethod: "상차", - shippingCost: "선불", - receiver: "조반장", - receiverContact: "010-2109-8765", - address: "경기도 고양시 일산서구 주엽동 987", - remarks: "출하 완료, 미수금 있음", - items: SAMPLE_ITEMS, - subtotal: 31200000, - discountRate: 0, - totalAmount: 31200000, - }, -}; // 상태 뱃지 헬퍼 function getOrderStatusBadge(status: OrderStatus) { @@ -350,9 +99,12 @@ export default function OrderDetailPage() { const params = useParams(); const orderId = params.id as string; - const [order, setOrder] = useState(null); + const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); + const [isConfirming, setIsConfirming] = useState(false); // 취소 폼 상태 const [cancelReason, setCancelReason] = useState(""); @@ -362,14 +114,27 @@ export default function OrderDetailPage() { const [documentModalOpen, setDocumentModalOpen] = useState(false); const [documentType, setDocumentType] = useState("contract"); - // 데이터 로드 (샘플 - ID로 매칭) + // 데이터 로드 (API 호출) useEffect(() => { - // 실제 구현에서는 API 호출 - setTimeout(() => { - const foundOrder = SAMPLE_ORDERS[orderId]; - setOrder(foundOrder || null); - setLoading(false); - }, 300); + async function loadOrder() { + try { + setLoading(true); + const result = await getOrderById(orderId); + if (result.success && result.data) { + setOrder(result.data); + } else { + toast.error(result.error || "수주 정보를 불러오는데 실패했습니다."); + setOrder(null); + } + } catch (error) { + console.error("Error loading order:", error); + toast.error("수주 정보를 불러오는 중 오류가 발생했습니다."); + setOrder(null); + } finally { + setLoading(false); + } + } + loadOrder(); }, [orderId]); const handleBack = () => { @@ -396,18 +161,57 @@ export default function OrderDetailPage() { setIsCancelDialogOpen(true); }; - const handleConfirmCancel = () => { + const handleConfirmCancel = async () => { if (!cancelReason) { toast.error("취소 사유를 선택해주세요."); return; } if (order) { - setOrder({ ...order, status: "cancelled" }); - toast.success("수주가 취소되었습니다."); + setIsCancelling(true); + try { + const result = await updateOrderStatus(order.id, "cancelled"); + if (result.success) { + setOrder({ ...order, status: "cancelled" }); + toast.success("수주가 취소되었습니다."); + setIsCancelDialogOpen(false); + setCancelReason(""); + setCancelDetail(""); + } else { + toast.error(result.error || "수주 취소에 실패했습니다."); + } + } catch (error) { + console.error("Error cancelling order:", error); + toast.error("수주 취소 중 오류가 발생했습니다."); + } finally { + setIsCancelling(false); + } + } + }; + + // 수주 확정 처리 + const handleConfirmOrder = () => { + setIsConfirmDialogOpen(true); + }; + + const handleConfirmOrderSubmit = async () => { + if (order) { + setIsConfirming(true); + try { + const result = await updateOrderStatus(order.id, "order_confirmed"); + if (result.success && result.data) { + setOrder(result.data); + toast.success("수주가 확정되었습니다."); + setIsConfirmDialogOpen(false); + } else { + toast.error(result.error || "수주 확정에 실패했습니다."); + } + } catch (error) { + console.error("Error confirming order:", error); + toast.error("수주 확정 중 오류가 발생했습니다."); + } finally { + setIsConfirming(false); + } } - setIsCancelDialogOpen(false); - setCancelReason(""); - setCancelDetail(""); }; // 문서 모달 열기 @@ -442,6 +246,8 @@ export default function OrderDetailPage() { // 상태별 버튼 표시 여부 const showEditButton = order.status !== "shipped" && order.status !== "cancelled"; + // 수주 확정 버튼: 수주등록 상태에서만 표시 + const showConfirmButton = order.status === "order_registered"; // 생산지시 생성 버튼: 출하완료, 취소, 생산지시완료 제외하고 표시 // (수주등록, 수주확정, 생산중, 재작업중, 작업완료에서 표시) const showProductionCreateButton = @@ -473,6 +279,12 @@ export default function OrderDetailPage() { 수정 )} + {showConfirmButton && ( + + )} {showProductionCreateButton && ( + + + + + {/* 수주 확정 다이얼로그 */} + + + + + + 수주 확정 + + + +
+ {/* 수주 정보 박스 */} +
+
+ 수주번호 + {order.lotNumber} +
+
+ 발주처 + {order.client} +
+
+ 현장명 + {order.siteName} +
+
+ 총금액 + + {formatAmount(order.totalAmount)}원 + +
+
+ 현재 상태 + {getOrderStatusBadge(order.status)} +
+
+ + {/* 확정 안내 */} +
+

확정 후 변경사항

+
    +
  • • 수주 상태가 '수주확정'으로 변경됩니다
  • +
  • • 생산지시를 생성할 수 있습니다
  • +
  • • 확정 후에도 수정이 가능합니다
  • +
+
+
+ + + +
diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx index 5f0e8443..aac8ee92 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx @@ -10,9 +10,11 @@ * - 스크린 품목 상세 * - 모터/전장품 사양 (읽기전용) * - 절곡물 BOM + * + * API 연동: getOrderById, createProductionOrder */ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -25,7 +27,7 @@ import { TableRow, } from "@/components/ui/table"; import { Textarea } from "@/components/ui/textarea"; -import { Factory, ArrowLeft, BarChart3, CheckCircle2 } from "lucide-react"; +import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle } from "lucide-react"; import { PageLayout } from "@/components/organisms/PageLayout"; import { AlertDialog, @@ -39,6 +41,15 @@ import { import { PageHeader } from "@/components/organisms/PageHeader"; import { BadgeSm } from "@/components/atoms/BadgeSm"; import { cn } from "@/lib/utils"; +import { + getOrderById, + createProductionOrder, + type Order, + type CreateProductionOrderData, +} from "@/components/orders/actions"; +import { getProcessList } from "@/components/process-management/actions"; +import type { Process } from "@/types/process"; +import { formatAmount } from "@/utils/formatAmount"; // 수주 정보 타입 interface OrderInfo { @@ -47,7 +58,6 @@ interface OrderInfo { siteName: string; dueDate: string; itemCount: number; - totalQuantity: string; creditGrade: string; status: string; } @@ -83,20 +93,17 @@ interface MaterialRequirement { status: "sufficient" | "insufficient"; } -// 스크린 품목 상세 타입 +// 스크린 품목 상세 타입 (order.items에서 변환) interface ScreenItemDetail { no: number; itemName: string; - location: string; - openWidth: number; - openHeight: number; - productWidth: number; - productHeight: number; - guideRail: string; - shaft: string; - capacity: string; - finish: string; + specification: string; quantity: number; + unit: string; + unitPrice: number; + supplyAmount: number; + taxAmount: number; + totalAmount: number; } // 가이드레일 BOM 타입 @@ -187,149 +194,134 @@ const PRIORITY_CONFIGS: PriorityConfig[] = [ }, ]; -// 샘플 수주 정보 -const SAMPLE_ORDER_INFO: OrderInfo = { - orderNumber: "KD-TS-251217-09", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - dueDate: "2026-02-25", - itemCount: 3, - totalQuantity: "3EA", - creditGrade: "A (우량)", - status: "재작업중", +// 상태 레이블 +const STATUS_LABELS: Record = { + order_registered: "수주등록", + order_confirmed: "수주확정", + production_ordered: "생산지시", + in_production: "생산중", + rework: "재작업중", + work_completed: "작업완료", + shipped: "출고완료", + cancelled: "취소", }; -// 샘플 작업지시 카드 -const SAMPLE_WORK_ORDER_CARDS: WorkOrderCard[] = [ - { - id: "1", - type: "스크린", - orderNumber: "KD-PL-251223-01", - itemCount: 3, - totalQuantity: "3EA", - processes: ["1. 원단절단", "2. 미싱", "3. 앤드락작업", "4. 중간검사", "5. 포장"], - }, - { - id: "2", - type: "절곡", - orderNumber: "KD-PL-251223-02", - itemCount: 3, - totalQuantity: "3EA", - processes: ["1. 절단", "2. 절곡", "3. 중간검사", "4. 포장"], - }, -]; +// 품목과 공정 매칭 함수 +function matchItemToProcess( + itemName: string, + itemCode: string | undefined, + processes: Process[] +): Process | null { + for (const process of processes) { + for (const rule of process.classificationRules) { + if (!rule.isActive) continue; -// 샘플 자재 소요량 -const SAMPLE_MATERIALS: MaterialRequirement[] = [ - { - materialCode: "SCR-MAT-001", - materialName: "스크린 원단", - unit: "㎡", - required: 45, - currentStock: 500, - status: "sufficient", - }, - { - materialCode: "SCR-MAT-002", - materialName: "앤드락", - unit: "EA", - required: 6, - currentStock: 800, - status: "sufficient", - }, - { - materialCode: "BND-MAT-001", - materialName: "철판", - unit: "KG", - required: 90, - currentStock: 2000, - status: "sufficient", - }, - { - materialCode: "BND-MAT-002", - materialName: "가이드레일", - unit: "M", - required: 18, - currentStock: 300, - status: "sufficient", - }, - { - materialCode: "BND-MAT-003", - materialName: "케이스", - unit: "EA", - required: 3, - currentStock: 100, - status: "sufficient", - }, -]; + // 패턴 매칭 규칙 + if (rule.registrationType === "pattern") { + let targetValue = ""; + if (rule.ruleType === "품목명") { + targetValue = itemName; + } else if (rule.ruleType === "품목코드" && itemCode) { + targetValue = itemCode; + } -// 샘플 스크린 품목 상세 -const SAMPLE_SCREEN_ITEMS: ScreenItemDetail[] = [ - { - no: 1, - itemName: "스크린 셔터 (프리미엄)", - location: "로비 I-01", - openWidth: 4500, - openHeight: 3500, - productWidth: 4640, - productHeight: 3900, - guideRail: "백면형 120-70", - shaft: '4"', - capacity: "160kg", - finish: "SUS마감", - quantity: 1, - }, - { - no: 2, - itemName: "스크린 셔터 (프리미엄)", - location: "카페 I-02", - openWidth: 4500, - openHeight: 3500, - productWidth: 4640, - productHeight: 3900, - guideRail: "백면형 120-70", - shaft: '4"', - capacity: "160kg", - finish: "SUS마감", - quantity: 1, - }, - { - no: 3, - itemName: "스크린 셔터 (프리미엄)", - location: "헬스장 I-03", - openWidth: 4500, - openHeight: 3500, - productWidth: 4640, - productHeight: 3900, - guideRail: "백면형 120-70", - shaft: '4"', - capacity: "160kg", - finish: "SUS마감", - quantity: 1, - }, -]; + if (!targetValue) continue; -// 샘플 가이드레일 BOM -const SAMPLE_GUIDE_RAIL_BOM: GuideRailBom[] = [ - { - type: "백면형", - spec: "120-70", - code: "KSE01/KWE01", - length: 3000, - quantity: 6, - }, -]; + let matched = false; + switch (rule.matchingType) { + case "startsWith": + matched = targetValue.startsWith(rule.conditionValue); + break; + case "endsWith": + matched = targetValue.endsWith(rule.conditionValue); + break; + case "contains": + matched = targetValue.includes(rule.conditionValue); + break; + case "equals": + matched = targetValue === rule.conditionValue; + break; + } -// 샘플 케이스(셔터박스) BOM -const SAMPLE_CASE_BOM: CaseBom[] = [ - { item: "케이스 본체", length: "L: 4000", quantity: 2 }, - { item: "측면 덮개", length: "500-355", quantity: 6 }, -]; + if (matched) return process; + } + } + } -// 샘플 하단 마감재 BOM -const SAMPLE_BOTTOM_FINISH_BOM: BottomFinishBom[] = [ - { item: "하단마감재", spec: "50-40", length: "L: 4000", quantity: 3 }, -]; + // 매칭되는 공정이 없으면 공정명으로 단순 매칭 시도 + // (예: 품목명에 "스크린"이 포함되면 "스크린" 공정 반환) + for (const process of processes) { + if (itemName.toLowerCase().includes(process.processName.toLowerCase())) { + return process; + } + } + + return null; +} + +// 공정별 작업지시 그룹 타입 +interface ProcessWorkOrderGroup { + process: Process; + items: Array<{ itemName: string; itemCode?: string; quantity: number }>; +} + +// 수주 품목들을 공정별로 그룹핑 +function groupItemsByProcess( + items: Array<{ itemName: string; itemCode?: string; quantity: number }>, + processes: Process[] +): ProcessWorkOrderGroup[] { + if (items.length === 0 || processes.length === 0) { + return []; + } + + // 공정별 품목 그룹 맵 + const processGroupMap = new Map(); + // 매칭되지 않은 품목들 + const unmatchedItems: Array<{ itemName: string; itemCode?: string; quantity: number }> = []; + + for (const item of items) { + const matchedProcess = matchItemToProcess( + item.itemName, + item.itemCode, + processes + ); + + if (matchedProcess) { + if (!processGroupMap.has(matchedProcess.id)) { + processGroupMap.set(matchedProcess.id, { + process: matchedProcess, + items: [], + }); + } + processGroupMap.get(matchedProcess.id)!.items.push(item); + } else { + unmatchedItems.push(item); + } + } + + // 매칭되지 않은 품목이 있으면 "기타" 그룹 생성 + const groups = Array.from(processGroupMap.values()); + + if (unmatchedItems.length > 0) { + // 기타 품목용 가상 공정 생성 + groups.push({ + process: { + id: "unmatched", + processName: "기타", + processCode: "ETC", + status: "사용중", + description: "", + classificationRules: [], + workSteps: [], + createdAt: "", + updatedAt: "", + } as Process, + items: unmatchedItems, + }); + } + + return groups; +} export default function ProductionOrderCreatePage() { const router = useRouter(); @@ -337,7 +329,9 @@ export default function ProductionOrderCreatePage() { const orderId = params.id as string; const [loading, setLoading] = useState(true); - const [orderInfo, setOrderInfo] = useState(null); + const [error, setError] = useState(null); + const [order, setOrder] = useState(null); + const [processes, setProcesses] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); // 우선순위 상태 @@ -346,37 +340,92 @@ export default function ProductionOrderCreatePage() { // 성공 다이얼로그 상태 const [showSuccessDialog, setShowSuccessDialog] = useState(false); - const [generatedOrderNumber, setGeneratedOrderNumber] = useState(""); + const [generatedWorkOrders, setGeneratedWorkOrders] = useState>([]); - // 데이터 로드 - useEffect(() => { - setTimeout(() => { - setOrderInfo(SAMPLE_ORDER_INFO); + // 수주 데이터 및 공정 목록 로드 + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + try { + // 수주 정보와 공정 목록을 병렬로 로드 + const [orderResult, processResult] = await Promise.all([ + getOrderById(orderId), + getProcessList({ status: "사용중" }), + ]); + + if (orderResult.success && orderResult.data) { + setOrder(orderResult.data); + } else { + setError(orderResult.error || "수주 정보 조회에 실패했습니다."); + } + + if (processResult.success && processResult.data) { + setProcesses(processResult.data.items); + } + } catch { + setError("서버 오류가 발생했습니다."); + } finally { setLoading(false); - }, 300); + } }, [orderId]); + useEffect(() => { + fetchData(); + }, [fetchData]); + const handleCancel = () => { - router.push(`/sales/order-management-sales/${orderId}`); + router.push(`/ko/sales/order-management-sales/${orderId}`); }; const handleBackToDetail = () => { - router.push(`/sales/order-management-sales/${orderId}`); + router.push(`/ko/sales/order-management-sales/${orderId}`); }; const handleConfirm = async () => { + if (!order) return; + setIsSubmitting(true); + setError(null); try { - // TODO: API 호출 - await new Promise((resolve) => setTimeout(resolve, 500)); + // 공정별 품목 그룹핑 (processIds 추출을 위해) + const groups = groupItemsByProcess( + (order.items || []).map((item) => ({ + itemName: item.itemName, + itemCode: item.itemCode, + quantity: item.quantity, + })), + processes + ); - // 생산지시번호 생성 (실제로는 API 응답에서 받아옴) - const today = new Date(); - const dateStr = `${String(today.getFullYear()).slice(2)}${String(today.getMonth() + 1).padStart(2, "0")}${String(today.getDate()).padStart(2, "0")}`; - const newOrderNumber = `PO-${orderInfo?.orderNumber.replace("KD-TS-", "KD-") || "KD-000000"}-${dateStr}`; + // 모든 매칭된 공정 ID 추출 (공정별로 각각 작업지시 생성) + const matchedGroups = groups.filter((g) => g.process.id !== "unmatched"); + const allProcessIds = matchedGroups + .map((g) => parseInt(g.process.id, 10)) + .filter((id) => !isNaN(id)); - setGeneratedOrderNumber(newOrderNumber); - setShowSuccessDialog(true); + const productionData: CreateProductionOrderData = { + priority: selectedPriority, + memo: memo || undefined, + processIds: allProcessIds.length > 0 ? allProcessIds : undefined, + }; + + const result = await createProductionOrder(orderId, productionData); + + if (result.success && result.data) { + // 다중 작업지시 응답 처리 + const workOrders = result.data.workOrders || (result.data.workOrder ? [result.data.workOrder] : []); + setGeneratedWorkOrders( + workOrders.map((wo: { workOrderNo: string; process?: { processName: string } }) => ({ + workOrderNo: wo.workOrderNo, + processName: wo.process?.processName, + })) + ); + setShowSuccessDialog(true); + } else { + setError(result.error || "생산지시 생성에 실패했습니다."); + } + } catch { + setError("서버 오류가 발생했습니다."); } finally { setIsSubmitting(false); } @@ -384,9 +433,8 @@ export default function ProductionOrderCreatePage() { const handleSuccessDialogClose = () => { setShowSuccessDialog(false); - // 생산지시 상세 페이지로 이동 (실제로는 API 응답에서 받은 생산지시 ID 사용) - // 임시로 PO-002 사용 (샘플 데이터와 매칭) - router.push("/sales/order-management-sales/production-orders/PO-002"); + // 수주 상세 페이지로 이동 (상태가 변경되었으므로) + router.push(`/ko/sales/order-management-sales/${orderId}`); }; // 선택된 우선순위 설정 가져오기 @@ -404,7 +452,22 @@ export default function ProductionOrderCreatePage() { ); } - if (!orderInfo) { + if (error && !order) { + return ( + +
+ +

{error}

+ +
+
+ ); + } + + if (!order) { return (
@@ -419,7 +482,48 @@ export default function ProductionOrderCreatePage() { } const selectedConfig = getSelectedPriorityConfig(); - const workOrderCount = SAMPLE_WORK_ORDER_CARDS.length; + + // 공정별 품목 그룹핑 + const allGroups = groupItemsByProcess( + (order.items || []).map((item) => ({ + itemName: item.itemName, + itemCode: item.itemCode, + quantity: item.quantity, + })), + processes + ); + + // 공정 매칭된 그룹과 기타(미분류) 그룹 분리 + const processGroups = allGroups.filter((g) => g.process.id !== "unmatched"); + const unmatchedGroup = allGroups.find((g) => g.process.id === "unmatched"); + + // 작업지시 수 = 공정 매칭된 그룹 수만 (기타 제외) + const workOrderCount = processGroups.length; + + // order.items에서 스크린 품목 상세 데이터 변환 + const screenItems: ScreenItemDetail[] = (order.items || []).map((item, index) => ({ + no: item.serialNo || index + 1, + itemName: item.itemName, + specification: item.specification || "-", + quantity: item.quantity, + unit: item.unit || "EA", + unitPrice: item.unitPrice, + supplyAmount: item.supplyAmount, + taxAmount: item.taxAmount, + totalAmount: item.totalAmount, + })); + + // Order에서 UI에 표시할 데이터 변환 + const orderInfo = { + orderNumber: order.lotNumber, + client: order.client, + siteName: order.siteName, + dueDate: order.expectedShipDate || "-", + itemCount: order.itemCount, + creditGrade: "B (관리)", // API에서 제공하지 않아 기본값 사용 + status: STATUS_LABELS[order.status] || order.status, + amount: order.amount, + }; return ( @@ -448,6 +552,17 @@ export default function ProductionOrderCreatePage() { } /> + {/* 에러 메시지 표시 */} + {error && order && ( +
+ +
+

생산지시 생성 실패

+

{error}

+
+
+ )} +
{/* 수주 정보 */} @@ -476,10 +591,6 @@ export default function ProductionOrderCreatePage() {

품목 수

{orderInfo.itemCount}건

-
-

총수량

-

{orderInfo.totalQuantity}

-

신용등급

@@ -601,98 +712,100 @@ export default function ProductionOrderCreatePage() {
- {SAMPLE_WORK_ORDER_CARDS.map((card) => ( -
-
- - {card.type} - - {card.orderNumber} -
-
-
+ {processGroups.length > 0 ? ( + processGroups.map((group, groupIdx) => ( +
+
+ + {group.process.processName} + + + {order.lotNumber}-{String(groupIdx + 1).padStart(2, "0")} + +
+

품목 수

-

{card.itemCount}건

+

{group.items.length}건

-

총 수량

-

{card.totalQuantity}

+

공정 순서

+
+ {group.process.workSteps.length > 0 ? ( + group.process.workSteps.map((step, idx) => ( + + {idx + 1}. {step} + + )) + ) : ( + + 공정관리에서 세부 작업단계를 등록해 주세요. + + )} +
-
-

공정 순서

-
- {card.processes.map((process, idx) => ( - - {process} - - ))} -
+ )) + ) : ( +
+
+

+ 매칭되는 공정이 없습니다. 공정관리에서 분류규칙을 등록해 주세요. +

- ))} + )}
- {/* 자재 소요량 및 재고 현황 */} + {/* 기타 품목 (공정 미매칭) */} + {unmatchedGroup && unmatchedGroup.items.length > 0 && ( + + + + 기타 품목 + + {unmatchedGroup.items.length}건 + + + + +

+ 공정에 매칭되지 않은 품목입니다. 출하 시 별도로 준비해 주세요. +

+
    + {unmatchedGroup.items.map((item, idx) => { + const qty = Number(item.quantity); + return ( +
  • + + {item.itemName} + + ({Number.isInteger(qty) ? qty : qty.toFixed(2).replace(/\.?0+$/, "")}개) + +
  • + ); + })} +
+
+
+ )} + + {/* 자재 소요량 및 재고 현황 - 추후 BOM API 연동 예정 */} 자재 소요량 및 재고 현황 -
- - - - 자재코드 - 자재명 - 단위 - 소요량 - 현재고 - 상태 - - - - {SAMPLE_MATERIALS.map((item) => ( - - - - {item.materialCode} - - - {item.materialName} - {item.unit} - {item.required} - {item.currentStock.toLocaleString()} - - - {item.status === "sufficient" ? "충분" : "부족"} - - - - ))} - -
+
+

BOM 데이터 연동 후 자재 소요량이 표시됩니다.

+

(추후 제공 예정)

@@ -700,7 +813,7 @@ export default function ProductionOrderCreatePage() { {/* 스크린 품목 상세 */} - 스크린 품목 상세 ({SAMPLE_SCREEN_ITEMS.length}건) + 스크린 품목 상세 ({screenItems.length}건)
@@ -709,37 +822,39 @@ export default function ProductionOrderCreatePage() { No 품목명 - 도면위치 - 개구폭 - 개구높이 - 제작폭 - 제작높이 - 가이드레일 - 샤프트 - 용량 - 마감 + 규격 수량 + 단위 + 단가 + 공급가 + 세액 + 합계 - {SAMPLE_SCREEN_ITEMS.map((item) => ( - - - {String(item.no).padStart(2, "0")} + {screenItems.length > 0 ? ( + screenItems.map((item) => ( + + + {String(item.no).padStart(2, "0")} + + {item.itemName} + {item.specification} + {item.quantity} + {item.unit} + {formatAmount(item.unitPrice)} + {formatAmount(item.supplyAmount)} + {formatAmount(item.taxAmount)} + {formatAmount(item.totalAmount)} + + )) + ) : ( + + + 품목 정보가 없습니다. - {item.itemName} - {item.location} - {item.openWidth.toLocaleString()} - {item.openHeight.toLocaleString()} - {item.productWidth.toLocaleString()} - {item.productHeight.toLocaleString()} - {item.guideRail} - {item.shaft} - {item.capacity} - {item.finish} - {item.quantity} - ))} + )}
@@ -771,91 +886,15 @@ export default function ProductionOrderCreatePage() {
- {/* 절곡물 BOM */} + {/* 절곡물 BOM - 추후 BOM API 연동 예정 */} 절곡물 BOM - - {/* 가이드레일 */} -
-

가이드레일

-
- - - - 형태 - 규격 - 코드 - 길이 - 수량 - - - - {SAMPLE_GUIDE_RAIL_BOM.map((item, index) => ( - - {item.type} - {item.spec} - {item.code} - {item.length.toLocaleString()} - {item.quantity} - - ))} - -
-
-
- - {/* 케이스(셔터박스) */} -
-

케이스(셔터박스) - 메인 규격: 500-330

-
- - - - 품목 - 길이 - 수량 - - - - {SAMPLE_CASE_BOM.map((item, index) => ( - - {item.item} - {item.length} - {item.quantity} - - ))} - -
-
-
- - {/* 하단 마감재 */} -
-

하단 마감재

-
- - - - 품목 - 규격 - 길이 - 수량 - - - - {SAMPLE_BOTTOM_FINISH_BOM.map((item, index) => ( - - {item.item} - {item.spec} - {item.length} - {item.quantity} - - ))} - -
-
+ +
+

BOM 데이터 연동 후 절곡물 정보가 표시됩니다.

+

(가이드레일, 케이스, 하단 마감재 - 추후 제공 예정)

@@ -882,7 +921,7 @@ export default function ProductionOrderCreatePage() {
@@ -894,16 +933,27 @@ export default function ProductionOrderCreatePage() { - 생산지시가 생성되었습니다. + 작업지시가 {generatedWorkOrders.length}건 생성되었습니다.
-

생산지시번호:

-

{generatedOrderNumber}

+

생성된 작업지시:

+
    + {generatedWorkOrders.map((wo, idx) => ( +
  • + {wo.workOrderNo} + {wo.processName && ( + + {wo.processName} + + )} +
  • + ))} +

- 생산관리 > 생산지시 관리에서 작업지시서를 생성하세요. + 생산관리 > 작업지시 관리에서 확인하세요.

diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx index 2f0dbbcb..c5e76adb 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx @@ -2,10 +2,11 @@ /** * 수주 등록 페이지 + * API 연동 완료 (2025-01-08) */ import { useRouter } from "next/navigation"; -import { OrderRegistration, OrderFormData } from "@/components/orders"; +import { OrderRegistration, OrderFormData, createOrder } from "@/components/orders"; import { toast } from "sonner"; export default function OrderNewPage() { @@ -16,14 +17,19 @@ export default function OrderNewPage() { }; const handleSave = async (formData: OrderFormData) => { - // TODO: API 연동 - console.log("수주 등록 데이터:", formData); + try { + const result = await createOrder(formData); - // 임시: 성공 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 500)); - - toast.success("수주가 등록되었습니다."); - router.push("/sales/order-management-sales"); + if (result.success) { + toast.success("수주가 등록되었습니다."); + router.push("/sales/order-management-sales"); + } else { + toast.error(result.error || "수주 등록에 실패했습니다."); + } + } catch (error) { + console.error("Error creating order:", error); + toast.error("수주 등록 중 오류가 발생했습니다."); + } }; return ; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx index fb5dbd94..7de2188a 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx @@ -7,9 +7,10 @@ * - 상단 통계 카드: 이번 달 수주, 분할 대기, 생산지시 대기, 출하 대기 * - 필터 탭: 전체, 수주등록, 수주확정, 생산지시완료, 미수 * - 완전한 반응형 지원 + * - API 연동 완료 (2025-01-08) */ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback, useTransition } from "react"; import { useRouter } from "next/navigation"; import { FileText, @@ -20,6 +21,8 @@ import { ClipboardList, Truck, Eye, + Loader2, + Bell, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -31,11 +34,7 @@ import { } from "@/components/templates/IntegratedListTemplateV2"; import { toast } from "sonner"; import { - Table, - TableHeader, TableRow, - TableHead, - TableBody, TableCell, } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; @@ -51,149 +50,18 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { + getOrders, + getOrderStats, + deleteOrder, + deleteOrders, + type Order, + type OrderStatus, + type OrderStats, +} from "@/components/orders/actions"; +import { sendSalesOrderNotification } from "@/lib/actions/fcm"; +import { isNextRedirectError } from "@/lib/utils/redirect-error"; -// 수주 상태 타입 -type OrderStatus = - | "order_registered" // 수주등록 - | "order_confirmed" // 수주확정 - | "production_ordered" // 생산지시완료 - | "in_production" // 생산중 - | "rework" // 재작업중 - | "work_completed" // 작업완료 - | "shipped" // 출하완료 - | "cancelled"; // 취소 - -// 수주 타입 -interface Order { - id: string; - lotNumber: string; // 로트번호 KD-TS-XXXXXX-XX - quoteNumber: string; // 견적번호 KD-PR-XXXXXX-XX - orderDate: string; // 수주일자 - client: string; // 발주처 - siteName: string; // 현장명 - status: OrderStatus; - expectedShipDate?: string; // 출고예정일 - deliveryMethod?: string; // 배송방식 - amount: number; // 금액 - itemCount: number; // 품목 수 - hasReceivable?: boolean; // 미수 여부 -} - -// 샘플 수주 데이터 -const SAMPLE_ORDERS: Order[] = [ - { - id: "ORD-001", - lotNumber: "KD-TS-251217-01", - quoteNumber: "KD-PR-251210-01", - orderDate: "2024-12-17", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - status: "order_confirmed", - expectedShipDate: "2025-01-15", - deliveryMethod: "직접배차", - amount: 38800000, - itemCount: 5, - hasReceivable: false, - }, - { - id: "ORD-002", - lotNumber: "KD-TS-251217-02", - quoteNumber: "KD-PR-251211-02", - orderDate: "2024-12-17", - client: "현대건설(주)", - siteName: "힐스테이트 판교역", - status: "in_production", - expectedShipDate: "2025-01-20", - deliveryMethod: "상차", - amount: 52500000, - itemCount: 8, - hasReceivable: false, - }, - { - id: "ORD-003", - lotNumber: "KD-TS-251216-01", - quoteNumber: "KD-PR-251208-03", - orderDate: "2024-12-16", - client: "GS건설(주)", - siteName: "자이 강남센터", - status: "production_ordered", - expectedShipDate: "2025-01-10", - deliveryMethod: "직접배차", - amount: 45000000, - itemCount: 6, - hasReceivable: false, - }, - { - id: "ORD-004", - lotNumber: "KD-TS-251215-01", - quoteNumber: "KD-PR-251205-04", - orderDate: "2024-12-15", - client: "대우건설(주)", - siteName: "푸르지오 송도", - status: "shipped", - expectedShipDate: "2024-12-20", - deliveryMethod: "상차", - amount: 28900000, - itemCount: 4, - hasReceivable: true, - }, - { - id: "ORD-005", - lotNumber: "KD-TS-251214-01", - quoteNumber: "KD-PR-251201-05", - orderDate: "2024-12-14", - client: "포스코건설", - siteName: "더샵 분당센트럴", - status: "rework", - expectedShipDate: "2025-01-25", - deliveryMethod: "직접배차", - amount: 62000000, - itemCount: 10, - hasReceivable: false, - }, - { - id: "ORD-006", - lotNumber: "KD-TS-251213-01", - quoteNumber: "KD-PR-251128-06", - orderDate: "2024-12-13", - client: "롯데건설(주)", - siteName: "캐슬 잠실파크", - status: "work_completed", - expectedShipDate: "2024-12-25", - deliveryMethod: "직접배차", - amount: 35500000, - itemCount: 5, - hasReceivable: false, - }, - { - id: "ORD-007", - lotNumber: "KD-TS-251212-01", - quoteNumber: "KD-PR-251125-07", - orderDate: "2024-12-12", - client: "삼성물산(주)", - siteName: "래미안 서초", - status: "order_registered", - expectedShipDate: undefined, - deliveryMethod: undefined, - amount: 48000000, - itemCount: 7, - hasReceivable: false, - }, - { - id: "ORD-008", - lotNumber: "KD-TS-251211-01", - quoteNumber: "KD-PR-251120-08", - orderDate: "2024-12-11", - client: "SK에코플랜트", - siteName: "SK VIEW 일산", - status: "shipped", - expectedShipDate: "2024-12-18", - deliveryMethod: "상차", - amount: 31200000, - itemCount: 4, - hasReceivable: true, - }, -]; // 상태 뱃지 헬퍼 함수 function getOrderStatusBadge(status: OrderStatus) { @@ -218,6 +86,7 @@ function getOrderStatusBadge(status: OrderStatus) { export default function OrderManagementSalesPage() { const router = useRouter(); + const [isPending, startTransition] = useTransition(); const [searchTerm, setSearchTerm] = useState(""); const [filterType, setFilterType] = useState("all"); const [selectedItems, setSelectedItems] = useState>(new Set()); @@ -236,8 +105,42 @@ export default function OrderManagementSalesPage() { const [mobileDisplayCount, setMobileDisplayCount] = useState(20); const sentinelRef = useRef(null); - // 로컬 데이터 state (실제 구현에서는 API 연동) - const [orders, setOrders] = useState(SAMPLE_ORDERS); + // API 연동 state + const [orders, setOrders] = useState([]); + const [apiStats, setApiStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isDeleting, setIsDeleting] = useState(false); + + // 데이터 로드 함수 + const loadData = useCallback(async () => { + try { + setIsLoading(true); + const [ordersResult, statsResult] = await Promise.all([ + getOrders(), + getOrderStats(), + ]); + + if (ordersResult.success && ordersResult.data) { + setOrders(ordersResult.data.items); + } else { + toast.error(ordersResult.error || "수주 목록을 불러오는데 실패했습니다."); + } + + if (statsResult.success && statsResult.data) { + setApiStats(statsResult.data); + } + } catch (error) { + console.error("Error loading orders:", error); + toast.error("데이터를 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, []); + + // 초기 데이터 로드 + useEffect(() => { + loadData(); + }, [loadData]); // 필터링 및 정렬 const filteredOrders = orders @@ -313,7 +216,7 @@ export default function OrderManagementSalesPage() { setMobileDisplayCount(20); }, [searchTerm, filterType]); - // 통계 계산 + // 통계 계산 (API stats 우선 사용, 없으면 로컬 계산) const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); @@ -321,18 +224,18 @@ export default function OrderManagementSalesPage() { const thisMonthOrders = orders.filter( (o) => new Date(o.orderDate) >= startOfMonth ); - const thisMonthAmount = thisMonthOrders.reduce((sum, o) => sum + o.amount, 0); + const thisMonthAmount = apiStats?.thisMonthAmount ?? thisMonthOrders.reduce((sum, o) => sum + o.amount, 0); // 분할 대기 (예시: 수주확정 상태) - const splitPendingCount = orders.filter((o) => o.status === "order_confirmed").length; + const splitPendingCount = apiStats?.splitPending ?? orders.filter((o) => o.status === "order_confirmed").length; // 생산지시 대기 (수주확정 상태 중 생산지시 안된 것) - const productionPendingCount = orders.filter( + const productionPendingCount = apiStats?.productionPending ?? orders.filter( (o) => o.status === "order_confirmed" || o.status === "order_registered" ).length; // 출하 대기 (작업완료 상태) - const shipPendingCount = orders.filter((o) => o.status === "work_completed").length; + const shipPendingCount = apiStats?.shipPending ?? orders.filter((o) => o.status === "work_completed").length; const stats = [ { @@ -370,7 +273,8 @@ export default function OrderManagementSalesPage() { router.push(`/sales/order-management-sales/${order.id}/edit`); }; - const handleCancel = (orderId: string) => { + // 개별 취소 기능은 상세 페이지에서 처리 + const _handleCancel = (orderId: string) => { setCancelTargetId(orderId); setIsCancelDialogOpen(true); }; @@ -399,25 +303,72 @@ export default function OrderManagementSalesPage() { // 다중 선택 삭제 (IntegratedListTemplateV2에서 확인 후 호출됨) // 템플릿 내부에서 이미 확인 팝업을 처리하므로 바로 삭제 실행 - const handleBulkDelete = () => { + const handleBulkDelete = async () => { const selectedIds = Array.from(selectedItems); if (selectedIds.length > 0) { - setOrders(orders.filter((o) => !selectedIds.includes(o.id))); - setSelectedItems(new Set()); - toast.success(`${selectedIds.length}개의 수주가 삭제되었습니다.`); + setIsDeleting(true); + try { + const result = await deleteOrders(selectedIds); + if (result.success) { + setOrders(orders.filter((o) => !selectedIds.includes(o.id))); + setSelectedItems(new Set()); + toast.success(`${selectedIds.length}개의 수주가 삭제되었습니다.`); + // 통계 새로고침 + const statsResult = await getOrderStats(); + if (statsResult.success && statsResult.data) { + setApiStats(statsResult.data); + } + } else { + toast.error(result.error || "삭제에 실패했습니다."); + } + } catch (error) { + console.error("Error deleting orders:", error); + toast.error("삭제 중 오류가 발생했습니다."); + } finally { + setIsDeleting(false); + } } }; // 삭제 확정 (단일/다중 모두 처리) - const handleConfirmDelete = () => { + const handleConfirmDelete = async () => { if (deleteTargetIds.length > 0) { const count = deleteTargetIds.length; - setOrders(orders.filter((o) => !deleteTargetIds.includes(o.id))); - // 선택 상태 초기화 - setSelectedItems(new Set()); - toast.success(`${count}개의 수주가 삭제되었습니다.`); - setIsDeleteDialogOpen(false); - setDeleteTargetIds([]); + setIsDeleting(true); + try { + let success = false; + if (count === 1) { + const result = await deleteOrder(deleteTargetIds[0]); + success = result.success; + if (!success) { + toast.error(result.error || "삭제에 실패했습니다."); + } + } else { + const result = await deleteOrders(deleteTargetIds); + success = result.success; + if (!success) { + toast.error(result.error || "삭제에 실패했습니다."); + } + } + + if (success) { + setOrders(orders.filter((o) => !deleteTargetIds.includes(o.id))); + setSelectedItems(new Set()); + toast.success(`${count}개의 수주가 삭제되었습니다.`); + // 통계 새로고침 + const statsResult = await getOrderStats(); + if (statsResult.success && statsResult.data) { + setApiStats(statsResult.data); + } + } + } catch (error) { + console.error("Error deleting orders:", error); + toast.error("삭제 중 오류가 발생했습니다."); + } finally { + setIsDeleting(false); + setIsDeleteDialogOpen(false); + setDeleteTargetIds([]); + } } }; @@ -443,6 +394,23 @@ export default function OrderManagementSalesPage() { } }; + // FCM 알림 발송 핸들러 + const handleSendNotification = useCallback(async () => { + startTransition(async () => { + try { + const result = await sendSalesOrderNotification(); + if (result.success) { + toast.success(`수주완료 알림을 발송했습니다. (${result.sentCount || 0}건)`); + } else { + toast.error(result.error || "알림 발송에 실패했습니다."); + } + } catch (error) { + if (isNextRedirectError(error)) throw error; + toast.error("알림 발송 중 오류가 발생했습니다."); + } + }); + }, []); + // 탭 구성 const tabs: TabOption[] = [ { @@ -654,6 +622,18 @@ export default function OrderManagementSalesPage() { ); }; + // 로딩 상태 표시 + if (isLoading) { + return ( +
+
+ +

수주 목록을 불러오는 중...

+
+
+ ); + } + return ( <> router.push("/sales/order-management-sales/new")}> - - 수주 등록 - +
+ + +
} stats={stats} searchValue={searchTerm} @@ -749,13 +735,18 @@ export default function OrderManagementSalesPage() {
- 취소 + 취소 - - 삭제 + {isDeleting ? ( + + ) : ( + + )} + {isDeleting ? "삭제 중..." : "삭제"} diff --git a/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx index f13b97c2..c688a319 100644 --- a/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx @@ -47,6 +47,9 @@ import { MessageCircle, X, FileCheck, + Package, + ChevronDown, + ChevronUp, } from "lucide-react"; import { ContentLoadingSpinner } from "@/components/ui/loading-spinner"; @@ -69,6 +72,9 @@ export default function QuoteDetailPage() { const [showDetailedBreakdown, setShowDetailedBreakdown] = useState(true); const [showMaterialList, setShowMaterialList] = useState(true); + // BOM 자재 상세 펼침/접힘 상태 + const [isBomExpanded, setIsBomExpanded] = useState(true); + // 견적 데이터 조회 const fetchQuote = useCallback(async () => { setIsLoading(true); @@ -464,6 +470,84 @@ export default function QuoteDetailPage() {
+ {/* BOM 자재 상세 */} + {quote.bomMaterials && quote.bomMaterials.length > 0 && ( + + +
+ + + BOM 자재 상세 + + {quote.bomMaterials.length}개 품목 + + + +
+
+ {isBomExpanded && ( + +
+ + + + + + + + + + + + + + + + {quote.bomMaterials.map((material, index) => ( + + + + + + + + + + + + ))} + + + + + + + +
No품목코드품목명유형규격단위수량단가금액
{index + 1}{material.itemCode}{material.itemName} + + {material.itemType === 'RM' ? '원자재' : + material.itemType === 'SM' ? '부자재' : + material.itemType === 'CS' ? '소모품' : material.itemType} + + {material.specification || '-'}{material.unit}{material.quantity.toLocaleString()}₩{material.unitPrice.toLocaleString()}₩{material.totalPrice.toLocaleString()}
합계 + ₩{quote.bomMaterials.reduce((sum, m) => sum + m.totalPrice, 0).toLocaleString()} +
+
+
+ )} +
+ )} + {/* 견적서 다이얼로그 */} diff --git a/src/components/approval/DocumentCreate/ApprovalLineSection.tsx b/src/components/approval/DocumentCreate/ApprovalLineSection.tsx index d21b6c48..37a86338 100644 --- a/src/components/approval/DocumentCreate/ApprovalLineSection.tsx +++ b/src/components/approval/DocumentCreate/ApprovalLineSection.tsx @@ -75,11 +75,13 @@ export function ApprovalLineSection({ data, onChange }: ApprovalLineSectionProps onValueChange={(value) => handleChange(index, value)} > - - {person.name && !person.id.startsWith('temp-') - ? `${person.department || ''} / ${person.position || ''} / ${person.name}` - : null} - + {employees.map((employee) => ( diff --git a/src/components/approval/DocumentCreate/ReferenceSection.tsx b/src/components/approval/DocumentCreate/ReferenceSection.tsx index daeeb54d..59cbdf15 100644 --- a/src/components/approval/DocumentCreate/ReferenceSection.tsx +++ b/src/components/approval/DocumentCreate/ReferenceSection.tsx @@ -75,11 +75,13 @@ export function ReferenceSection({ data, onChange }: ReferenceSectionProps) { onValueChange={(value) => handleChange(index, value)} > - - {person.name && !person.id.startsWith('temp-') - ? `${person.department || ''} / ${person.position || ''} / ${person.name}` - : null} - + {employees.map((employee) => ( diff --git a/src/components/approval/DraftBox/actions.ts b/src/components/approval/DraftBox/actions.ts index 88bd718f..1e7c495d 100644 --- a/src/components/approval/DraftBox/actions.ts +++ b/src/components/approval/DraftBox/actions.ts @@ -457,4 +457,4 @@ export async function cancelDraft(id: string): Promise<{ success: boolean; error error: '서버 오류가 발생했습니다.', }; } -} \ No newline at end of file +} diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx index 39fc90d4..21759ca9 100644 --- a/src/components/approval/DraftBox/index.tsx +++ b/src/components/approval/DraftBox/index.tsx @@ -8,6 +8,7 @@ import { Trash2, Plus, Pencil, + Bell, } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -19,6 +20,7 @@ import { submitDraft, submitDrafts, } from './actions'; +import { sendApprovalNotification } from '@/lib/actions/fcm'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; @@ -243,6 +245,24 @@ export function DraftBox() { router.push('/ko/approval/draft/new'); }, [router]); + // ===== FCM 알림 발송 핸들러 ===== + const handleSendNotification = useCallback(async () => { + startTransition(async () => { + try { + const result = await sendApprovalNotification(); + if (result.success) { + toast.success(`결재 알림을 발송했습니다. (${result.sentCount || 0}건)`); + } else { + toast.error(result.error || '알림 발송에 실패했습니다.'); + } + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('Notification error:', error); + toast.error('알림 발송 중 오류가 발생했습니다.'); + } + }); + }, []); + // ===== 문서 클릭/수정 핸들러 (조건부 로직) ===== // 임시저장 → 문서 작성 페이지 (수정 모드) // 그 외 → 문서 상세 모달 (상세 API 호출하여 content 포함된 데이터 가져옴) @@ -597,6 +617,10 @@ export function DraftBox() { )} + + + + + { + if (e.key === 'Enter' && attendeeSearchValue.trim()) { + e.preventDefault(); + handleAttendeeAdd(); + } + }} + /> + + + {attendeeSearchValue.trim() ? ( + + ) : ( + '검색 결과가 없습니다.' + )} + + + {employees + .filter((emp) => + emp.name.toLowerCase().includes(attendeeSearchValue.toLowerCase()) + ) + .map((employee) => { + const isSelected = formData.attendeeItems.some( + (item) => item.id === employee.id + ); + return ( + handleAttendeeSelect(employee)} + > + + {employee.name} + + ); + })} + + + + + + )} +
{renderSelectField('상태', 'attendanceStatus', formData.attendanceStatus, ATTENDANCE_STATUS_OPTIONS)} @@ -483,10 +811,87 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
- {renderField('현장명', 'projectName', formData.projectName, { - required: true, - placeholder: '현장명', - })} + {/* 현장명 - 거래처 연동 Combobox */} +
+ +
+ { + setSiteInputValue(e.target.value); + handleChange('projectName', e.target.value); + setShowSiteDropdown(true); + }} + onFocus={() => formData.partnerId && setShowSiteDropdown(true)} + onBlur={() => setTimeout(() => setShowSiteDropdown(false), 200)} + placeholder={ + !formData.partnerId + ? '거래처를 먼저 선택해주세요' + : isLoadingSites + ? '현장 목록 로딩 중...' + : '현장명 입력 또는 선택' + } + disabled={isViewMode || !formData.partnerId} + className="bg-white pr-10" + /> + {!isViewMode && formData.partnerId && ( + + )} + {/* 현장 드롭다운 */} + {showSiteDropdown && sites.length > 0 && ( +
+ {sites + .filter((site) => + site.siteName.toLowerCase().includes(siteInputValue.toLowerCase()) + ) + .map((site) => ( + + ))} + {sites.filter((site) => + site.siteName.toLowerCase().includes(siteInputValue.toLowerCase()) + ).length === 0 && ( +
+ 일치하는 현장이 없습니다. 신규 등록하려면 + 버튼을 클릭하세요. +
+ )} +
+ )} +
+ {!formData.partnerId && !isViewMode && ( +

거래처를 먼저 선택하면 해당 거래처의 현장 목록이 표시됩니다.

+ )} +
{renderField('입찰일자', 'bidDate', formData.bidDate, { type: 'date', })} @@ -736,6 +1141,53 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site + + {/* 현장 신규 등록 다이얼로그 */} + + + + 현장 신규 등록 + + 선택한 거래처에 새로운 현장을 등록합니다. +
+ 등록된 현장은 현장관리 목록에도 추가됩니다. +
+
+
+
+ + p.id === formData.partnerId)?.partnerName || ''} + disabled + className="bg-gray-50" + /> +
+
+ + setNewSiteName(e.target.value)} + placeholder="현장명을 입력하세요" + disabled={isCreatingSite} + autoFocus + /> +
+
+ + 취소 + + {isCreatingSite && } + 등록 + + +
+
); } \ No newline at end of file diff --git a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx index 37fddaca..5f01d896 100644 --- a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx +++ b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx @@ -130,12 +130,11 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin if (listResult.success && listResult.data) { setBriefings(listResult.data.items); - // 목업 통계 계산 (참석 상태 기준) + // 통계 계산 (참석 상태 기준) const items = listResult.data.items; const total = items.length; - // 목업: scheduled 상태는 참석예정, 나머지는 참석완료로 처리 - const scheduled = items.filter((b) => b.status === 'scheduled').length; - const attended = items.filter((b) => b.status !== 'scheduled').length; + const scheduled = items.filter((b) => b.attendanceStatus === 'scheduled').length; + const attended = items.filter((b) => b.attendanceStatus === 'attended').length; setStatsData({ total, scheduled, attended }); } } catch { @@ -155,9 +154,9 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin // 필터링된 데이터 const filteredBriefings = useMemo(() => { return briefings.filter((briefing) => { - // Stats 탭 필터 - if (activeStatTab === 'scheduled' && briefing.status !== 'scheduled') return false; - if (activeStatTab === 'attended' && briefing.status === 'scheduled') return false; + // Stats 탭 필터 (참석 상태 기준) + if (activeStatTab === 'scheduled' && briefing.attendanceStatus !== 'scheduled') return false; + if (activeStatTab === 'attended' && briefing.attendanceStatus !== 'attended') return false; // 거래처 필터 (다중선택 - 빈 배열 = 전체) if (partnerFilters.length > 0) { @@ -174,8 +173,8 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin if (!attendeeFilters.includes(attendeeId)) return false; } - // 상태 필터 - if (statusFilter !== 'all' && briefing.status !== statusFilter) return false; + // 상태 필터 (참석 상태 기준) + if (statusFilter !== 'all' && briefing.attendanceStatus !== statusFilter) return false; // 검색 필터 if (searchValue) { @@ -431,8 +430,8 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin const renderTableRow = useCallback( (briefing: SiteBriefing, index: number, globalIndex: number) => { const isSelected = selectedItems.has(briefing.id); - // 목업 데이터에서 상태 매핑 - const displayStatus = briefing.status === 'scheduled' ? 'scheduled' : 'attended'; + // 참석 상태 표시 + const displayStatus = briefing.attendanceStatus || 'scheduled'; return ( void) => { - const displayStatus = briefing.status === 'scheduled' ? 'scheduled' : 'attended'; + const displayStatus = briefing.attendanceStatus || 'scheduled'; return ( | null; // 백엔드는 attendees (복수형), array 타입 + attendance_status: string | null; + project_name: string | null; + site_count: number | null; + construction_start_date: string | null; + construction_end_date: string | null; + vat_type: string | null; + work_report: string | null; + attendee_count: number | null; + created_at: string; + updated_at: string; + created_by: string | null; +} + +interface ApiSiteBriefingStats { + total: number; + scheduled: number; + ongoing: number; + completed: number; + cancelled: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → SiteBriefing 타입 변환 + */ +function transformSiteBriefing(apiData: ApiSiteBriefing): SiteBriefing { + // attendees를 JSON 문자열로 변환 (types.ts의 parseAttendeeItems에서 파싱) + const attendeeJson = apiData.attendees ? JSON.stringify(apiData.attendees) : ''; + + return { + id: String(apiData.id), + briefingCode: apiData.briefing_code || '', + title: apiData.title || apiData.project_name || '', + description: apiData.description || '', + partnerId: apiData.partner_id ? String(apiData.partner_id) : '', + partnerName: apiData.partner_name || '', + briefingDate: apiData.briefing_date || '', + briefingTime: apiData.briefing_time || '', + location: apiData.location || '', + address: apiData.address || '', + status: (apiData.status as SiteBriefing['status']) || 'scheduled', + bidStatus: (apiData.bid_status as SiteBriefing['bidStatus']) || 'pending', + bidDate: apiData.bid_date, + attendee: attendeeJson, // JSON 문자열로 저장 (parseAttendeeItems에서 파싱) attendees: [], - attendeeCount: 5, - createdAt: '2025-01-01', - updatedAt: '2025-01-01', - createdBy: '홍길동', - }, - { - id: '2', - briefingCode: 'SB-002', - title: '서초 아파트 리모델링', - description: '서초구 반포동 아파트 리모델링 현장설명회', - partnerId: '2', - partnerName: '삼성시공', - briefingDate: '2025-05-12', - briefingTime: '10:00', - location: '서초구청 소회의실', - address: '서울특별시 서초구 남부순환로 2584', - status: 'ongoing', - bidStatus: 'bidding', - bidDate: '2025-05-18', - attendees: [], - attendeeCount: 8, - createdAt: '2025-01-02', - updatedAt: '2025-01-02', - createdBy: '김철수', - }, - { - id: '3', - briefingCode: 'SB-003', - title: '여의도 상업시설 신축', - description: '영등포구 여의도동 상업시설 신축 현장설명회', - partnerId: '3', - partnerName: 'LG건설', - briefingDate: '2025-05-13', - briefingTime: '15:00', - location: 'LG트윈타워 회의실', - address: '서울특별시 영등포구 여의대로 128', - status: 'completed', - bidStatus: 'awarded', - bidDate: '2025-05-20', - attendees: [], - attendeeCount: 12, - createdAt: '2025-01-03', - updatedAt: '2025-01-03', - createdBy: '박영수', - }, - { - id: '4', - briefingCode: 'SB-004', - title: '송파 주상복합 공사', - description: '송파구 잠실동 주상복합 건축 현장설명회', - partnerId: '1', - partnerName: '대한건설', - briefingDate: '2025-05-14', - briefingTime: '11:00', - location: '롯데월드타워 회의실', - address: '서울특별시 송파구 올림픽로 300', - status: 'cancelled', - bidStatus: 'failed', - bidDate: null, - attendees: [], - attendeeCount: 0, - createdAt: '2025-01-04', - updatedAt: '2025-01-04', - createdBy: '최민수', - }, - { - id: '5', - briefingCode: 'SB-005', - title: '마포 물류센터 증축', - description: '마포구 상암동 물류센터 증축 현장설명회', - partnerId: '2', - partnerName: '삼성시공', - briefingDate: '2025-05-15', - briefingTime: '09:00', - location: '상암 DMC 회의실', - address: '서울특별시 마포구 상암산로 76', - status: 'postponed', - bidStatus: 'pending', - bidDate: null, - attendees: [], - attendeeCount: 3, - createdAt: '2025-01-05', - updatedAt: '2025-01-05', - createdBy: '이영희', - }, -]; + attendeeCount: apiData.attendee_count || 0, + attendanceStatus: (apiData.attendance_status as SiteBriefing['attendanceStatus']) || 'scheduled', + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', + createdBy: apiData.created_by || '', + }; +} + +/** + * SiteBriefingFormData → API 요청 데이터 변환 + */ +function transformFormDataToApi(data: SiteBriefingFormData): Record { + // attendeeItems 배열을 백엔드 형식으로 변환 + // - id가 있으면 internal (직원), 없으면 external (외부인/직접입력) + const attendees = data.attendeeItems && data.attendeeItems.length > 0 + ? data.attendeeItems.map(item => ({ + ...item, + type: item.id ? 'internal' : 'external', + })) + : null; + + return { + briefing_code: data.briefingCode, + title: data.projectName, + description: data.workReport, + partner_id: data.partnerId ? Number(data.partnerId) : null, + partner_name: data.partnerName, + briefing_date: data.briefingDate, + briefing_time: data.briefingTime, + briefing_type: data.briefingType, + location: data.location, + attendees: attendees, // 백엔드 필드명: attendees (복수형, array) + attendance_status: data.attendanceStatus, + project_name: data.projectName, + bid_date: data.bidDate, + site_count: data.siteCount, + construction_start_date: data.constructionStartDate, + construction_end_date: data.constructionEndDate, + vat_type: data.vatType, + work_report: data.workReport, + }; +} // 현장설명회 목록 조회 export async function getSiteBriefingList( filter?: SiteBriefingFilter ): Promise<{ success: boolean; data?: SiteBriefingListResponse; error?: string }> { try { - let filtered = [...mockSiteBriefings]; + // API 쿼리 파라미터 구성 (모든 값을 문자열로 변환) + const params: Record = { + per_page: String(filter?.size ?? 20), + page: String(filter?.page ?? 1), + }; - // 검색 필터 if (filter?.search) { - const search = filter.search.toLowerCase(); - filtered = filtered.filter( - (b) => - b.title.toLowerCase().includes(search) || - b.briefingCode.toLowerCase().includes(search) || - b.partnerName.toLowerCase().includes(search) - ); + params.search = filter.search; } - - // 상태 필터 if (filter?.status && filter.status !== 'all') { - filtered = filtered.filter((b) => b.status === filter.status); + params.status = filter.status; } - - // 입찰 상태 필터 if (filter?.bidStatus && filter.bidStatus !== 'all') { - filtered = filtered.filter((b) => b.bidStatus === filter.bidStatus); + params.bid_status = filter.bidStatus; } - - // 거래처 필터 if (filter?.partnerId) { - filtered = filtered.filter((b) => b.partnerId === filter.partnerId); + params.partner_id = filter.partnerId; } - - // 날짜 필터 if (filter?.startDate) { - filtered = filtered.filter((b) => b.briefingDate >= filter.startDate!); + params.start_date = filter.startDate; } if (filter?.endDate) { - filtered = filtered.filter((b) => b.briefingDate <= filter.endDate!); + params.end_date = filter.endDate; } - - // 정렬 if (filter?.sortBy) { - switch (filter.sortBy) { - case 'latest': - filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - break; - case 'oldest': - filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - break; - case 'dateAsc': - filtered.sort((a, b) => new Date(a.briefingDate).getTime() - new Date(b.briefingDate).getTime()); - break; - case 'dateDesc': - filtered.sort((a, b) => new Date(b.briefingDate).getTime() - new Date(a.briefingDate).getTime()); - break; + const sortMapping: Record = { + latest: { sort_by: 'created_at', sort_dir: 'desc' }, + oldest: { sort_by: 'created_at', sort_dir: 'asc' }, + dateAsc: { sort_by: 'briefing_date', sort_dir: 'asc' }, + dateDesc: { sort_by: 'briefing_date', sort_dir: 'desc' }, + }; + const sort = sortMapping[filter.sortBy]; + if (sort) { + params.sort_by = sort.sort_by; + params.sort_dir = sort.sort_dir; } } - const page = filter?.page ?? 1; - const size = filter?.size ?? 20; - const start = (page - 1) * size; - const paginatedItems = filtered.slice(start, start + size); + const response = await apiClient.get<{ + success: boolean; + data: { + data: ApiSiteBriefing[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }; + }>('/site-briefings', { params }); + + const apiData = response.data; + const items = apiData.data.map(transformSiteBriefing); return { success: true, data: { - items: paginatedItems, - total: filtered.length, - page, - size, - totalPages: Math.ceil(filtered.length / size), + items, + total: apiData.total, + page: apiData.current_page, + size: apiData.per_page, + totalPages: apiData.last_page, }, }; } catch (error) { @@ -196,13 +197,12 @@ export async function getSiteBriefing( id: string ): Promise<{ success: boolean; data?: SiteBriefing; error?: string }> { try { - const briefing = mockSiteBriefings.find((b) => b.id === id); + const response = await apiClient.get<{ + success: boolean; + data: ApiSiteBriefing; + }>(`/site-briefings/${id}`); - if (!briefing) { - return { success: false, error: '현장설명회를 찾을 수 없습니다.' }; - } - - return { success: true, data: briefing }; + return { success: true, data: transformSiteBriefing(response.data) }; } catch (error) { console.error('getSiteBriefing error:', error); return { success: false, error: '현장설명회 조회에 실패했습니다.' }; @@ -212,46 +212,86 @@ export async function getSiteBriefing( // 현장설명회 통계 조회 export async function getSiteBriefingStats(): Promise<{ success: boolean; data?: SiteBriefingStats; error?: string }> { try { - const total = mockSiteBriefings.length; - const scheduled = mockSiteBriefings.filter((b) => b.status === 'scheduled').length; - const ongoing = mockSiteBriefings.filter((b) => b.status === 'ongoing').length; - const completed = mockSiteBriefings.filter((b) => b.status === 'completed').length; - const cancelled = mockSiteBriefings.filter((b) => b.status === 'cancelled' || b.status === 'postponed').length; + const response = await apiClient.get<{ + success: boolean; + data: ApiSiteBriefingStats; + }>('/site-briefings/stats'); - return { - success: true, - data: { - total, - scheduled, - ongoing, - completed, - cancelled, - }, - }; + return { success: true, data: response.data }; } catch (error) { console.error('getSiteBriefingStats error:', error); return { success: false, error: '통계 조회에 실패했습니다.' }; } } -// 현장설명회 삭제 +// ======================================== +// API 함수 (CRUD) +// ======================================== + +/** + * 현장설명회 등록 + * POST /api/v1/site-briefings + */ +export async function createSiteBriefing(data: SiteBriefingFormData): Promise<{ + success: boolean; + data?: SiteBriefing; + error?: string; +}> { + try { + const apiData = transformFormDataToApi(data); + const response = await apiClient.post<{ success: boolean; data: ApiSiteBriefing }>('/site-briefings', apiData); + return { success: true, data: transformSiteBriefing(response.data) }; + } catch (error) { + console.error('현장설명회 등록 오류:', error); + return { success: false, error: '현장설명회 등록에 실패했습니다.' }; + } +} + +/** + * 현장설명회 수정 + * PUT /api/v1/site-briefings/{id} + */ +export async function updateSiteBriefing(id: string, data: SiteBriefingFormData): Promise<{ + success: boolean; + data?: SiteBriefing; + error?: string; +}> { + try { + const apiData = transformFormDataToApi(data); + const response = await apiClient.put<{ success: boolean; data: ApiSiteBriefing }>(`/site-briefings/${id}`, apiData); + return { success: true, data: transformSiteBriefing(response.data) }; + } catch (error) { + console.error('현장설명회 수정 오류:', error); + return { success: false, error: '현장설명회 수정에 실패했습니다.' }; + } +} + +/** + * 현장설명회 삭제 + * DELETE /api/v1/site-briefings/{id} + */ export async function deleteSiteBriefing(id: string): Promise<{ success: boolean; error?: string }> { try { - console.log('Delete site briefing:', id); + await apiClient.delete(`/site-briefings/${id}`); return { success: true }; } catch (error) { - console.error('deleteSiteBriefing error:', error); + console.error('현장설명회 삭제 오류:', error); return { success: false, error: '현장설명회 삭제에 실패했습니다.' }; } } -// 현장설명회 일괄 삭제 +/** + * 현장설명회 일괄 삭제 + * DELETE /api/v1/site-briefings/bulk + */ export async function deleteSiteBriefings(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> { try { - console.log('Delete site briefings:', ids); + await apiClient.delete('/site-briefings/bulk', { + data: { ids: ids.map((id) => Number(id)) }, + }); return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deleteSiteBriefings error:', error); + console.error('현장설명회 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } diff --git a/src/components/business/construction/site-briefings/types.ts b/src/components/business/construction/site-briefings/types.ts index af9beebd..ed7dbee3 100644 --- a/src/components/business/construction/site-briefings/types.ts +++ b/src/components/business/construction/site-briefings/types.ts @@ -8,7 +8,7 @@ export type SiteBriefingStatus = 'scheduled' | 'ongoing' | 'completed' | 'cancel // 입찰 상태 export type BidStatus = 'pending' | 'bidding' | 'closed' | 'failed' | 'awarded'; -// 참석자 타입 +// 참석자 타입 (외부 참석자 - 상세 정보) export interface Attendee { id: string; name: string; @@ -18,6 +18,12 @@ export interface Attendee { isAttended: boolean; } +// 참석자 항목 타입 (내부 직원 또는 직접 입력) +export interface AttendeeItem { + id: string; // 직원 ID (직접 입력 시 빈 문자열) + name: string; // 이름 +} + // 현장설명회 타입 export interface SiteBriefing { id: string; @@ -41,8 +47,10 @@ export interface SiteBriefing { bidDate: string | null; // 입찰 날짜 // 참석자 정보 + attendee: string; // 참석자 attendees: Attendee[]; attendeeCount: number; // 참석자 수 + attendanceStatus: AttendanceStatus; // 참석 상태 // 메타 정보 createdAt: string; @@ -173,7 +181,7 @@ export interface SiteBriefingFormData { briefingTime: string; // 현장설명회 시간 briefingType: BriefingType; // 구분 (온라인/오프라인) location: string; // 현장설명회 장소 - attendee: string; // 참석자 + attendeeItems: AttendeeItem[]; // 참석자 목록 (JSON으로 저장) attendanceStatus: AttendanceStatus; // 상태 // 입찰 정보 @@ -219,7 +227,7 @@ export function getEmptySiteBriefingFormData(): SiteBriefingFormData { briefingTime: '', briefingType: 'offline', location: '', - attendee: '', + attendeeItems: [], attendanceStatus: 'scheduled', projectName: '', bidDate: '', @@ -233,6 +241,26 @@ export function getEmptySiteBriefingFormData(): SiteBriefingFormData { }; } +// attendee JSON 문자열을 AttendeeItem[]로 파싱 +export function parseAttendeeItems(attendeeJson: string | null): AttendeeItem[] { + if (!attendeeJson) return []; + try { + const parsed = JSON.parse(attendeeJson); + if (Array.isArray(parsed)) { + return parsed.filter((item): item is AttendeeItem => + typeof item === 'object' && item !== null && typeof item.name === 'string' + ); + } + return []; + } catch { + // JSON 파싱 실패 시 기존 단일 문자열을 AttendeeItem으로 변환 + if (attendeeJson.trim()) { + return [{ id: '', name: attendeeJson.trim() }]; + } + return []; + } +} + // SiteBriefing을 FormData로 변환 export function siteBriefingToFormData(briefing: SiteBriefing): SiteBriefingFormData { return { @@ -243,8 +271,8 @@ export function siteBriefingToFormData(briefing: SiteBriefing): SiteBriefingForm briefingTime: briefing.briefingTime, briefingType: 'offline', // 기본값 location: briefing.location, - attendee: '', // 기본값 - attendanceStatus: 'scheduled', // 기본값 + attendeeItems: parseAttendeeItems(briefing.attendee), + attendanceStatus: briefing.attendanceStatus || 'scheduled', projectName: briefing.title, bidDate: briefing.bidDate || '', siteCount: 0, // 기본값 diff --git a/src/components/business/construction/site-management/actions.ts b/src/components/business/construction/site-management/actions.ts index edc3245b..2dab97ae 100644 --- a/src/components/business/construction/site-management/actions.ts +++ b/src/components/business/construction/site-management/actions.ts @@ -1,195 +1,249 @@ 'use server'; -import type { Site, SiteStats } from './types'; +import type { Site, SiteStats, SiteStatus } from './types'; +import { apiClient } from '@/lib/api'; -// 목업 현장 데이터 -const MOCK_SITES: Site[] = [ - { - id: '1', - siteCode: '123123', - partnerId: '1', - partnerName: '회사명', - siteName: '현장명', - address: '-', - status: 'unregistered', - createdAt: '2025-09-01T00:00:00Z', - updatedAt: '2025-09-01T00:00:00Z', - }, - { - id: '2', - siteCode: '123123', - partnerId: '1', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'suspended', - createdAt: '2025-09-02T00:00:00Z', - updatedAt: '2025-09-02T00:00:00Z', - }, - { - id: '3', - siteCode: '123123', - partnerId: '2', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'active', - createdAt: '2025-09-03T00:00:00Z', - updatedAt: '2025-09-03T00:00:00Z', - }, - { - id: '4', - siteCode: '123123', - partnerId: '1', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'active', - createdAt: '2025-09-04T00:00:00Z', - updatedAt: '2025-09-04T00:00:00Z', - }, - { - id: '5', - siteCode: '123123', - partnerId: '3', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'active', - createdAt: '2025-09-05T00:00:00Z', - updatedAt: '2025-09-05T00:00:00Z', - }, - { - id: '6', - siteCode: '123123', - partnerId: '1', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'active', - createdAt: '2025-09-06T00:00:00Z', - updatedAt: '2025-09-06T00:00:00Z', - }, - { - id: '7', - siteCode: '123123', - partnerId: '2', - partnerName: '회사명', - siteName: '현장명', - address: '서울시 강남구 대현빌라 123길', - status: 'pending', - createdAt: '2025-09-07T00:00:00Z', - updatedAt: '2025-09-07T00:00:00Z', - }, -]; +/** + * 주일 기업 - 현장관리 Server Actions + * 표준화된 apiClient 사용 버전 + */ + +// ======================================== +// API 응답 타입 +// ======================================== + +interface ApiSite { + id: number; + site_code: string | null; + client_id: number | null; + name: string; + address: string | null; + status: SiteStatus; + created_at: string; + updated_at: string; + client?: { + id: number; + name: string; + } | null; +} + +interface ApiSiteStats { + total: number; + construction: number; + unregistered: number; + suspended: number; + pending: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → Site 타입 변환 + */ +function transformSite(apiData: ApiSite): Site { + return { + id: String(apiData.id), + siteCode: apiData.site_code || '', + partnerId: apiData.client_id ? String(apiData.client_id) : '', + partnerName: apiData.client?.name || '', + siteName: apiData.name || '', + address: apiData.address || '', + status: apiData.status || 'unregistered', + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', + }; +} + +// ======================================== +// API 함수 +// ======================================== interface GetSiteListParams { size?: number; + page?: number; startDate?: string; endDate?: string; + search?: string; + status?: string; + clientId?: string; + sortBy?: string; } -interface GetSiteListResult { +/** + * 현장 목록 조회 + * GET /api/v1/sites + */ +export async function getSiteList(params: GetSiteListParams = {}): Promise<{ success: boolean; data?: { items: Site[]; - totalCount: number; + total: number; + page: number; + size: number; + totalPages: number; }; error?: string; -} - -// 현장 목록 조회 -export async function getSiteList(params: GetSiteListParams = {}): Promise { +}> { try { - // TODO: API 연동 시 실제 API 호출로 변경 - await new Promise((resolve) => setTimeout(resolve, 500)); + const queryParams: Record = {}; - let filteredSites = [...MOCK_SITES]; + // 검색 + if (params.search) queryParams.search = params.search; - // 날짜 필터 - if (params.startDate) { - filteredSites = filteredSites.filter( - (site) => new Date(site.createdAt) >= new Date(params.startDate!) - ); - } - if (params.endDate) { - filteredSites = filteredSites.filter( - (site) => new Date(site.createdAt) <= new Date(params.endDate!) - ); + // 필터 + if (params.status && params.status !== 'all') queryParams.status = params.status; + if (params.clientId && params.clientId !== 'all') queryParams.client_id = params.clientId; + + // 날짜 범위 + if (params.startDate) queryParams.start_date = params.startDate; + if (params.endDate) queryParams.end_date = params.endDate; + + // 페이지네이션 + if (params.page) queryParams.page = String(params.page); + if (params.size) queryParams.per_page = String(params.size); + + // 정렬 + if (params.sortBy) { + const sortMap: Record = { + latest: { field: 'created_at', dir: 'desc' }, + oldest: { field: 'created_at', dir: 'asc' }, + partnerNameAsc: { field: 'client_id', dir: 'asc' }, + partnerNameDesc: { field: 'client_id', dir: 'desc' }, + siteNameAsc: { field: 'name', dir: 'asc' }, + siteNameDesc: { field: 'name', dir: 'desc' }, + }; + const sort = sortMap[params.sortBy]; + if (sort) { + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; + } } + const response = await apiClient.get<{ + data: ApiSite[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }>('/sites', { params: queryParams }); + + const items = (response.data || []).map(transformSite); + return { success: true, data: { - items: filteredSites, - totalCount: filteredSites.length, + items, + total: response.total || 0, + page: response.current_page || 1, + size: response.per_page || 20, + totalPages: response.last_page || 1, }, }; } catch (error) { - console.error('getSiteList error:', error); - return { - success: false, - error: '현장 목록을 불러오는데 실패했습니다.', - }; + console.error('현장 목록 조회 오류:', error); + return { success: false, error: '현장 목록을 불러오는데 실패했습니다.' }; } } -// 현장 통계 조회 -export async function getSiteStats(): Promise<{ success: boolean; data?: SiteStats; error?: string }> { +/** + * 현장 통계 조회 + * GET /api/v1/sites/stats + */ +export async function getSiteStats(): Promise<{ + success: boolean; + data?: SiteStats; + error?: string; +}> { try { - // TODO: API 연동 시 실제 API 호출로 변경 - await new Promise((resolve) => setTimeout(resolve, 300)); - - const total = MOCK_SITES.length; - const construction = MOCK_SITES.filter((s) => s.status === 'active').length; - const unregistered = MOCK_SITES.filter((s) => s.status === 'unregistered').length; + const response = await apiClient.get('/sites/stats'); return { success: true, data: { - total, - construction, - unregistered, + total: response.total || 0, + construction: response.construction || 0, + unregistered: response.unregistered || 0, + suspended: response.suspended || 0, + pending: response.pending || 0, }, }; } catch (error) { - console.error('getSiteStats error:', error); - return { - success: false, - error: '현장 통계를 불러오는데 실패했습니다.', - }; + console.error('현장 통계 조회 오류:', error); + return { success: false, error: '현장 통계를 불러오는데 실패했습니다.' }; } } -// 현장 삭제 -export async function deleteSite(id: string): Promise<{ success: boolean; error?: string }> { +/** + * 현장 삭제 + * DELETE /api/v1/sites/{id} + */ +export async function deleteSite(id: string): Promise<{ + success: boolean; + error?: string; +}> { try { - // TODO: API 연동 시 실제 API 호출로 변경 - await new Promise((resolve) => setTimeout(resolve, 500)); + await apiClient.delete(`/sites/${id}`); return { success: true }; } catch (error) { - console.error('deleteSite error:', error); - return { - success: false, - error: '현장 삭제에 실패했습니다.', - }; + console.error('현장 삭제 오류:', error); + return { success: false, error: '현장 삭제에 실패했습니다.' }; } } -// 현장 일괄 삭제 -export async function deleteSites(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> { +/** + * 현장 일괄 삭제 + * DELETE /api/v1/sites/bulk + */ +export async function deleteSites(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; +}> { try { - // TODO: API 연동 시 실제 API 호출로 변경 - await new Promise((resolve) => setTimeout(resolve, 500)); - return { - success: true, - deletedCount: ids.length, - }; + await apiClient.delete('/sites/bulk', { + data: { ids: ids.map((id) => Number(id)) }, + }); + return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deleteSites error:', error); - return { - success: false, - error: '현장 일괄 삭제에 실패했습니다.', - }; + console.error('현장 일괄 삭제 오류:', error); + return { success: false, error: '현장 일괄 삭제에 실패했습니다.' }; } } + +// ======================================== +// 현장 생성/수정 타입 +// ======================================== + +export interface CreateSiteData { + siteName: string; + partnerId: string; + address?: string; + status?: SiteStatus; +} + +/** + * 현장 등록 + * POST /api/v1/sites + */ +export async function createSite(data: CreateSiteData): Promise<{ + success: boolean; + data?: Site; + error?: string; +}> { + try { + const apiData = { + name: data.siteName, + client_id: data.partnerId ? Number(data.partnerId) : null, + address: data.address || null, + status: data.status || 'unregistered', + }; + + const response = await apiClient.post<{ success: boolean; data: ApiSite }>('/sites', apiData); + return { success: true, data: transformSite(response.data) }; + } catch (error) { + console.error('현장 등록 오류:', error); + return { success: false, error: '현장 등록에 실패했습니다.' }; + } +} \ No newline at end of file diff --git a/src/components/business/construction/site-management/types.ts b/src/components/business/construction/site-management/types.ts index 85eaabbc..5051d47c 100644 --- a/src/components/business/construction/site-management/types.ts +++ b/src/components/business/construction/site-management/types.ts @@ -17,8 +17,10 @@ export type SiteStatus = 'unregistered' | 'suspended' | 'active' | 'pending'; // 현장 통계 export interface SiteStats { total: number; // 전체 현장 - construction: number; // 시공 현장 + construction: number; // 시공 현장 (active) unregistered: number; // 미등록 현장 + suspended: number; // 중지 현장 + pending: number; // 보류 현장 } // 상태 옵션 diff --git a/src/components/business/construction/structure-review/actions.ts b/src/components/business/construction/structure-review/actions.ts index 5717198a..e5f5f952 100644 --- a/src/components/business/construction/structure-review/actions.ts +++ b/src/components/business/construction/structure-review/actions.ts @@ -1,184 +1,256 @@ 'use server'; -import type { StructureReview, StructureReviewStats } from './types'; +import type { StructureReview, StructureReviewStats, StructureReviewStatus } from './types'; +import { apiClient } from '@/lib/api'; -// 목업 데이터 -const MOCK_STRUCTURE_REVIEWS: StructureReview[] = [ - { - id: '1', - reviewNumber: '123123', - partnerId: '1', - partnerName: '회사명', - siteId: '1', - siteName: '현장명', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: '2025-12-15', - status: 'pending', - createdAt: '2025-12-01T00:00:00Z', - updatedAt: '2025-12-01T00:00:00Z', - }, - { - id: '2', - reviewNumber: '123123', - partnerId: '1', - partnerName: '회사명', - siteId: '2', - siteName: '현장명', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: null, - status: 'pending', - createdAt: '2025-12-02T00:00:00Z', - updatedAt: '2025-12-02T00:00:00Z', - }, - { - id: '3', - reviewNumber: '123123', - partnerId: '2', - partnerName: '회사명', - siteId: '3', - siteName: '현장명', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: null, - completionDate: null, - status: 'pending', - createdAt: '2025-12-03T00:00:00Z', - updatedAt: '2025-12-03T00:00:00Z', - }, - { - id: '4', - reviewNumber: '123123', - partnerId: '2', - partnerName: '회사명', - siteId: '4', - siteName: '현장명', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: '2025-12-15', - status: 'completed', - createdAt: '2025-12-04T00:00:00Z', - updatedAt: '2025-12-04T00:00:00Z', - }, - { - id: '5', - reviewNumber: '123123', - partnerId: '3', - partnerName: '회사명', - siteId: '5', - siteName: '현장명', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: '2025-12-15', - status: 'completed', - createdAt: '2025-12-05T00:00:00Z', - updatedAt: '2025-12-05T00:00:00Z', - }, -]; +/** + * 구조검토관리 Server Actions + * 표준화된 apiClient 사용 버전 + */ -// 구조검토 목록 조회 -export async function getStructureReviewList(params?: { - size?: number; - startDate?: string; - endDate?: string; -}): Promise<{ - success: boolean; - data?: { items: StructureReview[]; total: number }; - error?: string; -}> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 500)); +// ======================================== +// API 응답 타입 +// ======================================== +interface ApiStructureReview { + id: number; + review_number: string | null; + partner_id: number | null; + partner_name: string | null; + site_id: number | null; + site_name: string | null; + request_date: string | null; + review_company: string | null; + reviewer_name: string | null; + review_date: string | null; + completion_date: string | null; + status: StructureReviewStatus; + file_url: string | null; + created_at: string; + updated_at: string; +} + +interface ApiStructureReviewStats { + total: number; + pending: number; + completed: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → StructureReview 타입 변환 + */ +function transformStructureReview(apiData: ApiStructureReview): StructureReview { return { - success: true, - data: { - items: MOCK_STRUCTURE_REVIEWS, - total: MOCK_STRUCTURE_REVIEWS.length, - }, + id: String(apiData.id), + reviewNumber: apiData.review_number || '', + partnerId: apiData.partner_id ? String(apiData.partner_id) : '', + partnerName: apiData.partner_name || '', + siteId: apiData.site_id ? String(apiData.site_id) : '', + siteName: apiData.site_name || '', + requestDate: apiData.request_date || '', + reviewCompany: apiData.review_company || '', + reviewerName: apiData.reviewer_name || '', + reviewDate: apiData.review_date || null, + completionDate: apiData.completion_date || null, + status: apiData.status || 'pending', + fileUrl: apiData.file_url || undefined, + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', }; } -// 구조검토 통계 조회 +/** + * StructureReview → API 요청 데이터 변환 + */ +function transformToApiData(data: Partial): Record { + return { + review_number: data.reviewNumber || null, + partner_id: data.partnerId ? Number(data.partnerId) : null, + partner_name: data.partnerName || null, + site_id: data.siteId ? Number(data.siteId) : null, + site_name: data.siteName || null, + request_date: data.requestDate || null, + review_company: data.reviewCompany || null, + reviewer_name: data.reviewerName || null, + review_date: data.reviewDate || null, + completion_date: data.completionDate || null, + status: data.status || 'pending', + file_url: data.fileUrl || null, + }; +} + +// ======================================== +// API 함수 +// ======================================== + +interface GetStructureReviewListParams { + size?: number; + page?: number; + startDate?: string; + endDate?: string; + search?: string; + status?: string; + partnerId?: string; + siteId?: string; + sortBy?: string; +} + +/** + * 구조검토 목록 조회 + * GET /api/v1/construction/structure-reviews + */ +export async function getStructureReviewList(params: GetStructureReviewListParams = {}): Promise<{ + success: boolean; + data?: { + items: StructureReview[]; + total: number; + page: number; + size: number; + totalPages: number; + }; + error?: string; +}> { + try { + const queryParams: Record = {}; + + // 검색 + if (params.search) queryParams.search = params.search; + + // 필터 + if (params.status && params.status !== 'all') queryParams.status = params.status; + if (params.partnerId && params.partnerId !== 'all') queryParams.partner_id = params.partnerId; + if (params.siteId && params.siteId !== 'all') queryParams.site_id = params.siteId; + + // 날짜 범위 + if (params.startDate) queryParams.start_date = params.startDate; + if (params.endDate) queryParams.end_date = params.endDate; + + // 페이지네이션 + if (params.page) queryParams.page = String(params.page); + if (params.size) queryParams.per_page = String(params.size); + + // 정렬 + if (params.sortBy) { + const sortMap: Record = { + latest: { field: 'created_at', dir: 'desc' }, + oldest: { field: 'created_at', dir: 'asc' }, + partnerNameAsc: { field: 'partner_name', dir: 'asc' }, + partnerNameDesc: { field: 'partner_name', dir: 'desc' }, + siteNameAsc: { field: 'site_name', dir: 'asc' }, + siteNameDesc: { field: 'site_name', dir: 'desc' }, + }; + const sort = sortMap[params.sortBy]; + if (sort) { + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; + } + } + + const response = await apiClient.get<{ + data: ApiStructureReview[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }>('/construction/structure-reviews', { params: queryParams }); + + const items = (response.data || []).map(transformStructureReview); + + return { + success: true, + data: { + items, + total: response.total || 0, + page: response.current_page || 1, + size: response.per_page || 20, + totalPages: response.last_page || 1, + }, + }; + } catch (error) { + console.error('구조검토 목록 조회 오류:', error); + return { success: false, error: '구조검토 목록을 불러오는데 실패했습니다.' }; + } +} + +/** + * 구조검토 통계 조회 + * GET /api/v1/construction/structure-reviews/stats + */ export async function getStructureReviewStats(): Promise<{ success: boolean; data?: StructureReviewStats; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 300)); + try { + const response = await apiClient.get('/construction/structure-reviews/stats'); - const pending = MOCK_STRUCTURE_REVIEWS.filter((r) => r.status === 'pending').length; - const completed = MOCK_STRUCTURE_REVIEWS.filter((r) => r.status === 'completed').length; - - return { - success: true, - data: { - total: MOCK_STRUCTURE_REVIEWS.length, - pending, - completed, - }, - }; + return { + success: true, + data: { + total: response.total || 0, + pending: response.pending || 0, + completed: response.completed || 0, + }, + }; + } catch (error) { + console.error('구조검토 통계 조회 오류:', error); + return { success: false, error: '구조검토 통계를 불러오는데 실패했습니다.' }; + } } -// 구조검토 상세 조회 +/** + * 구조검토 상세 조회 + * GET /api/v1/construction/structure-reviews/{id} + */ export async function getStructureReview(id: string): Promise<{ success: boolean; data?: StructureReview; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 300)); + try { + const response = await apiClient.get(`/construction/structure-reviews/${id}`); - const review = MOCK_STRUCTURE_REVIEWS.find((r) => r.id === id); - - if (!review) { + return { + success: true, + data: transformStructureReview(response), + }; + } catch (error) { + console.error('구조검토 상세 조회 오류:', error); return { success: false, error: '구조검토 정보를 찾을 수 없습니다.' }; } - - return { success: true, data: review }; } -// 구조검토 생성 +/** + * 구조검토 생성 + * POST /api/v1/construction/structure-reviews + */ export async function createStructureReview(data: Partial): Promise<{ success: boolean; data?: StructureReview; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const apiData = transformToApiData(data); + const response = await apiClient.post('/construction/structure-reviews', apiData); - return { - success: true, - data: { - id: String(Date.now()), - reviewNumber: data.reviewNumber || '', - partnerId: data.partnerId || '', - partnerName: data.partnerName || '', - siteId: data.siteId || '', - siteName: data.siteName || '', - requestDate: data.requestDate || '', - reviewCompany: data.reviewCompany || '', - reviewerName: data.reviewerName || '', - reviewDate: data.reviewDate || null, - completionDate: data.completionDate || null, - status: data.status || 'pending', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }, - }; + return { + success: true, + data: transformStructureReview(response), + }; + } catch (error) { + console.error('구조검토 생성 오류:', error); + return { success: false, error: '구조검토 등록에 실패했습니다.' }; + } } -// 구조검토 수정 +/** + * 구조검토 수정 + * PUT /api/v1/construction/structure-reviews/{id} + */ export async function updateStructureReview( id: string, data: Partial @@ -187,43 +259,53 @@ export async function updateStructureReview( data?: StructureReview; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const apiData = transformToApiData(data); + const response = await apiClient.put(`/construction/structure-reviews/${id}`, apiData); - const existing = MOCK_STRUCTURE_REVIEWS.find((r) => r.id === id); - if (!existing) { - return { success: false, error: '구조검토 정보를 찾을 수 없습니다.' }; + return { + success: true, + data: transformStructureReview(response), + }; + } catch (error) { + console.error('구조검토 수정 오류:', error); + return { success: false, error: '구조검토 수정에 실패했습니다.' }; } - - return { - success: true, - data: { - ...existing, - ...data, - updatedAt: new Date().toISOString(), - }, - }; } -// 구조검토 삭제 +/** + * 구조검토 삭제 + * DELETE /api/v1/construction/structure-reviews/{id} + */ export async function deleteStructureReview(id: string): Promise<{ success: boolean; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 500)); - - return { success: true }; + try { + await apiClient.delete(`/construction/structure-reviews/${id}`); + return { success: true }; + } catch (error) { + console.error('구조검토 삭제 오류:', error); + return { success: false, error: '구조검토 삭제에 실패했습니다.' }; + } } -// 구조검토 일괄 삭제 +/** + * 구조검토 일괄 삭제 + * DELETE /api/v1/construction/structure-reviews/bulk + */ export async function deleteStructureReviews(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; }> { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 500)); - - return { success: true, deletedCount: ids.length }; + try { + await apiClient.delete('/construction/structure-reviews/bulk', { + data: { ids: ids.map((id) => Number(id)) }, + }); + return { success: true, deletedCount: ids.length }; + } catch (error) { + console.error('구조검토 일괄 삭제 오류:', error); + return { success: false, error: '구조검토 일괄 삭제에 실패했습니다.' }; + } } \ No newline at end of file diff --git a/src/components/hr/EmployeeManagement/EmployeeForm.tsx b/src/components/hr/EmployeeManagement/EmployeeForm.tsx index 3d3cc533..116f2dac 100644 --- a/src/components/hr/EmployeeManagement/EmployeeForm.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeForm.tsx @@ -311,6 +311,32 @@ export function EmployeeForm({ })); }; + // 부서 선택 변경 (id와 name 모두 업데이트) + const handleDepartmentSelect = (dpId: string, departmentId: string) => { + const dept = departments.find(d => String(d.id) === departmentId); + if (dept) { + setFormData(prev => ({ + ...prev, + departmentPositions: prev.departmentPositions.map(dp => + dp.id === dpId ? { ...dp, departmentId: String(dept.id), departmentName: dept.name } : dp + ), + })); + } + }; + + // 직책 선택 변경 (id와 name 모두 업데이트) + const handlePositionSelect = (dpId: string, positionId: string) => { + const position = titles.find(t => String(t.id) === positionId); + if (position) { + setFormData(prev => ({ + ...prev, + departmentPositions: prev.departmentPositions.map(dp => + dp.id === dpId ? { ...dp, positionId: String(position.id), positionName: position.name } : dp + ), + })); + } + }; + // 저장 const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -624,13 +650,20 @@ export function EmployeeForm({ {fieldSettings.showRank && (
- handleChange('rank', e.target.value)} - placeholder="직급 입력" + onValueChange={(value) => handleChange('rank', value)} disabled={isViewMode} - /> + > + + + + + {ranks.map((rank) => ( + {rank.name} + ))} + +
)} @@ -681,20 +714,38 @@ export function EmployeeForm({
{formData.departmentPositions.map((dp) => (
- handleDepartmentPositionChange(dp.id, 'departmentName', e.target.value)} - placeholder="부서명" - className="flex-1" + handleDepartmentPositionChange(dp.id, 'positionName', e.target.value)} - placeholder="직책" - className="flex-1" + > + + + {dp.departmentName || '부서 선택'} + + + + {departments.map((dept) => ( + {dept.name} + ))} + + + {!isViewMode && (
@@ -448,7 +452,7 @@ export function OrderRegistration({ {item.unit} - {formatAmount(item.unitPrice)}원 + {formatAmount(item.unitPrice)} - {formatAmount(item.amount)}원 + {formatAmount(item.amount)}
@@ -855,7 +857,7 @@ export function OrderRegistration({
총금액: - {formatAmount(form.totalAmount)}원 + {formatAmount(form.totalAmount)}
diff --git a/src/components/orders/QuotationSelectDialog.tsx b/src/components/orders/QuotationSelectDialog.tsx index e930155e..952e8c53 100644 --- a/src/components/orders/QuotationSelectDialog.tsx +++ b/src/components/orders/QuotationSelectDialog.tsx @@ -4,9 +4,10 @@ * 견적 선택 팝업 * * 확정된 견적 목록에서 수주 전환할 견적을 선택하는 다이얼로그 + * API 연동: getQuotesForSelect (FINALIZED 상태 견적만 조회) */ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Dialog, DialogContent, @@ -15,37 +16,10 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Search, FileText, Check } from "lucide-react"; +import { Search, FileText, Check, Loader2 } from "lucide-react"; import { formatAmount } from "@/utils/formatAmount"; import { cn } from "@/lib/utils"; - -// 견적 타입 -export interface QuotationForSelect { - id: string; - quoteNumber: string; // KD-PR-XXXXXX-XX - grade: string; // A(우량), B(관리), C(주의) - client: string; // 발주처 - siteName: string; // 현장명 - amount: number; // 총 금액 - itemCount: number; // 품목 수 - registrationDate: string; // 견적일 - manager?: string; // 담당자 - contact?: string; // 연락처 - items?: QuotationItem[]; // 품목 내역 -} - -export interface QuotationItem { - id: string; - itemCode: string; - itemName: string; - type: string; // 종 - symbol: string; // 부호 - spec: string; // 규격 - quantity: number; - unit: string; - unitPrice: number; - amount: number; -} +import { getQuotesForSelect, type QuotationForSelect } from "./actions"; interface QuotationSelectDialogProps { open: boolean; @@ -54,81 +28,6 @@ interface QuotationSelectDialogProps { selectedId?: string; } -// 샘플 견적 데이터 (실제 구현에서는 API 연동) -const SAMPLE_QUOTATIONS: QuotationForSelect[] = [ - { - id: "QT-001", - quoteNumber: "KD-PR-251210-01", - grade: "A", - client: "태영건설(주)", - siteName: "데시앙 동탄 파크뷰", - amount: 38800000, - itemCount: 5, - registrationDate: "2024-12-10", - manager: "김철수", - contact: "010-1234-5678", - items: [ - { id: "1", itemCode: "PRD-001", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS1", spec: "7260×2600", quantity: 2, unit: "EA", unitPrice: 8000000, amount: 16000000 }, - { id: "2", itemCode: "PRD-002", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "5000×2400", quantity: 3, unit: "EA", unitPrice: 7600000, amount: 22800000 }, - ], - }, - { - id: "QT-002", - quoteNumber: "KD-PR-251211-02", - grade: "A", - client: "현대건설(주)", - siteName: "힐스테이트 판교역", - amount: 52500000, - itemCount: 8, - registrationDate: "2024-12-11", - manager: "이영희", - contact: "010-2345-6789", - items: [ - { id: "1", itemCode: "PRD-003", itemName: "국민방화스크린세터", type: "B2", symbol: "FSS1", spec: "6000×3000", quantity: 4, unit: "EA", unitPrice: 9500000, amount: 38000000 }, - { id: "2", itemCode: "PRD-004", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "4500×2500", quantity: 2, unit: "EA", unitPrice: 7250000, amount: 14500000 }, - ], - }, - { - id: "QT-003", - quoteNumber: "KD-PR-251208-03", - grade: "B", - client: "GS건설(주)", - siteName: "자이 강남센터", - amount: 45000000, - itemCount: 6, - registrationDate: "2024-12-08", - manager: "박민수", - contact: "010-3456-7890", - items: [], - }, - { - id: "QT-004", - quoteNumber: "KD-PR-251205-04", - grade: "B", - client: "대우건설(주)", - siteName: "푸르지오 송도", - amount: 28900000, - itemCount: 4, - registrationDate: "2024-12-05", - manager: "최지원", - contact: "010-4567-8901", - items: [], - }, - { - id: "QT-005", - quoteNumber: "KD-PR-251201-05", - grade: "A", - client: "포스코건설", - siteName: "더샵 분당센트럴", - amount: 62000000, - itemCount: 10, - registrationDate: "2024-12-01", - manager: "정수민", - contact: "010-5678-9012", - items: [], - }, -]; - // 등급 배지 컴포넌트 function GradeBadge({ grade }: { grade: string }) { const config: Record = { @@ -151,25 +50,48 @@ export function QuotationSelectDialog({ selectedId, }: QuotationSelectDialogProps) { const [searchTerm, setSearchTerm] = useState(""); - const [quotations] = useState(SAMPLE_QUOTATIONS); + const [quotations, setQuotations] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); - // 검색 필터링 - const filteredQuotations = quotations.filter((q) => { - const searchLower = searchTerm.toLowerCase(); - return ( - !searchTerm || - q.quoteNumber.toLowerCase().includes(searchLower) || - q.client.toLowerCase().includes(searchLower) || - q.siteName.toLowerCase().includes(searchLower) - ); - }); + // 견적 목록 조회 + const fetchQuotations = useCallback(async (query?: string) => { + setIsLoading(true); + setError(null); + try { + const result = await getQuotesForSelect({ q: query, size: 50 }); + if (result.success && result.data) { + setQuotations(result.data.items); + } else { + setError(result.error || "견적 목록 조회에 실패했습니다."); + setQuotations([]); + } + } catch { + setError("서버 오류가 발생했습니다."); + setQuotations([]); + } finally { + setIsLoading(false); + } + }, []); - // 다이얼로그 열릴 때 검색어 초기화 + // 다이얼로그 열릴 때 데이터 로드 useEffect(() => { if (open) { setSearchTerm(""); + fetchQuotations(); } - }, [open]); + }, [open, fetchQuotations]); + + // 검색어 변경 시 디바운스 적용하여 API 호출 + useEffect(() => { + if (!open) return; + + const timer = setTimeout(() => { + fetchQuotations(searchTerm || undefined); + }, 300); + + return () => clearTimeout(timer); + }, [searchTerm, open, fetchQuotations]); const handleSelect = (quotation: QuotationForSelect) => { onSelect(quotation); @@ -199,60 +121,77 @@ export function QuotationSelectDialog({ {/* 안내 문구 */}
- 전환 가능한 견적 {filteredQuotations.length}건 (최종확정 상태) + {isLoading ? ( + + + 견적 목록을 불러오는 중... + + ) : error ? ( + {error} + ) : ( + `전환 가능한 견적 ${quotations.length}건 (최종확정 상태)` + )}
{/* 견적 목록 */}
- {filteredQuotations.map((quotation) => ( -
handleSelect(quotation)} - className={cn( - "p-4 border rounded-lg cursor-pointer transition-colors", - "hover:bg-muted/50 hover:border-primary/50", - selectedId === quotation.id && "border-primary bg-primary/5" - )} - > - {/* 상단: 견적번호 + 등급 */} -
-
- - {quotation.quoteNumber} - - + {isLoading ? ( +
+ +
+ ) : ( + <> + {quotations.map((quotation) => ( +
handleSelect(quotation)} + className={cn( + "p-4 border rounded-lg cursor-pointer transition-colors", + "hover:bg-muted/50 hover:border-primary/50", + selectedId === quotation.id && "border-primary bg-primary/5" + )} + > + {/* 상단: 견적번호 + 등급 */} +
+
+ + {quotation.quoteNumber} + + +
+ {selectedId === quotation.id && ( + + )} +
+ + {/* 발주처 */} +
+ {quotation.client} +
+ + {/* 현장명 + 금액 */} +
+ + [{quotation.siteName}] + + + {formatAmount(quotation.amount)}원 + +
+ + {/* 품목 수 */} +
+ {quotation.itemCount}개 품목 +
- {selectedId === quotation.id && ( - - )} -
+ ))} - {/* 발주처 */} -
- {quotation.client} -
- - {/* 현장명 + 금액 */} -
- - [{quotation.siteName}] - - - {formatAmount(quotation.amount)}원 - -
- - {/* 품목 수 */} -
- {quotation.itemCount}개 품목 -
-
- ))} - - {filteredQuotations.length === 0 && ( -
- 검색 결과가 없습니다. -
+ {quotations.length === 0 && !error && ( +
+ 검색 결과가 없습니다. +
+ )} + )}
diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts new file mode 100644 index 00000000..8d60ee8b --- /dev/null +++ b/src/components/orders/actions.ts @@ -0,0 +1,1080 @@ +'use server'; + +import { serverFetch } from '@/lib/api/fetch-wrapper'; + +// ============================================================================ +// API 타입 정의 +// ============================================================================ + +interface ApiOrder { + id: number; + tenant_id: number; + quote_id: number | null; + order_no: string; + order_type_code: string; + status_code: string; + category_code: string | null; + client_id: number | null; + client_name: string | null; + client_contact: string | null; + site_name: string | null; + quantity: number; + supply_amount: number; + tax_amount: number; + total_amount: number; + discount_rate: number; + discount_amount: number; + delivery_date: string | null; + delivery_method_code: string | null; + received_at: string | null; + memo: string | null; + remarks: string | null; + note: string | null; + created_by: number | null; + updated_by: number | null; + created_at: string; + updated_at: string; + client?: ApiClient | null; + items?: ApiOrderItem[]; + quote?: ApiQuote | null; +} + +interface ApiOrderItem { + id: number; + order_id: number; + item_id: number | null; + item_name: string; + specification: string | null; + quantity: number; + unit: string | null; + unit_price: number; + supply_amount: number; + tax_amount: number; + total_amount: number; + sort_order: number; +} + +interface ApiClient { + id: number; + name: string; + business_no?: string; + representative?: string; + phone?: string; + email?: string; +} + +interface ApiQuote { + id: number; + quote_no: string; + quote_number?: string; + site_name: string | null; +} + +// 견적 목록 조회용 상세 타입 +interface ApiQuoteForSelect { + id: number; + quote_number: string; + registration_date: string; + status: string; + client_id: number | null; + client_name: string | null; + site_name: string | null; + supply_amount: number; + tax_amount: number; + total_amount: number; + item_count?: number; + author?: string | null; + manager?: string | null; + contact?: string | null; + client?: { + id: number; + name: string; + contact_person?: string; // 담당자 + phone?: string; + } | null; + items?: ApiQuoteItem[]; +} + +interface ApiQuoteItem { + id: number; + item_code?: string; + item_name: string; + type_code?: string; + symbol?: string; + specification?: string; + // QuoteItem 모델 필드명 (calculated_quantity, total_price) + calculated_quantity?: number; + quantity?: number; // fallback + unit?: string; + unit_price: number; + total_price?: number; + // 수주 품목에서 사용하는 필드명 + supply_amount?: number; + tax_amount?: number; + total_amount?: number; +} + +interface ApiWorkOrder { + id: number; + tenant_id: number; + work_order_no: string; + sales_order_id: number; + project_name: string | null; + process_id: number | null; + process_type: string; + status: string; + assignee_id: number | null; + team_id: number | null; + scheduled_date: string | null; + memo: string | null; + is_active: boolean; + created_at: string; + updated_at: string; + assignee?: { id: number; name: string } | null; + team?: { id: number; name: string } | null; + process?: { id: number; process_name: string } | null; +} + +interface ApiProductionOrderResponse { + work_order?: ApiWorkOrder; + work_orders?: ApiWorkOrder[]; + order: ApiOrder; +} + +interface ApiOrderStats { + total: number; + draft: number; + confirmed: number; + in_progress: number; + completed: number; + cancelled: number; + total_amount: number; + confirmed_amount: number; +} + +interface ApiResponse { + success: boolean; + message: string; + data: T; +} + +interface PaginatedResponse { + current_page: number; + data: T[]; + last_page: number; + per_page: number; + total: number; +} + +// ============================================================================ +// Frontend 타입 정의 +// ============================================================================ + +// 수주 상태 타입 (API와 매핑) +export type OrderStatus = + | 'order_registered' // DRAFT + | 'order_confirmed' // CONFIRMED + | 'production_ordered' // IN_PROGRESS + | 'in_production' // IN_PROGRESS (세부) + | 'rework' // IN_PROGRESS (세부) + | 'work_completed' // IN_PROGRESS (세부) + | 'shipped' // COMPLETED + | 'cancelled'; // CANCELLED + +export interface Order { + id: string; + lotNumber: string; // order_no + quoteNumber: string; // quote.quote_no + quoteId?: number; + orderDate: string; // received_at + client: string; // client_name + clientId?: number; + siteName: string; // site_name + status: OrderStatus; + statusCode: string; // 원본 status_code + expectedShipDate?: string; // delivery_date + deliveryMethod?: string; // delivery_method_code + amount: number; // total_amount + supplyAmount: number; + taxAmount: number; + itemCount: number; // items.length + hasReceivable?: boolean; // 미수 여부 (추후 구현) + memo?: string; + remarks?: string; + note?: string; + items?: OrderItem[]; + // 상세 페이지용 추가 필드 + manager?: string; // 담당자 + contact?: string; // 연락처 (client_contact) + deliveryRequestDate?: string; // 납품요청일 + shippingCost?: string; // 운임비용 + receiver?: string; // 수신자 + receiverContact?: string; // 수신처 연락처 + address?: string; // 수신처 주소 + addressDetail?: string; // 상세주소 + subtotal?: number; // 소계 (supply_amount와 동일) + discountRate?: number; // 할인율 + totalAmount?: number; // 총금액 (amount와 동일하지만 명시적) +} + +export interface OrderItem { + id: string; + itemId?: number; + itemCode?: string; // 품목코드 + itemName: string; + specification?: string; + spec?: string; // specification alias + type?: string; // 층 (layer) + symbol?: string; // 부호 + quantity: number; + unit?: string; + unitPrice: number; + supplyAmount: number; + taxAmount: number; + totalAmount: number; + amount?: number; // totalAmount alias + sortOrder: number; +} + +export interface OrderFormData { + orderTypeCode?: string; + categoryCode?: string; + clientId?: number; + clientName?: string; + clientContact?: string; + siteName?: string; + supplyAmount?: number; + taxAmount?: number; + totalAmount?: number; + discountRate?: number; + discountAmount?: number; + deliveryDate?: string; + deliveryMethodCode?: string; + receivedAt?: string; + memo?: string; + remarks?: string; + note?: string; + items?: OrderItemFormData[]; + // 수정 페이지용 추가 필드 + expectedShipDate?: string; // 출고예정일 + deliveryRequestDate?: string; // 납품요청일 + deliveryMethod?: string; // 배송방식 (deliveryMethodCode alias) + shippingCost?: string; // 운임비용 + receiver?: string; // 수신자 + receiverContact?: string; // 수신처 연락처 + address?: string; // 수신처 주소 + addressDetail?: string; // 상세주소 +} + +export interface OrderItemFormData { + itemId?: number; + itemCode?: string; // 품목코드 + itemName: string; + specification?: string; + quantity: number; + unit?: string; + unitPrice: number; +} + +export interface OrderStats { + total: number; + draft: number; + confirmed: number; + inProgress: number; + completed: number; + cancelled: number; + totalAmount: number; + confirmedAmount: number; +} + +// 견적→수주 변환용 +export interface CreateFromQuoteData { + deliveryDate?: string; + memo?: string; +} + +// 생산지시 생성용 +export interface CreateProductionOrderData { + processId?: number; + processIds?: number[]; // 공정별 다중 작업지시 생성용 + priority?: 'urgent' | 'high' | 'normal' | 'low'; + assigneeId?: number; + teamId?: number; + scheduledDate?: string; + memo?: string; +} + +// 생산지시(작업지시) 타입 +export interface WorkOrder { + id: string; + workOrderNo: string; + salesOrderId: number; + projectName: string | null; + processId?: number; + processType: string; + status: string; + assigneeId?: number; + assigneeName?: string; + teamId?: number; + teamName?: string; + scheduledDate?: string; + memo?: string; + isActive: boolean; + createdAt: string; + updatedAt: string; + process?: { id: number; processName: string }; +} + +// 생산지시 생성 결과 +export interface ProductionOrderResult { + workOrder?: WorkOrder; + workOrders?: WorkOrder[]; + order: Order; +} + +// 견적 선택용 타입 (QuotationSelectDialog용) +export interface QuotationForSelect { + id: string; + quoteNumber: string; // KD-PR-XXXXXX-XX + grade: string; // A(우량), B(관리), C(주의) + clientId: string | null; // 발주처 ID + client: string; // 발주처명 + siteName: string; // 현장명 + amount: number; // 총 금액 + itemCount: number; // 품목 수 + registrationDate: string; // 견적일 + manager?: string; // 담당자 + contact?: string; // 연락처 + items?: QuotationItem[]; // 품목 내역 +} + +export interface QuotationItem { + id: string; + itemCode: string; + itemName: string; + type: string; // 종 + symbol: string; // 부호 + spec: string; // 규격 + quantity: number; + unit: string; + unitPrice: number; + amount: number; +} + +// ============================================================================ +// 상태 매핑 +// ============================================================================ + +const API_TO_FRONTEND_STATUS: Record = { + 'DRAFT': 'order_registered', + 'CONFIRMED': 'order_confirmed', + 'IN_PROGRESS': 'production_ordered', + 'COMPLETED': 'shipped', + 'CANCELLED': 'cancelled', +}; + +const FRONTEND_TO_API_STATUS: Record = { + 'order_registered': 'DRAFT', + 'order_confirmed': 'CONFIRMED', + 'production_ordered': 'IN_PROGRESS', + 'in_production': 'IN_PROGRESS', + 'rework': 'IN_PROGRESS', + 'work_completed': 'IN_PROGRESS', + 'shipped': 'COMPLETED', + 'cancelled': 'CANCELLED', +}; + +// ============================================================================ +// 데이터 변환 함수 +// ============================================================================ + +function transformApiToFrontend(apiData: ApiOrder): Order { + return { + id: String(apiData.id), + lotNumber: apiData.order_no, + quoteNumber: apiData.quote?.quote_number || '', + quoteId: apiData.quote_id ?? undefined, + orderDate: apiData.received_at || apiData.created_at.split('T')[0], + client: apiData.client_name || apiData.client?.name || '', + clientId: apiData.client_id ?? undefined, + siteName: apiData.site_name || '', + status: API_TO_FRONTEND_STATUS[apiData.status_code] || 'order_registered', + statusCode: apiData.status_code, + expectedShipDate: apiData.delivery_date ?? undefined, + deliveryMethod: apiData.delivery_method_code ?? undefined, + amount: apiData.total_amount, + supplyAmount: apiData.supply_amount, + taxAmount: apiData.tax_amount, + itemCount: apiData.items?.length || 0, + hasReceivable: false, // 추후 구현 + memo: apiData.memo ?? undefined, + remarks: apiData.remarks ?? undefined, + note: apiData.note ?? undefined, + items: apiData.items?.map(transformItemApiToFrontend) || [], // 상세 페이지용 추가 필드 (API에서 매핑) + manager: apiData.client?.representative ?? undefined, + contact: apiData.client_contact ?? apiData.client?.phone ?? undefined, + deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유 + shippingCost: undefined, // API에 해당 필드 없음 - 추후 구현 + receiver: undefined, // API에 해당 필드 없음 - 추후 구현 + receiverContact: undefined, // API에 해당 필드 없음 - 추후 구현 + address: undefined, // API에 해당 필드 없음 - 추후 구현 + addressDetail: undefined, // API에 해당 필드 없음 - 추후 구현 + subtotal: apiData.supply_amount, + discountRate: apiData.discount_rate, + totalAmount: apiData.total_amount, + }; +} + +function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem { + return { + id: String(apiItem.id), + itemId: apiItem.item_id ?? undefined, + itemCode: apiItem.item_id ? `ITEM-${apiItem.item_id}` : undefined, // 임시: 실제 item_code는 API에서 제공 필요 + itemName: apiItem.item_name, + specification: apiItem.specification ?? undefined, + spec: apiItem.specification ?? undefined, // specification alias + type: undefined, // 층 - API에 해당 필드 없음 + symbol: undefined, // 부호 - API에 해당 필드 없음 + quantity: apiItem.quantity, + unit: apiItem.unit ?? undefined, + unitPrice: apiItem.unit_price, + supplyAmount: apiItem.supply_amount, + taxAmount: apiItem.tax_amount, + totalAmount: apiItem.total_amount, + amount: apiItem.total_amount, // totalAmount alias + sortOrder: apiItem.sort_order, + }; +} + +function transformFrontendToApi(data: OrderFormData | Record): Record { + // Handle both API OrderFormData and Registration form's OrderFormData + const formData = data as Record; + + // Get client_id - handle both string (form) and number (api) types + const clientId = formData.clientId; + const clientIdValue = clientId ? (typeof clientId === 'string' ? parseInt(clientId, 10) || null : clientId) : null; + + // Get items - handle both form's OrderItem[] and API's OrderItemFormData[] + const items = (formData.items as Array>) || []; + + // Get quote_id from selectedQuotation (견적에서 수주 생성 시) + const selectedQuotation = formData.selectedQuotation as { id?: string } | undefined; + const quoteIdValue = selectedQuotation?.id ? parseInt(selectedQuotation.id, 10) || null : null; + + return { + quote_id: quoteIdValue, + order_type_code: formData.orderTypeCode || 'ORDER', + category_code: formData.categoryCode || null, + client_id: clientIdValue, + client_name: formData.clientName || null, + client_contact: formData.clientContact || formData.contact || null, + site_name: formData.siteName || null, + supply_amount: formData.supplyAmount || formData.subtotal || 0, + tax_amount: formData.taxAmount || 0, + total_amount: formData.totalAmount || 0, + discount_rate: formData.discountRate || 0, + discount_amount: formData.discountAmount || 0, + delivery_date: formData.deliveryDate || formData.deliveryRequestDate || null, + delivery_method_code: formData.deliveryMethodCode || formData.deliveryMethod || null, + received_at: formData.receivedAt || null, + memo: formData.memo || null, + remarks: formData.remarks || null, + note: formData.note || null, + items: items.map((item) => { + // Handle both form's OrderItem (id, spec) and API's OrderItemFormData (itemId, specification) + // 중요: 문자열로 전달될 수 있으므로 반드시 Number()로 변환 + const quantity = Number(item.quantity) || 0; + const unitPrice = Number(item.unitPrice) || 0; + const supplyAmount = quantity * unitPrice; + const taxAmount = Math.round(supplyAmount * 0.1); + + return { + item_id: item.itemId || null, + item_code: item.itemCode || null, + item_name: item.itemName, + specification: item.specification || item.spec || null, + quantity, + unit: item.unit || 'EA', + unit_price: unitPrice, + supply_amount: supplyAmount, + tax_amount: taxAmount, + total_amount: supplyAmount + taxAmount, + }; + }), + }; +} + +function transformWorkOrderApiToFrontend(apiData: ApiWorkOrder): WorkOrder { + return { + id: String(apiData.id), + workOrderNo: apiData.work_order_no, + salesOrderId: apiData.sales_order_id, + projectName: apiData.project_name, + processType: apiData.process_type, + status: apiData.status, + assigneeId: apiData.assignee_id ?? undefined, + assigneeName: apiData.assignee?.name ?? undefined, + teamId: apiData.team_id ?? undefined, + teamName: apiData.team?.name ?? undefined, + scheduledDate: apiData.scheduled_date ?? undefined, + memo: apiData.memo ?? undefined, + isActive: apiData.is_active, + createdAt: apiData.created_at, + updatedAt: apiData.updated_at, + processId: apiData.process_id ?? undefined, + process: apiData.process ? { + id: apiData.process.id, + processName: apiData.process.process_name, + } : undefined, + }; +} + +function transformQuoteForSelect(apiData: ApiQuoteForSelect): QuotationForSelect { + return { + id: String(apiData.id), + quoteNumber: apiData.quote_number, + grade: apiData.client?.grade || 'B', // 기본값 B(관리) + clientId: apiData.client_id ? String(apiData.client_id) : null, + client: apiData.client_name || apiData.client?.name || '', + siteName: apiData.site_name || '', + amount: apiData.total_amount, + itemCount: apiData.item_count || apiData.items?.length || 0, + registrationDate: apiData.registration_date, + manager: apiData.manager ?? undefined, + contact: apiData.contact ?? apiData.client?.phone ?? undefined, + items: apiData.items?.map(transformQuoteItemForSelect), + }; +} + +function transformQuoteItemForSelect(apiItem: ApiQuoteItem): QuotationItem { + // QuoteItem 모델 필드명: calculated_quantity, total_price + // 수주 품목 필드명: quantity, total_amount (fallback) + // 중요: API에서 문자열로 반환될 수 있으므로 반드시 Number()로 변환 + const quantity = Number(apiItem.calculated_quantity ?? apiItem.quantity ?? 0); + const unitPrice = Number(apiItem.unit_price ?? 0); + // amount fallback: total_price → total_amount → 수량 * 단가 계산 + const amount = Number(apiItem.total_price ?? apiItem.total_amount ?? 0) || (quantity * unitPrice); + + return { + id: String(apiItem.id), + itemCode: apiItem.item_code || '', + itemName: apiItem.item_name, + type: apiItem.type_code || '', + symbol: apiItem.symbol || '', + spec: apiItem.specification || '', + quantity, + unit: apiItem.unit || 'EA', + unitPrice, + amount, + }; +} + +// ============================================================================ +// API 함수 +// ============================================================================ + +/** + * 수주 목록 조회 + */ +export async function getOrders(params?: { + page?: number; + size?: number; + q?: string; + status?: string; + order_type?: string; + client_id?: number; + date_from?: string; + date_to?: string; +}): Promise<{ + success: boolean; + data?: { items: Order[]; total: number; page: number; totalPages: number }; + error?: string; + __authError?: boolean; +}> { + try { + const searchParams = new URLSearchParams(); + + if (params?.page) searchParams.set('page', String(params.page)); + if (params?.size) searchParams.set('size', String(params.size)); + if (params?.q) searchParams.set('q', params.q); + if (params?.status) { + // Frontend status를 API status로 변환 + const apiStatus = FRONTEND_TO_API_STATUS[params.status as OrderStatus]; + if (apiStatus) searchParams.set('status', apiStatus); + } + if (params?.order_type) searchParams.set('order_type', params.order_type); + if (params?.client_id) searchParams.set('client_id', String(params.client_id)); + if (params?.date_from) searchParams.set('date_from', params.date_from); + if (params?.date_to) searchParams.set('date_to', params.date_to); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders?${searchParams.toString()}`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '목록 조회에 실패했습니다.' }; + } + + const result: ApiResponse> = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '목록 조회에 실패했습니다.' }; + } + + return { + success: true, + data: { + items: result.data.data.map(transformApiToFrontend), + total: result.data.total, + page: result.data.current_page, + totalPages: result.data.last_page, + }, + }; + } catch (error) { + console.error('[getOrders] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 상세 조회 + */ +export async function getOrderById(id: string): Promise<{ + success: boolean; + data?: Order; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '조회에 실패했습니다.' }; + } + + return { success: true, data: transformApiToFrontend(result.data) }; + } catch (error) { + console.error('[getOrderById] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 생성 + */ +export async function createOrder(data: OrderFormData): Promise<{ + success: boolean; + data?: Order; + error?: string; + __authError?: boolean; +}> { + try { + const apiData = transformFrontendToApi(data); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders`, + { method: 'POST', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '등록에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '등록에 실패했습니다.' }; + } + + return { success: true, data: transformApiToFrontend(result.data) }; + } catch (error) { + console.error('[createOrder] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 수정 + */ +export async function updateOrder(id: string, data: OrderFormData): Promise<{ + success: boolean; + data?: Order; + error?: string; + __authError?: boolean; +}> { + try { + const apiData = transformFrontendToApi(data); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, + { method: 'PUT', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '수정에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '수정에 실패했습니다.' }; + } + + return { success: true, data: transformApiToFrontend(result.data) }; + } catch (error) { + console.error('[updateOrder] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 삭제 + */ +export async function deleteOrder(id: string): Promise<{ + success: boolean; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, + { method: 'DELETE' } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '삭제에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '삭제에 실패했습니다.' }; + } + + return { success: true }; + } catch (error) { + console.error('[deleteOrder] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 상태 변경 + */ +export async function updateOrderStatus(id: string, status: OrderStatus): Promise<{ + success: boolean; + data?: Order; + error?: string; + __authError?: boolean; +}> { + try { + const apiStatus = FRONTEND_TO_API_STATUS[status]; + if (!apiStatus) { + return { success: false, error: '유효하지 않은 상태입니다.' }; + } + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}/status`, + { method: 'PATCH', body: JSON.stringify({ status: apiStatus }) } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '상태 변경에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '상태 변경에 실패했습니다.' }; + } + + return { success: true, data: transformApiToFrontend(result.data) }; + } catch (error) { + console.error('[updateOrderStatus] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 통계 조회 + */ +export async function getOrderStats(): Promise<{ + success: boolean; + data?: OrderStats; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/stats`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '통계 조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '통계 조회에 실패했습니다.' }; + } + + return { + success: true, + data: { + total: result.data.total, + draft: result.data.draft, + confirmed: result.data.confirmed, + inProgress: result.data.in_progress, + completed: result.data.completed, + cancelled: result.data.cancelled, + totalAmount: result.data.total_amount, + confirmedAmount: result.data.confirmed_amount, + }, + }; + } catch (error) { + console.error('[getOrderStats] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 일괄 삭제 + */ +export async function deleteOrders(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; + __authError?: boolean; +}> { + try { + // 순차적으로 삭제 (API에 bulk delete가 없으므로) + let deletedCount = 0; + const errors: string[] = []; + + for (const id of ids) { + const result = await deleteOrder(id); + if (result.success) { + deletedCount++; + } else { + errors.push(result.error || `ID ${id} 삭제 실패`); + } + } + + if (deletedCount === 0 && errors.length > 0) { + return { success: false, error: errors[0] }; + } + + return { success: true, deletedCount }; + } catch (error) { + console.error('[deleteOrders] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 견적에서 수주 생성 + */ +export async function createOrderFromQuote( + quoteId: number, + data?: CreateFromQuoteData +): Promise<{ + success: boolean; + data?: Order; + error?: string; + __authError?: boolean; +}> { + try { + const apiData: Record = {}; + if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate; + if (data?.memo) apiData.memo = data.memo; + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/from-quote/${quoteId}`, + { method: 'POST', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '수주 생성에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '수주 생성에 실패했습니다.' }; + } + + return { success: true, data: transformApiToFrontend(result.data) }; + } catch (error) { + console.error('[createOrderFromQuote] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 생산지시 생성 + */ +export async function createProductionOrder( + orderId: string, + data?: CreateProductionOrderData +): Promise<{ + success: boolean; + data?: ProductionOrderResult; + error?: string; + __authError?: boolean; +}> { + try { + const apiData: Record = {}; + // 다중 공정 ID (우선) 또는 단일 공정 ID + if (data?.processIds && data.processIds.length > 0) { + apiData.process_ids = data.processIds; + } else if (data?.processId) { + apiData.process_id = data.processId; + } + if (data?.priority) apiData.priority = data.priority; + if (data?.assigneeId) apiData.assignee_id = data.assigneeId; + if (data?.teamId) apiData.team_id = data.teamId; + if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate; + if (data?.memo) apiData.memo = data.memo; + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/production-order`, + { method: 'POST', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '생산지시 생성에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '생산지시 생성에 실패했습니다.' }; + } + + // 다중 또는 단일 작업지시 응답 처리 + const responseData: ProductionOrderResult = { + order: transformApiToFrontend(result.data.order), + }; + + if (result.data.work_orders && result.data.work_orders.length > 0) { + // 다중 작업지시 응답 + responseData.workOrders = result.data.work_orders.map(transformWorkOrderApiToFrontend); + } else if (result.data.work_order) { + // 단일 작업지시 응답 (하위 호환성) + responseData.workOrder = transformWorkOrderApiToFrontend(result.data.work_order); + } + + return { + success: true, + data: responseData, + }; + } catch (error) { + console.error('[createProductionOrder] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +/** + * 수주 변환용 확정 견적 목록 조회 + * QuotationSelectDialog에서 사용 + */ +export async function getQuotesForSelect(params?: { + q?: string; + page?: number; + size?: number; +}): Promise<{ + success: boolean; + data?: { items: QuotationForSelect[]; total: number }; + error?: string; + __authError?: boolean; +}> { + try { + const searchParams = new URLSearchParams(); + + // 확정(finalized) 상태의 견적만 조회 + searchParams.set('status', 'finalized'); + // 품목 포함 (수주 전환용) + searchParams.set('with_items', 'true'); + if (params?.q) searchParams.set('q', params.q); + if (params?.page) searchParams.set('page', String(params.page)); + if (params?.size) searchParams.set('size', String(params.size || 50)); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes?${searchParams.toString()}`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; + } + + if (!response) { + return { success: false, error: '견적 목록 조회에 실패했습니다.' }; + } + + const result: ApiResponse> = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '견적 목록 조회에 실패했습니다.' }; + } + + return { + success: true, + data: { + items: result.data.data.map(transformQuoteForSelect), + total: result.data.total, + }, + }; + } catch (error) { + console.error('[getQuotesForSelect] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} diff --git a/src/components/orders/index.ts b/src/components/orders/index.ts index 8f264045..bdceb67d 100644 --- a/src/components/orders/index.ts +++ b/src/components/orders/index.ts @@ -1,7 +1,26 @@ /** - * 수주 관련 컴포넌트 + * 수주 관련 컴포넌트 및 API 함수 */ +// API Actions +export { + getOrders, + getOrderById, + createOrder, + updateOrder, + deleteOrder, + deleteOrders, + updateOrderStatus, + getOrderStats, + type Order, + type OrderItem as OrderItemApi, + type OrderFormData as OrderApiFormData, + type OrderItemFormData, + type OrderStats, + type OrderStatus, +} from "./actions"; + +// Components export { OrderRegistration, type OrderFormData } from "./OrderRegistration"; export { QuotationSelectDialog, type QuotationForSelect, type QuotationItem } from "./QuotationSelectDialog"; export { ItemAddDialog, type OrderItem } from "./ItemAddDialog"; diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx index bb8ba47e..a370e09f 100644 --- a/src/components/process-management/ProcessDetail.tsx +++ b/src/components/process-management/ProcessDetail.tsx @@ -178,39 +178,36 @@ export function ProcessDetail({ process }: ProcessDetailProps) { 개별 품목 + {individualItems.length > 0 && individualItems[0].items && ( + + {individualItems[0].items.length}개 + + )} - {individualItems.length === 0 ? ( + {individualItems.length === 0 || !individualItems[0].items?.length ? (

등록된 개별 품목이 없습니다

) : ( -
- {individualItems.map((rule) => ( -
-
- - {rule.isActive ? '활성' : '비활성'} - -
-
- {rule.conditionValue} -
- {rule.description && ( -
- {rule.description} -
- )} +
+
+ {individualItems[0].items.map((item) => ( +
+
+ + {item.code} + + {item.name}
- 우선순위: {rule.priority} -
- ))} + ))} +
)} diff --git a/src/components/process-management/RuleModal.tsx b/src/components/process-management/RuleModal.tsx index d8fd37f0..8ce0f161 100644 --- a/src/components/process-management/RuleModal.tsx +++ b/src/components/process-management/RuleModal.tsx @@ -74,7 +74,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 개별 품목용 상태 const [searchKeyword, setSearchKeyword] = useState(''); const [selectedItemType, setSelectedItemType] = useState('all'); - const [selectedItemCodes, setSelectedItemCodes] = useState>(new Set()); + const [selectedItemIds, setSelectedItemIds] = useState>(new Set()); // 품목 목록 API 상태 const [itemList, setItemList] = useState([]); @@ -86,16 +86,35 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp const items = await getItemList({ q: q || undefined, itemType: itemType === 'all' ? undefined : itemType, - size: 100, + size: 1000, // 전체 품목 조회 }); setItemList(items); setIsItemsLoading(false); }, []); + // 검색어 유효성 검사 함수 + const isValidSearchKeyword = (keyword: string): boolean => { + if (!keyword || keyword.trim() === '') return false; + + const trimmed = keyword.trim(); + // 한글이 포함되어 있으면 1자 이상 + const hasKorean = /[가-힣]/.test(trimmed); + if (hasKorean) return trimmed.length >= 1; + + // 영어/숫자만 있으면 2자 이상 + return trimmed.length >= 2; + }; + // 검색어/품목유형 변경 시 API 호출 (debounce) useEffect(() => { if (registrationType !== 'individual') return; + // 검색어 유효성 검사 - 유효하지 않으면 빈 목록 + if (!isValidSearchKeyword(searchKeyword)) { + setItemList([]); + return; + } + const timer = setTimeout(() => { loadItems(searchKeyword, selectedItemType); }, 300); @@ -103,21 +122,30 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp return () => clearTimeout(timer); }, [searchKeyword, selectedItemType, registrationType, loadItems]); - // 모달 열릴 때 품목 목록 초기 로드 + // 품목유형 변경 시 검색어가 유효하면 재검색 + useEffect(() => { + if (registrationType !== 'individual') return; + if (!isValidSearchKeyword(searchKeyword)) return; + + loadItems(searchKeyword, selectedItemType); + }, [selectedItemType]); + + // 모달 열릴 때 품목 목록 초기화 (초기 로드 안함) useEffect(() => { if (open && registrationType === 'individual') { - loadItems('', 'all'); + setItemList([]); + setSearchKeyword(''); } - }, [open, registrationType, loadItems]); + }, [open, registrationType]); // 체크박스 토글 - const handleToggleItem = (code: string) => { - setSelectedItemCodes((prev) => { + const handleToggleItem = (id: string) => { + setSelectedItemIds((prev) => { const newSet = new Set(prev); - if (newSet.has(code)) { - newSet.delete(code); + if (newSet.has(id)) { + newSet.delete(id); } else { - newSet.add(code); + newSet.add(id); } return newSet; }); @@ -125,13 +153,13 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 전체 선택 const handleSelectAll = () => { - const allCodes = itemList.map((item) => item.code); - setSelectedItemCodes(new Set(allCodes)); + const allIds = itemList.map((item) => item.id); + setSelectedItemIds(new Set(allIds)); }; // 초기화 const handleResetSelection = () => { - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); }; // 모달 열릴 때 초기화 또는 수정 데이터 로드 @@ -149,12 +177,12 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setSearchKeyword(''); setSelectedItemType('all'); - // 개별 품목인 경우 선택된 품목 코드 설정 + // 개별 품목인 경우 선택된 품목 ID 설정 if (editRule.registrationType === 'individual') { - const codes = editRule.conditionValue.split(',').filter(Boolean); - setSelectedItemCodes(new Set(codes)); + const ids = editRule.conditionValue.split(',').filter(Boolean); + setSelectedItemIds(new Set(ids)); } else { - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); } } else { // 추가 모드: 초기화 @@ -167,7 +195,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setIsActive(true); setSearchKeyword(''); setSelectedItemType('all'); - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); } } }, [open, editRule]); @@ -179,7 +207,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp return; } } else { - if (selectedItemCodes.size === 0) { + if (selectedItemIds.size === 0) { alert('품목을 최소 1개 이상 선택해주세요.'); return; } @@ -188,7 +216,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp // 개별 품목의 경우 conditionValue에 품목코드들을 저장 const finalConditionValue = registrationType === 'individual' - ? Array.from(selectedItemCodes).join(',') + ? Array.from(selectedItemIds).join(',') : conditionValue.trim(); onAdd({ @@ -211,7 +239,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp setIsActive(true); setSearchKeyword(''); setSelectedItemType('all'); - setSelectedItemCodes(new Set()); + setSelectedItemIds(new Set()); onOpenChange(false); }; @@ -389,7 +417,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp {isItemsLoading ? ( '로딩 중...' ) : ( - <>품목 목록 ({itemList.length}개) | 선택됨 ({selectedItemCodes.size}개) + <>품목 목록 ({itemList.length}개) | 선택됨 ({selectedItemIds.size}개) )}
@@ -411,7 +439,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp size="sm" onClick={handleResetSelection} className="text-xs h-7" - disabled={selectedItemCodes.size === 0} + disabled={selectedItemIds.size === 0} > 초기화 @@ -439,7 +467,9 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp ) : itemList.length === 0 ? ( - 검색 결과가 없습니다 + {searchKeyword.trim() === '' + ? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)' + : '검색 결과가 없습니다'} ) : ( @@ -447,12 +477,12 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp handleToggleItem(item.code)} + onClick={() => handleToggleItem(item.id)} > handleToggleItem(item.code)} + checked={selectedItemIds.has(item.id)} + onCheckedChange={() => handleToggleItem(item.id)} onClick={(e) => e.stopPropagation()} /> diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index c3698c27..e52ae0fb 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -3,7 +3,7 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; -import type { Process, ProcessFormData, ClassificationRule } from '@/types/process'; +import type { Process, ProcessFormData, ClassificationRule, IndividualItem } from '@/types/process'; // ============================================================================ // API 타입 정의 @@ -26,6 +26,7 @@ interface ApiProcess { created_at: string; updated_at: string; classification_rules?: ApiClassificationRule[]; + process_items?: ApiProcessItem[]; } interface ApiClassificationRule { @@ -42,6 +43,19 @@ interface ApiClassificationRule { updated_at: string; } +interface ApiProcessItem { + id: number; + process_id: number; + item_id: number; + priority: number; + is_active: boolean; + item?: { + id: number; + code: string; + name: string; + }; +} + interface ApiResponse { success: boolean; message: string; @@ -61,6 +75,12 @@ interface PaginatedResponse { // ============================================================================ function transformApiToFrontend(apiData: ApiProcess): Process { + // Pattern 규칙 변환 + const patternRules = (apiData.classification_rules ?? []).map(transformRuleApiToFrontend); + + // 개별 품목 → individual 분류 규칙으로 변환 + const individualRules = transformProcessItemsToRules(apiData.process_items ?? []); + return { id: String(apiData.id), processCode: apiData.process_code, @@ -69,7 +89,7 @@ function transformApiToFrontend(apiData: ApiProcess): Process { processType: apiData.process_type as Process['processType'], department: apiData.department ?? '', workLogTemplate: apiData.work_log_template ?? undefined, - classificationRules: (apiData.classification_rules ?? []).map(transformRuleApiToFrontend), + classificationRules: [...patternRules, ...individualRules], requiredWorkers: apiData.required_workers, equipmentInfo: apiData.equipment_info ?? undefined, workSteps: apiData.work_steps ?? [], @@ -80,6 +100,44 @@ function transformApiToFrontend(apiData: ApiProcess): Process { }; } +/** + * process_items 배열을 individual 분류 규칙으로 변환 + * 모든 개별 품목을 하나의 규칙으로 통합 + */ +function transformProcessItemsToRules(processItems: ApiProcessItem[]): ClassificationRule[] { + if (processItems.length === 0) return []; + + const activeItems = processItems.filter(pi => pi.is_active); + if (activeItems.length === 0) return []; + + // 모든 품목 ID를 쉼표로 구분하여 하나의 규칙으로 통합 + const itemIds = activeItems + .map(pi => String(pi.item_id)) + .join(','); + + // 품목 상세 정보 추출 (code, name 포함) + const items: IndividualItem[] = activeItems + .filter(pi => pi.item) // item 정보가 있는 것만 + .map(pi => ({ + id: String(pi.item!.id), + code: pi.item!.code, + name: pi.item!.name, + })); + + return [{ + id: `individual-${Date.now()}`, + registrationType: 'individual', + ruleType: '품목코드', + matchingType: 'equals', + conditionValue: itemIds, + priority: 0, + description: `개별 품목 ${activeItems.length}개`, + isActive: true, + createdAt: new Date().toISOString(), + items, // 품목 상세 정보 추가 + }]; +} + function transformRuleApiToFrontend(apiRule: ApiClassificationRule): ClassificationRule { return { id: String(apiRule.id), @@ -95,6 +153,24 @@ function transformRuleApiToFrontend(apiRule: ApiClassificationRule): Classificat } function transformFrontendToApi(data: ProcessFormData): Record { + // 패턴 규칙만 분리 (individual 제외) + const patternRules = data.classificationRules.filter( + (rule) => rule.registrationType === 'pattern' + ); + + // 개별 품목 규칙에서 item_ids 추출 + const individualRules = data.classificationRules.filter( + (rule) => rule.registrationType === 'individual' + ); + + // 개별 품목의 conditionValue에서 ID 배열 추출 (쉼표 구분) + const itemIds: number[] = individualRules.flatMap((rule) => + rule.conditionValue + .split(',') + .map((id) => parseInt(id.trim(), 10)) + .filter((n) => !isNaN(n) && n > 0) + ); + return { process_name: data.processName, process_type: data.processType, @@ -105,8 +181,8 @@ function transformFrontendToApi(data: ProcessFormData): Record work_steps: data.workSteps ? data.workSteps.split(',').map((s) => s.trim()).filter(Boolean) : [], note: data.note || null, is_active: data.isActive, - classification_rules: data.classificationRules.map((rule) => ({ - registration_type: rule.registrationType, + // 패턴 규칙만 전송 (registration_type 제외) + classification_rules: patternRules.map((rule) => ({ rule_type: rule.ruleType, matching_type: rule.matchingType, condition_value: rule.conditionValue, @@ -114,6 +190,8 @@ function transformFrontendToApi(data: ProcessFormData): Record description: rule.description || null, is_active: rule.isActive, })), + // 개별 품목 ID 배열 전송 + item_ids: itemIds, }; } @@ -494,8 +572,8 @@ export async function getDepartmentOptions(): Promise { } const result = await response.json(); - if (result.success && result.data) { - return result.data.map((dept: { id: number; name: string }) => ({ + if (result.success && result.data?.data) { + return result.data.data.map((dept: { id: number; name: string }) => ({ value: dept.name, label: dept.name, })); @@ -552,12 +630,12 @@ export async function getItemList(params?: GetItemListParams): Promise ({ + return result.data.data.map((item: { id: number; name: string; code?: string; item_type?: string }) => ({ value: String(item.id), - label: item.item_name, - code: item.item_code || '', + label: item.name, + code: item.code || '', id: String(item.id), - fullName: item.item_name, + fullName: item.name, type: item.item_type || '', })); } diff --git a/src/components/production/WorkOrders/SalesOrderSelectModal.tsx b/src/components/production/WorkOrders/SalesOrderSelectModal.tsx index ba566c74..32f126c3 100644 --- a/src/components/production/WorkOrders/SalesOrderSelectModal.tsx +++ b/src/components/production/WorkOrders/SalesOrderSelectModal.tsx @@ -21,6 +21,18 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getSalesOrdersForWorkOrder } from './actions'; import type { SalesOrder } from './types'; +// Debounce 훅 +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +} + interface SalesOrderSelectModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -36,12 +48,15 @@ export function SalesOrderSelectModal({ const [salesOrders, setSalesOrders] = useState([]); const [isLoading, setIsLoading] = useState(false); + // 디바운스된 검색어 (300ms 딜레이) + const debouncedSearchTerm = useDebounce(searchTerm, 300); + // API로 수주 목록 로드 const loadSalesOrders = useCallback(async () => { setIsLoading(true); try { const result = await getSalesOrdersForWorkOrder({ - q: searchTerm || undefined, + q: debouncedSearchTerm || undefined, }); if (result.success) { // API 응답을 SalesOrder 타입으로 변환 @@ -66,7 +81,7 @@ export function SalesOrderSelectModal({ } finally { setIsLoading(false); } - }, [searchTerm]); + }, [debouncedSearchTerm]); // 모달이 열릴 때 데이터 로드 useEffect(() => { diff --git a/src/components/production/WorkOrders/WorkOrderCreate.tsx b/src/components/production/WorkOrders/WorkOrderCreate.tsx index 06017510..41f5650f 100644 --- a/src/components/production/WorkOrders/WorkOrderCreate.tsx +++ b/src/components/production/WorkOrders/WorkOrderCreate.tsx @@ -5,7 +5,7 @@ * API 연동 완료 (2025-12-26) */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft, FileText, X, Edit2, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -26,7 +26,7 @@ import { SalesOrderSelectModal } from './SalesOrderSelectModal'; import { AssigneeSelectModal } from './AssigneeSelectModal'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { createWorkOrder } from './actions'; +import { createWorkOrder, getProcessOptions, type ProcessOption } from './actions'; import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types'; // Validation 에러 타입 @@ -39,6 +39,7 @@ const FIELD_NAME_MAP: Record = { selectedOrder: '수주', client: '발주처', projectName: '현장명', + processId: '공정', shipmentDate: '출고예정일', }; @@ -56,7 +57,7 @@ interface FormData { itemCount: number; // 작업지시 정보 - processType: ProcessType; + processId: number | null; // 공정 ID (FK → processes.id) shipmentDate: string; priority: number; assignees: string[]; @@ -72,7 +73,7 @@ const initialFormData: FormData = { projectName: '', orderNo: '', itemCount: 0, - processType: 'screen', + processId: null, shipmentDate: '', priority: 5, assignees: [], @@ -88,6 +89,27 @@ export function WorkOrderCreate() { const [assigneeNames, setAssigneeNames] = useState([]); const [validationErrors, setValidationErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); + const [processOptions, setProcessOptions] = useState([]); + const [isLoadingProcesses, setIsLoadingProcesses] = useState(true); + + // 공정 옵션 로드 + useEffect(() => { + async function loadProcessOptions() { + setIsLoadingProcesses(true); + const result = await getProcessOptions(); + if (result.success) { + setProcessOptions(result.data); + // 첫 번째 공정을 기본값으로 설정 + if (result.data.length > 0 && !formData.processId) { + setFormData(prev => ({ ...prev, processId: result.data[0].id })); + } + } else { + toast.error(result.error || '공정 목록을 불러오는데 실패했습니다.'); + } + setIsLoadingProcesses(false); + } + loadProcessOptions(); + }, []); // 수주 선택 핸들러 const handleSelectOrder = (order: SalesOrder) => { @@ -105,7 +127,7 @@ export function WorkOrderCreate() { const handleClearOrder = () => { setFormData({ ...initialFormData, - processType: formData.processType, + processId: formData.processId, shipmentDate: formData.shipmentDate, priority: formData.priority, }); @@ -129,6 +151,10 @@ export function WorkOrderCreate() { } } + if (!formData.processId) { + errors.processId = '공정을 선택해주세요'; + } + if (!formData.shipmentDate) { errors.shipmentDate = '출고예정일을 선택해주세요'; } @@ -150,9 +176,9 @@ export function WorkOrderCreate() { const result = await createWorkOrder({ salesOrderId: formData.selectedOrder?.id ? parseInt(formData.selectedOrder.id) : undefined, projectName: formData.projectName, - processType: formData.processType, + processId: formData.processId!, // 공정 ID (FK → processes.id) scheduledDate: formData.shipmentDate, - assigneeId: formData.assignees.length > 0 ? parseInt(formData.assignees[0]) : undefined, + assigneeIds: formData.assignees.map(id => parseInt(id)), memo: formData.note || undefined, }); @@ -176,14 +202,10 @@ export function WorkOrderCreate() { router.back(); }; - // 공정 코드 표시 - const getProcessCode = (type: ProcessType) => { - const codes: Record = { - screen: 'P-001 | 작업일지: WL-SCR', - slat: 'P-002 | 작업일지: WL-SLT', - bending: 'P-003 | 작업일지: WL-FLD', - }; - return codes[type]; + // 선택된 공정의 코드 가져오기 + const getSelectedProcessCode = (): string => { + const selectedProcess = processOptions.find(p => p.id === formData.processId); + return selectedProcess?.processCode || '-'; }; return ( @@ -398,22 +420,23 @@ export function WorkOrderCreate() {

- 공정코드: {getProcessCode(formData.processType)} + 공정코드: {getSelectedProcessCode()}

diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx index 6c6b7752..37efcff9 100644 --- a/src/components/production/WorkOrders/WorkOrderDetail.tsx +++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { FileText, List, AlertTriangle, Play, CheckCircle2 } from 'lucide-react'; +import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2 } from 'lucide-react'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -23,9 +23,8 @@ import { PageLayout } from '@/components/organisms/PageLayout'; import { WorkLogModal } from '../WorkerScreen/WorkLogModal'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { getWorkOrderById } from './actions'; +import { getWorkOrderById, updateWorkOrderStatus, updateWorkOrderItemStatus, type WorkOrderItemStatus } from './actions'; import { - PROCESS_TYPE_LABELS, WORK_ORDER_STATUS_LABELS, WORK_ORDER_STATUS_COLORS, ITEM_STATUS_LABELS, @@ -35,33 +34,47 @@ import { BENDING_PROCESS_STEPS, type WorkOrder, type ProcessType, + type ProcessStep, } from './types'; // 공정 진행 단계 컴포넌트 function ProcessSteps({ processType, currentStep, + workSteps, }: { processType: ProcessType; currentStep: number; + workSteps?: ProcessStep[]; }) { - const steps = - processType === 'screen' + // 동적 workSteps 우선 사용, 없으면 하드코딩 폴백 + const steps = workSteps && workSteps.length > 0 + ? workSteps + : processType === 'screen' ? SCREEN_PROCESS_STEPS : processType === 'slat' - ? SLAT_PROCESS_STEPS - : BENDING_PROCESS_STEPS; + ? SLAT_PROCESS_STEPS + : BENDING_PROCESS_STEPS; + + if (steps.length === 0) { + return ( +
+

공정 진행

+

공정 단계가 설정되지 않았습니다.

+
+ ); + } return (

공정 진행 ({steps.length}단계)

-
+
{steps.map((step, index) => { const isCompleted = index < currentStep; const isCurrent = index === currentStep; return ( -
+
(null); const [isLoading, setIsLoading] = useState(true); + const [isStatusUpdating, setIsStatusUpdating] = useState(false); + const [updatingItemId, setUpdatingItemId] = useState(null); // API에서 데이터 로드 const loadData = useCallback(async () => { @@ -205,7 +220,6 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { toast.error(result.error || '작업지시 조회에 실패했습니다.'); } } catch (error) { - if (isNextRedirectError(error)) throw error; console.error('[WorkOrderDetail] loadData error:', error); toast.error('데이터 로드 중 오류가 발생했습니다.'); } finally { @@ -217,12 +231,83 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { loadData(); }, [loadData]); + // 상태 변경 핸들러 + const handleStatusChange = useCallback(async (newStatus: 'waiting' | 'in_progress' | 'completed') => { + if (!order) return; + + setIsStatusUpdating(true); + try { + const result = await updateWorkOrderStatus(orderId, newStatus); + if (result.success && result.data) { + setOrder(result.data); + const statusLabels = { + waiting: '작업대기', + in_progress: '작업중', + completed: '작업완료', + }; + toast.success(`상태가 '${statusLabels[newStatus]}'(으)로 변경되었습니다.`); + } else { + toast.error(result.error || '상태 변경에 실패했습니다.'); + } + } catch (error) { + console.error('[WorkOrderDetail] handleStatusChange error:', error); + toast.error('상태 변경 중 오류가 발생했습니다.'); + } finally { + setIsStatusUpdating(false); + } + }, [order, orderId]); + + // 품목 상태 변경 핸들러 + const handleItemStatusChange = useCallback(async (itemId: number, newStatus: WorkOrderItemStatus) => { + if (!order) return; + + setUpdatingItemId(itemId); + try { + const result = await updateWorkOrderItemStatus(orderId, itemId, newStatus); + if (result.success) { + // 로컬 상태 업데이트 (품목 + 작업지시 상태) + setOrder(prev => { + if (!prev) return prev; + return { + ...prev, + status: result.workOrderStatus || prev.status, + items: prev.items.map(item => + item.id === itemId ? { ...item, status: newStatus } : item + ), + }; + }); + + const statusLabels: Record = { + waiting: '대기', + in_progress: '작업중', + completed: '완료', + }; + toast.success(`품목 상태가 '${statusLabels[newStatus]}'(으)로 변경되었습니다.`); + + // 작업지시 상태가 변경된 경우 추가 알림 + if (result.workOrderStatusChanged && result.workOrderStatus) { + const workOrderStatusLabel = WORK_ORDER_STATUS_LABELS[result.workOrderStatus as keyof typeof WORK_ORDER_STATUS_LABELS] || result.workOrderStatus; + toast.info(`작업지시 상태가 '${workOrderStatusLabel}'(으)로 자동 변경되었습니다.`); + } + } else { + toast.error(result.error || '품목 상태 변경에 실패했습니다.'); + } + } catch (error) { + console.error('[WorkOrderDetail] handleItemStatusChange error:', error); + toast.error('품목 상태 변경 중 오류가 발생했습니다.'); + } finally { + setUpdatingItemId(null); + } + }, [order, orderId]); + // 로딩 상태 if (isLoading) { return (

작업지시 상세

- +
+ +
); } @@ -249,7 +334,9 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { quantity: order.items.reduce((sum, item) => sum + item.quantity, 0), progress: order.currentStep * 20, // 대략적인 진행률 process: order.processType as 'screen' | 'slat' | 'bending', - assignees: [order.assignee], + assignees: order.assignees && order.assignees.length > 0 + ? order.assignees.map(a => a.name) + : [order.assignee], instruction: order.note || '', status: 'in_progress' as const, priority: order.priority <= 3 ? 'high' : order.priority <= 6 ? 'medium' : 'low', @@ -261,6 +348,35 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {

작업지시 상세

+ {/* 상태 변경 버튼 */} + {order.status === 'waiting' && ( + + )} + {order.status === 'in_progress' && ( + + )}

공정구분

-

{PROCESS_TYPE_LABELS[order.processType]}

+

{order.processName}

작업상태

@@ -309,13 +425,21 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {

작업자

-

{order.assignee}

+

+ {order.assignees && order.assignees.length > 0 + ? order.assignees.map(a => a.name).join(', ') + : order.assignee} +

{/* 공정 진행 */} - + {/* 작업 품목 */}
@@ -346,14 +470,32 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { {item.quantity} {item.status === 'waiting' && ( - )} {item.status === 'in_progress' && ( - )} diff --git a/src/components/production/WorkOrders/WorkOrderList.tsx b/src/components/production/WorkOrders/WorkOrderList.tsx index 2e4572f9..c4a3f3a0 100644 --- a/src/components/production/WorkOrders/WorkOrderList.tsx +++ b/src/components/production/WorkOrders/WorkOrderList.tsx @@ -23,7 +23,6 @@ import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard import { toast } from 'sonner'; import { getWorkOrders, getWorkOrderStats } from './actions'; import { - PROCESS_TYPE_LABELS, WORK_ORDER_STATUS_LABELS, WORK_ORDER_STATUS_COLORS, type WorkOrder, @@ -31,6 +30,18 @@ import { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +// Debounce 훅 +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +} + // 탭 필터 정의 type TabFilter = 'all' | 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed'; @@ -44,6 +55,9 @@ export function WorkOrderList() { const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); + // 디바운스된 검색어 (300ms 딜레이) + const debouncedSearchTerm = useDebounce(searchTerm, 300); + // API 데이터 상태 const [workOrders, setWorkOrders] = useState([]); const [statsData, setStatsData] = useState({ @@ -58,45 +72,56 @@ export function WorkOrderList() { const [totalItems, setTotalItems] = useState(0); const [totalPages, setTotalPages] = useState(1); - // 데이터 로드 - const loadData = useCallback(async () => { - setIsLoading(true); - try { - // 목록과 통계를 병렬로 조회 - const [listResult, statsResult] = await Promise.all([ - getWorkOrders({ - page: currentPage, - perPage: ITEMS_PER_PAGE, - status: activeTab === 'all' ? undefined : activeTab, - search: searchTerm || undefined, - }), - getWorkOrderStats(), - ]); - - if (listResult.success) { - setWorkOrders(listResult.data); - setTotalItems(listResult.pagination.total); - setTotalPages(listResult.pagination.lastPage); - } else { - toast.error(listResult.error || '목록 조회에 실패했습니다.'); - } - - if (statsResult.success && statsResult.data) { - setStatsData(statsResult.data); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('[WorkOrderList] loadData error:', error); - toast.error('데이터 로드 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - }, [currentPage, activeTab, searchTerm]); - // 초기 로드 및 필터 변경 시 데이터 다시 로드 useEffect(() => { + let isMounted = true; + + const loadData = async () => { + setIsLoading(true); + try { + // 목록과 통계를 병렬로 조회 + const [listResult, statsResult] = await Promise.all([ + getWorkOrders({ + page: currentPage, + perPage: ITEMS_PER_PAGE, + status: activeTab === 'all' ? undefined : activeTab, + search: debouncedSearchTerm || undefined, + }), + getWorkOrderStats(), + ]); + + // 컴포넌트가 언마운트되었으면 상태 업데이트 중단 + if (!isMounted) return; + + if (listResult.success) { + setWorkOrders(listResult.data); + setTotalItems(listResult.pagination.total); + setTotalPages(listResult.pagination.lastPage); + } else { + toast.error(listResult.error || '목록 조회에 실패했습니다.'); + } + + if (statsResult.success && statsResult.data) { + setStatsData(statsResult.data); + } + } catch (error) { + if (!isMounted) return; + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderList] loadData error:', error); + toast.error('데이터 로드 중 오류가 발생했습니다.'); + } finally { + if (isMounted) { + setIsLoading(false); + } + } + }; + loadData(); - }, [loadData]); + + return () => { + isMounted = false; + }; + }, [currentPage, activeTab, debouncedSearchTerm]); // 탭 옵션 (통계 데이터 기반) const tabs: TabOption[] = [ @@ -224,7 +249,7 @@ export function WorkOrderList() { {globalIndex} {order.workOrderNo} - {PROCESS_TYPE_LABELS[order.processType]} + {order.processName} {order.lotNo} {order.orderDate} {order.isAssigned ? 'Y' : '-'} @@ -273,7 +298,7 @@ export function WorkOrderList() { } infoGrid={
- + diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index fce18028..6e56f968 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -13,6 +13,7 @@ * - PATCH /api/v1/work-orders/{id}/bending/toggle - 벤딩 필드 토글 * - POST /api/v1/work-orders/{id}/issues - 이슈 등록 * - PATCH /api/v1/work-orders/{id}/issues/{issueId}/resolve - 이슈 해결 + * - PATCH /api/v1/work-orders/{id}/items/{itemId}/status - 품목 상태 변경 */ 'use server'; @@ -24,7 +25,6 @@ import type { WorkOrder, WorkOrderStats, WorkOrderStatus, - ProcessType, WorkOrderApiPaginatedResponse, WorkOrderStatsApi, } from './types'; @@ -47,7 +47,7 @@ export async function getWorkOrders(params?: { page?: number; perPage?: number; status?: WorkOrderStatus | 'all'; - processType?: ProcessType | 'all'; + processId?: number | 'all'; // 공정 ID (FK → processes.id) search?: string; startDate?: string; endDate?: string; @@ -71,8 +71,8 @@ export async function getWorkOrders(params?: { if (params?.status && params.status !== 'all') { searchParams.set('status', params.status); } - if (params?.processType && params.processType !== 'all') { - searchParams.set('process_type', params.processType); + if (params?.processId && params.processId !== 'all') { + searchParams.set('process_id', String(params.processId)); } if (params?.search) searchParams.set('search', params.search); if (params?.startDate) searchParams.set('start_date', params.startDate); @@ -220,15 +220,23 @@ export async function getWorkOrderById(id: string): Promise<{ export async function createWorkOrder( data: Partial & { salesOrderId?: number; - assigneeId?: number; + assigneeId?: number; // 단일 담당자 (하위 호환) + assigneeIds?: number[]; // 다중 담당자 teamId?: number; } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { try { + // 다중 담당자 우선, 없으면 단일 담당자 배열로 변환 + const assigneeIds = data.assigneeIds && data.assigneeIds.length > 0 + ? data.assigneeIds + : data.assigneeId + ? [data.assigneeId] + : undefined; + const apiData = { ...transformFrontendToApi(data), sales_order_id: data.salesOrderId, - assignee_id: data.assigneeId, + assignee_ids: assigneeIds, // 배열로 전송 team_id: data.teamId, }; @@ -384,11 +392,13 @@ export async function updateWorkOrderStatus( // ===== 담당자 배정 ===== export async function assignWorkOrder( id: string, - assigneeId: number, + assigneeIds: number | number[], // 단일 또는 다중 담당자 teamId?: number ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { try { - const body: { assignee_id: number; team_id?: number } = { assignee_id: assigneeId }; + // 배열로 통일 + const ids = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds]; + const body: { assignee_ids: number[]; team_id?: number } = { assignee_ids: ids }; if (teamId) body.team_id = teamId; console.log('[WorkOrderActions] PATCH assign request:', body); @@ -550,6 +560,62 @@ export async function resolveWorkOrderIssue( } } +// ===== 품목 상태 변경 ===== +export type WorkOrderItemStatus = 'waiting' | 'in_progress' | 'completed'; + +export async function updateWorkOrderItemStatus( + workOrderId: string, + itemId: number, + status: WorkOrderItemStatus +): Promise<{ + success: boolean; + itemId: number; + status: WorkOrderItemStatus; + workOrderStatus?: string; + workOrderStatusChanged?: boolean; + error?: string; +}> { + try { + console.log('[WorkOrderActions] PATCH item status request:', { workOrderId, itemId, status }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/status`, + { + method: 'PATCH', + body: JSON.stringify({ status }), + } + ); + + if (error || !response) { + return { success: false, itemId, status, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH item status response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + itemId, + status, + error: result.message || '품목 상태 변경에 실패했습니다.', + }; + } + + return { + success: true, + itemId, + status: result.data?.item?.status || status, + workOrderStatus: result.data?.work_order_status, + workOrderStatusChanged: result.data?.work_order_status_changed || false, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] updateWorkOrderItemStatus error:', error); + return { success: false, itemId, status, error: '서버 오류가 발생했습니다.' }; + } +} + // ===== 수주 목록 조회 (작업지시 생성용) ===== export interface SalesOrderForWorkOrder { id: number; @@ -579,9 +645,9 @@ export async function getSalesOrdersForWorkOrder(params?: { if (params?.status) searchParams.set('status', params.status); const queryString = searchParams.toString(); - const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales-orders${queryString ? `?${queryString}` : ''}`; + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders${queryString ? `?${queryString}` : ''}`; - console.log('[WorkOrderActions] GET sales-orders for work-order:', url); + console.log('[WorkOrderActions] GET orders for work-order:', url); const { response, error } = await serverFetch(url, { method: 'GET' }); @@ -590,7 +656,7 @@ export async function getSalesOrdersForWorkOrder(params?: { } if (!response.ok) { - console.warn('[WorkOrderActions] GET sales-orders error:', response.status); + console.warn('[WorkOrderActions] GET orders error:', response.status); return { success: false, data: [], error: `API 오류: ${response.status}` }; } @@ -717,3 +783,65 @@ export async function getDepartmentsWithUsers(): Promise<{ return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } } + +// ===== 공정 목록 조회 (작업지시 생성용) ===== +export interface ProcessOption { + id: number; + processCode: string; + processName: string; +} + +export async function getProcessOptions(): Promise<{ + success: boolean; + data: ProcessOption[]; + error?: string; +}> { + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`; + + console.log('[WorkOrderActions] GET process options:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, data: [], error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET process options error:', response.status); + return { success: false, data: [], error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + data: [], + error: result.message || '공정 목록 조회에 실패했습니다.', + }; + } + + // API 응답 변환 + const processes: ProcessOption[] = (result.data || []).map( + (item: { + id: number; + process_code: string; + process_name: string; + }) => ({ + id: item.id, + processCode: item.process_code, + processName: item.process_name, + }) + ); + + return { + success: true, + data: processes, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getProcessOptions error:', error); + return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; + } +} diff --git a/src/components/production/WorkOrders/types.ts b/src/components/production/WorkOrders/types.ts index 09c156c7..a69c702f 100644 --- a/src/components/production/WorkOrders/types.ts +++ b/src/components/production/WorkOrders/types.ts @@ -2,7 +2,15 @@ * 작업지시 관리 타입 정의 */ -// 공정 구분 +// 공정 정보 (API 관계) +export interface ProcessInfo { + id: number; + process_code: string; + process_name: string; +} + +// @deprecated process_type은 process_id FK로 변경됨 +// 하위 호환성을 위해 유지 export type ProcessType = 'screen' | 'slat' | 'bending'; export const PROCESS_TYPE_LABELS: Record = { @@ -134,22 +142,36 @@ export const ISSUE_STATUS_LABELS: Record = { resolved: '해결됨', }; +// 공정 단계 타입 +export interface ProcessStep { + key: string; + label: string; + order: number; +} + // 작업지시 메인 타입 export interface WorkOrder { id: string; workOrderNo: string; // 작업지시번호 (KD-WO-251217-12) lotNo: string; // 로트번호 (KD-TS-251217-10) - processType: ProcessType; // 공정구분 + processId: number; // 공정 ID (FK) + processName: string; // 공정명 (표시용) + processCode: string; // 공정코드 (표시용) + workSteps?: ProcessStep[]; // 공정 단계 (동적, DB에서 로드) + /** @deprecated process_id FK 사용 */ + processType: ProcessType; // 하위 호환용 status: WorkOrderStatus; // 작업상태 // 기본 정보 client: string; // 발주처 projectName: string; // 현장명 dueDate: string; // 납기일 - assignee: string; // 작업자 + assignee: string; // 작업자 (주 담당자) + assignees?: { id: string; name: string; isPrimary: boolean }[]; // 다중 담당자 // 날짜 정보 orderDate: string; // 지시일 + scheduledDate: string; // 예정일 (API: scheduled_date) shipmentDate: string; // 출고예정일 // 플래그 @@ -237,6 +259,17 @@ export interface WorkOrderBendingDetailApi { updated_at: string; } +// API 응답 - 담당자 (다중 담당자) +export interface WorkOrderAssigneeApi { + id: number; + work_order_id: number; + user_id: number; + is_primary: boolean; + created_at: string; + updated_at: string; + user?: { id: number; name: string }; +} + // API 응답 - 이슈 export interface WorkOrderIssueApi { id: number; @@ -259,7 +292,7 @@ export interface WorkOrderApi { work_order_no: string; sales_order_id: number | null; project_name: string | null; - process_type: 'screen' | 'slat' | 'bending'; + process_id: number; // FK to processes.id status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped'; assignee_id: number | null; team_id: number | null; @@ -277,7 +310,14 @@ export interface WorkOrderApi { order_no: string; client?: { id: number; name: string }; }; + process?: { + id: number; + process_code: string; + process_name: string; + work_steps?: string[] | { key: string; label: string; order: number }[]; + }; assignee?: { id: number; name: string }; + assignees?: WorkOrderAssigneeApi[]; team?: { id: number; name: string }; items?: WorkOrderItemApi[]; bending_detail?: WorkOrderBendingDetailApi; @@ -308,19 +348,53 @@ export interface WorkOrderStatsApi { // API → Frontend 변환 export function transformApiToFrontend(api: WorkOrderApi): WorkOrder { + // 다중 담당자 변환 + const assignees = (api.assignees || []).map(a => ({ + id: String(a.user_id), + name: a.user?.name || '-', + isPrimary: a.is_primary, + })); + + // 주 담당자 이름 (기존 호환) + const primaryAssignee = assignees.find(a => a.isPrimary); + const assigneeName = primaryAssignee?.name || api.assignee?.name || '-'; + + // 공정명 → 하위호환용 processType 매핑 + const processNameToType = (name: string): ProcessType => { + const mapping: Record = { + '스크린': 'screen', + '슬랫': 'slat', + '절곡': 'bending', + }; + return mapping[name] || 'screen'; + }; + return { id: String(api.id), workOrderNo: api.work_order_no, lotNo: api.sales_order?.order_no || '-', - processType: api.process_type, + processId: api.process_id, + processName: api.process?.process_name || '-', + processCode: api.process?.process_code || '-', + // work_steps: string[] 또는 ProcessStep[] 형식 모두 지원 + workSteps: api.process?.work_steps + ? (api.process.work_steps as (string | { key: string; label: string; order: number })[]).map((step, idx) => + typeof step === 'string' + ? { key: `step-${idx}`, label: step, order: idx + 1 } + : step + ) + : undefined, + processType: processNameToType(api.process?.process_name || ''), // 하위 호환 status: api.status, client: api.sales_order?.client?.name || '-', projectName: api.project_name || '-', dueDate: api.scheduled_date || '-', - assignee: api.assignee?.name || '-', + assignee: assigneeName, + assignees: assignees.length > 0 ? assignees : undefined, orderDate: api.created_at.split('T')[0], + scheduledDate: api.scheduled_date || '', shipmentDate: api.scheduled_date || '-', - isAssigned: api.assignee_id !== null, + isAssigned: api.assignee_id !== null || assignees.length > 0, isStarted: ['in_progress', 'completed', 'shipped'].includes(api.status), priority: 5, // Default priority currentStep: getStatusStep(api.status), @@ -387,13 +461,14 @@ function getStatusStep(status: WorkOrderStatus): number { } // Frontend → API 변환 (등록/수정용) -export function transformFrontendToApi(data: Partial): Record { +export function transformFrontendToApi(data: Partial & { processId?: number }): Record { const result: Record = {}; if (data.projectName !== undefined) result.project_name = data.projectName; - if (data.processType !== undefined) result.process_type = data.processType; + if (data.processId !== undefined) result.process_id = data.processId; if (data.status !== undefined) result.status = data.status; - if (data.dueDate !== undefined) result.scheduled_date = data.dueDate; + if (data.scheduledDate !== undefined) result.scheduled_date = data.scheduledDate; + if (data.dueDate !== undefined && data.scheduledDate === undefined) result.scheduled_date = data.dueDate; if (data.note !== undefined) result.memo = data.note; // items 변환 diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 3de2efeb..c0c11f51 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -69,6 +69,7 @@ export interface QuoteItem { id: string; quoteId: string; productId?: string; + itemCode?: string; // 품목코드 (item_code) productName: string; specification?: string; unit?: string; @@ -298,6 +299,7 @@ export function transformItemApiToFrontend(apiData: QuoteItemApiData): QuoteItem id: String(apiData.id), quoteId: String(apiData.quote_id), productId: apiData.item_id ? String(apiData.item_id) : (apiData.product_id ? String(apiData.product_id) : undefined), + itemCode: apiData.item_code || undefined, // 품목코드 productName, specification: apiData.specification || undefined, unit: apiData.unit || undefined, @@ -545,7 +547,7 @@ export function transformQuoteToFormData(quote: Quote): QuoteFormData { ? quote.items.map((item, index) => ({ itemIndex: index, finishedGoodsCode: '', - itemCode: item.productId || item.id || '', + itemCode: item.itemCode || '', // 품목코드 사용 itemName: item.productName, itemType: '', itemCategory: '', diff --git a/src/lib/actions/fcm.ts b/src/lib/actions/fcm.ts new file mode 100644 index 00000000..352c178f --- /dev/null +++ b/src/lib/actions/fcm.ts @@ -0,0 +1,178 @@ +/** + * FCM 푸시 알림 공통 서버 액션 + * + * 사용 예시: + * import { sendFcmNotification, sendApprovalNotification } from '@/lib/actions/fcm'; + * + * // 기본 알림 발송 + * const result = await sendFcmNotification({ title: '알림', body: '내용' }); + * + * // 결재 알림 발송 (프리셋) + * const result = await sendApprovalNotification(); + */ + +'use server'; + +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { serverFetch } from '@/lib/api/fetch-wrapper'; + +// ============================================ +// 타입 정의 +// ============================================ + +export interface FcmNotificationParams { + /** 알림 제목 (필수) */ + title: string; + /** 알림 본문 (필수) */ + body: string; + /** 특정 테넌트에게만 발송 */ + tenant_id?: number; + /** 특정 사용자에게만 발송 */ + user_id?: number; + /** 플랫폼 필터 (android, ios, web) */ + platform?: 'android' | 'ios' | 'web'; + /** 알림 채널 ID (Android) */ + channel_id?: string; + /** 알림 타입 (앱에서 분기 처리용) */ + type?: string; + /** 클릭 시 이동할 URL */ + url?: string; + /** 알림 사운드 키 */ + sound_key?: string; +} + +export interface FcmResult { + success: boolean; + error?: string; + sentCount?: number; + __authError?: boolean; +} + +// ============================================ +// 기본 FCM 발송 함수 +// ============================================ + +/** + * FCM 푸시 알림 발송 + */ +export async function sendFcmNotification( + params: FcmNotificationParams +): Promise { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/admin/fcm/send`, + { + method: 'POST', + body: JSON.stringify(params), + } + ); + + if (error?.__authError) { + return { success: false, error: '인증이 필요합니다.', __authError: true }; + } + + if (!response) { + return { success: false, error: error?.message || 'FCM 발송에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || 'FCM 발송에 실패했습니다.', + }; + } + + return { + success: true, + sentCount: result.data?.success || 0, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[FCM] sendFcmNotification error:', error); + return { + success: false, + error: '서버 오류가 발생했습니다.', + }; + } +} + +// ============================================ +// 프리셋 함수들 (자주 사용하는 알림 타입) +// ============================================ + +/** + * 결재 알림 발송 (프리셋) + */ +export async function sendApprovalNotification( + customParams?: Partial +): Promise { + return sendFcmNotification({ + title: '결재 알림', + body: '결재 문서가 완료되었습니다.', + type: 'approval', + channel_id: 'push_payment', + ...customParams, + }); +} + +/** + * 작업지시 알림 발송 (프리셋) + */ +export async function sendWorkOrderNotification( + customParams?: Partial +): Promise { + return sendFcmNotification({ + title: '작업지시 알림', + body: '새로운 작업지시가 있습니다.', + type: 'work_order', + channel_id: 'work_order', + ...customParams, + }); +} + +/** + * 일반 공지 알림 발송 (프리셋) + */ +export async function sendNoticeNotification( + customParams?: Partial +): Promise { + return sendFcmNotification({ + title: '공지사항', + body: '새로운 공지사항이 있습니다.', + type: 'notice', + channel_id: 'notice', + ...customParams, + }); +} + +/** + * 신규업체 알림 발송 (프리셋) + */ +export async function sendNewClientNotification( + customParams?: Partial +): Promise { + return sendFcmNotification({ + title: '신규업체 알림', + body: '새로운 업체가 등록되었습니다.', + type: 'new_client', + channel_id: 'push_urgent', + ...customParams, + }); +} + +/** + * 수주 알림 발송 (프리셋) + */ +export async function sendSalesOrderNotification( + customParams?: Partial +): Promise { + return sendFcmNotification({ + title: '수주 알림', + body: '수주가 완료되었습니다.', + type: 'sales_order', + channel_id: 'push_sales_order', + ...customParams, + }); +} \ No newline at end of file diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 9920adcf..f6b65adc 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -102,9 +102,16 @@ export class ApiClient { /** * GET 요청 + * @param endpoint API 엔드포인트 + * @param options 쿼리 파라미터 등 옵션 */ - async get(endpoint: string): Promise { - return this.request(endpoint, { method: 'GET' }); + async get(endpoint: string, options?: { params?: Record }): Promise { + let url = endpoint; + if (options?.params) { + const searchParams = new URLSearchParams(options.params); + url = `${endpoint}?${searchParams.toString()}`; + } + return this.request(url, { method: 'GET' }); } /** @@ -128,10 +135,25 @@ export class ApiClient { } /** - * DELETE 요청 + * PATCH 요청 */ - async delete(endpoint: string): Promise { - return this.request(endpoint, { method: 'DELETE' }); + async patch(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'PATCH', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * DELETE 요청 + * @param endpoint API 엔드포인트 + * @param options body 데이터 (일괄 삭제 등에서 사용) + */ + async delete(endpoint: string, options?: { data?: unknown }): Promise { + return this.request(endpoint, { + method: 'DELETE', + body: options?.data ? JSON.stringify(options.data) : undefined, + }); } /** diff --git a/src/lib/api/common-codes.ts b/src/lib/api/common-codes.ts new file mode 100644 index 00000000..bc3ab97e --- /dev/null +++ b/src/lib/api/common-codes.ts @@ -0,0 +1,121 @@ +'use server'; + +import { apiClient } from './index'; + +// ======================================== +// 공통 코드 타입 +// ======================================== + +export interface CommonCode { + id: number; + code: string; + name: string; + description: string | null; + sort_order: number; + attributes: Record | null; +} + +// ======================================== +// 공통 코드 조회 함수 +// ======================================== + +/** + * 특정 그룹의 공통 코드 목록 조회 + * GET /api/v1/settings/common/{group} + */ +export async function getCommonCodes(group: string): Promise<{ + success: boolean; + data?: CommonCode[]; + error?: string; +}> { + try { + const response = await apiClient.get(`/settings/common/${group}`); + return { success: true, data: response }; + } catch (error) { + console.error(`공통코드 조회 오류 (${group}):`, error); + return { success: false, error: '공통코드를 불러오는데 실패했습니다.' }; + } +} + +/** + * 공통 코드 옵션 형태로 변환 + * Select/ComboBox 등에서 사용 + */ +export async function getCommonCodeOptions(group: string): Promise<{ + success: boolean; + data?: { value: string; label: string }[]; + error?: string; +}> { + const result = await getCommonCodes(group); + + if (!result.success || !result.data) { + return { success: false, error: result.error }; + } + + const options = result.data.map((code) => ({ + value: code.code, + label: code.name, + })); + + return { success: true, data: options }; +} + +// ======================================== +// 자주 사용하는 코드 그룹 함수 +// ======================================== + +/** + * 수주 상태 코드 조회 + */ +export async function getOrderStatusCodes() { + return getCommonCodes('order_status'); +} + +/** + * 수주 상태 옵션 조회 + */ +export async function getOrderStatusOptions() { + return getCommonCodeOptions('order_status'); +} + +/** + * 수주 유형 코드 조회 + */ +export async function getOrderTypeCodes() { + return getCommonCodes('order_type'); +} + +/** + * 수주 유형 옵션 조회 + */ +export async function getOrderTypeOptions() { + return getCommonCodeOptions('order_type'); +} + +/** + * 거래처 유형 코드 조회 + */ +export async function getClientTypeCodes() { + return getCommonCodes('client_type'); +} + +/** + * 거래처 유형 옵션 조회 + */ +export async function getClientTypeOptions() { + return getCommonCodeOptions('client_type'); +} + +/** + * 품목 유형 코드 조회 + */ +export async function getItemTypeCodes() { + return getCommonCodes('item_type'); +} + +/** + * 품목 유형 옵션 조회 + */ +export async function getItemTypeOptions() { + return getCommonCodeOptions('item_type'); +} \ No newline at end of file diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts new file mode 100644 index 00000000..2944a4fe --- /dev/null +++ b/src/lib/api/index.ts @@ -0,0 +1,250 @@ +// lib/api/index.ts +// API 클라이언트 배럴 익스포트 + +export { ApiClient, withTokenRefresh } from './client'; +export { serverFetch } from './fetch-wrapper'; +export { AUTH_CONFIG } from './auth/auth-config'; + +// 공통 코드 유틸리티 +export { + getCommonCodes, + getCommonCodeOptions, + getOrderStatusCodes, + getOrderStatusOptions, + getOrderTypeCodes, + getOrderTypeOptions, + getClientTypeCodes, + getClientTypeOptions, + getItemTypeCodes, + getItemTypeOptions, + type CommonCode, +} from './common-codes'; + +// Server-side API 클라이언트 +// 서버 액션에서 쿠키 기반 Bearer 토큰 자동 포함 +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { AUTH_CONFIG } from './auth/auth-config'; +import { refreshAccessToken } from './refresh-token'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; + +/** + * Server Actions 전용 API 클라이언트 + * + * 특징: + * - 쿠키에서 access_token 자동 읽기 + * - X-API-KEY + Bearer 토큰 자동 포함 + * - 401 발생 시 토큰 자동 갱신 후 재시도 + */ +class ServerApiClient { + private baseURL: string; + private apiKey: string; + + constructor() { + // API URL에 /api/v1 prefix 자동 추가 + const apiUrl = AUTH_CONFIG.apiUrl.replace(/\/$/, ''); // trailing slash 제거 + this.baseURL = `${apiUrl}/api/v1`; + this.apiKey = process.env.API_KEY || ''; + } + + /** + * 쿠키에서 인증 헤더 생성 (async) + */ + private async getAuthHeaders(token?: string): Promise> { + const cookieStore = await cookies(); + const accessToken = token || cookieStore.get('access_token')?.value; + + return { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-API-KEY': this.apiKey, + ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}), + }; + } + + /** + * 토큰 쿠키 삭제 (인증 실패 시) + */ + private async clearTokenCookies() { + const cookieStore = await cookies(); + cookieStore.delete('access_token'); + cookieStore.delete('refresh_token'); + cookieStore.delete('token_refreshed_at'); + cookieStore.delete('is_authenticated'); + console.log('🗑️ [ServerApiClient] 토큰 쿠키 삭제 완료'); + } + + /** + * 새 토큰을 쿠키에 저장 + */ + private async setNewTokenCookies(tokens: { + accessToken?: string; + refreshToken?: string; + expiresIn?: number; + }) { + const cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + + if (tokens.accessToken) { + cookieStore.set('access_token', tokens.accessToken, { + httpOnly: true, + secure: isProduction, + sameSite: 'lax', + path: '/', + maxAge: tokens.expiresIn || 7200, + }); + + cookieStore.set('token_refreshed_at', Date.now().toString(), { + httpOnly: false, + secure: isProduction, + sameSite: 'lax', + path: '/', + maxAge: 60, + }); + } + + if (tokens.refreshToken) { + cookieStore.set('refresh_token', tokens.refreshToken, { + httpOnly: true, + secure: isProduction, + sameSite: 'lax', + path: '/', + maxAge: 604800, + }); + } + } + + /** + * HTTP 요청 실행 (토큰 자동 갱신 포함) + */ + private async request( + endpoint: string, + options?: RequestInit & { skipAuthRetry?: boolean } + ): Promise { + try { + const cookieStore = await cookies(); + const refreshToken = cookieStore.get('refresh_token')?.value; + const headers = await this.getAuthHeaders(); + + const url = `${this.baseURL}${endpoint}`; + let response = await fetch(url, { + ...options, + headers: { + ...headers, + ...options?.headers, + }, + cache: 'no-store', + }); + + // 401 발생 시 토큰 갱신 후 재시도 + if (response.status === 401 && !options?.skipAuthRetry && refreshToken) { + console.log('🔄 [ServerApiClient] 401 발생, 토큰 갱신 시도...'); + + const refreshResult = await refreshAccessToken(refreshToken, 'ServerApiClient'); + + if (refreshResult.success && refreshResult.accessToken) { + console.log('✅ [ServerApiClient] 토큰 갱신 성공, 재시도...'); + await this.setNewTokenCookies(refreshResult); + + const newHeaders = await this.getAuthHeaders(refreshResult.accessToken); + response = await fetch(url, { + ...options, + headers: { + ...newHeaders, + ...options?.headers, + }, + cache: 'no-store', + }); + + if (response.status === 401) { + console.warn('🔴 [ServerApiClient] 재시도 실패, 로그인 리다이렉트'); + await this.clearTokenCookies(); + redirect('/login'); + } + } else { + console.warn('🔴 [ServerApiClient] 토큰 갱신 실패, 로그인 리다이렉트'); + await this.clearTokenCookies(); + redirect('/login'); + } + } else if (response.status === 401 && !options?.skipAuthRetry) { + console.warn('🔴 [ServerApiClient] 401 (refresh token 없음), 로그인 리다이렉트'); + await this.clearTokenCookies(); + redirect('/login'); + } + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw { + status: response.status, + message: errorData.message || 'An error occurred', + errors: errorData.errors, + code: errorData.code, + }; + } + + if (response.status === 204) { + return undefined as T; + } + + return await response.json(); + } catch (error) { + if (isNextRedirectError(error)) throw error; + throw error; + } + } + + /** + * GET 요청 + */ + async get(endpoint: string, options?: { params?: Record }): Promise { + let url = endpoint; + if (options?.params) { + const searchParams = new URLSearchParams(options.params); + url = `${endpoint}?${searchParams.toString()}`; + } + return this.request(url, { method: 'GET' }); + } + + /** + * POST 요청 + */ + async post(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'POST', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * PUT 요청 + */ + async put(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'PUT', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * PATCH 요청 + */ + async patch(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'PATCH', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * DELETE 요청 + */ + async delete(endpoint: string, options?: { data?: unknown }): Promise { + return this.request(endpoint, { + method: 'DELETE', + body: options?.data ? JSON.stringify(options.data) : undefined, + }); + } +} + +// 서버 액션용 API 클라이언트 인스턴스 +export const apiClient = new ServerApiClient(); \ No newline at end of file diff --git a/src/types/process.ts b/src/types/process.ts index 16aad75f..3f35861d 100644 --- a/src/types/process.ts +++ b/src/types/process.ts @@ -17,6 +17,13 @@ export type RuleType = '품목코드' | '품목명' | '품목구분'; // 매칭 방식 export type MatchingType = 'startsWith' | 'endsWith' | 'contains' | 'equals'; +// 개별 품목 정보 +export interface IndividualItem { + id: string; + code: string; + name: string; +} + // 자동 분류 규칙 export interface ClassificationRule { id: string; @@ -28,6 +35,7 @@ export interface ClassificationRule { description?: string; isActive: boolean; createdAt: string; + items?: IndividualItem[]; // 개별 품목인 경우 품목 정보 } // 자동 분류 규칙 입력용 (id, createdAt 제외) diff --git a/src/utils/formatAmount.ts b/src/utils/formatAmount.ts index f4c8ddc1..dcc845a4 100644 --- a/src/utils/formatAmount.ts +++ b/src/utils/formatAmount.ts @@ -6,6 +6,11 @@ */ export function formatAmount(amount: number): string { + // NaN, undefined, null 처리 + if (amount == null || isNaN(amount)) { + return "0원"; + } + if (amount < 10000) { return `${amount.toLocaleString("ko-KR")}원`; } else { @@ -18,6 +23,10 @@ export function formatAmount(amount: number): string { * 금액을 원 단위로 포맷 (항상 "원" 단위) */ export function formatAmountWon(amount: number): string { + // NaN, undefined, null 처리 + if (amount == null || isNaN(amount)) { + return "0원"; + } return `${amount.toLocaleString("ko-KR")}원`; }