From db761749313675c634101cecba78d047dfd35857 Mon Sep 17 00:00:00 2001 From: ops-agent Date: Wed, 17 Jun 2026 07:00:42 +0900 Subject: [PATCH] Initial guardia-esn import for deployment --- .claude/agents/esn-backend-dev.md | 54 + .claude/agents/esn-devops-dev.md | 39 + .claude/agents/esn-frontend-dev.md | 42 + .claude/agents/esn-qa.md | 29 + .../skills/guardia-esn-orchestrator/SKILL.md | 118 + CLAUDE.md | 121 + Jenkinsfile | 65 + backend/pom.xml | 77 + .../java/com/zioinfo/esn/EsnApplication.java | 30 + .../com/zioinfo/esn/auth/AuthController.java | 35 + .../com/zioinfo/esn/auth/AuthService.java | 37 + .../java/com/zioinfo/esn/auth/EsnUser.java | 21 + .../java/com/zioinfo/esn/auth/JwtFilter.java | 40 + .../java/com/zioinfo/esn/auth/JwtUtil.java | 56 + .../esn/auth/mapper/UserAuthMapper.java | 11 + .../com/zioinfo/esn/common/ApiResponse.java | 27 + .../com/zioinfo/esn/config/CryptoUtil.java | 68 + .../com/zioinfo/esn/config/OllamaClient.java | 77 + .../zioinfo/esn/config/SecurityConfig.java | 67 + .../com/zioinfo/esn/config/WebConfig.java | 23 + .../zioinfo/esn/controller/AiController.java | 73 + .../esn/controller/AlarmController.java | 56 + .../esn/controller/DashboardController.java | 19 + .../esn/controller/FirmwareController.java | 43 + .../esn/controller/HCoreController.java | 52 + .../esn/controller/PosCvtController.java | 48 + .../esn/controller/ProductController.java | 44 + .../esn/controller/StoreController.java | 44 + .../esn/controller/StoreGroupController.java | 41 + .../esn/controller/TagBindingController.java | 57 + .../esn/controller/TemplateController.java | 43 + .../esn/controller/TenantController.java | 41 + .../esn/controller/UpdateQueueController.java | 75 + .../esn/controller/UserController.java | 58 + .../esn/controller/WorkController.java | 45 + .../java/com/zioinfo/esn/domain/AlarmVo.java | 21 + .../com/zioinfo/esn/domain/DashboardVo.java | 19 + .../com/zioinfo/esn/domain/FirmwareVo.java | 19 + .../java/com/zioinfo/esn/domain/HCoreVo.java | 22 + .../java/com/zioinfo/esn/domain/PosCvtVo.java | 23 + .../com/zioinfo/esn/domain/ProductVo.java | 22 + .../com/zioinfo/esn/domain/StoreGroupVo.java | 15 + .../java/com/zioinfo/esn/domain/StoreVo.java | 19 + .../com/zioinfo/esn/domain/TagBindingVo.java | 27 + .../com/zioinfo/esn/domain/TemplateVo.java | 19 + .../java/com/zioinfo/esn/domain/TenantVo.java | 14 + .../com/zioinfo/esn/domain/UpdateQueueVo.java | 31 + .../com/zioinfo/esn/domain/WorkHistoryVo.java | 20 + .../com/zioinfo/esn/mapper/AlarmMapper.java | 23 + .../zioinfo/esn/mapper/DashboardMapper.java | 10 + .../zioinfo/esn/mapper/FirmwareMapper.java | 19 + .../com/zioinfo/esn/mapper/HCoreMapper.java | 23 + .../com/zioinfo/esn/mapper/PosCvtMapper.java | 20 + .../com/zioinfo/esn/mapper/ProductMapper.java | 19 + .../zioinfo/esn/mapper/StoreGroupMapper.java | 15 + .../com/zioinfo/esn/mapper/StoreMapper.java | 20 + .../zioinfo/esn/mapper/TagBindingMapper.java | 25 + .../zioinfo/esn/mapper/TemplateMapper.java | 16 + .../com/zioinfo/esn/mapper/TenantMapper.java | 16 + .../zioinfo/esn/mapper/UpdateQueueMapper.java | 24 + .../com/zioinfo/esn/mapper/UserMapper.java | 18 + .../com/zioinfo/esn/mapper/WorkMapper.java | 20 + .../com/zioinfo/esn/service/AlarmService.java | 36 + .../zioinfo/esn/service/FirmwareService.java | 38 + .../com/zioinfo/esn/service/HCoreService.java | 36 + .../zioinfo/esn/service/PosCvtService.java | 51 + .../zioinfo/esn/service/ProductService.java | 32 + .../esn/service/StoreGroupService.java | 29 + .../com/zioinfo/esn/service/StoreService.java | 32 + .../esn/service/TagBindingService.java | 70 + .../zioinfo/esn/service/TemplateService.java | 32 + .../zioinfo/esn/service/TenantService.java | 29 + .../esn/service/UpdateQueueService.java | 60 + .../com/zioinfo/esn/service/UserService.java | 39 + .../com/zioinfo/esn/service/WorkService.java | 32 + backend/src/main/resources/application.yml | 48 + backend/src/main/resources/db/schema.sql | 333 ++ .../src/main/resources/mapper/AlarmMapper.xml | 86 + .../main/resources/mapper/DashboardMapper.xml | 46 + .../main/resources/mapper/FirmwareMapper.xml | 69 + .../src/main/resources/mapper/HCoreMapper.xml | 99 + .../main/resources/mapper/PosCvtMapper.xml | 75 + .../main/resources/mapper/ProductMapper.xml | 74 + .../resources/mapper/StoreGroupMapper.xml | 45 + .../src/main/resources/mapper/StoreMapper.xml | 84 + .../resources/mapper/TagBindingMapper.xml | 139 + .../main/resources/mapper/TemplateMapper.xml | 58 + .../main/resources/mapper/TenantMapper.xml | 47 + .../resources/mapper/UpdateQueueMapper.xml | 131 + .../main/resources/mapper/UserAuthMapper.xml | 30 + .../src/main/resources/mapper/UserMapper.xml | 61 + .../src/main/resources/mapper/WorkMapper.xml | 75 + frontend/index.html | 13 + frontend/package-lock.json | 3416 +++++++++++++++++ frontend/package.json | 30 + frontend/postcss.config.js | 6 + frontend/public/login.png | Bin 0 -> 32760 bytes frontend/src/App.tsx | 49 + frontend/src/api/client.ts | 105 + frontend/src/components/AlarmBadge.tsx | 16 + frontend/src/components/Header.tsx | 34 + frontend/src/components/Layout.tsx | 35 + frontend/src/components/Sidebar.tsx | 55 + frontend/src/components/StatCard.tsx | 34 + frontend/src/index.css | 13 + frontend/src/main.tsx | 10 + frontend/src/pages/AiAnalysis.tsx | 85 + frontend/src/pages/AlarmList.tsx | 105 + frontend/src/pages/Dashboard.tsx | 101 + frontend/src/pages/FirmwareList.tsx | 64 + frontend/src/pages/HCoreStatus.tsx | 94 + frontend/src/pages/Login.tsx | 68 + frontend/src/pages/PosCvtList.tsx | 94 + frontend/src/pages/ProductList.tsx | 69 + frontend/src/pages/StoreList.tsx | 138 + frontend/src/pages/TagBindingList.tsx | 193 + frontend/src/pages/TemplateList.tsx | 51 + frontend/src/pages/TenantAdmin.tsx | 52 + frontend/src/pages/UpdateQueueList.tsx | 154 + frontend/src/pages/UserList.tsx | 64 + frontend/src/pages/WorkHistory.tsx | 61 + frontend/tailwind.config.js | 19 + frontend/tsconfig.json | 21 + frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 18 + 125 files changed, 9608 insertions(+) create mode 100644 .claude/agents/esn-backend-dev.md create mode 100644 .claude/agents/esn-devops-dev.md create mode 100644 .claude/agents/esn-frontend-dev.md create mode 100644 .claude/agents/esn-qa.md create mode 100644 .claude/skills/guardia-esn-orchestrator/SKILL.md create mode 100644 CLAUDE.md create mode 100644 Jenkinsfile create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/zioinfo/esn/EsnApplication.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/AuthController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/AuthService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java create mode 100644 backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java create mode 100644 backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java create mode 100644 backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java create mode 100644 backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java create mode 100644 backend/src/main/java/com/zioinfo/esn/config/WebConfig.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/AiController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/ProductController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/StoreController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/TagBindingController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/TenantController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/UpdateQueueController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/UserController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/controller/WorkController.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/TagBindingVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/UpdateQueueVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/TagBindingMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/UpdateQueueMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/AlarmService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/HCoreService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/ProductService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/StoreService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/TagBindingService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/TemplateService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/TenantService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/UpdateQueueService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/UserService.java create mode 100644 backend/src/main/java/com/zioinfo/esn/service/WorkService.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/db/schema.sql create mode 100644 backend/src/main/resources/mapper/AlarmMapper.xml create mode 100644 backend/src/main/resources/mapper/DashboardMapper.xml create mode 100644 backend/src/main/resources/mapper/FirmwareMapper.xml create mode 100644 backend/src/main/resources/mapper/HCoreMapper.xml create mode 100644 backend/src/main/resources/mapper/PosCvtMapper.xml create mode 100644 backend/src/main/resources/mapper/ProductMapper.xml create mode 100644 backend/src/main/resources/mapper/StoreGroupMapper.xml create mode 100644 backend/src/main/resources/mapper/StoreMapper.xml create mode 100644 backend/src/main/resources/mapper/TagBindingMapper.xml create mode 100644 backend/src/main/resources/mapper/TemplateMapper.xml create mode 100644 backend/src/main/resources/mapper/TenantMapper.xml create mode 100644 backend/src/main/resources/mapper/UpdateQueueMapper.xml create mode 100644 backend/src/main/resources/mapper/UserAuthMapper.xml create mode 100644 backend/src/main/resources/mapper/UserMapper.xml create mode 100644 backend/src/main/resources/mapper/WorkMapper.xml create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/login.png create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/components/AlarmBadge.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/StatCard.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/AiAnalysis.tsx create mode 100644 frontend/src/pages/AlarmList.tsx create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/pages/FirmwareList.tsx create mode 100644 frontend/src/pages/HCoreStatus.tsx create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/PosCvtList.tsx create mode 100644 frontend/src/pages/ProductList.tsx create mode 100644 frontend/src/pages/StoreList.tsx create mode 100644 frontend/src/pages/TagBindingList.tsx create mode 100644 frontend/src/pages/TemplateList.tsx create mode 100644 frontend/src/pages/TenantAdmin.tsx create mode 100644 frontend/src/pages/UpdateQueueList.tsx create mode 100644 frontend/src/pages/UserList.tsx create mode 100644 frontend/src/pages/WorkHistory.tsx create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.claude/agents/esn-backend-dev.md b/.claude/agents/esn-backend-dev.md new file mode 100644 index 0000000..beccab8 --- /dev/null +++ b/.claude/agents/esn-backend-dev.md @@ -0,0 +1,54 @@ +--- +name: esn-backend-dev +description: GUARDiA ESN 백엔드 구현 에이전트. Spring Boot 3.5 Java 17 + MyBatis + PostgreSQL(esn_db) 패턴으로 ESL(전자 가격표) 통합 관리 도메인(테넌트·매장·템플릿·POS변환·알람·HCore·펌웨어·작업이력·상품·업데이트큐)을 com.zioinfo.esn 패키지에 구현한다. Ollama 온프레미스 AI(알람 원인 분석·POS 데이터 자동 분류)와 ITSM 연계 담당. +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN 백엔드 개발 에이전트 + +## 역할 +`C:\GUARDiA\workspace\guardia-esn\backend\` 에서 Spring Boot 3.5 + Java 17 + MyBatis + PostgreSQL ESN 플랫폼 백엔드를 개발·유지한다. + +## 기술 스택 +- Spring Boot 3.5 / Java 17 +- MyBatis 3.0.3 + PostgreSQL(esn_db/esn_user) +- JWT(jjwt 0.12.6) + Spring Security +- Ollama 온프레미스(폴백 필수) + ITSM 연계 + +## 불변 규칙 +1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 절대 금지 +2. Hikari `maximum-pool-size: 3` — 공유 PG 서버 +3. Ollama 호출 실패 시 fallback 응답 반환 (예외 던지지 않음) +4. API 응답에서 `passwordHash`, 자격증명 필드 완전 제외 +5. AES-256-GCM 자격증명 암호화 저장 +6. admin 시드: bcrypt `$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm` (admin/admin123) + +## 도메인 구조 +``` +com.zioinfo.esn/ +├── EsnApplication.java (@MapperScan(annotationClass=Mapper.class)) +├── auth/ (JwtUtil, JwtFilter, AuthController, AuthService, EsnUser) +├── common/ (ApiResponse) +├── config/ (SecurityConfig, WebConfig, CryptoUtil, OllamaClient) +├── domain/ (VO 클래스: TenantVo, StoreVo, TemplateVo, AlarmVo...) +├── mapper/ (@Mapper 인터페이스: TenantMapper, StoreMapper...) +├── service/ (TenantService, StoreService, AlarmService...) +└── controller/ (TenantController, StoreController, AlarmController...) +``` + +## 멀티테넌트 핵심 +모든 데이터는 `tenant_code VARCHAR(20)` 컬럼으로 격리: +- LGINNOTEK — LG 이노텍 +- LGIT — LG IT +- EMART — 이마트 +- ZIOINFO — 지오정보기술 (기본) + +## 엔드포인트 패턴 +- `GET /api/{도메인}s` — 목록 (tenantCode 파라미터) +- `POST /api/{도메인}s` — 생성 +- `PUT /api/{도메인}s/{id}` — 수정 +- `DELETE /api/{도메인}s/{id}` — 삭제 +- `GET /api/ai/alarm-analysis` — Ollama AI 알람 분석 +- `POST /api/ai/pos-classify` — POS 데이터 자동 분류 diff --git a/.claude/agents/esn-devops-dev.md b/.claude/agents/esn-devops-dev.md new file mode 100644 index 0000000..8a44df3 --- /dev/null +++ b/.claude/agents/esn-devops-dev.md @@ -0,0 +1,39 @@ +--- +name: esn-devops-dev +description: GUARDiA ESN 인프라·배포·CI/CD 에이전트. Gitea 저장소(zio/guardia-esn) 관리, deploy_server.py guardia-esn 블록(포트 8016) 추가, Jenkinsfile·setup 스크립트 작성, systemd 서비스 관리, DB 초기화, 홈페이지 솔루션 페이지 연동. +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN DevOps 에이전트 + +## 역할 +`C:\GUARDiA\workspace\guardia-esn\` 프로젝트의 배포·인프라·CI/CD 전체를 담당한다. + +## 서버 정보 +- 서버: `101.79.17.164` root / `1q2w3e!Q` +- Gitea: `http://127.0.0.1:9003` (내부), `https://git.zioinfo.co.kr` (외부) +- Gitea 계정: `zio` / `1q2w3e!Q` +- 포트: backend **8016**, frontend 빌드 → 단일 JAR + +## DB 정보 +- DB 이름: `guardia_esn_db` +- DB 사용자: `guardia_esn_user` +- DB 비밀번호: `guardia_esn_pass2026` +- 참고: `esn_db`(esn_user)는 기존 zioinfo-esn용 DB — 충돌 방지를 위해 별도 DB 사용 + +## 배포 절차 +1. repos에 `fresh git init` (모노레포 .git 상속 금지) +2. 프론트 빌드: `npm run build` → static/ 복사 +3. 백엔드 빌드: `mvn clean package -DskipTests` +4. JAR → `/opt/guardia-esn/app.jar` +5. systemd `guardia-esn.service` → `spring.sql.init.mode=never` +6. Gitea 저장소 생성 → push + +## 함정 목록 +- repos에 fresh `git init` 필수 (모노레포 bundle 1.5GB 방지) +- deploy_server.py 수정 후 zioinfo-deploy 서비스 재시작 필수 +- Gitea webhook secret: `zioinfo-deploy-2026` +- HikariCP `maximum-pool-size=3` 반드시 유지 +- DB는 앱 사용자 소유로 생성 후 schema.sql 적용 diff --git a/.claude/agents/esn-frontend-dev.md b/.claude/agents/esn-frontend-dev.md new file mode 100644 index 0000000..6c855d3 --- /dev/null +++ b/.claude/agents/esn-frontend-dev.md @@ -0,0 +1,42 @@ +--- +name: esn-frontend-dev +description: GUARDiA ESN 프론트엔드 구현 에이전트. React 19 + Vite + TypeScript + Tailwind CSS로 ESL 통합 관리자 웹(대시보드·테넌트·매장·템플릿·POS변환·알람·HCore·펌웨어·작업이력·상품·업데이트큐·AI분석·사용자관리)을 구현한다. +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN 프론트엔드 개발 에이전트 + +## 역할 +`C:\GUARDiA\workspace\guardia-esn\frontend\` 에서 React 19 + Vite + TypeScript + Tailwind CSS ESN 관리자 웹을 개발·유지한다. + +## 기술 스택 +- React 19 + Vite + TypeScript +- Tailwind CSS (다크 테마) +- Recharts (통계 차트) +- React Router v6 + +## 페이지 목록 (src/pages/) +| 파일 | 경로 | 설명 | +|------|------|------| +| Dashboard.tsx | / | 전체 현황 차트 | +| TenantAdmin.tsx | /tenants | 테넌트 관리 | +| StoreList.tsx | /stores | 매장 관리 | +| TemplateList.tsx | /templates | ESL 템플릿 | +| PosCvtList.tsx | /pos-cvt | POS 변환 데이터 | +| ProductList.tsx | /products | 상품/가격 | +| AlarmList.tsx | /alarms | 알람 모니터링 | +| HCoreStatus.tsx | /hcore | HCore 게이트웨이 | +| FirmwareList.tsx | /firmware | 펌웨어 관리 | +| WorkHistory.tsx | /work | 작업 이력 | +| TagBindingList.tsx | /tags | 태그 바인딩 | +| UpdateQueueList.tsx | /queue | 업데이트 큐 | +| UserList.tsx | /users | 사용자 관리 | +| AiAnalysis.tsx | /ai | AI 분석 | +| Login.tsx | /login | 로그인 | + +## 빌드 원칙 +- Vite build → `backend/src/main/resources/static/` 복사 → `mvn package` (단일 JAR) +- `vite.config.ts` outDir: `../backend/src/main/resources/static` +- API 호출: `src/api/client.ts` (axios 기반, JWT Bearer 헤더) diff --git a/.claude/agents/esn-qa.md b/.claude/agents/esn-qa.md new file mode 100644 index 0000000..fce69ab --- /dev/null +++ b/.claude/agents/esn-qa.md @@ -0,0 +1,29 @@ +--- +name: esn-qa +description: GUARDiA ESN QA 에이전트. 백엔드 API 응답 shape와 프론트엔드 호출을 대조 검증한다. 테넌트 격리, 알람 이력, HCore 상태, 펌웨어 배포 경계면의 필드 불일치·인증 흐름·민감정보(SSH·자격증명) 노출·RBAC 가드 누락·AI 온프레미스 준수를 검증한다. +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN QA 에이전트 + +## 검증 항목 +1. **테넌트 격리** — tenant_code 필터 누락 시 크로스 테넌트 데이터 노출 여부 +2. **인증 흐름** — JWT 만료·무효 토큰 시 401 반환 확인 +3. **민감정보 제외** — API 응답에 `passwordHash`, SSH 자격증명 필드 미포함 +4. **AI 온프레미스** — Ollama 외 외부 AI API 호출 없음 +5. **알람 통계** — Dashboard 집계 쿼리 정합성 +6. **HCore 상태** — 연결/비연결 상태 전이 정확성 + +## 테스트 방법 +```bash +# 서버에서 ESN API 테스트 +curl -X POST http://localhost:8016/api/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"admin123"}' + +# 테넌트 격리 검증 +curl -H 'Authorization: Bearer {token}' \ + 'http://localhost:8016/api/stores?tenantCode=LGINNOTEK' +``` diff --git a/.claude/skills/guardia-esn-orchestrator/SKILL.md b/.claude/skills/guardia-esn-orchestrator/SKILL.md new file mode 100644 index 0000000..746a736 --- /dev/null +++ b/.claude/skills/guardia-esn-orchestrator/SKILL.md @@ -0,0 +1,118 @@ +# guardia-esn-orchestrator + +## 역할 +레거시 ESL(전자 가격표) 관리 시스템(`workspace/esn/` 6개 프로젝트 — Spring Boot 1.5 + Java 8 + Thymeleaf)을 Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL 단일 플랫폼으로 통합한 `workspace/guardia-esn/` 프로젝트를 운영·확장하는 오케스트레이터 스킬. + +## 트리거 +"guardia-esn", "ESN 통합", "ESL 플랫폼", "전자가격표", "HCore", "POS 변환", "PosCvt", "ESN 알람", "ESL 템플릿", "업데이트 큐", "다시 실행", "업데이트" 요청 시 이 스킬을 사용하라. + +## 프로젝트 정보 +- **위치:** `C:\GUARDiA\workspace\guardia-esn\` +- **포트:** backend:8016 (단일 JAR, Vite 번들) +- **DB:** `guardia_esn_db` / `guardia_esn_user` / `guardia_esn_pass2026` +- **패키지:** `com.zioinfo.esn` +- **멀티테넌트:** `tenant_code VARCHAR(20)` — LGINNOTEK / LGIT / EMART / ZIOINFO +- **Gitea:** `http://git.zioinfo.co.kr/zio/guardia-esn` +- **서버:** `101.79.17.164` `/opt/guardia-esn/` + +## 레거시 소스 (C:\GUARDiA\workspace\esn\) +| 레거시 프로젝트 | 용도 | 현대화 대상 | +|----------------|------|------------| +| ESN_WEB_ZIOINFO | ZIOINFO ESL 웹 관리 (Thymeleaf) | → React 19 관리자 웹 | +| ESN_WEB_LGInnotek | LG Innotek ESL 웹 (Thymeleaf) | → 멀티테넌트 LGINNOTEK | +| ESN_DAEMON_EMART | 이마트 ESL 데몬 | → EMART 테넌트 | +| ESN_DAEMON_LGIT | LG IT ESL 데몬 | → LGIT 테넌트 | +| esn | ESN 코어 (Maven) | → 도메인 모델 | +| ESL_AUTO_UPDATE | 펌웨어 자동 업데이트 | → firmware 도메인 | +| restapi_lgit | LGIT REST API | → API 통합 | +| UCS_LGU | UCS 시스템 | → HCore 통합 | + +## 아키텍처 +``` +레거시 ESL 8개 프로젝트 (Spring Boot 1.5 + Java 8 + Thymeleaf) + ↓ 현대화 +guardia-esn (Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL) + backend:8016 ←→ frontend static bundle (단일 JAR) + ↕ + ITSM:9001 · Ollama:11434 +``` + +## 도메인 개념 +| 도메인 | 테이블 | 설명 | +|--------|--------|------| +| Tenant | esn_tenants | 멀티테넌트 — LGINNOTEK·LGIT·EMART·ZIOINFO | +| StoreGroup | esn_store_groups | 매장 그룹 | +| Store | esn_stores | 매장 (tenantCode 격리) | +| Template | esn_templates | ESL 표시 템플릿 | +| PosCvt | esn_pos_cvt | POS→ESL 가격 변환 데이터 | +| Alarm / AlarmHistory | esn_alarms | 장치 알람·이력 | +| HCore | esn_hcores | 게이트웨이/허브 장치 | +| WorkHistory | esn_work_history | 작업 이력 | +| Firmware | esn_firmware | 장치 펌웨어 | +| User | esn_users | 사용자 (멀티테넌트 RBAC) | +| Product | esn_products | 상품 가격 | +| TagBinding | esn_tag_bindings | ESL 태그↔상품 바인딩 | +| UpdateQueue | esn_update_queue | 업데이트 큐 | + +## 에이전트 팀 +| 에이전트 | 역할 | +|----------|------| +| `esn-backend-dev` | Spring Boot 3.5 백엔드 — 도메인·매퍼·서비스·컨트롤러 | +| `esn-frontend-dev` | React 19 + Vite + TypeScript — 관리자 웹 | +| `esn-devops-dev` | Gitea·deploy·Jenkinsfile·setup·홈페이지 | +| `esn-qa` | API 통합 테스트 + 크로스 경계면 검증 | + +## 구현 원칙 (GUARDiA 불변 규칙) +1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 아님 +2. Hikari `maximum-pool-size=3` — 공유 PG 서버 +3. repos에 `fresh git init` — 모노레포 .git 상속 금지 +4. 단일 JAR: 프론트 빌드 → `backend/src/main/resources/static/` → mvn package +5. Ollama 폴백: 연결 실패 시 기본 응답 반환 (예외 던지지 않음) +6. admin 시드: bcrypt `$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm` (admin/admin123) +7. API 응답에 `password_hash`, 자격증명 필드 절대 미포함 +8. AES-256-GCM 자격증명 암호화 저장 +9. SecurityConfig에 static 리소스 `permitAll` 필수 (SPA 403 방지) + +## 파일 구조 +``` +workspace/guardia-esn/ +├── .claude/ +│ ├── agents/ +│ │ ├── esn-backend-dev.md +│ │ ├── esn-frontend-dev.md +│ │ ├── esn-devops-dev.md +│ │ └── esn-qa.md +│ └── skills/guardia-esn-orchestrator/SKILL.md +├── CLAUDE.md +├── Jenkinsfile +├── backend/ +│ ├── pom.xml +│ └── src/main/ +│ ├── java/com/zioinfo/esn/ +│ │ ├── EsnApplication.java (@MapperScan(annotationClass=Mapper.class)) +│ │ ├── auth/ +│ │ ├── common/ +│ │ ├── config/ (SecurityConfig, WebConfig, CryptoUtil, OllamaClient) +│ │ ├── domain/ (VO 클래스) +│ │ ├── mapper/ (@Mapper 인터페이스) +│ │ ├── service/ +│ │ └── controller/ +│ └── resources/ +│ ├── application.yml +│ ├── mapper/*.xml +│ ├── db/schema.sql +│ └── static/ (Vite 빌드 산출물) +└── frontend/ + ├── package.json + ├── vite.config.ts + └── src/ + ├── App.tsx + ├── api/client.ts + ├── components/ + └── pages/ (15개 관리자 화면) +``` + +## 변경 이력 +| 날짜 | 내용 | 사유 | +|------|------|------| +| 2026-06-16 | 초기 하네스 구성 + 통합 소스 생성 | 레거시 ESN 8개 프로젝트 → 단일 현대화 플랫폼 | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..de411e5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# GUARDiA ESN — ESL 통합 관리 플랫폼 + +> 레거시 ESL(전자 가격표) 8개 Spring Boot 1.5 프로젝트를 단일 현대화 플랫폼으로 통합한 프로젝트 + +--- + +## 프로젝트 개요 + +`C:\GUARDiA\workspace\esn\` 의 레거시 ESN 시스템(ESN_WEB_ZIOINFO·ESN_WEB_LGInnotek·ESN_DAEMON_EMART·ESN_DAEMON_LGIT·esn·ESL_AUTO_UPDATE·restapi_lgit·UCS_LGU)을 +**Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL** 단일 플랫폼으로 재구현. + +멀티테넌트 (TENANT_A / TENANT_B / EMART / ZIOINFO), Ollama 온프레미스 AI 알람 분석·POS 자동 분류. + +--- + +## 기술 스택 + +| 레이어 | 기술 | +|--------|------| +| Backend | Spring Boot 3.5 · Java 17 · MyBatis 3.0.3 | +| Database | PostgreSQL (`guardia_esn_db` / `guardia_esn_user`) | +| Frontend | React 19 · Vite · TypeScript · Tailwind CSS | +| AI | Ollama 온프레미스 (llama3, llava) | +| 인증 | Spring Security · JWT (jjwt 0.12.6) | +| 배포 | 단일 JAR (Vite 번들 → static/) | + +--- + +## 실행 명령어 + +```bash +# 프론트엔드 빌드 (필수 — 백엔드 static/ 번들) +cd frontend && npm install && npm run build + +# 백엔드 빌드 + 실행 +cd backend && mvn clean package -DskipTests +java -jar target/zioinfo-esn-1.0.0.jar \ + --spring.datasource.url=jdbc:postgresql://localhost:5432/guardia_esn_db \ + --spring.datasource.username=guardia_esn_user \ + --spring.datasource.password=guardia_esn_pass2026 \ + --server.port=8016 +``` + +--- + +## 환경변수 + +| 변수 | 기본값 | 설명 | +|------|--------|------| +| DB_URL | jdbc:postgresql://localhost:5432/esn_db | 운영에서는 guardia_esn_db | +| DB_USER | esn_user | 운영에서는 guardia_esn_user | +| DB_PASS | esn_pass2026 | 운영에서는 guardia_esn_pass2026 | +| OLLAMA_URL | http://localhost:11434 | Ollama 서버 | +| JWT_SECRET | ZioInfoEsnJwtSecret... | JWT 서명 키 | + +--- + +## 디렉터리 구조 + +``` +workspace/guardia-esn/ +├── .claude/ +│ ├── agents/ (에이전트 4종) +│ └── skills/guardia-esn-orchestrator/SKILL.md +├── CLAUDE.md ← 이 파일 +├── Jenkinsfile +├── backend/ +│ ├── pom.xml +│ └── src/main/ +│ ├── java/com/zioinfo/esn/ (67개 Java 파일) +│ └── resources/ +│ ├── application.yml +│ ├── db/schema.sql (13개 테이블) +│ └── mapper/*.xml (15개 MyBatis XML) +└── frontend/ + └── src/ + ├── App.tsx + ├── api/client.ts + ├── components/ (Layout, Sidebar, Header...) + └── pages/ (15개 관리자 화면) +``` + +--- + +## 도메인 (13개) + +| 도메인 | 테이블 | 설명 | +|--------|--------|------| +| Tenant | esn_tenants | 멀티테넌트 (LGINNOTEK·LGIT·EMART·ZIOINFO) | +| StoreGroup | esn_store_groups | 매장 그룹 | +| Store | esn_stores | 매장 (tenantCode 격리) | +| Template | esn_templates | ESL 표시 템플릿 | +| PosCvt | esn_pos_cvt | POS→ESL 가격 변환 | +| Alarm | esn_alarms | 장치 알람 | +| HCore | esn_hcores | 게이트웨이/허브 | +| WorkHistory | esn_work_history | 작업 이력 | +| Firmware | esn_firmware | 펌웨어 버전 | +| User | esn_users | 사용자 (RBAC) | +| Product | esn_products | 상품/가격 | +| TagBinding | esn_tag_bindings | ESL↔상품 바인딩 | +| UpdateQueue | esn_update_queue | 업데이트 큐 | + +--- + +## 보안 제약 (불변) + +1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 절대 금지 +2. Hikari `maximum-pool-size=3` — 공유 PG 서버 보호 +3. 외부 AI API 절대 금지 — Ollama localhost:11434만 허용 +4. API 응답에서 `passwordHash`, `ssh_*` 필드 완전 제외 +5. SecurityConfig에 static 리소스 `permitAll` 필수 (SPA 403 방지) + +--- + +## 하네스 트리거 + +`guardia-esn-orchestrator` 스킬을 사용하라: +- ESN 기능 추가·수정 +- 배포 작업 +- 레거시 소스 분석·통합 +- 알람·POS·HCore 관련 작업 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..578dfb9 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,65 @@ +pipeline { + agent any + + environment { + APP_NAME = 'guardia-esn' + DEPLOY_DIR = '/opt/guardia-esn' + SERVICE = 'guardia-esn' + JAR_NAME = 'zioinfo-esn-1.0.0.jar' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Build Frontend') { + steps { + dir('frontend') { + sh 'npm ci --legacy-peer-deps' + sh 'npm run build' + } + } + } + + stage('Build Backend') { + steps { + dir('backend') { + sh 'mvn clean package -DskipTests -q' + } + } + } + + stage('Deploy') { + steps { + sh """ + sudo systemctl stop ${SERVICE} || true + sudo mkdir -p ${DEPLOY_DIR} + sudo cp backend/target/${JAR_NAME} ${DEPLOY_DIR}/app.jar + sudo systemctl start ${SERVICE} + """ + } + } + + stage('Health Check') { + steps { + sh """ + sleep 20 + curl -sf http://localhost:8016/actuator/health | grep -q UP || exit 1 + """ + } + } + } + + post { + success { + echo "guardia-esn 배포 성공" + } + failure { + sh "sudo systemctl stop ${SERVICE} || true" + echo "guardia-esn 배포 실패" + } + } +} diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..1c2fd89 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.zioinfo + zioinfo-esn + 1.0.0 + zioinfo-esn + ESL(전자 가격표) 통합 관리 플랫폼 — 레거시 Spring Boot 1.5 현대화 (Spring Boot 3.5 + Java 17 + React 19) + + + 17 + 0.12.6 + 3.0.3 + 42.7.3 + + + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-validation + org.springframework.bootspring-boot-starter-actuator + org.springframework.bootspring-boot-starter-aop + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.version} + + + + + org.postgresql + postgresql + ${postgresql.version} + + + + io.jsonwebtokenjjwt-api${jjwt.version} + io.jsonwebtokenjjwt-impl${jjwt.version}runtime + io.jsonwebtokenjjwt-jackson${jjwt.version}runtime + + + org.projectlomboklomboktrue + + + org.springframework.bootspring-boot-starter-testtest + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + esn-1.0.0 + + diff --git a/backend/src/main/java/com/zioinfo/esn/EsnApplication.java b/backend/src/main/java/com/zioinfo/esn/EsnApplication.java new file mode 100644 index 0000000..4855edf --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/EsnApplication.java @@ -0,0 +1,30 @@ +package com.zioinfo.esn; + +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * zioinfo-esn ESL 통합 관리 플랫폼. + * + *

레거시 Spring Boot 1.5 / Java 8 ESL 시스템 6개를 Spring Boot 3.5 / Java 17 단일 플랫폼으로 재구현. + * 멀티테넌트(LGINNOTEK·LGIT·EMART·ZIOINFO), 매장/HCore 장치 관리, POS 가격 연동, + * 알람 모니터링, 펌웨어 관리, Ollama 온프레미스 AI 이상 감지. + * + *

보안 불변 규칙: 외부 AI API 절대 금지 (Ollama localhost:11434만 허용), + * 자격증명은 AES-256-GCM 암호화 저장, API 응답에서 password_hash 완전 제외. + * + *

CRITICAL: @MapperScan(annotationClass = Mapper.class) 패턴 사용 — basePackages 아님. + */ +@SpringBootApplication +@MapperScan(annotationClass = Mapper.class) +@EnableScheduling +@EnableAsync +public class EsnApplication { + public static void main(String[] args) { + SpringApplication.run(EsnApplication.class, args); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java new file mode 100644 index 0000000..957e896 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java @@ -0,0 +1,35 @@ +package com.zioinfo.esn.auth; + +import com.zioinfo.esn.common.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ApiResponse> login(@RequestBody LoginRequest req) { + String token = authService.login(req.username(), req.password()); + return ApiResponse.ok(Map.of("token", token, "type", "Bearer")); + } + + @GetMapping("/me") + public ApiResponse> me(@RequestHeader("Authorization") String header) { + String token = header.replace("Bearer ", ""); + return ApiResponse.ok(authService.me(token)); + } + + @PostMapping("/logout") + public ApiResponse logout() { + // JWT stateless — 클라이언트에서 토큰 삭제 + return ApiResponse.ok("로그아웃 성공", null); + } + + record LoginRequest(String username, String password) {} +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java new file mode 100644 index 0000000..58a69bb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java @@ -0,0 +1,37 @@ +package com.zioinfo.esn.auth; + +import com.zioinfo.esn.auth.mapper.UserAuthMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserAuthMapper userMapper; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + public String login(String username, String password) { + EsnUser user = userMapper.findByUsername(username); + if (user == null || !user.isActive()) { + throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정"); + } + if (!passwordEncoder.matches(password, user.getPasswordHash())) { + throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치"); + } + userMapper.updateLastLogin(username); + return jwtUtil.generate(username, user.getRole(), user.getTenantCode()); + } + + public Map me(String token) { + return Map.of( + "username", jwtUtil.getUsername(token), + "role", jwtUtil.getRole(token), + "tenant", jwtUtil.getTenant(token) != null ? jwtUtil.getTenant(token) : "" + ); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java b/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java new file mode 100644 index 0000000..dc6b130 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java @@ -0,0 +1,21 @@ +package com.zioinfo.esn.auth; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class EsnUser { + private Long id; + private String tenantCode; + private String username; + @JsonIgnore + private String passwordHash; // API 응답 미노출 + private String role; // ADMIN, MANAGER, USER + private String email; + private String phone; + private boolean active; + private LocalDateTime lastLoginAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java new file mode 100644 index 0000000..d8c39ee --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java @@ -0,0 +1,40 @@ +package com.zioinfo.esn.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, + FilterChain chain) throws ServletException, IOException { + String header = req.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + if (jwtUtil.isValid(token)) { + String username = jwtUtil.getUsername(token); + String role = jwtUtil.getRole(token); + var auth = new UsernamePasswordAuthenticationToken( + username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role)) + ); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + chain.doFilter(req, res); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java new file mode 100644 index 0000000..06d23f0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java @@ -0,0 +1,56 @@ +package com.zioinfo.esn.auth; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Slf4j +@Component +public class JwtUtil { + + @Value("${guardia.jwt.secret:ZioInfoEsnJwtSecret2026VeryLongSecretKeyForHS256AlgorithmAtLeast256Bits}") + private String secret; + + @Value("${guardia.jwt.expiration:86400000}") + private long expirationMs; + + private SecretKey key() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generate(String username, String role, String tenantCode) { + return Jwts.builder() + .subject(username) + .claim("role", role) + .claim("tenant", tenantCode) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(key()) + .compact(); + } + + public Claims parse(String token) { + return Jwts.parser().verifyWith(key()).build() + .parseSignedClaims(token).getPayload(); + } + + public boolean isValid(String token) { + try { + parse(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.debug("JWT 검증 실패: {}", e.getMessage()); + return false; + } + } + + public String getUsername(String token) { return parse(token).getSubject(); } + public String getRole(String token) { return parse(token).get("role", String.class); } + public String getTenant(String token) { return parse(token).get("tenant", String.class); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java b/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java new file mode 100644 index 0000000..f834e7d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java @@ -0,0 +1,11 @@ +package com.zioinfo.esn.auth.mapper; + +import com.zioinfo.esn.auth.EsnUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserAuthMapper { + EsnUser findByUsername(@Param("username") String username); + int updateLastLogin(@Param("username") String username); +} diff --git a/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java b/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java new file mode 100644 index 0000000..c4a45d2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java @@ -0,0 +1,27 @@ +package com.zioinfo.esn.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ApiResponse { + private boolean success; + private String message; + private T data; + private LocalDateTime timestamp; + + public static ApiResponse ok(T data) { + return new ApiResponse<>(true, "OK", data, LocalDateTime.now()); + } + + public static ApiResponse ok(String message, T data) { + return new ApiResponse<>(true, message, data, LocalDateTime.now()); + } + + public static ApiResponse fail(String message) { + return new ApiResponse<>(false, message, null, LocalDateTime.now()); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java b/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java new file mode 100644 index 0000000..615ba21 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java @@ -0,0 +1,68 @@ +package com.zioinfo.esn.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * AES-256-GCM 자격증명 암호화 유틸리티. + * 보안 불변 규칙: 서버 자격증명은 암호화 DB에만 저장, API 응답에 절대 노출 금지. + */ +@Slf4j +@Component +public class CryptoUtil { + + private static final String ALGO = "AES/GCM/NoPadding"; + private static final int GCM_IV_LEN = 12; + private static final int GCM_TAG_LEN = 128; + // 운영 시 환경변수로 주입 권장 + private static final String KEY_HEX = "7a696f696e666f65736e6b657932303236736563726574"; + + private SecretKey secretKey() { + byte[] key = new byte[32]; + byte[] src = KEY_HEX.getBytes(StandardCharsets.UTF_8); + System.arraycopy(src, 0, key, 0, Math.min(src.length, 32)); + return new SecretKeySpec(key, "AES"); + } + + public String encrypt(String plain) { + if (plain == null) return null; + try { + byte[] iv = new byte[GCM_IV_LEN]; + new SecureRandom().nextBytes(iv); + Cipher cipher = Cipher.getInstance(ALGO); + cipher.init(Cipher.ENCRYPT_MODE, secretKey(), new GCMParameterSpec(GCM_TAG_LEN, iv)); + byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8)); + byte[] result = new byte[iv.length + enc.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(enc, 0, result, iv.length, enc.length); + return Base64.getEncoder().encodeToString(result); + } catch (Exception e) { + log.error("암호화 실패", e); + throw new RuntimeException("CRYPTO_ERR", e); + } + } + + public String decrypt(String encrypted) { + if (encrypted == null) return null; + try { + byte[] raw = Base64.getDecoder().decode(encrypted); + byte[] iv = new byte[GCM_IV_LEN]; + System.arraycopy(raw, 0, iv, 0, GCM_IV_LEN); + Cipher cipher = Cipher.getInstance(ALGO); + cipher.init(Cipher.DECRYPT_MODE, secretKey(), new GCMParameterSpec(GCM_TAG_LEN, iv)); + byte[] dec = cipher.doFinal(raw, GCM_IV_LEN, raw.length - GCM_IV_LEN); + return new String(dec, StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("복호화 실패", e); + throw new RuntimeException("CRYPTO_ERR", e); + } + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java new file mode 100644 index 0000000..a907d38 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java @@ -0,0 +1,77 @@ +package com.zioinfo.esn.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; + +/** + * Ollama 온프레미스 AI 클라이언트. + * 보안 불변 규칙: 외부 AI API 절대 금지 — Ollama localhost:11434만 허용. + * 연결 실패 시 폴백 응답 반환(예외 미전파). + */ +@Slf4j +@Component +public class OllamaClient { + + @Value("${guardia.ollama-url:http://localhost:11434}") + private String baseUrl; + + @Value("${guardia.ollama-text-model:llama3}") + private String model; + + private final HttpClient http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .build(); + private final ObjectMapper mapper = new ObjectMapper(); + + public String chat(String prompt) { + return chat(prompt, "ESN AI 이상 분석 전문가 역할입니다."); + } + + public String chat(String prompt, String systemPrompt) { + try { + var body = Map.of( + "model", model, + "messages", new Object[]{ + Map.of("role", "system", "content", systemPrompt), + Map.of("role", "user", "content", prompt) + }, + "stream", false + ); + String json = mapper.writeValueAsString(body); + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/chat")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .timeout(Duration.ofSeconds(30)) + .build(); + HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() == 200) { + var res = mapper.readTree(resp.body()); + return res.path("message").path("content").asText("분석 결과를 생성했습니다."); + } + return fallback(prompt); + } catch (Exception e) { + log.warn("Ollama 연결 실패 — 폴백 응답 반환: {}", e.getMessage()); + return fallback(prompt); + } + } + + private String fallback(String prompt) { + if (prompt.toLowerCase().contains("alarm") || prompt.contains("알람")) { + return "알람 분석: 장치 연결 상태를 확인하고 네트워크 환경을 점검하세요. 지속 발생 시 현장 엔지니어 파견이 필요합니다."; + } + if (prompt.toLowerCase().contains("pos") || prompt.contains("가격")) { + return "POS 데이터 분류: 정상 가격 변환 데이터입니다. 이상 감지된 항목은 수동 검토가 필요합니다."; + } + return "AI 분석 결과: 현재 시스템 상태를 검토하고 운영 매뉴얼을 참조하세요."; + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java new file mode 100644 index 0000000..df39263 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java @@ -0,0 +1,67 @@ +package com.zioinfo.esn.config; + +import com.zioinfo.esn.auth.JwtFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Spring Security 6 — JWT 무상태 인증 + 멀티테넌트 RBAC. + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtFilter jwtFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> {}) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // 인증 불필요 + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/actuator/health").permitAll() + // 정적 리소스 (React SPA) + .requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll() + + // 관리자 전용 + .requestMatchers("/api/tenants/**").hasRole("ADMIN") + .requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER") + .requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER") + .requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MANAGER") + .requestMatchers(HttpMethod.POST, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER") + .requestMatchers(HttpMethod.PUT, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER") + + // 그 외 GET은 인증 사용자 허용 + .anyRequest().authenticated() + ) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java b/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java new file mode 100644 index 0000000..0a843e5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java @@ -0,0 +1,23 @@ +package com.zioinfo.esn.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class WebConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + config.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/AiController.java b/backend/src/main/java/com/zioinfo/esn/controller/AiController.java new file mode 100644 index 0000000..ba727e9 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/AiController.java @@ -0,0 +1,73 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.config.OllamaClient; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +@RestController +@RequestMapping("/api/ai") +@RequiredArgsConstructor +public class AiController { + + private final OllamaClient ollama; + + /** + * 알람 AI 이상 분석 — Ollama 온프레미스만 허용. + */ + @PostMapping("/analyze-alarm") + public ApiResponse> analyzeAlarm(@RequestBody Map body) { + String alarmType = (String) body.getOrDefault("alarmType", "UNKNOWN"); + String message = (String) body.getOrDefault("message", ""); + String severity = (String) body.getOrDefault("severity", "LOW"); + + String prompt = String.format( + "ESL 장치 알람 분석:\n유형: %s\n심각도: %s\n메시지: %s\n\n" + + "원인 분석 및 조치 방안을 3줄 이내로 간결하게 제시하세요.", + alarmType, severity, message + ); + + String result = ollama.chat(prompt, + "당신은 ESL(전자 가격표) 시스템 전문 AI 엔지니어입니다. 알람을 분석하고 실용적인 조치를 제안하세요."); + + return ApiResponse.ok(Map.of("analysis", result, "alarmType", alarmType, "severity", severity)); + } + + /** + * POS 데이터 AI 분류 — 이상 데이터 자동 감지. + */ + @PostMapping("/classify-pos") + public ApiResponse> classifyPos(@RequestBody Map body) { + String productCode = (String) body.getOrDefault("productCode", ""); + String price = String.valueOf(body.getOrDefault("price", "0")); + String salePrice = String.valueOf(body.getOrDefault("salePrice", "0")); + + String prompt = String.format( + "POS 가격 데이터 검증:\n상품코드: %s\n정가: %s\n판매가: %s\n\n" + + "이 데이터가 정상인지 이상(오류/이상값)인지 판단하고, 분류(NORMAL/ABNORMAL)와 이유를 2줄 이내로 제시하세요.", + productCode, price, salePrice + ); + + String result = ollama.chat(prompt, + "당신은 POS 데이터 품질 관리 AI입니다. 가격 데이터의 이상을 감지하세요."); + + String classification = result.toUpperCase().contains("ABNORMAL") ? "ABNORMAL" : "NORMAL"; + + return ApiResponse.ok(Map.of( + "classification", classification, + "analysis", result, + "productCode", productCode + )); + } + + /** + * 일반 AI 채팅 — ESN 운영 관련 질문. + */ + @PostMapping("/chat") + public ApiResponse> chat(@RequestBody Map body) { + String message = body.getOrDefault("message", ""); + String result = ollama.chat(message, "ESL 전자 가격표 통합 관리 플랫폼 운영 전문가입니다."); + return ApiResponse.ok(Map.of("response", result)); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java b/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java new file mode 100644 index 0000000..390ecfa --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java @@ -0,0 +1,56 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.AlarmVo; +import com.zioinfo.esn.service.AlarmService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/alarms") +@RequiredArgsConstructor +public class AlarmController { + private final AlarmService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String severity, + @RequestParam(required = false) String status) { + return ApiResponse.ok(service.list(tenantCode, storeId, severity, status)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody AlarmVo v) { + return ApiResponse.ok("알람 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody AlarmVo v) { + return ApiResponse.ok("알람 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/resolve") + public ApiResponse resolve(@PathVariable Long id, + @RequestBody Map body, + Authentication auth) { + String resolvedBy = auth != null ? auth.getName() : "system"; + service.resolve(id, resolvedBy, body.getOrDefault("resolution", "")); + return ApiResponse.ok("알람 해결 완료", null); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("알람 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java b/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java new file mode 100644 index 0000000..f611990 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.DashboardVo; +import com.zioinfo.esn.mapper.DashboardMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/dashboard") +@RequiredArgsConstructor +public class DashboardController { + private final DashboardMapper mapper; + + @GetMapping + public ApiResponse summary(@RequestParam(required = false) String tenantCode) { + return ApiResponse.ok(mapper.getSummary(tenantCode)); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java new file mode 100644 index 0000000..f5a0944 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java @@ -0,0 +1,43 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.FirmwareVo; +import com.zioinfo.esn.service.FirmwareService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/firmware") +@RequiredArgsConstructor +public class FirmwareController { + private final FirmwareService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String deviceType) { + return ApiResponse.ok(service.list(tenantCode, deviceType)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody FirmwareVo v) { + return ApiResponse.ok("펌웨어 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody FirmwareVo v) { + return ApiResponse.ok("펌웨어 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("펌웨어 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java b/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java new file mode 100644 index 0000000..fb848f2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java @@ -0,0 +1,52 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.HCoreVo; +import com.zioinfo.esn.service.HCoreService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/hcore") +@RequiredArgsConstructor +public class HCoreController { + private final HCoreService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String deviceType, + @RequestParam(required = false) String status) { + return ApiResponse.ok(service.list(tenantCode, storeId, deviceType, status)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody HCoreVo v) { + return ApiResponse.ok("장치 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody HCoreVo v) { + return ApiResponse.ok("장치 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/status") + public ApiResponse updateStatus(@PathVariable Long id, @RequestBody Map body) { + service.updateStatus(id, body.getOrDefault("status", "OFFLINE")); + return ApiResponse.ok("상태 업데이트 완료", null); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("장치 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java new file mode 100644 index 0000000..89c3b21 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java @@ -0,0 +1,48 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.PosCvtVo; +import com.zioinfo.esn.service.PosCvtService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/pos-cvt") +@RequiredArgsConstructor +public class PosCvtController { + private final PosCvtService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String keyword) { + return ApiResponse.ok(service.list(tenantCode, storeId, status, keyword)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PutMapping("/{id}/process") + public ApiResponse process(@PathVariable Long id) { + int queued = service.process(id); + return ApiResponse.ok("처리 완료 (태그 큐 " + queued + "건)", queued); + } + + @PutMapping("/{id}/ignore") + public ApiResponse ignore(@PathVariable Long id) { + service.ignore(id); + return ApiResponse.ok("무시 처리 완료", null); + } + + @PutMapping("/{id}/error") + public ApiResponse error(@PathVariable Long id, @RequestBody Map body) { + service.markError(id, body.getOrDefault("message", "처리 오류")); + return ApiResponse.ok("오류 처리 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java b/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java new file mode 100644 index 0000000..cf1f2c3 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java @@ -0,0 +1,44 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.ProductVo; +import com.zioinfo.esn.service.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/products") +@RequiredArgsConstructor +public class ProductController { + private final ProductService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String keyword) { + return ApiResponse.ok(service.list(tenantCode, storeId, keyword)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody ProductVo v) { + return ApiResponse.ok("상품 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody ProductVo v) { + return ApiResponse.ok("상품 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("상품 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java b/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java new file mode 100644 index 0000000..f0e549d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java @@ -0,0 +1,44 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.StoreVo; +import com.zioinfo.esn.service.StoreService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/stores") +@RequiredArgsConstructor +public class StoreController { + private final StoreService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeGroupId, + @RequestParam(required = false) String keyword) { + return ApiResponse.ok(service.list(tenantCode, storeGroupId, keyword)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody StoreVo v) { + return ApiResponse.ok("매장 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody StoreVo v) { + return ApiResponse.ok("매장 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("매장 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java b/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java new file mode 100644 index 0000000..31a7ff0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java @@ -0,0 +1,41 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.StoreGroupVo; +import com.zioinfo.esn.service.StoreGroupService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/store-groups") +@RequiredArgsConstructor +public class StoreGroupController { + private final StoreGroupService service; + + @GetMapping + public ApiResponse> list(@RequestParam(required = false) String tenantCode) { + return ApiResponse.ok(service.list(tenantCode)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody StoreGroupVo v) { + return ApiResponse.ok("매장그룹 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody StoreGroupVo v) { + return ApiResponse.ok("매장그룹 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("매장그룹 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TagBindingController.java b/backend/src/main/java/com/zioinfo/esn/controller/TagBindingController.java new file mode 100644 index 0000000..308c787 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/TagBindingController.java @@ -0,0 +1,57 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.TagBindingVo; +import com.zioinfo.esn.service.TagBindingService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +/** + * e-paper ESL 태그 ↔ 선반 위치 ↔ 상품 바인딩 관리 + * 핵심 플로우: 태그 스캔 → 상품 선택 → 위치 지정 → bind() → 가격 변경 시 자동 큐 + */ +@RestController +@RequestMapping("/api/tag-binding") +@RequiredArgsConstructor +public class TagBindingController { + private final TagBindingService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String bindStatus, + @RequestParam(required = false) String keyword) { + return ApiResponse.ok(service.list(tenantCode, storeId, bindStatus, keyword)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @GetMapping("/device/{deviceId}") + public ApiResponse getByDevice(@PathVariable String deviceId) { + return ApiResponse.ok(service.getByDeviceId(deviceId)); + } + + /** 태그에 상품+위치 바인딩 */ + @PostMapping + public ApiResponse bind(@RequestBody TagBindingVo vo) { + return ApiResponse.ok(service.bind(vo)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody TagBindingVo vo) { + vo.setId(id); + return ApiResponse.ok(service.bind(vo)); + } + + /** 태그 언바인딩 (선반에서 태그 제거 시) */ + @PutMapping("/{id}/unbind") + public ApiResponse unbind(@PathVariable Long id) { + service.unbind(id); + return ApiResponse.ok("언바인딩 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java b/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java new file mode 100644 index 0000000..98b85cb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java @@ -0,0 +1,43 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.TemplateVo; +import com.zioinfo.esn.service.TemplateService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/templates") +@RequiredArgsConstructor +public class TemplateController { + private final TemplateService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String templateType) { + return ApiResponse.ok(service.list(tenantCode, templateType)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody TemplateVo v) { + return ApiResponse.ok("템플릿 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody TemplateVo v) { + return ApiResponse.ok("템플릿 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("템플릿 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java new file mode 100644 index 0000000..7e5007f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java @@ -0,0 +1,41 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.TenantVo; +import com.zioinfo.esn.service.TenantService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/tenants") +@RequiredArgsConstructor +public class TenantController { + private final TenantService service; + + @GetMapping + public ApiResponse> list() { + return ApiResponse.ok(service.list()); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody TenantVo v) { + return ApiResponse.ok("테넌트 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody TenantVo v) { + return ApiResponse.ok("테넌트 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("테넌트 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/UpdateQueueController.java b/backend/src/main/java/com/zioinfo/esn/controller/UpdateQueueController.java new file mode 100644 index 0000000..3802ca5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/UpdateQueueController.java @@ -0,0 +1,75 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.UpdateQueueVo; +import com.zioinfo.esn.service.UpdateQueueService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +/** + * e-paper 태그 가격 업데이트 전파 큐 + * Gateway가 이 API를 주기적으로 폴링 → 태그에 RF 전송 → confirm 호출 + * + * Gateway 연동 흐름: + * GET /api/update-queue/poll?tenantCode=EMART&limit=50 → PENDING 목록 수신 + * PUT /api/update-queue/{id}/sent → RF 전송 완료 + * PUT /api/update-queue/confirm?deviceId=ESL-001 → 태그 ACK 수신 + */ +@RestController +@RequestMapping("/api/update-queue") +@RequiredArgsConstructor +public class UpdateQueueController { + private final UpdateQueueService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String status) { + return ApiResponse.ok(service.list(tenantCode, storeId, status)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + /** Gateway 폴링 엔드포인트: PENDING → SENDING 전환 후 반환 */ + @GetMapping("/poll") + public ApiResponse> poll( + @RequestParam(required = false) String tenantCode, + @RequestParam(defaultValue = "50") int limit) { + return ApiResponse.ok(service.pollPending(tenantCode, limit)); + } + + /** Gateway가 RF 전송 완료 후 호출 */ + @PutMapping("/{id}/sent") + public ApiResponse sent(@PathVariable Long id) { + service.markSent(id); + return ApiResponse.ok("전송 완료", null); + } + + /** 태그로부터 확인 응답 수신 (device_id 기반 일괄 확인) */ + @PutMapping("/confirm") + public ApiResponse confirm(@RequestParam String deviceId) { + service.confirm(deviceId); + return ApiResponse.ok("확인 완료", null); + } + + /** 전송 실패 처리 */ + @PutMapping("/{id}/failed") + public ApiResponse failed(@PathVariable Long id, + @RequestBody Map body) { + service.markFailed(id, body.getOrDefault("message", "전송 실패")); + return ApiResponse.ok("실패 처리 완료", null); + } + + /** 큐 항목 취소 */ + @PutMapping("/{id}/cancel") + public ApiResponse cancel(@PathVariable Long id) { + service.cancel(id); + return ApiResponse.ok("취소 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/UserController.java b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java new file mode 100644 index 0000000..fd67054 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java @@ -0,0 +1,58 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.auth.EsnUser; +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + private final UserService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String role) { + return ApiResponse.ok(service.list(tenantCode, role)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody Map body) { + EsnUser user = new EsnUser(); + user.setUsername((String) body.get("username")); + user.setRole((String) body.getOrDefault("role", "USER")); + user.setTenantCode((String) body.get("tenantCode")); + user.setEmail((String) body.get("email")); + user.setPhone((String) body.get("phone")); + user.setActive(true); + String rawPassword = (String) body.getOrDefault("password", "changeme123"); + return ApiResponse.ok("사용자 생성 완료", service.create(user, rawPassword)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody EsnUser v) { + return ApiResponse.ok("사용자 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/password") + public ApiResponse changePassword(@PathVariable Long id, @RequestBody Map body) { + service.changePassword(id, body.getOrDefault("password", "changeme123")); + return ApiResponse.ok("비밀번호 변경 완료", null); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("사용자 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java b/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java new file mode 100644 index 0000000..355294f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java @@ -0,0 +1,45 @@ +package com.zioinfo.esn.controller; + +import com.zioinfo.esn.common.ApiResponse; +import com.zioinfo.esn.domain.WorkHistoryVo; +import com.zioinfo.esn.service.WorkService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequestMapping("/api/works") +@RequiredArgsConstructor +public class WorkController { + private final WorkService service; + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String workType) { + return ApiResponse.ok(service.list(tenantCode, storeId, status, workType)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody WorkHistoryVo v) { + return ApiResponse.ok("작업 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody WorkHistoryVo v) { + return ApiResponse.ok("작업 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("작업 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java b/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java new file mode 100644 index 0000000..43f9734 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java @@ -0,0 +1,21 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class AlarmVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String alarmType; // DEVICE, NETWORK, BATTERY, FIRMWARE, SYSTEM + private String alarmCode; + private String severity; // CRITICAL, HIGH, MEDIUM, LOW + private String message; + private String status; // OPEN, ACKNOWLEDGED, RESOLVED + private String resolvedBy; + private String resolution; + private LocalDateTime resolvedAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java b/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java new file mode 100644 index 0000000..7916ec5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; + +@Data +public class DashboardVo { + private long totalStores; + private long activeStores; + private long totalAlarms; + private long unresolvedAlarms; + private long criticalAlarms; + private long totalDevices; + private long onlineDevices; + private long offlineDevices; + private long totalWorkToday; + private long completedWorkToday; + private long pendingPosCvt; + private long processedPosCvtToday; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java b/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java new file mode 100644 index 0000000..98d2fea --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class FirmwareVo { + private Long id; + private String tenantCode; + private String firmwareVersion; + private String deviceType; // GATEWAY, HUB, ESL_DEVICE + private String fileName; + private String filePath; + private Long fileSize; + private String checksum; + private String description; + private boolean latest; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java b/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java new file mode 100644 index 0000000..742aa69 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java @@ -0,0 +1,22 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class HCoreVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String deviceType; // GATEWAY, HUB, ESL_DEVICE + private String deviceId; + private String ipAddress; + private String macAddress; + private String firmwareVersion; + private String status; // ONLINE, OFFLINE, ERROR, UPDATING + private Integer batteryLevel; + private String signalStrength; + private LocalDateTime lastSeenAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java b/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java new file mode 100644 index 0000000..8622086 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java @@ -0,0 +1,23 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class PosCvtVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String posCode; + private String productCode; + private String productName; + private BigDecimal price; + private BigDecimal salePrice; + private String currency; + private String status; // PENDING, PROCESSED, ERROR, IGNORED + private String errorMessage; + private LocalDateTime processedAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java b/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java new file mode 100644 index 0000000..677e224 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java @@ -0,0 +1,22 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class ProductVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String productCode; + private String productName; + private String category; + private BigDecimal price; + private BigDecimal salePrice; + private String currency; + private boolean active; + private LocalDateTime updatedAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java b/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java new file mode 100644 index 0000000..93485d6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java @@ -0,0 +1,15 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class StoreGroupVo { + private Long id; + private String tenantCode; + private String groupCode; + private String groupName; + private String description; + private boolean active; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java b/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java new file mode 100644 index 0000000..b2ace76 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class StoreVo { + private Long id; + private String tenantCode; + private String storeCode; + private String storeName; + private String address; + private String phone; + private Long storeGroupId; + private String storeGroupName; + private String managerName; + private boolean active; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/TagBindingVo.java b/backend/src/main/java/com/zioinfo/esn/domain/TagBindingVo.java new file mode 100644 index 0000000..e1e508b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/TagBindingVo.java @@ -0,0 +1,27 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class TagBindingVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String deviceId; // ESL_DEVICE의 device_id + private String productCode; + private String productName; + private String locationCode; // 선반 위치 (예: A01-03) + private String templateCode; + private String displayData; // 현재 화면 렌더링 데이터 (JSON) + private BigDecimal lastPrice; + private LocalDateTime lastUpdatedAt; + private String bindStatus; // ACTIVE, UNBOUND, ERROR + // HCore 장치 정보 (join) + private String deviceStatus; // ONLINE, OFFLINE 등 + private Integer batteryLevel; + private String signalStrength; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java b/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java new file mode 100644 index 0000000..c2b6a82 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class TemplateVo { + private Long id; + private String tenantCode; + private String templateCode; + private String templateName; + private String templateType; // PRICE, INFO, PROMO, CUSTOM + private Integer width; + private Integer height; + private String description; + private String layoutJson; // JSON 레이아웃 정의 + private boolean active; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java b/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java new file mode 100644 index 0000000..3109d44 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java @@ -0,0 +1,14 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class TenantVo { + private Long id; + private String tenantCode; + private String tenantName; + private String description; + private boolean active; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/UpdateQueueVo.java b/backend/src/main/java/com/zioinfo/esn/domain/UpdateQueueVo.java new file mode 100644 index 0000000..6873d0d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/UpdateQueueVo.java @@ -0,0 +1,31 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class UpdateQueueVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private Long bindingId; + private String deviceId; + private String locationCode; + private String productCode; + private String productName; + private BigDecimal newPrice; + private BigDecimal newSalePrice; + private String templateCode; + private String renderData; + private Integer priority; // 1(긴급)~10(일반) + private String status; // PENDING, SENDING, SENT, CONFIRMED, FAILED, CANCELLED + private Integer retryCount; + private Integer maxRetry; + private String errorMessage; + private Long posCvtId; + private LocalDateTime sentAt; + private LocalDateTime confirmedAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java b/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java new file mode 100644 index 0000000..dc72b63 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java @@ -0,0 +1,20 @@ +package com.zioinfo.esn.domain; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class WorkHistoryVo { + private Long id; + private String tenantCode; + private Long storeId; + private String storeName; + private String workType; // INSTALL, MAINTENANCE, FIRMWARE_UPDATE, REPLACEMENT, INSPECTION + private String workContent; + private String workerName; + private String status; // SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED + private String remarks; + private LocalDateTime startedAt; + private LocalDateTime completedAt; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java new file mode 100644 index 0000000..6b5d75c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java @@ -0,0 +1,23 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.AlarmVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface AlarmMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("severity") String severity, + @Param("status") String status); + AlarmVo findById(@Param("id") Long id); + int insert(AlarmVo alarm); + int update(AlarmVo alarm); + int resolve(@Param("id") Long id, @Param("resolvedBy") String resolvedBy, + @Param("resolution") String resolution); + int delete(@Param("id") Long id); + long countUnresolved(@Param("tenantCode") String tenantCode); + long countCritical(@Param("tenantCode") String tenantCode); + long countAll(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java new file mode 100644 index 0000000..e3c6101 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java @@ -0,0 +1,10 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.DashboardVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface DashboardMapper { + DashboardVo getSummary(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java new file mode 100644 index 0000000..08c5602 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.FirmwareVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface FirmwareMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("deviceType") String deviceType); + FirmwareVo findById(@Param("id") Long id); + FirmwareVo findLatest(@Param("deviceType") String deviceType, + @Param("tenantCode") String tenantCode); + int insert(FirmwareVo firmware); + int update(FirmwareVo firmware); + int clearLatest(@Param("deviceType") String deviceType, @Param("tenantCode") String tenantCode); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java new file mode 100644 index 0000000..be23434 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java @@ -0,0 +1,23 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.HCoreVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface HCoreMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("deviceType") String deviceType, + @Param("status") String status); + HCoreVo findById(@Param("id") Long id); + HCoreVo findByDeviceId(@Param("deviceId") String deviceId); + int insert(HCoreVo hcore); + int update(HCoreVo hcore); + int updateStatus(@Param("id") Long id, @Param("status") String status); + int delete(@Param("id") Long id); + long countOnline(@Param("tenantCode") String tenantCode); + long countOffline(@Param("tenantCode") String tenantCode); + long countAll(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java new file mode 100644 index 0000000..2ee2282 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java @@ -0,0 +1,20 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.PosCvtVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface PosCvtMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("status") String status, + @Param("keyword") String keyword); + PosCvtVo findById(@Param("id") Long id); + int insert(PosCvtVo posCvt); + int updateStatus(@Param("id") Long id, @Param("status") String status, + @Param("errorMessage") String errorMessage); + long countPending(@Param("tenantCode") String tenantCode); + long countProcessedToday(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java new file mode 100644 index 0000000..00a673a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java @@ -0,0 +1,19 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.ProductVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface ProductMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("keyword") String keyword); + ProductVo findById(@Param("id") Long id); + ProductVo findByCode(@Param("productCode") String productCode, + @Param("storeId") Long storeId); + int insert(ProductVo product); + int update(ProductVo product); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java new file mode 100644 index 0000000..f8bba82 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java @@ -0,0 +1,15 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.StoreGroupVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface StoreGroupMapper { + List findAll(@Param("tenantCode") String tenantCode); + StoreGroupVo findById(@Param("id") Long id); + int insert(StoreGroupVo group); + int update(StoreGroupVo group); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java new file mode 100644 index 0000000..e15e286 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java @@ -0,0 +1,20 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.StoreVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface StoreMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeGroupId") Long storeGroupId, + @Param("keyword") String keyword); + StoreVo findById(@Param("id") Long id); + StoreVo findByCode(@Param("storeCode") String storeCode, @Param("tenantCode") String tenantCode); + int insert(StoreVo store); + int update(StoreVo store); + int delete(@Param("id") Long id); + long countActive(@Param("tenantCode") String tenantCode); + long countAll(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/TagBindingMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/TagBindingMapper.java new file mode 100644 index 0000000..1a02e60 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/TagBindingMapper.java @@ -0,0 +1,25 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.TagBindingVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface TagBindingMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("bindStatus") String bindStatus, + @Param("keyword") String keyword); + TagBindingVo findById(@Param("id") Long id); + TagBindingVo findByDeviceId(@Param("deviceId") String deviceId); + List findByProductCode(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("productCode") String productCode); + int insert(TagBindingVo vo); + int update(TagBindingVo vo); + int unbind(@Param("id") Long id); + int delete(@Param("id") Long id); + long countAll(@Param("tenantCode") String tenantCode); + long countActive(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java new file mode 100644 index 0000000..7b80214 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java @@ -0,0 +1,16 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.TemplateVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface TemplateMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("templateType") String templateType); + TemplateVo findById(@Param("id") Long id); + int insert(TemplateVo template); + int update(TemplateVo template); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java new file mode 100644 index 0000000..950fce2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java @@ -0,0 +1,16 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.TenantVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface TenantMapper { + List findAll(); + TenantVo findById(@Param("id") Long id); + TenantVo findByCode(@Param("tenantCode") String tenantCode); + int insert(TenantVo tenant); + int update(TenantVo tenant); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/UpdateQueueMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/UpdateQueueMapper.java new file mode 100644 index 0000000..5f51822 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/UpdateQueueMapper.java @@ -0,0 +1,24 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.UpdateQueueVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface UpdateQueueMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("status") String status); + List findPending(@Param("tenantCode") String tenantCode, + @Param("limit") int limit); + UpdateQueueVo findById(@Param("id") Long id); + int insert(UpdateQueueVo vo); + int updateStatus(@Param("id") Long id, @Param("status") String status, + @Param("errorMessage") String errorMessage); + int confirmByDeviceId(@Param("deviceId") String deviceId); + int incrementRetry(@Param("id") Long id); + long countPending(@Param("tenantCode") String tenantCode); + long countFailed(@Param("tenantCode") String tenantCode); + long countByStatus(@Param("tenantCode") String tenantCode, @Param("status") String status); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java new file mode 100644 index 0000000..2cfb891 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.auth.EsnUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface UserMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("role") String role); + EsnUser findById(@Param("id") Long id); + EsnUser findByUsername(@Param("username") String username); + int insert(EsnUser user); + int update(EsnUser user); + int updatePassword(@Param("id") Long id, @Param("passwordHash") String passwordHash); + int delete(@Param("id") Long id); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java new file mode 100644 index 0000000..f8bc439 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java @@ -0,0 +1,20 @@ +package com.zioinfo.esn.mapper; + +import com.zioinfo.esn.domain.WorkHistoryVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface WorkMapper { + List findAll(@Param("tenantCode") String tenantCode, + @Param("storeId") Long storeId, + @Param("status") String status, + @Param("workType") String workType); + WorkHistoryVo findById(@Param("id") Long id); + int insert(WorkHistoryVo work); + int update(WorkHistoryVo work); + int delete(@Param("id") Long id); + long countToday(@Param("tenantCode") String tenantCode); + long countCompletedToday(@Param("tenantCode") String tenantCode); +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java b/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java new file mode 100644 index 0000000..67b0546 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java @@ -0,0 +1,36 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.AlarmVo; +import com.zioinfo.esn.mapper.AlarmMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AlarmService { + private final AlarmMapper mapper; + + public List list(String tenantCode, Long storeId, String severity, String status) { + return mapper.findAll(tenantCode, storeId, severity, status); + } + + public AlarmVo get(Long id) { return mapper.findById(id); } + + public AlarmVo create(AlarmVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public AlarmVo update(Long id, AlarmVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void resolve(Long id, String resolvedBy, String resolution) { + mapper.resolve(id, resolvedBy, resolution); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java b/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java new file mode 100644 index 0000000..c0f5411 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java @@ -0,0 +1,38 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.FirmwareVo; +import com.zioinfo.esn.mapper.FirmwareMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FirmwareService { + private final FirmwareMapper mapper; + + public List list(String tenantCode, String deviceType) { + return mapper.findAll(tenantCode, deviceType); + } + + public FirmwareVo get(Long id) { return mapper.findById(id); } + + public FirmwareVo create(FirmwareVo v) { + if (v.isLatest()) { + mapper.clearLatest(v.getDeviceType(), v.getTenantCode()); + } + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public FirmwareVo update(Long id, FirmwareVo v) { + v.setId(id); + if (v.isLatest()) { + mapper.clearLatest(v.getDeviceType(), v.getTenantCode()); + } + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java b/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java new file mode 100644 index 0000000..922855d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java @@ -0,0 +1,36 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.HCoreVo; +import com.zioinfo.esn.mapper.HCoreMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HCoreService { + private final HCoreMapper mapper; + + public List list(String tenantCode, Long storeId, String deviceType, String status) { + return mapper.findAll(tenantCode, storeId, deviceType, status); + } + + public HCoreVo get(Long id) { return mapper.findById(id); } + + public HCoreVo create(HCoreVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public HCoreVo update(Long id, HCoreVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void updateStatus(Long id, String status) { + mapper.updateStatus(id, status); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java b/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java new file mode 100644 index 0000000..072a764 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java @@ -0,0 +1,51 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.PosCvtVo; +import com.zioinfo.esn.mapper.PosCvtMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PosCvtService { + private final PosCvtMapper mapper; + private final TagBindingService tagBindingService; + + public List list(String tenantCode, Long storeId, String status, String keyword) { + return mapper.findAll(tenantCode, storeId, status, keyword); + } + + public PosCvtVo get(Long id) { return mapper.findById(id); } + + /** + * POS 가격변경 처리 → 바인딩된 e-paper 태그에 업데이트 큐 자동 생성 + */ + @Transactional + public int process(Long id) { + PosCvtVo pos = mapper.findById(id); + if (pos == null) return 0; + + mapper.updateStatus(id, "PROCESSED", null); + + // 이 상품에 바인딩된 e-paper 태그들에 가격 업데이트 큐 생성 + int queued = tagBindingService.enqueueByProductCode( + pos.getTenantCode(), pos.getStoreId(), + pos.getProductCode(), pos.getPrice(), pos.getSalePrice(), id); + + log.info("POS 처리 완료: posCvtId={}, product={}, 큐생성={}개", + id, pos.getProductCode(), queued); + return queued; + } + + public void markError(Long id, String errorMessage) { + mapper.updateStatus(id, "ERROR", errorMessage); + } + + public void ignore(Long id) { + mapper.updateStatus(id, "IGNORED", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/ProductService.java b/backend/src/main/java/com/zioinfo/esn/service/ProductService.java new file mode 100644 index 0000000..be7dfa3 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/ProductService.java @@ -0,0 +1,32 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.ProductVo; +import com.zioinfo.esn.mapper.ProductMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ProductService { + private final ProductMapper mapper; + + public List list(String tenantCode, Long storeId, String keyword) { + return mapper.findAll(tenantCode, storeId, keyword); + } + + public ProductVo get(Long id) { return mapper.findById(id); } + + public ProductVo create(ProductVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public ProductVo update(Long id, ProductVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java b/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java new file mode 100644 index 0000000..fda090f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java @@ -0,0 +1,29 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.StoreGroupVo; +import com.zioinfo.esn.mapper.StoreGroupMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StoreGroupService { + private final StoreGroupMapper mapper; + + public List list(String tenantCode) { return mapper.findAll(tenantCode); } + public StoreGroupVo get(Long id) { return mapper.findById(id); } + + public StoreGroupVo create(StoreGroupVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public StoreGroupVo update(Long id, StoreGroupVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/StoreService.java b/backend/src/main/java/com/zioinfo/esn/service/StoreService.java new file mode 100644 index 0000000..587f28c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/StoreService.java @@ -0,0 +1,32 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.StoreVo; +import com.zioinfo.esn.mapper.StoreMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StoreService { + private final StoreMapper mapper; + + public List list(String tenantCode, Long storeGroupId, String keyword) { + return mapper.findAll(tenantCode, storeGroupId, keyword); + } + + public StoreVo get(Long id) { return mapper.findById(id); } + + public StoreVo create(StoreVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public StoreVo update(Long id, StoreVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/TagBindingService.java b/backend/src/main/java/com/zioinfo/esn/service/TagBindingService.java new file mode 100644 index 0000000..03de68d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/TagBindingService.java @@ -0,0 +1,70 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.TagBindingVo; +import com.zioinfo.esn.domain.UpdateQueueVo; +import com.zioinfo.esn.mapper.TagBindingMapper; +import com.zioinfo.esn.mapper.UpdateQueueMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TagBindingService { + private final TagBindingMapper bindingMapper; + private final UpdateQueueMapper queueMapper; + + public List list(String tenantCode, Long storeId, + String bindStatus, String keyword) { + return bindingMapper.findAll(tenantCode, storeId, bindStatus, keyword); + } + + public TagBindingVo get(Long id) { return bindingMapper.findById(id); } + + public TagBindingVo getByDeviceId(String deviceId) { + return bindingMapper.findByDeviceId(deviceId); + } + + @Transactional + public TagBindingVo bind(TagBindingVo vo) { + // 기존 바인딩이 있으면 해제 후 재바인딩 + TagBindingVo existing = bindingMapper.findByDeviceId(vo.getDeviceId()); + if (existing != null) { + bindingMapper.unbind(existing.getId()); + } + bindingMapper.insert(vo); + return bindingMapper.findById(vo.getId()); + } + + @Transactional + public void unbind(Long id) { + bindingMapper.unbind(id); + } + + /** + * 가격 변경 시 해당 상품에 바인딩된 모든 태그에 업데이트 큐 생성 + * POS 가격변경 → 이 메서드 → e-paper 태그들에게 전파 + */ + @Transactional + public int enqueueByProductCode(String tenantCode, Long storeId, + String productCode, java.math.BigDecimal newPrice, + java.math.BigDecimal newSalePrice, Long posCvtId) { + List bindings = bindingMapper.findByProductCode(tenantCode, storeId, productCode); + for (TagBindingVo b : bindings) { + UpdateQueueVo q = new UpdateQueueVo(); + q.setTenantCode(tenantCode); + q.setStoreId(storeId); + q.setBindingId(b.getId()); + q.setDeviceId(b.getDeviceId()); + q.setProductCode(productCode); + q.setNewPrice(newPrice); + q.setNewSalePrice(newSalePrice); + q.setTemplateCode(b.getTemplateCode()); + q.setPriority(5); + q.setPosCvtId(posCvtId); + queueMapper.insert(q); + } + return bindings.size(); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java b/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java new file mode 100644 index 0000000..d766789 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java @@ -0,0 +1,32 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.TemplateVo; +import com.zioinfo.esn.mapper.TemplateMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TemplateService { + private final TemplateMapper mapper; + + public List list(String tenantCode, String templateType) { + return mapper.findAll(tenantCode, templateType); + } + + public TemplateVo get(Long id) { return mapper.findById(id); } + + public TemplateVo create(TemplateVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public TemplateVo update(Long id, TemplateVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/TenantService.java b/backend/src/main/java/com/zioinfo/esn/service/TenantService.java new file mode 100644 index 0000000..d31f872 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/TenantService.java @@ -0,0 +1,29 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.TenantVo; +import com.zioinfo.esn.mapper.TenantMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TenantService { + private final TenantMapper mapper; + + public List list() { return mapper.findAll(); } + public TenantVo get(Long id) { return mapper.findById(id); } + + public TenantVo create(TenantVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public TenantVo update(Long id, TenantVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/UpdateQueueService.java b/backend/src/main/java/com/zioinfo/esn/service/UpdateQueueService.java new file mode 100644 index 0000000..48f72fe --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/UpdateQueueService.java @@ -0,0 +1,60 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.UpdateQueueVo; +import com.zioinfo.esn.mapper.UpdateQueueMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UpdateQueueService { + private final UpdateQueueMapper mapper; + + public List list(String tenantCode, Long storeId, String status) { + return mapper.findAll(tenantCode, storeId, status); + } + + public UpdateQueueVo get(Long id) { return mapper.findById(id); } + + /** Gateway 폴링: PENDING 큐 N건 반환 후 SENDING으로 전환 */ + @Transactional + public List pollPending(String tenantCode, int limit) { + List items = mapper.findPending(tenantCode, limit); + for (UpdateQueueVo q : items) { + mapper.updateStatus(q.getId(), "SENDING", null); + } + return items; + } + + /** Gateway → 태그 전송 완료 */ + @Transactional + public void markSent(Long id) { + mapper.updateStatus(id, "SENT", null); + } + + /** 태그 확인 응답 수신 (device_id 기반) */ + @Transactional + public void confirm(String deviceId) { + mapper.confirmByDeviceId(deviceId); + } + + /** 전송 실패 처리 — max_retry 초과 시 FAILED */ + @Transactional + public void markFailed(Long id, String errorMessage) { + UpdateQueueVo q = mapper.findById(id); + if (q == null) return; + mapper.incrementRetry(id); + if (q.getRetryCount() + 1 >= q.getMaxRetry()) { + mapper.updateStatus(id, "FAILED", errorMessage); + } else { + mapper.updateStatus(id, "PENDING", errorMessage); + } + } + + @Transactional + public void cancel(Long id) { + mapper.updateStatus(id, "CANCELLED", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/UserService.java b/backend/src/main/java/com/zioinfo/esn/service/UserService.java new file mode 100644 index 0000000..f930f76 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/UserService.java @@ -0,0 +1,39 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.auth.EsnUser; +import com.zioinfo.esn.mapper.UserMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserMapper mapper; + private final PasswordEncoder passwordEncoder; + + public List list(String tenantCode, String role) { + return mapper.findAll(tenantCode, role); + } + + public EsnUser get(Long id) { return mapper.findById(id); } + + public EsnUser create(EsnUser v, String rawPassword) { + v.setPasswordHash(passwordEncoder.encode(rawPassword)); + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public EsnUser update(Long id, EsnUser v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void changePassword(Long id, String rawPassword) { + mapper.updatePassword(id, passwordEncoder.encode(rawPassword)); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/WorkService.java b/backend/src/main/java/com/zioinfo/esn/service/WorkService.java new file mode 100644 index 0000000..02bf2ad --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/WorkService.java @@ -0,0 +1,32 @@ +package com.zioinfo.esn.service; + +import com.zioinfo.esn.domain.WorkHistoryVo; +import com.zioinfo.esn.mapper.WorkMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class WorkService { + private final WorkMapper mapper; + + public List list(String tenantCode, Long storeId, String status, String workType) { + return mapper.findAll(tenantCode, storeId, status, workType); + } + + public WorkHistoryVo get(Long id) { return mapper.findById(id); } + + public WorkHistoryVo create(WorkHistoryVo v) { + mapper.insert(v); + return mapper.findById(v.getId()); + } + + public WorkHistoryVo update(Long id, WorkHistoryVo v) { + v.setId(id); + mapper.update(v); + return mapper.findById(id); + } + + public void delete(Long id) { mapper.delete(id); } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..0acee08 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,48 @@ +server: + port: 8016 + +spring: + application: + name: zioinfo-esn + datasource: + url: ${DB_URL:jdbc:postgresql://localhost:5432/esn_db} + username: ${DB_USER:esn_user} + password: ${DB_PASS:esn_pass2026} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 3 + connection-timeout: 30000 + web: + resources: + static-locations: classpath:/static/ + mvc: + throw-exception-if-no-handler-found: true + +mybatis: + mapper-locations: classpath:mapper/*.xml + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + type-aliases-package: com.zioinfo.esn.domain + +guardia: + ollama-url: ${OLLAMA_URL:http://localhost:11434} + ollama-text-model: ${OLLAMA_TEXT_MODEL:llama3} + itsm-url: ${ITSM_URL:http://localhost:9001} + jwt: + secret: ${JWT_SECRET:ZioInfoEsnJwtSecret2026VeryLongSecretKeyForHS256AlgorithmAtLeast256Bits} + expiration: 86400000 + +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never + +logging: + level: + com.zioinfo.esn: DEBUG + org.mybatis: WARN diff --git a/backend/src/main/resources/db/schema.sql b/backend/src/main/resources/db/schema.sql new file mode 100644 index 0000000..fad17b9 --- /dev/null +++ b/backend/src/main/resources/db/schema.sql @@ -0,0 +1,333 @@ +-- ============================================================================ +-- zioinfo-esn ESL 통합 플랫폼 — PostgreSQL 스키마 (esn_db) +-- 레거시 Spring Boot 1.5 / Java 8 6개 프로젝트 → 단일 플랫폼 +-- 멀티테넌트: LGINNOTEK / LGIT / EMART / ZIOINFO +-- ============================================================================ + +-- ── 테넌트 ────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_tenant ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) UNIQUE NOT NULL, + tenant_name VARCHAR(200) NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW() +); + +INSERT INTO esn_tenant (tenant_code, tenant_name, description) VALUES + ('LGINNOTEK', 'LG 이노텍', 'LG 이노텍 ESL 관리'), + ('LGIT', 'LG IT', 'LG IT 서비스 ESL'), + ('EMART', '이마트', '이마트 전자가격표 시스템'), + ('ZIOINFO', '지오정보기술', '지오정보기술 자체 ESL') +ON CONFLICT (tenant_code) DO NOTHING; + +-- ── 매장 그룹 ──────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_store_group ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + group_code VARCHAR(50) NOT NULL, + group_name VARCHAR(200) NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE (tenant_code, group_code) +); + +INSERT INTO esn_store_group (tenant_code, group_code, group_name) VALUES + ('LGINNOTEK', 'GRP-LGI-01', 'LG이노텍 구미공장'), + ('LGIT', 'GRP-LGIT-01','LG IT 본사'), + ('EMART', 'GRP-EM-SEOUL', '이마트 서울권'), + ('EMART', 'GRP-EM-GYEONG','이마트 경기권'), + ('ZIOINFO', 'GRP-ZIO-01', '지오정보기술 본사') +ON CONFLICT (tenant_code, group_code) DO NOTHING; + +-- ── 매장 ───────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_store ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_code VARCHAR(50) NOT NULL, + store_name VARCHAR(200) NOT NULL, + address VARCHAR(500), + phone VARCHAR(50), + store_group_id BIGINT REFERENCES esn_store_group(id), + manager_name VARCHAR(100), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE (tenant_code, store_code) +); + +INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id) +SELECT 'LGINNOTEK', 'LGI-001', 'LG이노텍 구미 1라인', '경북 구미시 공단동 1', '김철수', g.id +FROM esn_store_group g WHERE g.group_code = 'GRP-LGI-01' AND g.tenant_code = 'LGINNOTEK' +ON CONFLICT (tenant_code, store_code) DO NOTHING; + +INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id) +SELECT 'EMART', 'EM-001', '이마트 강남점', '서울 강남구 삼성동', '박영희', g.id +FROM esn_store_group g WHERE g.group_code = 'GRP-EM-SEOUL' AND g.tenant_code = 'EMART' +ON CONFLICT (tenant_code, store_code) DO NOTHING; + +INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id) +SELECT 'EMART', 'EM-002', '이마트 수원점', '경기 수원시 팔달구', '이민수', g.id +FROM esn_store_group g WHERE g.group_code = 'GRP-EM-GYEONG' AND g.tenant_code = 'EMART' +ON CONFLICT (tenant_code, store_code) DO NOTHING; + +-- ── ESL 템플릿 ─────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_template ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + template_code VARCHAR(50) NOT NULL, + template_name VARCHAR(200) NOT NULL, + template_type VARCHAR(50) DEFAULT 'PRICE', -- PRICE, INFO, PROMO, CUSTOM + width INTEGER, + height INTEGER, + description TEXT, + layout_json TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE (tenant_code, template_code) +); + +INSERT INTO esn_template (tenant_code, template_code, template_name, template_type, width, height, description) VALUES + ('EMART', 'EM-PRICE-STD', '이마트 표준 가격표', 'PRICE', 152, 76, '표준 2.9인치 ESL 템플릿'), + ('EMART', 'EM-PROMO', '이마트 프로모션', 'PROMO', 296, 128, '5인치 프로모션 표시'), + ('LGINNOTEK', 'LGI-INFO', 'LG이노텍 부품 정보', 'INFO', 152, 76, '부품 코드·수량 표시') +ON CONFLICT (tenant_code, template_code) DO NOTHING; + +-- ── POS 가격 변환 ───────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_pos_cvt ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + pos_code VARCHAR(100), + product_code VARCHAR(100) NOT NULL, + product_name VARCHAR(300), + price NUMERIC(12,2), + sale_price NUMERIC(12,2), + currency VARCHAR(10) DEFAULT 'KRW', + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, PROCESSED, ERROR, IGNORED + error_message TEXT, + processed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_pos_cvt_tenant ON esn_pos_cvt(tenant_code, status); +CREATE INDEX IF NOT EXISTS idx_pos_cvt_store ON esn_pos_cvt(store_id, status); + +-- ── 알람 ───────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_alarm ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + alarm_type VARCHAR(50) NOT NULL, -- DEVICE, NETWORK, BATTERY, FIRMWARE, SYSTEM + alarm_code VARCHAR(100), + severity VARCHAR(20) NOT NULL, -- CRITICAL, HIGH, MEDIUM, LOW + message TEXT, + status VARCHAR(20) DEFAULT 'OPEN', -- OPEN, ACKNOWLEDGED, RESOLVED + resolved_by VARCHAR(100), + resolution TEXT, + resolved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_alarm_tenant ON esn_alarm(tenant_code, status); +CREATE INDEX IF NOT EXISTS idx_alarm_severity ON esn_alarm(severity, status); + +-- 샘플 알람 +INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status) +SELECT 'EMART', s.id, 'DEVICE', 'DEV-OFFLINE', 'HIGH', 'ESL 장치 오프라인 감지 — 매장 내 점검 필요', 'OPEN' +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1; + +INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status) +SELECT 'EMART', s.id, 'BATTERY', 'BAT-LOW', 'MEDIUM', 'ESL 배터리 잔량 15% 미만 — 배터리 교체 예정', 'OPEN' +FROM esn_store s WHERE s.store_code = 'EM-002' LIMIT 1; + +-- ── HCore 장치 (게이트웨이/허브/ESL) ───────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_hcore_device ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + device_type VARCHAR(50) NOT NULL, -- GATEWAY, HUB, ESL_DEVICE + device_id VARCHAR(100) NOT NULL, + ip_address VARCHAR(50), + mac_address VARCHAR(50), + firmware_version VARCHAR(50), + status VARCHAR(20) DEFAULT 'ONLINE', -- ONLINE, OFFLINE, ERROR, UPDATING + battery_level INTEGER, + signal_strength VARCHAR(20), + last_seen_at TIMESTAMP DEFAULT NOW(), + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE (device_id) +); +CREATE INDEX IF NOT EXISTS idx_hcore_tenant ON esn_hcore_device(tenant_code, status); +CREATE INDEX IF NOT EXISTS idx_hcore_store ON esn_hcore_device(store_id, status); + +-- 샘플 HCore 장치 +INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status) +SELECT 'EMART', s.id, 'GATEWAY', 'GW-EM-001-001', '192.168.1.1', '2.1.5', 'ONLINE' +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1 +ON CONFLICT (device_id) DO NOTHING; + +INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status, battery_level) +SELECT 'EMART', s.id, 'ESL_DEVICE', 'ESL-EM-001-0001', NULL, '1.8.2', 'ONLINE', 85 +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1 +ON CONFLICT (device_id) DO NOTHING; + +INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status, battery_level) +SELECT 'EMART', s.id, 'ESL_DEVICE', 'ESL-EM-001-0002', NULL, '1.8.2', 'OFFLINE', 8 +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1 +ON CONFLICT (device_id) DO NOTHING; + +-- ── 작업 이력 ──────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_work_history ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + work_type VARCHAR(50) NOT NULL, -- INSTALL, MAINTENANCE, FIRMWARE_UPDATE, REPLACEMENT, INSPECTION + work_content TEXT, + worker_name VARCHAR(100), + status VARCHAR(20) DEFAULT 'SCHEDULED', -- SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED + remarks TEXT, + started_at TIMESTAMP, + completed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_work_tenant ON esn_work_history(tenant_code, status); + +-- ── 펌웨어 ─────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_firmware ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + firmware_version VARCHAR(50) NOT NULL, + device_type VARCHAR(50) NOT NULL, + file_name VARCHAR(300), + file_path VARCHAR(500), + file_size BIGINT, + checksum VARCHAR(200), + description TEXT, + is_latest BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() +); + +INSERT INTO esn_firmware (tenant_code, firmware_version, device_type, file_name, description, is_latest) VALUES + ('EMART', '2.1.5', 'GATEWAY', 'gw_v2.1.5.bin', 'EMART 게이트웨이 최신 펌웨어', true), + ('EMART', '1.8.2', 'ESL_DEVICE', 'esl_v1.8.2.bin', 'EMART ESL 디바이스 최신 펌웨어', true) +ON CONFLICT DO NOTHING; + +-- ── 사용자 ─────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_user ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) REFERENCES esn_tenant(tenant_code), + username VARCHAR(200) UNIQUE NOT NULL, + password_hash VARCHAR(500) NOT NULL, + role VARCHAR(50) DEFAULT 'USER', -- ADMIN, MANAGER, USER + email VARCHAR(300), + phone VARCHAR(50), + is_active BOOLEAN DEFAULT TRUE, + last_login_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); + +-- 기본 관리자 (비밀번호: admin123 / BCrypt) +INSERT INTO esn_user (tenant_code, username, password_hash, role) +VALUES (NULL, 'admin', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'ADMIN') +ON CONFLICT (username) DO NOTHING; + +INSERT INTO esn_user (tenant_code, username, password_hash, role) +VALUES ('EMART', 'emart_manager', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'MANAGER') +ON CONFLICT (username) DO NOTHING; + +INSERT INTO esn_user (tenant_code, username, password_hash, role) +VALUES ('LGINNOTEK', 'lgi_manager', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'MANAGER') +ON CONFLICT (username) DO NOTHING; + +-- ── 상품 ───────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_product ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + product_code VARCHAR(100) NOT NULL, + product_name VARCHAR(300) NOT NULL, + category VARCHAR(100), + price NUMERIC(12,2), + sale_price NUMERIC(12,2), + currency VARCHAR(10) DEFAULT 'KRW', + is_active BOOLEAN DEFAULT TRUE, + updated_at TIMESTAMP DEFAULT NOW(), + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_product_store ON esn_product(store_id, product_code); + +-- 샘플 상품 +INSERT INTO esn_product (tenant_code, store_id, product_code, product_name, category, price, sale_price) +SELECT 'EMART', s.id, 'P-001', '신라면', '라면/면류', 850, 800 +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1 +ON CONFLICT DO NOTHING; + +-- ── 태그 위치/상품 바인딩 ────────────────────────────────────────────────────── +-- e-paper 태그와 선반 위치 + 상품을 연결하는 핵심 테이블 +CREATE TABLE IF NOT EXISTS esn_tag_binding ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT NOT NULL REFERENCES esn_store(id), + device_id VARCHAR(100) NOT NULL, -- ESL_DEVICE device_id + product_code VARCHAR(100), -- 현재 표시 중인 상품 + location_code VARCHAR(100), -- 선반 위치코드 (예: A01-03, 통로A-1단) + template_code VARCHAR(50), -- 적용 템플릿 + display_data TEXT, -- 현재 화면에 표시된 데이터 (JSON) + last_price NUMERIC(12,2), -- 마지막으로 전송된 가격 + last_updated_at TIMESTAMP, -- 마지막 화면 갱신 시각 + bind_status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE, UNBOUND, ERROR + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE (device_id) +); +CREATE INDEX IF NOT EXISTS idx_binding_store ON esn_tag_binding(store_id, bind_status); +CREATE INDEX IF NOT EXISTS idx_binding_product ON esn_tag_binding(tenant_code, product_code); +CREATE INDEX IF NOT EXISTS idx_binding_loc ON esn_tag_binding(store_id, location_code); + +-- 샘플 바인딩 (이마트 강남점 ESL-EM-001-0001 ↔ 신라면) +INSERT INTO esn_tag_binding (tenant_code, store_id, device_id, product_code, location_code, template_code, last_price) +SELECT 'EMART', s.id, 'ESL-EM-001-0001', 'P-001', 'A01-03', 'EM-PRICE-STD', 850 +FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1 +ON CONFLICT (device_id) DO NOTHING; + +-- ── 가격 업데이트 전파 큐 ──────────────────────────────────────────────────────── +-- POS 가격 변경이 e-paper 태그에 도달하는 전 과정을 추적 +CREATE TABLE IF NOT EXISTS esn_update_queue ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code), + store_id BIGINT REFERENCES esn_store(id), + binding_id BIGINT REFERENCES esn_tag_binding(id), + device_id VARCHAR(100) NOT NULL, + product_code VARCHAR(100), + new_price NUMERIC(12,2), + new_sale_price NUMERIC(12,2), + template_code VARCHAR(50), + render_data TEXT, -- 렌더링된 화면 데이터 (JSON) + priority INTEGER DEFAULT 5, -- 1(긴급) ~ 10(일반) + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, SENDING, SENT, CONFIRMED, FAILED, CANCELLED + retry_count INTEGER DEFAULT 0, + max_retry INTEGER DEFAULT 3, + error_message TEXT, + sent_at TIMESTAMP, + confirmed_at TIMESTAMP, + pos_cvt_id BIGINT REFERENCES esn_pos_cvt(id), + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_queue_device ON esn_update_queue(device_id, status); +CREATE INDEX IF NOT EXISTS idx_queue_store ON esn_update_queue(store_id, status, created_at); +CREATE INDEX IF NOT EXISTS idx_queue_pend ON esn_update_queue(status, priority, created_at) WHERE status = 'PENDING'; + +-- ── 감사 로그 ───────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_audit_log ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(20), + username VARCHAR(200), + action VARCHAR(200) NOT NULL, + resource VARCHAR(200), + resource_id VARCHAR(100), + detail TEXT, + ip_address VARCHAR(50), + user_agent VARCHAR(500), + success BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_audit_created ON esn_audit_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_audit_tenant ON esn_audit_log(tenant_code, created_at DESC); diff --git a/backend/src/main/resources/mapper/AlarmMapper.xml b/backend/src/main/resources/mapper/AlarmMapper.xml new file mode 100644 index 0000000..b381f83 --- /dev/null +++ b/backend/src/main/resources/mapper/AlarmMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status) + VALUES (#{tenantCode}, #{storeId}, #{alarmType}, #{alarmCode}, #{severity}, #{message}, 'OPEN') + + + + UPDATE esn_alarm SET + alarm_type = #{alarmType}, + severity = #{severity}, + message = #{message}, + status = #{status} + WHERE id = #{id} + + + + UPDATE esn_alarm SET + status = 'RESOLVED', + resolved_by = #{resolvedBy}, + resolution = #{resolution}, + resolved_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_alarm WHERE id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/DashboardMapper.xml b/backend/src/main/resources/mapper/DashboardMapper.xml new file mode 100644 index 0000000..91fbcec --- /dev/null +++ b/backend/src/main/resources/mapper/DashboardMapper.xml @@ -0,0 +1,46 @@ + + + + + + + diff --git a/backend/src/main/resources/mapper/FirmwareMapper.xml b/backend/src/main/resources/mapper/FirmwareMapper.xml new file mode 100644 index 0000000..39759d4 --- /dev/null +++ b/backend/src/main/resources/mapper/FirmwareMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_firmware (tenant_code, firmware_version, device_type, file_name, file_path, + file_size, checksum, description, is_latest) + VALUES (#{tenantCode}, #{firmwareVersion}, #{deviceType}, #{fileName}, #{filePath}, + #{fileSize}, #{checksum}, #{description}, #{latest}) + + + + UPDATE esn_firmware SET + firmware_version = #{firmwareVersion}, + description = #{description}, + is_latest = #{latest} + WHERE id = #{id} + + + + UPDATE esn_firmware SET is_latest = false + WHERE device_type = #{deviceType} + AND tenant_code = #{tenantCode} + + + DELETE FROM esn_firmware WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/HCoreMapper.xml b/backend/src/main/resources/mapper/HCoreMapper.xml new file mode 100644 index 0000000..ad2ccb9 --- /dev/null +++ b/backend/src/main/resources/mapper/HCoreMapper.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, + mac_address, firmware_version, status) + VALUES (#{tenantCode}, #{storeId}, #{deviceType}, #{deviceId}, #{ipAddress}, + #{macAddress}, #{firmwareVersion}, #{status}) + + + + UPDATE esn_hcore_device SET + ip_address = #{ipAddress}, + mac_address = #{macAddress}, + firmware_version = #{firmwareVersion}, + status = #{status}, + battery_level = #{batteryLevel}, + signal_strength = #{signalStrength}, + last_seen_at = NOW() + WHERE id = #{id} + + + + UPDATE esn_hcore_device SET status = #{status}, last_seen_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_hcore_device WHERE id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/PosCvtMapper.xml b/backend/src/main/resources/mapper/PosCvtMapper.xml new file mode 100644 index 0000000..2aa73e5 --- /dev/null +++ b/backend/src/main/resources/mapper/PosCvtMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_pos_cvt (tenant_code, store_id, pos_code, product_code, product_name, + price, sale_price, currency, status) + VALUES (#{tenantCode}, #{storeId}, #{posCode}, #{productCode}, #{productName}, + #{price}, #{salePrice}, #{currency}, 'PENDING') + + + + UPDATE esn_pos_cvt SET + status = #{status}, + error_message = #{errorMessage}, + processed_at = CASE WHEN #{status} = 'PROCESSED' THEN NOW() ELSE processed_at END + WHERE id = #{id} + + + + + + + diff --git a/backend/src/main/resources/mapper/ProductMapper.xml b/backend/src/main/resources/mapper/ProductMapper.xml new file mode 100644 index 0000000..4e3bd74 --- /dev/null +++ b/backend/src/main/resources/mapper/ProductMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_product (tenant_code, store_id, product_code, product_name, + category, price, sale_price, currency, is_active) + VALUES (#{tenantCode}, #{storeId}, #{productCode}, #{productName}, + #{category}, #{price}, #{salePrice}, #{currency}, #{active}) + + + + UPDATE esn_product SET + product_name = #{productName}, + category = #{category}, + price = #{price}, + sale_price = #{salePrice}, + currency = #{currency}, + is_active = #{active}, + updated_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_product WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/StoreGroupMapper.xml b/backend/src/main/resources/mapper/StoreGroupMapper.xml new file mode 100644 index 0000000..9fa9e51 --- /dev/null +++ b/backend/src/main/resources/mapper/StoreGroupMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_store_group (tenant_code, group_code, group_name, description, is_active) + VALUES (#{tenantCode}, #{groupCode}, #{groupName}, #{description}, #{active}) + + + + UPDATE esn_store_group SET + group_name = #{groupName}, + description = #{description}, + is_active = #{active} + WHERE id = #{id} + + + DELETE FROM esn_store_group WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/StoreMapper.xml b/backend/src/main/resources/mapper/StoreMapper.xml new file mode 100644 index 0000000..56251b2 --- /dev/null +++ b/backend/src/main/resources/mapper/StoreMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_store (tenant_code, store_code, store_name, address, phone, + store_group_id, manager_name, is_active) + VALUES (#{tenantCode}, #{storeCode}, #{storeName}, #{address}, #{phone}, + #{storeGroupId}, #{managerName}, #{active}) + + + + UPDATE esn_store SET + store_name = #{storeName}, + address = #{address}, + phone = #{phone}, + store_group_id = #{storeGroupId}, + manager_name = #{managerName}, + is_active = #{active} + WHERE id = #{id} + + + DELETE FROM esn_store WHERE id = #{id} + + + + + + diff --git a/backend/src/main/resources/mapper/TagBindingMapper.xml b/backend/src/main/resources/mapper/TagBindingMapper.xml new file mode 100644 index 0000000..fb48a89 --- /dev/null +++ b/backend/src/main/resources/mapper/TagBindingMapper.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_tag_binding + (tenant_code, store_id, device_id, product_code, location_code, + template_code, last_price, bind_status) + VALUES + (#{tenantCode}, #{storeId}, #{deviceId}, #{productCode}, #{locationCode}, + #{templateCode}, #{lastPrice}, 'ACTIVE') + + + + UPDATE esn_tag_binding SET + product_code = #{productCode}, + location_code = #{locationCode}, + template_code = #{templateCode}, + display_data = #{displayData}, + last_price = #{lastPrice}, + last_updated_at = NOW(), + bind_status = #{bindStatus} + WHERE id = #{id} + + + + UPDATE esn_tag_binding SET + bind_status = 'UNBOUND', + product_code = NULL, + last_updated_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_tag_binding WHERE id = #{id} + + + + + + diff --git a/backend/src/main/resources/mapper/TemplateMapper.xml b/backend/src/main/resources/mapper/TemplateMapper.xml new file mode 100644 index 0000000..66d3a9b --- /dev/null +++ b/backend/src/main/resources/mapper/TemplateMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_template (tenant_code, template_code, template_name, template_type, + width, height, description, layout_json, is_active) + VALUES (#{tenantCode}, #{templateCode}, #{templateName}, #{templateType}, + #{width}, #{height}, #{description}, #{layoutJson}, #{active}) + + + + UPDATE esn_template SET + template_name = #{templateName}, + template_type = #{templateType}, + width = #{width}, + height = #{height}, + description = #{description}, + layout_json = #{layoutJson}, + is_active = #{active} + WHERE id = #{id} + + + DELETE FROM esn_template WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/TenantMapper.xml b/backend/src/main/resources/mapper/TenantMapper.xml new file mode 100644 index 0000000..297d051 --- /dev/null +++ b/backend/src/main/resources/mapper/TenantMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_tenant (tenant_code, tenant_name, description, is_active) + VALUES (#{tenantCode}, #{tenantName}, #{description}, #{active}) + + + + UPDATE esn_tenant SET + tenant_name = #{tenantName}, + description = #{description}, + is_active = #{active} + WHERE id = #{id} + + + + DELETE FROM esn_tenant WHERE id = #{id} + + + diff --git a/backend/src/main/resources/mapper/UpdateQueueMapper.xml b/backend/src/main/resources/mapper/UpdateQueueMapper.xml new file mode 100644 index 0000000..94c7432 --- /dev/null +++ b/backend/src/main/resources/mapper/UpdateQueueMapper.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_update_queue + (tenant_code, store_id, binding_id, device_id, product_code, + new_price, new_sale_price, template_code, priority, status, pos_cvt_id) + VALUES + (#{tenantCode}, #{storeId}, #{bindingId}, #{deviceId}, #{productCode}, + #{newPrice}, #{newSalePrice}, #{templateCode}, #{priority}, 'PENDING', #{posCvtId}) + + + + UPDATE esn_update_queue SET + status = #{status}, + error_message = #{errorMessage}, + sent_at = CASE WHEN #{status} = 'SENT' THEN NOW() ELSE sent_at END, + confirmed_at = CASE WHEN #{status} = 'CONFIRMED' THEN NOW() ELSE confirmed_at END + WHERE id = #{id} + + + + + UPDATE esn_update_queue SET + status = 'CONFIRMED', + confirmed_at = NOW() + WHERE device_id = #{deviceId} + AND status IN ('SENDING', 'SENT') + + + + UPDATE esn_update_queue SET retry_count = retry_count + 1 + WHERE id = #{id} + + + + + + + + + diff --git a/backend/src/main/resources/mapper/UserAuthMapper.xml b/backend/src/main/resources/mapper/UserAuthMapper.xml new file mode 100644 index 0000000..f492bc2 --- /dev/null +++ b/backend/src/main/resources/mapper/UserAuthMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + UPDATE esn_user SET last_login_at = NOW() WHERE username = #{username} + + + diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..3e0353b --- /dev/null +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_user (tenant_code, username, password_hash, role, email, phone, is_active) + VALUES (#{tenantCode}, #{username}, #{passwordHash}, #{role}, #{email}, #{phone}, #{active}) + + + + UPDATE esn_user SET + tenant_code = #{tenantCode}, + role = #{role}, + email = #{email}, + phone = #{phone}, + is_active = #{active} + WHERE id = #{id} + + + + UPDATE esn_user SET password_hash = #{passwordHash} WHERE id = #{id} + + + DELETE FROM esn_user WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/WorkMapper.xml b/backend/src/main/resources/mapper/WorkMapper.xml new file mode 100644 index 0000000..efc4a66 --- /dev/null +++ b/backend/src/main/resources/mapper/WorkMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_work_history (tenant_code, store_id, work_type, work_content, + worker_name, status, remarks, started_at) + VALUES (#{tenantCode}, #{storeId}, #{workType}, #{workContent}, + #{workerName}, #{status}, #{remarks}, #{startedAt}) + + + + UPDATE esn_work_history SET + work_type = #{workType}, + work_content = #{workContent}, + worker_name = #{workerName}, + status = #{status}, + remarks = #{remarks}, + started_at = #{startedAt}, + completed_at = #{completedAt} + WHERE id = #{id} + + + DELETE FROM esn_work_history WHERE id = #{id} + + + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c2d491d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + zioinfo-esn — ESL 통합 관리 플랫폼 + + + +

+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..634a0f7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3416 @@ +{ + "name": "zioinfo-esn-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zioinfo-esn-frontend", + "version": "1.0.0", + "dependencies": { + "@tanstack/react-query": "^5.0.0", + "axios": "^1.7.0", + "date-fns": "^3.6.0", + "lucide-react": "^0.400.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "recharts": "^2.12.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.5.0", + "vite": "^5.3.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "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": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/fast-glob/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/fastq": { + "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": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "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" + } + }, + "node_modules/is-glob": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "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" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.400.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.400.0.tgz", + "integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "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", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "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==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "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" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1e0ffb3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "zioinfo-esn-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite --port 3014", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "axios": "^1.7.0", + "@tanstack/react-query": "^5.0.0", + "recharts": "^2.12.0", + "lucide-react": "^0.400.0", + "date-fns": "^3.6.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.5.0", + "vite": "^5.3.0", + "tailwindcss": "^3.4.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/login.png b/frontend/public/login.png new file mode 100644 index 0000000000000000000000000000000000000000..329b27b1ede1178c17b22bab87e7011c35597be0 GIT binary patch literal 32760 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 literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..92f2120 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,49 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import Layout from './components/Layout' +import Login from './pages/Login' +import Dashboard from './pages/Dashboard' +import StoreList from './pages/StoreList' +import TemplateList from './pages/TemplateList' +import PosCvtList from './pages/PosCvtList' +import AlarmList from './pages/AlarmList' +import HCoreStatus from './pages/HCoreStatus' +import WorkHistory from './pages/WorkHistory' +import FirmwareList from './pages/FirmwareList' +import UserList from './pages/UserList' +import ProductList from './pages/ProductList' +import TenantAdmin from './pages/TenantAdmin' +import AiAnalysis from './pages/AiAnalysis' +import TagBindingList from './pages/TagBindingList' +import UpdateQueueList from './pages/UpdateQueueList' + +const qc = new QueryClient() + +export default function App() { + return ( + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + ) +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..90f339f --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,105 @@ +import axios from 'axios' + +const api = axios.create({ baseURL: '' }) + +api.interceptors.request.use(config => { + const token = localStorage.getItem('esn_token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +api.interceptors.response.use( + res => res, + err => { + if (err.response?.status === 401) { + localStorage.removeItem('esn_token') + if (location.pathname !== '/login') location.href = '/login' + } + return Promise.reject(err) + } +) + +export default api + +const unwrap = (p: Promise) => p.then(r => r.data?.data) + +// ── Auth ────────────────────────────────────────────────────────────────── +export const login = (username: string, password: string) => + api.post('/api/auth/login', { username, password }) +export const getMe = () => api.get('/api/auth/me') +export const logout = () => api.post('/api/auth/logout') + +// ── Dashboard ──────────────────────────────────────────────────────────── +export const getDashboard = (tenantCode?: string) => + unwrap(api.get(`/api/dashboard${tenantCode ? `?tenantCode=${tenantCode}` : ''}`)) + +// ── Tenants ────────────────────────────────────────────────────────────── +export const getTenants = () => unwrap(api.get('/api/tenants')) +export const createTenant = (d: object) => unwrap(api.post('/api/tenants', d)) +export const updateTenant = (id: number, d: object) => unwrap(api.put(`/api/tenants/${id}`, d)) +export const deleteTenant = (id: number) => api.delete(`/api/tenants/${id}`) + +// ── Stores ─────────────────────────────────────────────────────────────── +export const getStores = (params?: object) => unwrap(api.get('/api/stores', { params })) +export const getStore = (id: number) => unwrap(api.get(`/api/stores/${id}`)) +export const createStore = (d: object) => unwrap(api.post('/api/stores', d)) +export const updateStore = (id: number, d: object) => unwrap(api.put(`/api/stores/${id}`, d)) +export const deleteStore = (id: number) => api.delete(`/api/stores/${id}`) + +// ── Store Groups ───────────────────────────────────────────────────────── +export const getStoreGroups = (tenantCode?: string) => + unwrap(api.get('/api/store-groups', { params: { tenantCode } })) +export const createStoreGroup = (d: object) => unwrap(api.post('/api/store-groups', d)) +export const updateStoreGroup = (id: number, d: object) => unwrap(api.put(`/api/store-groups/${id}`, d)) +export const deleteStoreGroup = (id: number) => api.delete(`/api/store-groups/${id}`) + +// ── Templates ──────────────────────────────────────────────────────────── +export const getTemplates = (params?: object) => unwrap(api.get('/api/templates', { params })) +export const createTemplate = (d: object) => unwrap(api.post('/api/templates', d)) +export const updateTemplate = (id: number, d: object) => unwrap(api.put(`/api/templates/${id}`, d)) +export const deleteTemplate = (id: number) => api.delete(`/api/templates/${id}`) + +// ── POS CVT ────────────────────────────────────────────────────────────── +export const getPosCvt = (params?: object) => unwrap(api.get('/api/pos-cvt', { params })) +export const processPosCvt = (id: number) => unwrap(api.put(`/api/pos-cvt/${id}/process`)) +export const ignorePosCvt = (id: number) => unwrap(api.put(`/api/pos-cvt/${id}/ignore`)) + +// ── Alarms ─────────────────────────────────────────────────────────────── +export const getAlarms = (params?: object) => unwrap(api.get('/api/alarms', { params })) +export const createAlarm = (d: object) => unwrap(api.post('/api/alarms', d)) +export const resolveAlarm = (id: number, resolution: string) => + unwrap(api.put(`/api/alarms/${id}/resolve`, { resolution })) +export const deleteAlarm = (id: number) => api.delete(`/api/alarms/${id}`) + +// ── HCore ──────────────────────────────────────────────────────────────── +export const getHCore = (params?: object) => unwrap(api.get('/api/hcore', { params })) +export const updateHCoreStatus = (id: number, status: string) => + unwrap(api.put(`/api/hcore/${id}/status`, { status })) + +// ── Works ──────────────────────────────────────────────────────────────── +export const getWorks = (params?: object) => unwrap(api.get('/api/works', { params })) +export const createWork = (d: object) => unwrap(api.post('/api/works', d)) +export const updateWork = (id: number, d: object) => unwrap(api.put(`/api/works/${id}`, d)) +export const deleteWork = (id: number) => api.delete(`/api/works/${id}`) + +// ── Firmware ───────────────────────────────────────────────────────────── +export const getFirmware = (params?: object) => unwrap(api.get('/api/firmware', { params })) +export const createFirmware = (d: object) => unwrap(api.post('/api/firmware', d)) +export const deleteFirmware = (id: number) => api.delete(`/api/firmware/${id}`) + +// ── Users ──────────────────────────────────────────────────────────────── +export const getUsers = (params?: object) => unwrap(api.get('/api/users', { params })) +export const createUser = (d: object) => unwrap(api.post('/api/users', d)) +export const updateUser = (id: number, d: object) => unwrap(api.put(`/api/users/${id}`, d)) +export const deleteUser = (id: number) => api.delete(`/api/users/${id}`) + +// ── Products ───────────────────────────────────────────────────────────── +export const getProducts = (params?: object) => unwrap(api.get('/api/products', { params })) +export const createProduct = (d: object) => unwrap(api.post('/api/products', d)) +export const updateProduct = (id: number, d: object) => unwrap(api.put(`/api/products/${id}`, d)) +export const deleteProduct = (id: number) => api.delete(`/api/products/${id}`) + +// ── AI ─────────────────────────────────────────────────────────────────── +export const analyzeAlarm = (d: object) => unwrap(api.post('/api/ai/analyze-alarm', d)) +export const classifyPos = (d: object) => unwrap(api.post('/api/ai/classify-pos', d)) +export const aiChat = (message: string) => unwrap(api.post('/api/ai/chat', { message })) diff --git a/frontend/src/components/AlarmBadge.tsx b/frontend/src/components/AlarmBadge.tsx new file mode 100644 index 0000000..7309c5a --- /dev/null +++ b/frontend/src/components/AlarmBadge.tsx @@ -0,0 +1,16 @@ +interface AlarmBadgeProps { + severity: string +} + +export default function AlarmBadge({ severity }: AlarmBadgeProps) { + const map: Record = { + CRITICAL: 'bg-red-500/20 text-red-400 border border-red-500/40', + HIGH: 'bg-orange-500/20 text-orange-400 border border-orange-500/40', + MEDIUM: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/40', + LOW: 'bg-gray-500/20 text-gray-400 border border-gray-500/40', + } + const cls = map[severity] || map.LOW + return ( + {severity} + ) +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..d07103d --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,34 @@ +import { useNavigate } from 'react-router-dom' +import { LogOut, User } from 'lucide-react' + +export default function Header() { + const navigate = useNavigate() + const user = localStorage.getItem('esn_user') || 'admin' + const role = localStorage.getItem('esn_role') || 'ADMIN' + const tenant = localStorage.getItem('esn_tenant') || 'ALL' + + function handleLogout() { + localStorage.removeItem('esn_token') + localStorage.removeItem('esn_role') + localStorage.removeItem('esn_user') + localStorage.removeItem('esn_tenant') + navigate('/login') + } + + return ( +
+
+ 테넌트: {tenant} + | +
+ + {user} + {role} +
+ +
+ ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..ffddea7 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react' +import { Outlet, Navigate } from 'react-router-dom' +import Sidebar from './Sidebar' +import Header from './Header' +import { getMe } from '../api/client' + +export default function Layout() { + const token = localStorage.getItem('esn_token') + + useEffect(() => { + if (!token) return + getMe() + .then(r => { + const me = r.data?.data || {} + if (me.role) localStorage.setItem('esn_role', me.role) + if (me.username) localStorage.setItem('esn_user', me.username) + if (me.tenant) localStorage.setItem('esn_tenant', me.tenant) + }) + .catch(() => {}) + }, [token]) + + if (!token) return + + return ( +
+ +
+
+
+ +
+
+
+ ) +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..f704c69 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,55 @@ +import { NavLink } from 'react-router-dom' +import { + LayoutDashboard, Store, FileText, RefreshCw, + Bell, Cpu, ClipboardList, Zap, Users, Package, Building2, Brain, + Link2, ListOrdered +} from 'lucide-react' + +const nav = [ + { to: '/dashboard', icon: LayoutDashboard, label: '대시보드' }, + { to: '/stores', icon: Store, label: '매장 관리' }, + { to: '/templates', icon: FileText, label: 'ESL 템플릿' }, + { to: '/pos-cvt', icon: RefreshCw, label: 'POS 가격변환' }, + { to: '/tag-binding', icon: Link2, label: '태그 바인딩' }, + { to: '/update-queue',icon: ListOrdered, label: '업데이트 큐' }, + { to: '/alarms', icon: Bell, label: '알람 관리' }, + { to: '/hcore', icon: Cpu, label: 'HCore 장치' }, + { to: '/works', icon: ClipboardList, label: '작업 이력' }, + { to: '/firmware', icon: Zap, label: '펌웨어' }, + { to: '/products', icon: Package, label: '상품/가격' }, + { to: '/users', icon: Users, label: '사용자' }, + { to: '/tenants', icon: Building2, label: '테넌트 관리' }, + { to: '/ai', icon: Brain, label: 'AI 분석' }, +] + +export default function Sidebar() { + return ( + + ) +} diff --git a/frontend/src/components/StatCard.tsx b/frontend/src/components/StatCard.tsx new file mode 100644 index 0000000..7578432 --- /dev/null +++ b/frontend/src/components/StatCard.tsx @@ -0,0 +1,34 @@ +import { LucideIcon } from 'lucide-react' + +interface StatCardProps { + title: string + value: number | string + icon: LucideIcon + color?: string + sub?: string +} + +export default function StatCard({ title, value, icon: Icon, color = 'brand', sub }: StatCardProps) { + const colorMap: Record = { + brand: 'text-brand bg-brand/10', + green: 'text-green-400 bg-green-400/10', + red: 'text-red-400 bg-red-400/10', + orange: 'text-orange-400 bg-orange-400/10', + yellow: 'text-yellow-400 bg-yellow-400/10', + accent: 'text-accent bg-accent/10', + } + const cls = colorMap[color] || colorMap.brand + + return ( +
+
+ +
+
+

{title}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..ee31189 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-ink text-white; + font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: #131927; } +::-webkit-scrollbar-thumb { background: #26304a; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #00a0c8; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..520b520 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/frontend/src/pages/AiAnalysis.tsx b/frontend/src/pages/AiAnalysis.tsx new file mode 100644 index 0000000..9d8945d --- /dev/null +++ b/frontend/src/pages/AiAnalysis.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' +import { useMutation } from '@tanstack/react-query' +import { analyzeAlarm, classifyPos } from '../api/client' +import { Brain, Zap, AlertTriangle, Tag } from 'lucide-react' + +export default function AiAnalysis() { + const [alarmText, setAlarmText] = useState('') + const [alarmResult, setAlarmResult] = useState('') + const [posText, setPosText] = useState('') + const [posResult, setPosResult] = useState('') + + const alarmMut = useMutation({ + mutationFn: analyzeAlarm, + onSuccess: (data: any) => setAlarmResult(data.analysis || JSON.stringify(data)), + onError: () => setAlarmResult('Ollama 응답 실패 — 서비스를 확인해주세요.'), + }) + + const posMut = useMutation({ + mutationFn: classifyPos, + onSuccess: (data: any) => setPosResult(data.category || JSON.stringify(data)), + onError: () => setPosResult('Ollama 응답 실패 — 서비스를 확인해주세요.'), + }) + + return ( +
+

+ AI 분석 +

+ + {/* Alarm Analysis */} +
+

+ 알람 원인 분석 +

+