diff --git a/.claude/agents/esn-backend-dev.md b/.claude/agents/esn-backend-dev.md deleted file mode 100644 index beccab8..0000000 --- a/.claude/agents/esn-backend-dev.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -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 deleted file mode 100644 index 8a44df3..0000000 --- a/.claude/agents/esn-devops-dev.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -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 deleted file mode 100644 index 6c855d3..0000000 --- a/.claude/agents/esn-frontend-dev.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -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 deleted file mode 100644 index fce69ab..0000000 --- a/.claude/agents/esn-qa.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -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 deleted file mode 100644 index 746a736..0000000 --- a/.claude/skills/guardia-esn-orchestrator/SKILL.md +++ /dev/null @@ -1,118 +0,0 @@ -# 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 deleted file mode 100644 index de411e5..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,121 +0,0 @@ -# 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 index 578dfb9..cc99583 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,10 +2,10 @@ pipeline { agent any environment { - APP_NAME = 'guardia-esn' - DEPLOY_DIR = '/opt/guardia-esn' - SERVICE = 'guardia-esn' - JAR_NAME = 'zioinfo-esn-1.0.0.jar' + APP_NAME = 'zioinfo-esn' + DEPLOY_DIR = '/opt/zioinfo-esn/src' + SERVICE = 'zioinfo-esn' + JAR_NAME = 'esn.jar' } stages { @@ -18,7 +18,7 @@ pipeline { stage('Build Frontend') { steps { dir('frontend') { - sh 'npm ci --legacy-peer-deps' + sh 'npm ci' sh 'npm run build' } } @@ -36,8 +36,8 @@ pipeline { steps { sh """ sudo systemctl stop ${SERVICE} || true - sudo mkdir -p ${DEPLOY_DIR} - sudo cp backend/target/${JAR_NAME} ${DEPLOY_DIR}/app.jar + sudo mkdir -p ${DEPLOY_DIR}/backend/target + sudo cp backend/target/${JAR_NAME} ${DEPLOY_DIR}/backend/target/${JAR_NAME} sudo systemctl start ${SERVICE} """ } @@ -46,8 +46,8 @@ pipeline { stage('Health Check') { steps { sh """ - sleep 20 - curl -sf http://localhost:8016/actuator/health | grep -q UP || exit 1 + sleep 15 + curl -sf http://localhost:8015/actuator/health | grep -q UP || exit 1 """ } } @@ -55,11 +55,10 @@ pipeline { post { success { - echo "guardia-esn 배포 성공" + echo "zioinfo-esn 배포 성공" } failure { - sh "sudo systemctl stop ${SERVICE} || true" - echo "guardia-esn 배포 실패" + echo "zioinfo-esn 배포 실패 — 롤백 필요" } } } diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java index 54fded8..957e896 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java @@ -1,35 +1,22 @@ package com.zioinfo.esn.auth; import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.auth.TwoFactorService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.Map; -/** - * ESN 인증 컨트롤러. - * - /login: 2FA off 면 { twofa:"false", token, type }, 2FA on 이면 { twofa:"true", verifyToken, step, maskedEmail }. - * - /verify: (UIWS 2FA 이식) verify-token + 인증코드 → access 토큰 발급. - * 기존 클라이언트는 응답에 token 필드가 그대로 존재(2FA off 시) → 회귀 0. - */ @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthService authService; - private final TwoFactorService twoFactorService; @PostMapping("/login") public ApiResponse> login(@RequestBody LoginRequest req) { - return ApiResponse.ok(authService.login(req.username(), req.password())); - } - - /** UIWS 2FA 이식: 2차 인증 코드 검증 → access 토큰 발급. */ - @PostMapping("/verify") - public ApiResponse> verify(@RequestBody VerifyRequest req) { - return ApiResponse.ok(twoFactorService.verify(req.verifyToken(), req.code())); + String token = authService.login(req.username(), req.password()); + return ApiResponse.ok(Map.of("token", token, "type", "Bearer")); } @GetMapping("/me") @@ -45,6 +32,4 @@ public class AuthController { } record LoginRequest(String username, String password) {} - - record VerifyRequest(String verifyToken, String code) {} } diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java index 0f070a2..58a69bb 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java @@ -1,21 +1,12 @@ package com.zioinfo.esn.auth; import com.zioinfo.esn.auth.mapper.UserAuthMapper; -import com.zioinfo.esn.uiws.auth.TwoFactorService; -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Map; -/** - * ESN 인증 서비스. - * - 기존 단일 JWT 로그인 보존(2FA off 또는 미설정 시 회귀 0). - * - UIWS 2FA 레이어(이메일 코드) 추가: 2FA on 이면 1차 통과 후 verify-token + 이메일코드 발급. - * 실패 누적 max-login-fail 회 시 계정 잠금. - */ @Service @RequiredArgsConstructor public class AuthService { @@ -23,52 +14,17 @@ public class AuthService { private final UserAuthMapper userMapper; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; - private final TwoFactorService twoFactorService; - /** - * 1차 로그인. 2FA 활성 시 verify-token + 이메일코드 흐름으로 분기, - * 비활성 시 기존처럼 access 토큰 즉시 발급. - * - * @return 2FA off: { token, type, twofa:"false" } - * 2FA on : { verifyToken, step:"EMAIL", maskedEmail, twofa:"true" } - */ - public Map login(String username, String password) { + public String login(String username, String password) { EsnUser user = userMapper.findByUsername(username); - - // 잠금 우선 차단(존재하는 계정에 한해 잠금 메시지 — 존재 여부 누설 최소화) - if (user != null && twoFactorService.isLocked(user)) { - throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED); - } if (user == null || !user.isActive()) { throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정"); } if (!passwordEncoder.matches(password, user.getPasswordHash())) { - // 2FA 활성 시 실패 누적/잠금. 비활성 시 기존 동작(메시지만) 유지. - if (twoFactorService.isEnabled()) { - twoFactorService.recordLoginFailure(username); - EsnUser after = userMapper.findByUsername(username); - if (after != null && Boolean.TRUE.equals(after.getLocked())) { - throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED); - } - } throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치"); } - - // 비밀번호 검증 통과 - if (twoFactorService.isEnabled()) { - Map step1 = twoFactorService.beginTwoFactor(user); - return Map.of( - "twofa", "true", - "verifyToken", step1.get("verifyToken"), - "step", step1.get("step"), - "maskedEmail", step1.getOrDefault("maskedEmail", "")); - } - - // 2FA 비활성 — 기존 단일 로그인 흐름(회귀 0) - userMapper.resetLoginFail(username); userMapper.updateLastLogin(username); - String token = jwtUtil.generate(username, user.getRole(), user.getTenantCode()); - return Map.of("twofa", "false", "token", token, "type", "Bearer"); + return jwtUtil.generate(username, user.getRole(), user.getTenantCode()); } public Map me(String 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 index 0c6ccb7..dc6b130 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java @@ -18,19 +18,4 @@ public class EsnUser { private boolean active; private LocalDateTime lastLoginAt; private LocalDateTime createdAt; - - // ── UIWS 2FA 이식 컬럼 (esn_user ALTER, db/91_uiws_port.sql) ───────────────── - /** 이메일 인증코드(6자리). 발급 후 verify 단계에서 검증. API 응답에는 절대 미포함. */ - @JsonIgnore - private String emailVerifyCode; - /** 인증코드 만료시각. */ - @JsonIgnore - private LocalDateTime emailVerifyExpire; - /** 로그인 실패 누적 횟수(기본 0). max-login-fail 도달 시 locked. */ - private Integer loginFailCount; - /** 계정 잠금 여부(기본 false). */ - private Boolean locked; - /** TOTP 시크릿(UIWS OTP 경로 대비, 현재 이메일 흐름에서는 미사용). */ - @JsonIgnore - private String otpSecret; } diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java index 8f4b280..d8c39ee 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java @@ -26,10 +26,7 @@ public class JwtFilter extends OncePerRequestFilter { String header = req.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); - // 보안(★치명 — UIWS 2FA): purpose=2fa verify-token 은 access 토큰이 아니다. - // 동일 서명키라 isValid()는 통과하므로 별도 차단하지 않으면 2차 인증 전 보호 API 접근(2FA 완전 우회)이 가능. - // → verify-token 은 인증 컨텍스트를 세우지 않고 무시한다(/api/auth/verify 에서만 사용). - if (jwtUtil.isValid(token) && !jwtUtil.isVerifyToken(token)) { + if (jwtUtil.isValid(token)) { String username = jwtUtil.getUsername(token); String role = jwtUtil.getRole(token); var auth = new UsernamePasswordAuthenticationToken( diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java index dadcd1b..06d23f0 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java @@ -35,48 +35,6 @@ public class JwtUtil { .compact(); } - /** - * UIWS 2FA 이식: 1차 로그인 통과 후 발급하는 단기 verify-token. - * purpose=2fa 클레임으로 access 토큰과 구분(verify-token 으로는 보호 API 접근 불가 — JwtFilter 에서 차단). - */ - public String generateVerifyToken(String username, long validitySeconds) { - return Jwts.builder() - .subject(username) - .claim("purpose", "2fa") - .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L)) - .signWith(key()) - .compact(); - } - - /** verify-token 검증 후 username 반환. 유효하지 않거나 purpose!=2fa 면 null. */ - public String parseVerifyTokenUsername(String token) { - try { - Claims c = parse(token); - if (!"2fa".equals(c.get("purpose", String.class))) { - return null; - } - return c.getSubject(); - } catch (JwtException | IllegalArgumentException e) { - log.debug("verify-token 검증 실패: {}", e.getMessage()); - return null; - } - } - - /** - * 보안(★치명): 토큰이 2FA verify-token(purpose=2fa)인지 판별. - * verify-token 은 access 와 같은 서명키라 isValid()는 통과하므로, JwtFilter 가 별도로 - * 걸러내지 않으면 2차 코드 검증 없이 보호 API 접근(2FA 완전 우회)이 된다. - * → JwtFilter 는 purpose=2fa 토큰을 인증 컨텍스트로 세우지 않는다(/api/auth/verify 에서만 사용). - */ - public boolean isVerifyToken(String token) { - try { - return "2fa".equals(parse(token).get("purpose", String.class)); - } catch (JwtException | IllegalArgumentException e) { - return false; - } - } - public Claims parse(String token) { return Jwts.parser().verifyWith(key()).build() .parseSignedClaims(token).getPayload(); 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 index 9f83a8c..f834e7d 100644 --- a/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java +++ b/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java @@ -4,29 +4,8 @@ import com.zioinfo.esn.auth.EsnUser; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; -import java.time.LocalDateTime; - @Mapper public interface UserAuthMapper { EsnUser findByUsername(@Param("username") String username); int updateLastLogin(@Param("username") String username); - - // ── UIWS 2FA 이식: 실패 카운트/잠금/인증코드 갱신 (멱등 UPDATE) ─────────────── - - /** 로그인 성공 시 실패 카운트 초기화. */ - int resetLoginFail(@Param("username") String username); - - /** 로그인 실패 누적(+1) 및 임계 도달 시 잠금. */ - int incrementLoginFail(@Param("username") String username, @Param("maxFail") int maxFail); - - /** 1차 통과 시 이메일 인증코드/만료 저장(verify 단계에서 검증). */ - int saveEmailCode(@Param("username") String username, - @Param("code") String code, - @Param("expire") LocalDateTime expire); - - /** 2차 검증 성공 시 코드 폐기. */ - int clearEmailCode(@Param("username") String username); - - /** 관리자 잠금 해제(실패 카운트/잠금 초기화). */ - int unlock(@Param("username") String username); } diff --git a/backend/src/main/java/com/zioinfo/esn/common/GlobalExceptionHandler.java b/backend/src/main/java/com/zioinfo/esn/common/GlobalExceptionHandler.java deleted file mode 100644 index 02e872b..0000000 --- a/backend/src/main/java/com/zioinfo/esn/common/GlobalExceptionHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.zioinfo.esn.common; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -/** - * 전역 예외 처리(보안 불변규칙: 스택트레이스/SQL/자격증명 미노출). - * - *

UIWS 이식 모듈 + 2FA 레이어 도입과 함께 추가. 기존 ESN 컨트롤러도 동일 정책으로 보호한다 - * (예외 메시지는 사용자 노출용 요약만 — 내부 원인은 서버 로그에만 남긴다). - */ -@Slf4j -@RestControllerAdvice -public class GlobalExceptionHandler { - - /** UIWS 이식/2FA 도메인 예외 → 400 + 안전한 사용자 메시지. */ - @ExceptionHandler(UiwsApiException.class) - public ResponseEntity> handleUiws(UiwsApiException e) { - log.debug("[UIWS] {}: {}", e.getErrorCode().getCode(), e.getMessage()); - return ResponseEntity.badRequest().body(ApiResponse.fail(e.getMessage())); - } - - /** 요청 검증 실패 → 400 + 필드 요약(스택트레이스 없음). */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { - String msg = e.getBindingResult().getFieldErrors().stream() - .findFirst() - .map(fe -> fe.getDefaultMessage()) - .orElse("요청 값이 올바르지 않습니다."); - return ResponseEntity.badRequest().body(ApiResponse.fail(msg)); - } - - /** 권한 거부 → 403 (RBAC 가드). */ - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity> handleDenied(AccessDeniedException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.fail("접근 권한이 없습니다.")); - } - - /** - * 그 외 모든 예외 → 400 + 요약 메시지(기존 ESN 의 RuntimeException("ERR-AUTH-...") 흐름 보존). - * 스택트레이스/원인은 서버 로그에만 기록하고 응답에는 노출하지 않는다(보안 불변규칙). - */ - @ExceptionHandler(RuntimeException.class) - public ResponseEntity> handleRuntime(RuntimeException e, HttpServletRequest req) { - log.warn("[ERR] {} {} — {}", req.getMethod(), req.getRequestURI(), e.getMessage()); - String msg = e.getMessage() != null ? e.getMessage() : "요청 처리 중 오류가 발생했습니다."; - return ResponseEntity.badRequest().body(ApiResponse.fail(msg)); - } - - /** 그 외 체크 예외/오류 → 500 + 요약(상세 미노출). */ - @ExceptionHandler(Exception.class) - public ResponseEntity> handleOther(Exception e, HttpServletRequest req) { - log.error("[ERR-500] {} {}", req.getMethod(), req.getRequestURI(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.fail("서버 처리 중 오류가 발생했습니다.")); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java index 58c1e7f..a907d38 100644 --- a/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java +++ b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java @@ -51,7 +51,7 @@ public class OllamaClient { .uri(URI.create(baseUrl + "/api/chat")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) - .timeout(Duration.ofSeconds(120)) + .timeout(Duration.ofSeconds(30)) .build(); HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() == 200) { diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/auth/LoginVerifyMapper.java b/backend/src/main/java/com/zioinfo/esn/uiws/auth/LoginVerifyMapper.java deleted file mode 100644 index b36d75a..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/auth/LoginVerifyMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.zioinfo.esn.uiws.auth; - -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Update; - -/** - * 2FA 로그인 검증 이력 매퍼 (tb_uiws_login_verify). 발급/검증 감사 기록. - * verify_code 는 감사 이력에만 남고 API 응답에는 절대 노출하지 않는다. - */ -@Mapper -public interface LoginVerifyMapper { - - int insert(UiwsLoginVerify v); - - /** 동일 user 의 미검증 EMAIL 이력 중 최신 1건을 검증완료(Y) 처리. */ - @Update(""" - UPDATE tb_uiws_login_verify - SET verified_yn = 'Y', updated_by = #{userId}, updated_at = now() - WHERE verify_id = ( - SELECT verify_id FROM tb_uiws_login_verify - WHERE user_id = #{userId} AND verify_method = 'EMAIL' AND verified_yn = 'N' - ORDER BY verify_id DESC LIMIT 1 - ) - """) - int markLatestVerified(String userId); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/auth/TwoFactorService.java b/backend/src/main/java/com/zioinfo/esn/uiws/auth/TwoFactorService.java deleted file mode 100644 index 8981773..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/auth/TwoFactorService.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.zioinfo.esn.uiws.auth; - -import com.zioinfo.esn.auth.EsnUser; -import com.zioinfo.esn.auth.JwtUtil; -import com.zioinfo.esn.auth.mapper.UserAuthMapper; -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.common.mail.MailSender; -import com.zioinfo.esn.uiws.config.UiwsProperties; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.util.Map; - -/** - * UIWS 2FA(이메일 코드) 레이어. ESN 기존 단일 로그인을 보존하면서 2단계 인증을 추가한다. - * - * 흐름: - * 1) 1차 로그인 성공 → {@link #beginTwoFactor}: verify-token 발급 + 이메일 인증코드 발송(LogMailSender 폴백) + 감사 기록. - * 2) {@code POST /api/auth/verify}(verifyToken + code) → {@link #verify}: 코드 검증 후 access 발급. - * 3) 로그인 실패 누적 max-login-fail 회 → 계정 잠금({@link #recordLoginFailure}). - * - * 보안: - * - 인증코드는 메일/감사 채널로만 전달. API 응답·로그 메시지에 코드/비밀번호/자격증명 절대 미노출(불변규칙). - * - 외부 API 호출 없음(MailSender 폴백은 로그). Ollama 외 외부 통신 금지 준수. - * - access 토큰은 ESN JwtUtil(username/role/tenantCode 3-클레임) 정책을 그대로 재사용한다. - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class TwoFactorService { - - private static final SecureRandom RANDOM = new SecureRandom(); - private static final String SYSTEM = "SYSTEM"; - - private final UserAuthMapper userMapper; - private final LoginVerifyMapper loginVerifyMapper; - private final JwtUtil jwtUtil; - private final MailSender mailSender; - private final UiwsProperties properties; - - public boolean isEnabled() { - return properties.getAuth().isTwofaEnabled(); - } - - /** 잠금 여부(1차 로그인 전 차단용). */ - public boolean isLocked(EsnUser user) { - return Boolean.TRUE.equals(user.getLocked()); - } - - /** - * 1차 로그인 성공 후 2차 인증 시작: verify-token 발급 + 이메일코드 발송 + 감사 기록 + 실패카운트 초기화. - * @return { verifyToken, step:"EMAIL", maskedEmail } - */ - @Transactional - public Map beginTwoFactor(EsnUser user) { - long codeValidity = properties.getAuth().getEmailCodeValiditySeconds(); - long tokenValidity = properties.getAuth().getVerifyTokenValiditySeconds(); - - String code = String.format("%06d", RANDOM.nextInt(1_000_000)); - LocalDateTime expire = LocalDateTime.now().plusSeconds(codeValidity); - - // user 테이블에 코드/만료 저장(실패카운트 초기화) + 감사 이력 기록 - userMapper.saveEmailCode(user.getUsername(), code, expire); - recordVerifyAttempt(user.getUsername(), code, expire); - - // 이메일 발송(미설정 환경은 LogMailSender 폴백). 코드는 메일 본문에만. - String subject = "[GUARDiA ESN] 로그인 2차 인증 코드"; - String body = String.format( - "안녕하세요 %s 님,\n로그인 2차 인증 코드는 [%s] 입니다.\n유효시간: %d초", - user.getUsername(), code, codeValidity); - if (user.getEmail() != null && !user.getEmail().isBlank()) { - mailSender.send(user.getEmail(), subject, body); - } else { - log.warn("[2FA] no email for user={} — code logged only", user.getUsername()); - } - - String verifyToken = jwtUtil.generateVerifyToken(user.getUsername(), tokenValidity); - // 응답에는 코드 미포함 — verifyToken/step/maskedEmail 만. - return Map.of( - "verifyToken", verifyToken, - "step", "EMAIL", - "maskedEmail", maskEmail(user.getEmail())); - } - - /** - * 2차 검증: verify-token + code 검증 → access 발급. 코드 폐기 + 감사 이력 검증완료. - * @return { token, type, username, role, tenant } - */ - @Transactional - public Map verify(String verifyToken, String code) { - String username = jwtUtil.parseVerifyTokenUsername(verifyToken); - if (username == null) { - throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID); - } - EsnUser user = userMapper.findByUsername(username); - if (user == null) { - throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID); - } - if (Boolean.TRUE.equals(user.getLocked())) { - throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED); - } - - boolean codeOk = user.getEmailVerifyCode() != null - && user.getEmailVerifyCode().equals(code) - && user.getEmailVerifyExpire() != null - && user.getEmailVerifyExpire().isAfter(LocalDateTime.now()); - if (!codeOk) { - throw new UiwsApiException(UiwsErrorCode.VERIFY_CODE_INVALID); - } - - // 코드 폐기 + 감사 검증완료 + 마지막 로그인 갱신 - userMapper.clearEmailCode(username); - loginVerifyMapper.markLatestVerified(username); - userMapper.updateLastLogin(username); - - String access = jwtUtil.generate(user.getUsername(), user.getRole(), user.getTenantCode()); - return Map.of( - "token", access, - "type", "Bearer", - "username", user.getUsername(), - "role", user.getRole() == null ? "" : user.getRole(), - "tenant", user.getTenantCode() == null ? "" : user.getTenantCode()); - } - - /** 로그인 비밀번호 실패 시 누적/잠금 처리. */ - @Transactional - public void recordLoginFailure(String username) { - userMapper.incrementLoginFail(username, properties.getAuth().getMaxLoginFail()); - } - - private void recordVerifyAttempt(String username, String code, LocalDateTime expire) { - UiwsLoginVerify v = new UiwsLoginVerify(); - v.setUserId(username); - v.setVerifyMethod("EMAIL"); - v.setVerifyCode(code); - v.setExpireAt(expire); - v.setVerifiedYn("N"); - v.setCreatedBy(SYSTEM); - v.setCreatedAt(LocalDateTime.now()); - loginVerifyMapper.insert(v); - } - - /** 이메일 마스킹(자격증명 보호): ab****@domain. */ - private static String maskEmail(String email) { - if (email == null || email.isBlank() || !email.contains("@")) { - return ""; - } - int at = email.indexOf('@'); - String local = email.substring(0, at); - String domain = email.substring(at); - if (local.length() <= 2) { - return local.charAt(0) + "*" + domain; - } - return local.substring(0, 2) + "*".repeat(Math.max(1, local.length() - 2)) + domain; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/auth/UiwsLoginVerify.java b/backend/src/main/java/com/zioinfo/esn/uiws/auth/UiwsLoginVerify.java deleted file mode 100644 index 14153b1..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/auth/UiwsLoginVerify.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zioinfo.esn.uiws.auth; - -import lombok.Data; -import java.time.LocalDateTime; - -/** 2FA 로그인 검증 이력 (tb_uiws_login_verify). 원본 com.urp.uiws.domain.LoginVerify 이식. */ -@Data -public class UiwsLoginVerify { - private Long verifyId; - private String userId; // ERP username 논리참조 - private String verifyMethod; // EMAIL | OTP - private String verifyCode; - private LocalDateTime expireAt; - private String verifiedYn; // Y | N - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsApiException.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsApiException.java deleted file mode 100644 index 73ac655..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsApiException.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zioinfo.esn.uiws.common; - -import lombok.Getter; - -/** - * UIWS 이식 모듈 업무 예외. RuntimeException 을 상속하여 ERP 기존 GlobalExceptionHandler - * (RuntimeException → 400 + message, 스택트레이스 미노출)에 그대로 포착된다. - * 추가 인프라/빈 없이 ERP 공통 에러 처리와 정합한다. - */ -@Getter -public class UiwsApiException extends RuntimeException { - - private final UiwsErrorCode errorCode; - - public UiwsApiException(UiwsErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; - } - - public UiwsApiException(UiwsErrorCode errorCode, String detail) { - super(detail); - this.errorCode = errorCode; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsCurrentUser.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsCurrentUser.java deleted file mode 100644 index 185c1b2..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsCurrentUser.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.zioinfo.esn.uiws.common; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -/** - * 현재 인증 사용자 식별자(=ERP username) 추출 헬퍼. 감사 컬럼(created_by/updated_by) 및 - * 소유자(writer_id/owner_id/sender_id) 기록에 사용한다. - * - * ERP JwtFilter 는 인증 principal 로 username(String)을 세팅하므로 그 값을 그대로 사용한다. - * (UIWS 원본의 CurrentUser/UserPrincipal 패턴을 ERP 인증 모델에 맞게 단순화 이식.) - */ -public final class UiwsCurrentUser { - - private static final String SYSTEM = "SYSTEM"; - - private UiwsCurrentUser() { - } - - /** 현재 사용자 ID(username). 미인증/시스템 컨텍스트는 "SYSTEM". */ - public static String id() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof String username - && !"anonymousUser".equals(username)) { - return username; - } - return SYSTEM; - } - - /** ADMIN 권한 보유 여부(데이터 가시범위 판정용 — ADMIN=전체). */ - public static boolean isAdmin() { - return hasRole("ADMIN"); - } - - /** MANAGER 이상(MANAGER/ADMIN) 여부 — 업무일지 댓글 권한 등에 사용. */ - public static boolean isManagerOrAbove() { - return hasRole("ADMIN") || hasRole("MANAGER") || hasRole("CFO"); - } - - private static boolean hasRole(String role) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth == null) { - return false; - } - return auth.getAuthorities().stream() - .anyMatch(a -> ("ROLE_" + role).equals(a.getAuthority())); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsDataScope.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsDataScope.java deleted file mode 100644 index 2240ee7..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsDataScope.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zioinfo.esn.uiws.common; - -import java.util.List; - -/** - * 데이터 가시범위(스코프) — UIWS DataScopeService 의 silo 단순화 이식. - * 원본은 부서계층(TB_DEPT) 기반 팀 스코프를 산출하나, ERP 이식본은 코어 dept 테이블을 이식하지 않으므로 - * 역할만으로 판정한다: - * - ADMIN → 전체(all=true) - * - 그 외 → 본인 데이터만(ownerIds = [본인]) - * (부서계층 스코프가 필요해지면 ERP tb_department 연계로 확장 — 후속 트랙.) - */ -public final class UiwsDataScope { - - private UiwsDataScope() { - } - - /** all=true 면 전체 조회(ownerIds 무시). ownerIds 는 항상 본인 포함(빈 IN 회피). */ - public record Scope(boolean all, List ownerIds) { - } - - public static Scope current() { - String me = UiwsCurrentUser.id(); - if (UiwsCurrentUser.isAdmin()) { - return new Scope(true, List.of(me)); - } - return new Scope(false, List.of(me)); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsErrorCode.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsErrorCode.java deleted file mode 100644 index 972fb89..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/UiwsErrorCode.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.zioinfo.esn.uiws.common; - -import lombok.Getter; - -/** - * UIWS 이식 모듈 도메인 오류 코드. - * 원본 com.urp.uiws.common.exception.ErrorCode 의 worklog/schedule/message/auth(2FA) 영역 발췌·이식. - * 메시지는 사용자 노출용 — 스택트레이스/SQL 등 민감정보는 절대 포함하지 않는다(보안 불변규칙). - */ -@Getter -public enum UiwsErrorCode { - - // 공통 - INVALID_REQUEST("ERR-UIWS-400", "요청이 올바르지 않습니다."), - FORBIDDEN("ERR-UIWS-403", "접근 권한이 없습니다."), - NOT_FOUND("ERR-UIWS-404", "대상을 찾을 수 없습니다."), - - // worklog - WORKLOG_NOT_FOUND("ERR-UIWS-WL-404", "업무일지를 찾을 수 없습니다."), - WORKLOG_TIME_OVERLAP("ERR-UIWS-WL-409", "동일 일지 내 시간대가 중복됩니다."), - WORKLOG_TIME_INVALID("ERR-UIWS-WL-422", "근무 시작/종료 시간이 올바르지 않습니다."), - - // schedule - SCHEDULE_NOT_FOUND("ERR-UIWS-SC-404", "일정을 찾을 수 없습니다."), - SCHEDULE_DT_INVALID("ERR-UIWS-SC-422", "일정 시작/종료 일시가 올바르지 않습니다."), - DIARY_NOT_FOUND("ERR-UIWS-DI-404", "일지를 찾을 수 없습니다."), - ATTACH_NOT_FOUND("ERR-UIWS-AT-404", "첨부파일을 찾을 수 없습니다."), - ATTACH_REF_TYPE_INVALID("ERR-UIWS-AT-422", "첨부 대상 유형은 SCHEDULE 또는 DIARY 여야 합니다."), - FILE_EMPTY("ERR-UIWS-FILE-400", "업로드할 파일이 비어 있습니다."), - FILE_STORAGE_ERROR("ERR-UIWS-FILE-500", "파일 저장 중 오류가 발생했습니다."), - - // message - MESSAGE_NOT_FOUND("ERR-UIWS-MSG-404", "쪽지를 찾을 수 없습니다."), - MESSAGE_RCV_TYPE_INVALID("ERR-UIWS-MSG-422", "수신구분은 RECV(수신) 또는 REF(참조) 여야 합니다."), - - // auth (2FA) - VERIFY_TOKEN_INVALID("ERR-UIWS-2FA-401", "2차 검증 토큰이 유효하지 않거나 만료되었습니다."), - VERIFY_CODE_INVALID("ERR-UIWS-2FA-401C", "인증 코드가 올바르지 않거나 만료되었습니다."), - ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요."); - - private final String code; - private final String message; - - UiwsErrorCode(String code, String message) { - this.code = code; - this.message = message; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/LogMailSender.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/LogMailSender.java deleted file mode 100644 index 0d82c7b..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/LogMailSender.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.zioinfo.esn.uiws.common.mail; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -/** - * 로컬/개발/미설정용 메일 발송 폴백. 실제 전송 없이 로그로만 기록한다(외부 호출 0). - * esn.uiws.mail.mode=log (기본) 일 때 활성. SMTP 운영 시 mode=smtp 로 별도 구현 빈 활성화. - * - * 보안: 본문에 인증코드/임시비밀번호가 포함되므로 로그 레벨은 운영에서 조정. - * (인증코드는 API 응답으로는 절대 반환하지 않는다 — 메일/로그 채널로만 전달.) - */ -@Slf4j -@Component -@ConditionalOnProperty(name = "esn.uiws.mail.mode", havingValue = "log", matchIfMissing = true) -public class LogMailSender implements MailSender { - - @Override - public void send(String to, String subject, String body) { - log.info("[UIWS-MAIL:LOG] to={} subject={}\n{}", to, subject, body); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/MailSender.java b/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/MailSender.java deleted file mode 100644 index 6ac1596..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/common/mail/MailSender.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zioinfo.esn.uiws.common.mail; - -/** - * 메일 발송 추상화(UIWS 이식). 로컬/미설정 환경은 LogMailSender(로그만), 운영은 SMTP 구현으로 교체. - * 외부 API 호출은 하지 않는다(보안 불변규칙 — Ollama 외 외부 호출 금지). - */ -public interface MailSender { - - void send(String to, String subject, String body); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/config/UiwsProperties.java b/backend/src/main/java/com/zioinfo/esn/uiws/config/UiwsProperties.java deleted file mode 100644 index c52855c..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/config/UiwsProperties.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.zioinfo.esn.uiws.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * UIWS 이식 모듈 설정. application.yml 의 esn.uiws.* 바인딩. - * 계획서 2FA 설정키(verify-token-validity / max-login-fail / email-code-validity) + - * 첨부 업로드 디렉터리. 모두 안전 기본값 보유(미설정이어도 동작). - */ -@Getter -@Setter -@Component -@ConfigurationProperties(prefix = "esn.uiws") -public class UiwsProperties { - - private final Auth auth = new Auth(); - private final Upload upload = new Upload(); - - @Getter - @Setter - public static class Auth { - /** 2FA 레이어 on/off. 기본 on(켜짐). off 시 기존 단일 JWT 로그인 흐름 유지(회귀 0). */ - private boolean twofaEnabled = true; - /** 1차 통과 후 verify-token 유효시간(초). 계획서 300(5분). */ - private long verifyTokenValiditySeconds = 300; - /** 이메일 인증코드 유효시간(초). 계획서 300(5분). */ - private long emailCodeValiditySeconds = 300; - /** 로그인 실패 누적 N회 시 계정 잠금. 계획서 5. */ - private int maxLoginFail = 5; - } - - @Getter - @Setter - public static class Upload { - /** 첨부파일 저장 루트. 기본 ./uploads/uiws. */ - private String uploadDir = "./uploads/uiws"; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/controller/MessageController.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/controller/MessageController.java deleted file mode 100644 index cf626c7..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/controller/MessageController.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.zioinfo.esn.uiws.message.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.message.dto.MessageDtos.*; -import com.zioinfo.esn.uiws.message.service.MessageService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; - -/** - * UIWS 이식 — 쪽지(모듈 03). 인증 필수(/api/messages). 원본 prefix·엔드포인트 보존. - * Spring Data Pageable 의존 회피: page/size 단순 파라미터로 이식(ERP는 spring-data-web 미사용). - */ -@RestController -@RequestMapping("/api/messages") -@RequiredArgsConstructor -public class MessageController { - - private final MessageService messageService; - - @PostMapping - public ApiResponse send(@Valid @RequestBody MessageSendDto dto) { - return ApiResponse.ok(messageService.send(dto)); - } - - @GetMapping("/sent") - public ApiResponse sent( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String titleKeyword, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - return ApiResponse.ok(messageService.listSent(fromDate, toDate, titleKeyword, page, size)); - } - - @GetMapping("/sent/{id}") - public ApiResponse sentDetail(@PathVariable("id") Long id) { - return ApiResponse.ok(messageService.sentDetail(id)); - } - - @DeleteMapping("/sent") - public ApiResponse deleteSent(@Valid @RequestBody MessageIdsRequest req) { - messageService.deleteSent(req.ids()); - return ApiResponse.ok(null); - } - - @GetMapping("/unread-count") - public ApiResponse unreadCount() { - return ApiResponse.ok(messageService.unreadCount()); - } - - @GetMapping("/received") - public ApiResponse received( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String titleKeyword, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - return ApiResponse.ok(messageService.listReceived(fromDate, toDate, titleKeyword, page, size)); - } - - @GetMapping("/received/{id}") - public ApiResponse receivedDetail(@PathVariable("id") Long id) { - return ApiResponse.ok(messageService.receivedDetail(id)); - } - - @DeleteMapping("/received") - public ApiResponse deleteReceived(@Valid @RequestBody MessageIdsRequest req) { - messageService.deleteReceived(req.ids()); - return ApiResponse.ok(null); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/dto/MessageDtos.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/dto/MessageDtos.java deleted file mode 100644 index 500555f..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/dto/MessageDtos.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.zioinfo.esn.uiws.message.dto; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; - -import java.util.List; - -/** - * 쪽지(모듈 03) 요청/응답 DTO 모음. 원본 com.urp.uiws.message.dto.* 이식(필드·shape 동일). - * 프론트 경계면 계약 유지: API 응답 필드명 그대로 보존. - */ -public final class MessageDtos { - - private MessageDtos() { - } - - /** 전송/답장 요청. */ - public record MessageSendDto( - @NotBlank String title, - @NotBlank String content, - Long refWorklogId, - Long replyToId, - @Valid @NotEmpty(message = "수신자는 1명 이상이어야 합니다.") List receivers - ) { - } - - /** 수신자 1건. rcvType: RECV(수신) | REF(참조). */ - public record MessageReceiverDto( - @NotBlank String receiverId, - @NotBlank String rcvType - ) { - } - - /** 전송 응답. */ - public record MessageSendResponse(Long messageId) { - } - - /** 보낸쪽지함 행. */ - public record SentMessageDto( - Long messageId, - String title, - String receiverSummary, - long openCount, - long totalCount, - String sentAt - ) { - } - - /** 보낸쪽지 상세(수신자 개봉현황). */ - public record SentMessageDetailDto( - Long messageId, - String title, - String content, - Long refWorklogId, - String sentAt, - List receivers, - long totalCount, - long openCount, - long unopenCount - ) { - } - - /** 수신자별 개봉현황. */ - public record ReceiverStatusDto( - String receiverNm, - String rcvType, - String readYn, - String readAt - ) { - } - - /** 받은쪽지함 행. */ - public record ReceivedMessageDto( - Long messageId, - String title, - String senderNm, - String sentAt, - String readYn - ) { - } - - /** 받은쪽지 상세(조회 시 개봉처리). */ - public record ReceivedMessageDetailDto( - Long messageId, - String title, - String content, - String senderNm, - String sentAt, - String receivedAt, - Long refWorklogId - ) { - } - - /** 다중삭제 본문. */ - public record MessageIdsRequest( - @NotEmpty(message = "ids는 필수입니다.") List ids - ) { - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/mapper/MessageMapper.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/mapper/MessageMapper.java deleted file mode 100644 index 58ecebd..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/mapper/MessageMapper.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.zioinfo.esn.uiws.message.mapper; - -import com.zioinfo.esn.uiws.message.model.UiwsMessage; -import com.zioinfo.esn.uiws.message.model.UiwsMessageRcv; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * 쪽지 MyBatis 매퍼. 원본 JPA MessageRepository/MessageRcvRepository → MyBatis 변환 이식. - * 사용자명은 esn_user(username 라벨) 조인으로 라벨화(코어 user 재사용, 별도 user 테이블 미이식). - */ -@Mapper -public interface MessageMapper { - - // ── message 헤더 - int insertMessage(UiwsMessage m); - - UiwsMessage findMessageById(@Param("messageId") Long messageId); - - boolean existsMessageById(@Param("messageId") Long messageId); - - /** 보낸쪽지 페이징(SENDER_DEL_YN='N' 제외). */ - List searchSent(@Param("senderId") String senderId, - @Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("titleKeyword") String titleKeyword, - @Param("limit") int limit, - @Param("offset") int offset); - - long countSent(@Param("senderId") String senderId, - @Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("titleKeyword") String titleKeyword); - - /** 보낸쪽지 다중 소프트삭제(본인 것만). */ - int softDeleteSent(@Param("senderId") String senderId, - @Param("ids") List ids, - @Param("actor") String actor); - - // ── 수신자 - int insertRcv(UiwsMessageRcv rcv); - - List findRcvByMessageId(@Param("messageId") Long messageId); - - List findRcvByMessageIds(@Param("ids") List ids); - - UiwsMessageRcv findRcvByMessageAndReceiver(@Param("messageId") Long messageId, - @Param("receiverId") String receiverId); - - long countUnread(@Param("receiverId") String receiverId); - - /** 받은쪽지 개봉처리(READ_YN='Y', READ_AT). */ - int markRead(@Param("rcvId") Long rcvId, @Param("actor") String actor, @Param("readAt") LocalDateTime readAt); - - int softDeleteReceived(@Param("receiverId") String receiverId, - @Param("ids") List ids, - @Param("actor") String actor); - - /** 받은쪽지 목록(조인: message + rcv). 행: messageId,title,senderId,sentAt,readYn. */ - List> searchReceived(@Param("receiverId") String receiverId, - @Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("titleKeyword") String titleKeyword, - @Param("limit") int limit, - @Param("offset") int offset); - - long countReceived(@Param("receiverId") String receiverId, - @Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("titleKeyword") String titleKeyword); - - // ── 사용자명 라벨(코어 user 재사용) - List> findUserNames(@Param("ids") List ids); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessage.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessage.java deleted file mode 100644 index 0cfe8e2..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zioinfo.esn.uiws.message.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** - * 쪽지 헤더 (tb_uiws_message). 원본 com.urp.uiws.domain.Message 이식. - * MyBatis map-underscore-to-camel-case 로 컬럼↔필드 자동 매핑. - */ -@Data -public class UiwsMessage { - private Long messageId; - private String senderId; - private String title; - private String content; - private Long refWorklogId; - private Long replyToId; - private LocalDateTime sentAt; - private String senderDelYn; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessageRcv.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessageRcv.java deleted file mode 100644 index f2fbea7..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/model/UiwsMessageRcv.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zioinfo.esn.uiws.message.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** - * 쪽지 수신자 (tb_uiws_message_rcv). 원본 com.urp.uiws.domain.MessageRcv 이식. - */ -@Data -public class UiwsMessageRcv { - private Long rcvId; - private Long messageId; - private String receiverId; - private String rcvType; // RECV | REF - private String readYn; // Y | N - private LocalDateTime readAt; - private String receiverDelYn; // Y | N - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/message/service/MessageService.java b/backend/src/main/java/com/zioinfo/esn/uiws/message/service/MessageService.java deleted file mode 100644 index 788e715..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/message/service/MessageService.java +++ /dev/null @@ -1,271 +0,0 @@ -package com.zioinfo.esn.uiws.message.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsCurrentUser; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.message.dto.MessageDtos.*; -import com.zioinfo.esn.uiws.message.mapper.MessageMapper; -import com.zioinfo.esn.uiws.message.model.UiwsMessage; -import com.zioinfo.esn.uiws.message.model.UiwsMessageRcv; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * 쪽지(모듈 03) 서비스 — 원본 com.urp.uiws.message.service.MessageService 를 MyBatis 로 변환 이식. - * 로직(전송/답장, 보낸함, 개봉현황, 받은함, 개봉처리, 다중삭제, 미열람 배지)을 동등하게 보존한다. - * 기본 조회기간 = 최근 1주일. - */ -@Service -@RequiredArgsConstructor -public class MessageService { - - private static final Set VALID_RCV_TYPES = Set.of("RECV", "REF"); - private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - - private final MessageMapper mapper; - - // ------------------------------------------------------------------ 전송/답장 - @Transactional - public MessageSendResponse send(MessageSendDto dto) { - String actor = UiwsCurrentUser.id(); - LocalDateTime now = LocalDateTime.now(); - - if (dto.replyToId() != null && !mapper.existsMessageById(dto.replyToId())) { - throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND, "답장 원본 쪽지를 찾을 수 없습니다."); - } - - UiwsMessage m = new UiwsMessage(); - m.setSenderId(actor); - m.setTitle(dto.title()); - m.setContent(dto.content()); - m.setRefWorklogId(dto.refWorklogId()); - m.setReplyToId(dto.replyToId()); - m.setSentAt(now); - m.setSenderDelYn("N"); - m.setCreatedBy(actor); - m.setCreatedAt(now); - mapper.insertMessage(m); // useGeneratedKeys → m.messageId - - // 수신자: 동일 수신자 중복 제거(UNIQUE(message_id,receiver_id) 보호). 첫 등장 rcvType 채택. - Set seen = new LinkedHashSet<>(); - for (MessageReceiverDto r : dto.receivers()) { - String receiverId = (r.receiverId() == null) ? null : r.receiverId().trim(); - String rcvType = (r.rcvType() == null) ? null : r.rcvType().trim().toUpperCase(); - if (receiverId == null || receiverId.isBlank()) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "수신자 ID가 비어 있습니다."); - } - if (!VALID_RCV_TYPES.contains(rcvType)) { - throw new UiwsApiException(UiwsErrorCode.MESSAGE_RCV_TYPE_INVALID); - } - if (!seen.add(receiverId)) { - continue; - } - UiwsMessageRcv rcv = new UiwsMessageRcv(); - rcv.setMessageId(m.getMessageId()); - rcv.setReceiverId(receiverId); - rcv.setRcvType(rcvType); - rcv.setReadYn("N"); - rcv.setReceiverDelYn("N"); - rcv.setCreatedBy(actor); - rcv.setCreatedAt(now); - mapper.insertRcv(rcv); - } - return new MessageSendResponse(m.getMessageId()); - } - - // ------------------------------------------------------------------ 보낸쪽지함 - @Transactional(readOnly = true) - public SentPage listSent(LocalDate fromDate, LocalDate toDate, String titleKeyword, int page, int size) { - LocalDateTime[] range = range(fromDate, toDate); - String me = UiwsCurrentUser.id(); - String kw = blankToNull(titleKeyword); - long total = mapper.countSent(me, range[0], range[1], kw); - List rows = mapper.searchSent(me, range[0], range[1], kw, size, page * size); - - List ids = rows.stream().map(UiwsMessage::getMessageId).toList(); - List rcvs = ids.isEmpty() ? List.of() : mapper.findRcvByMessageIds(ids); - Map> byMsg = rcvs.stream().collect(Collectors.groupingBy(UiwsMessageRcv::getMessageId)); - Map names = userNames(rcvs.stream().map(UiwsMessageRcv::getReceiverId).toList()); - - List content = rows.stream().map(m -> { - List list = byMsg.getOrDefault(m.getMessageId(), List.of()); - long openCount = list.stream().filter(r -> "Y".equals(r.getReadYn())).count(); - return new SentMessageDto(m.getMessageId(), m.getTitle(), - receiverSummary(list, names), openCount, list.size(), fmt(m.getSentAt())); - }).toList(); - return new SentPage(content, total, page, size, totalPages(total, size)); - } - - // ------------------------------------------------------------------ 보낸쪽지 상세 - @Transactional(readOnly = true) - public SentMessageDetailDto sentDetail(Long messageId) { - String me = UiwsCurrentUser.id(); - UiwsMessage m = mapper.findMessageById(messageId); - if (m == null) { - throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND); - } - if (!me.equals(m.getSenderId())) { - throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "본인이 보낸 쪽지만 조회할 수 있습니다."); - } - List list = mapper.findRcvByMessageId(messageId); - Map names = userNames(list.stream().map(UiwsMessageRcv::getReceiverId).toList()); - - List receivers = list.stream() - .map(r -> new ReceiverStatusDto( - names.getOrDefault(r.getReceiverId(), r.getReceiverId()), - r.getRcvType(), r.getReadYn(), fmt(r.getReadAt()))) - .toList(); - long total = receivers.size(); - long open = list.stream().filter(r -> "Y".equals(r.getReadYn())).count(); - return new SentMessageDetailDto(m.getMessageId(), m.getTitle(), m.getContent(), m.getRefWorklogId(), - fmt(m.getSentAt()), receivers, total, open, total - open); - } - - // ------------------------------------------------------------------ 보낸쪽지 다중삭제 - @Transactional - public void deleteSent(List ids) { - if (ids == null || ids.isEmpty()) { - return; - } - String me = UiwsCurrentUser.id(); - mapper.softDeleteSent(me, ids, me); - } - - // ------------------------------------------------------------------ 미열람 수(배지) - @Transactional(readOnly = true) - public long unreadCount() { - return mapper.countUnread(UiwsCurrentUser.id()); - } - - // ------------------------------------------------------------------ 받은쪽지함 - @Transactional(readOnly = true) - public ReceivedPage listReceived(LocalDate fromDate, LocalDate toDate, String titleKeyword, int page, int size) { - LocalDateTime[] range = range(fromDate, toDate); - String me = UiwsCurrentUser.id(); - String kw = blankToNull(titleKeyword); - long total = mapper.countReceived(me, range[0], range[1], kw); - List> rows = mapper.searchReceived(me, range[0], range[1], kw, size, page * size); - Map names = userNames(rows.stream().map(r -> str(r.get("senderId"))).toList()); - - List content = rows.stream() - .map(r -> new ReceivedMessageDto( - toLongObj(r.get("messageId")), - str(r.get("title")), - names.getOrDefault(str(r.get("senderId")), str(r.get("senderId"))), - fmt(toDt(r.get("sentAt"))), - str(r.get("readYn")))) - .toList(); - return new ReceivedPage(content, total, page, size, totalPages(total, size)); - } - - // ------------------------------------------------------------------ 받은쪽지 상세(개봉처리) - @Transactional - public ReceivedMessageDetailDto receivedDetail(Long messageId) { - String me = UiwsCurrentUser.id(); - UiwsMessageRcv rcv = mapper.findRcvByMessageAndReceiver(messageId, me); - if (rcv == null || "Y".equals(rcv.getReceiverDelYn())) { - throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND, "받은 쪽지를 찾을 수 없습니다."); - } - UiwsMessage m = mapper.findMessageById(messageId); - if (m == null) { - throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND); - } - LocalDateTime readAt = rcv.getReadAt(); - if (!"Y".equals(rcv.getReadYn())) { - readAt = LocalDateTime.now(); - mapper.markRead(rcv.getRcvId(), me, readAt); - } - String senderNm = userNames(List.of(m.getSenderId())).getOrDefault(m.getSenderId(), m.getSenderId()); - return new ReceivedMessageDetailDto(m.getMessageId(), m.getTitle(), m.getContent(), - senderNm, fmt(m.getSentAt()), fmt(readAt), m.getRefWorklogId()); - } - - // ------------------------------------------------------------------ 받은쪽지 다중삭제 - @Transactional - public void deleteReceived(List ids) { - if (ids == null || ids.isEmpty()) { - return; - } - String me = UiwsCurrentUser.id(); - mapper.softDeleteReceived(me, ids, me); - } - - // ================================================================== helpers - - private LocalDateTime[] range(LocalDate fromDate, LocalDate toDate) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusWeeks(1); - return new LocalDateTime[]{from.atStartOfDay(), to.atTime(LocalTime.MAX)}; - } - - private String receiverSummary(List list, Map names) { - if (list.isEmpty()) { - return ""; - } - String first = names.getOrDefault(list.get(0).getReceiverId(), list.get(0).getReceiverId()); - return list.size() == 1 ? first : first + " 외 " + (list.size() - 1) + "명"; - } - - private Map userNames(List userIds) { - List ids = userIds.stream().filter(s -> s != null && !s.isBlank()).distinct().toList(); - if (ids.isEmpty()) { - return Map.of(); - } - return mapper.findUserNames(ids).stream() - .collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a)); - } - - private static int totalPages(long total, int size) { - return size <= 0 ? 0 : (int) ((total + size - 1) / size); - } - - private static String blankToNull(String s) { - return (s == null || s.isBlank()) ? null : s; - } - - private static String str(Object o) { - return o == null ? "" : o.toString(); - } - - private static Long toLongObj(Object o) { - if (o == null) { - return null; - } - return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString()); - } - - private static LocalDateTime toDt(Object o) { - if (o == null) { - return null; - } - if (o instanceof LocalDateTime dt) { - return dt; - } - if (o instanceof java.sql.Timestamp ts) { - return ts.toLocalDateTime(); - } - return null; - } - - private static String fmt(LocalDateTime dt) { - return dt != null ? dt.format(DT) : null; - } - - // 페이지 응답(ERP ApiResponse 안에 그대로 직렬화 — content/totalElements/page/size/totalPages 보존) - public record SentPage(List content, long totalElements, int page, int size, int totalPages) { - } - - public record ReceivedPage(List content, long totalElements, int page, int size, int totalPages) { - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/AttachmentController.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/AttachmentController.java deleted file mode 100644 index 6616bb4..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/AttachmentController.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.AttachmentDto; -import com.zioinfo.esn.uiws.schedule.service.AttachmentService; -import lombok.RequiredArgsConstructor; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; - -/** - * UIWS 이식 — 첨부파일(모듈 02, 폴리모픽 SCHEDULE/DIARY). 인증 필수(/api/attachments). - */ -@RestController -@RequestMapping("/api/attachments") -@RequiredArgsConstructor -public class AttachmentController { - - private final AttachmentService attachmentService; - - @PostMapping - public ApiResponse upload( - @RequestParam("refType") String refType, - @RequestParam("refId") Long refId, - @RequestParam("file") MultipartFile file) { - return ApiResponse.ok(attachmentService.upload(refType, refId, file)); - } - - @GetMapping("/{id}/download") - public ResponseEntity download(@PathVariable("id") Long id) { - AttachmentService.DownloadFile f = attachmentService.download(id); - Resource resource = new FileSystemResource(f.path()); - String contentType; - try { - contentType = Files.probeContentType(f.path()); - } catch (IOException e) { - contentType = null; - } - ContentDisposition cd = ContentDisposition.attachment() - .filename(f.fileNm(), StandardCharsets.UTF_8) - .build(); - HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition(cd); - long len = f.path().toFile().length(); - if (len > 0) { - headers.setContentLength(len); - } - return ResponseEntity.ok() - .headers(headers) - .contentType(contentType != null ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM) - .body(resource); - } - - @DeleteMapping("/{id}") - public ApiResponse delete(@PathVariable("id") Long id) { - attachmentService.delete(id); - return ApiResponse.ok(null); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/DiaryController.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/DiaryController.java deleted file mode 100644 index 5d655e9..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/DiaryController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*; -import com.zioinfo.esn.uiws.schedule.service.DiaryService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; - -/** - * UIWS 이식 — 일지(모듈 02). 인증 필수(/api/diaries). - */ -@RestController -@RequestMapping("/api/diaries") -@RequiredArgsConstructor -public class DiaryController { - - private final DiaryService diaryService; - - @GetMapping - public ApiResponse list( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - return ApiResponse.ok(diaryService.list(fromDate, toDate, page, size)); - } - - @PostMapping - public ApiResponse create(@Valid @RequestBody DiarySaveDto dto) { - return ApiResponse.ok(diaryService.create(dto)); - } - - @GetMapping("/{id}") - public ApiResponse detail(@PathVariable("id") Long id) { - return ApiResponse.ok(diaryService.detail(id)); - } - - @PutMapping("/{id}") - public ApiResponse update(@PathVariable("id") Long id, @Valid @RequestBody DiarySaveDto dto) { - return ApiResponse.ok(diaryService.update(id, dto)); - } - - @DeleteMapping("/{id}") - public ApiResponse delete(@PathVariable("id") Long id) { - diaryService.delete(id); - return ApiResponse.ok(null); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/ScheduleController.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/ScheduleController.java deleted file mode 100644 index 184d49f..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/controller/ScheduleController.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*; -import com.zioinfo.esn.uiws.schedule.service.ScheduleService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; -import java.util.List; - -/** - * UIWS 이식 — 일정(모듈 02). 인증 필수(/api/schedules). 원본 엔드포인트 보존. - */ -@RestController -@RequestMapping("/api/schedules") -@RequiredArgsConstructor -public class ScheduleController { - - private final ScheduleService scheduleService; - - @GetMapping - public ApiResponse> calendar( - @RequestParam(defaultValue = "PERSONAL") String type, - @RequestParam(defaultValue = "month") String view, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate baseDate, - @RequestParam(required = false) String deptId) { - return ApiResponse.ok(scheduleService.calendar(type, view, baseDate, deptId)); - } - - @GetMapping("/all") - public ApiResponse all( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String type, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - return ApiResponse.ok(scheduleService.all(fromDate, toDate, type, page, size)); - } - - @GetMapping("/search") - public ApiResponse> search(@RequestParam(required = false) String keyword) { - return ApiResponse.ok(scheduleService.search(keyword)); - } - - @PostMapping - public ApiResponse create(@Valid @RequestBody ScheduleSaveDto dto) { - return ApiResponse.ok(scheduleService.create(dto)); - } - - @GetMapping("/{id}") - public ApiResponse detail(@PathVariable("id") Long id) { - return ApiResponse.ok(scheduleService.detail(id)); - } - - @PutMapping("/{id}") - public ApiResponse update(@PathVariable("id") Long id, @Valid @RequestBody ScheduleSaveDto dto) { - return ApiResponse.ok(scheduleService.update(id, dto)); - } - - @DeleteMapping("/{id}") - public ApiResponse delete(@PathVariable("id") Long id) { - scheduleService.delete(id); - return ApiResponse.ok(null); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/dto/ScheduleDtos.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/dto/ScheduleDtos.java deleted file mode 100644 index e38d780..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/dto/ScheduleDtos.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.dto; - -import com.zioinfo.esn.uiws.schedule.model.UiwsAttach; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; - -import java.util.List; - -/** - * 일정·일지·첨부(모듈 02) DTO. 원본 com.urp.uiws.schedule.dto.* 이식(shape 동일). - */ -public final class ScheduleDtos { - - private ScheduleDtos() { - } - - public record ScheduleDto( - Long scheduleId, - String scheType, - String title, - String scheGubunCd, - String importanceCd, - String startDt, - String endDt, - String ownerId, - String deptId - ) { - } - - public record ScheduleDetailDto( - Long scheduleId, - String scheType, - String title, - String scheGubunCd, - String importanceCd, - String startDt, - String endDt, - String ownerId, - String deptId, - String content, - String chargerId, - String chargerNm, - List attachments - ) { - } - - public record ScheduleSaveDto( - @NotBlank @Pattern(regexp = "PERSONAL|DEPT") String scheType, - @NotBlank String title, - String scheGubunCd, - String importanceCd, - @NotNull String startDt, - @NotNull String endDt, - String content, - String deptId, - String chargerId, - List attachmentIds - ) { - } - - public record DiaryDto( - Long diaryId, - String title, - String writerNm, - String diaryDate, - Long scheduleId - ) { - } - - public record DiaryDetailDto( - Long diaryId, - String title, - String writerNm, - String diaryDate, - Long scheduleId, - String content, - List attachments - ) { - } - - public record DiarySaveDto( - @NotBlank String title, - String content, - Long scheduleId, - String diaryDate, - List attachmentIds - ) { - } - - public record AttachmentDto( - Long attachId, - String refType, - Long refId, - String fileNm, - Long fileSize - ) { - public static AttachmentDto from(UiwsAttach a) { - return new AttachmentDto(a.getAttachId(), a.getRefType(), a.getRefId(), a.getFileNm(), a.getFileSize()); - } - } - - /** 페이지 래퍼(content/totalElements/page/size/totalPages). */ - public record DiaryPage(List content, long totalElements, int page, int size, int totalPages) { - } - - public record SchedulePage(List content, long totalElements, int page, int size, int totalPages) { - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/mapper/ScheduleMapper.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/mapper/ScheduleMapper.java deleted file mode 100644 index 2c821c6..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/mapper/ScheduleMapper.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.mapper; - -import com.zioinfo.esn.uiws.schedule.model.UiwsAttach; -import com.zioinfo.esn.uiws.schedule.model.UiwsDiary; -import com.zioinfo.esn.uiws.schedule.model.UiwsSchedule; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * 일정·일지·첨부 MyBatis 매퍼. 원본 JPA ScheduleRepository/DiaryRepository/AttachRepository 변환 이식. - * 데이터 가시범위: scopeAll(true=ADMIN 전체) OR owner IN (ownerIds). ownerIds 는 항상 본인 포함(빈 IN 회피). - */ -@Mapper -public interface ScheduleMapper { - - // ── schedule - int insertSchedule(UiwsSchedule s); - - int updateSchedule(UiwsSchedule s); - - UiwsSchedule findScheduleById(@Param("scheduleId") Long scheduleId); - - boolean existsScheduleById(@Param("scheduleId") Long scheduleId); - - int deleteScheduleById(@Param("scheduleId") Long scheduleId); - - /** 달력: 기간 겹침([from,to) 배타 상한) + type + 개인소유/부서필터. */ - List findInRange(@Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("scheType") String scheType, - @Param("ownerId") String ownerId, - @Param("deptId") String deptId); - - List searchAll(@Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("scheType") String scheType, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds, - @Param("limit") int limit, - @Param("offset") int offset); - - long countAll(@Param("from") LocalDateTime from, - @Param("to") LocalDateTime to, - @Param("scheType") String scheType, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - List searchPopup(@Param("keyword") String keyword, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - // ── diary - int insertDiary(UiwsDiary d); - - int updateDiary(UiwsDiary d); - - UiwsDiary findDiaryById(@Param("diaryId") Long diaryId); - - boolean existsDiaryById(@Param("diaryId") Long diaryId); - - int deleteDiaryById(@Param("diaryId") Long diaryId); - - List searchDiary(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("limit") int limit, - @Param("offset") int offset); - - long countDiary(@Param("from") LocalDate from, @Param("to") LocalDate to); - - // ── attach - int insertAttach(UiwsAttach a); - - UiwsAttach findAttachById(@Param("attachId") Long attachId); - - int updateAttachRef(@Param("attachId") Long attachId, - @Param("refType") String refType, - @Param("refId") Long refId, - @Param("actor") String actor); - - List findAttachByRef(@Param("refType") String refType, @Param("refId") Long refId); - - int deleteAttachById(@Param("attachId") Long attachId); - - int deleteAttachByRef(@Param("refType") String refType, @Param("refId") Long refId); - - // ── 사용자명 라벨(코어 user 재사용) - List> findUserNames(@Param("ids") List ids); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsAttach.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsAttach.java deleted file mode 100644 index 0baed20..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsAttach.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** 첨부파일 폴리모픽 (tb_uiws_attach, ref_type=SCHEDULE|DIARY). 원본 com.urp.uiws.domain.Attach 이식. */ -@Data -public class UiwsAttach { - private Long attachId; - private String refType; - private Long refId; - private String fileNm; - private String filePath; - private Long fileSize; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsDiary.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsDiary.java deleted file mode 100644 index 2e88964..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsDiary.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.model; - -import lombok.Data; -import java.time.LocalDate; -import java.time.LocalDateTime; - -/** 일지 (tb_uiws_diary). 원본 com.urp.uiws.domain.Diary 이식. */ -@Data -public class UiwsDiary { - private Long diaryId; - private String title; - private String content; - private Long scheduleId; - private String writerId; - private LocalDate diaryDate; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsSchedule.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsSchedule.java deleted file mode 100644 index b4ff24d..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/model/UiwsSchedule.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** 일정 (tb_uiws_schedule). 원본 com.urp.uiws.domain.Schedule 이식. */ -@Data -public class UiwsSchedule { - private Long scheduleId; - private String scheType; // PERSONAL | DEPT - private String title; - private String scheGubunCd; - private String importanceCd; - private LocalDateTime startDt; - private LocalDateTime endDt; - private String content; - private String ownerId; - private String deptId; - private String chargerId; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/AttachmentService.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/AttachmentService.java deleted file mode 100644 index 31b63de..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/AttachmentService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsCurrentUser; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.AttachmentDto; -import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper; -import com.zioinfo.esn.uiws.schedule.model.UiwsAttach; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Set; - -/** - * 첨부파일(폴리모픽 SCHEDULE/DIARY) 업로드·삭제·연결. 원본 com.urp.uiws.schedule.service.AttachmentService 이식. - */ -@Service -@RequiredArgsConstructor -public class AttachmentService { - - public static final String REF_SCHEDULE = "SCHEDULE"; - public static final String REF_DIARY = "DIARY"; - private static final Set VALID_REF_TYPES = Set.of(REF_SCHEDULE, REF_DIARY); - - private final ScheduleMapper mapper; - private final FileStorageService fileStorageService; - - @Transactional - public AttachmentDto upload(String refType, Long refId, MultipartFile file) { - String type = normalizeRefType(refType); - if (refId == null) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "refId 는 필수입니다."); - } - FileStorageService.StoredFile stored = fileStorageService.store(file); - - String actor = UiwsCurrentUser.id(); - UiwsAttach a = new UiwsAttach(); - a.setRefType(type); - a.setRefId(refId); - a.setFileNm(stored.originalName()); - a.setFilePath(stored.relativePath()); - a.setFileSize(stored.size()); - a.setCreatedBy(actor); - a.setCreatedAt(LocalDateTime.now()); - mapper.insertAttach(a); - return AttachmentDto.from(a); - } - - public record DownloadFile(String fileNm, Path path) { - } - - @Transactional(readOnly = true) - public DownloadFile download(Long attachId) { - UiwsAttach a = mapper.findAttachById(attachId); - if (a == null) { - throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND); - } - Path path = fileStorageService.resolve(a.getFilePath()); - if (!Files.exists(path)) { - throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND, "파일이 존재하지 않습니다."); - } - return new DownloadFile(a.getFileNm(), path); - } - - @Transactional - public void delete(Long attachId) { - UiwsAttach a = mapper.findAttachById(attachId); - if (a == null) { - throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND); - } - fileStorageService.delete(a.getFilePath()); - mapper.deleteAttachById(attachId); - } - - /** 일정/일지 저장 시 attachmentIds 의 첨부들을 해당 대상으로 귀속(확정). */ - @Transactional - public void link(String refType, Long refId, List attachmentIds) { - String type = normalizeRefType(refType); - if (attachmentIds == null || attachmentIds.isEmpty()) { - return; - } - String actor = UiwsCurrentUser.id(); - for (Long id : attachmentIds) { - if (id == null) { - continue; - } - mapper.updateAttachRef(id, type, refId, actor); - } - } - - @Transactional(readOnly = true) - public List list(String refType, Long refId) { - return mapper.findAttachByRef(normalizeRefType(refType), refId) - .stream().map(AttachmentDto::from).toList(); - } - - /** 대상 삭제 시 귀속 첨부 일괄 제거(물리파일 포함). */ - @Transactional - public void deleteByRef(String refType, Long refId) { - String type = normalizeRefType(refType); - List rows = mapper.findAttachByRef(type, refId); - for (UiwsAttach a : rows) { - fileStorageService.delete(a.getFilePath()); - } - mapper.deleteAttachByRef(type, refId); - } - - private String normalizeRefType(String refType) { - String type = refType == null ? "" : refType.trim().toUpperCase(); - if (!VALID_REF_TYPES.contains(type)) { - throw new UiwsApiException(UiwsErrorCode.ATTACH_REF_TYPE_INVALID); - } - return type; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/DiaryService.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/DiaryService.java deleted file mode 100644 index 675ffc8..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/DiaryService.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsCurrentUser; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*; -import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper; -import com.zioinfo.esn.uiws.schedule.model.UiwsDiary; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 일지(모듈 02) 서비스 — 원본 com.urp.uiws.schedule.service.DiaryService MyBatis 변환 이식. - * 목록(페이징)·CRUD·첨부 연계. - */ -@Service -@RequiredArgsConstructor -public class DiaryService { - - private final ScheduleMapper mapper; - private final AttachmentService attachmentService; - - @Transactional(readOnly = true) - public DiaryPage list(LocalDate fromDate, LocalDate toDate, int page, int size) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1); - long total = mapper.countDiary(from, to); - List rows = mapper.searchDiary(from, to, size, page * size); - Map names = userNames(rows.stream().map(UiwsDiary::getWriterId).toList()); - List content = rows.stream() - .map(d -> new DiaryDto(d.getDiaryId(), d.getTitle(), - names.getOrDefault(d.getWriterId(), d.getWriterId()), - d.getDiaryDate() != null ? d.getDiaryDate().toString() : null, - d.getScheduleId())) - .toList(); - return new DiaryPage(content, total, page, size, totalPages(total, size)); - } - - @Transactional - public DiaryDetailDto create(DiarySaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsDiary d = new UiwsDiary(); - applySave(d, dto); - d.setWriterId(actor); - d.setCreatedBy(actor); - d.setCreatedAt(LocalDateTime.now()); - mapper.insertDiary(d); - attachmentService.link(AttachmentService.REF_DIARY, d.getDiaryId(), dto.attachmentIds()); - return toDetail(d); - } - - @Transactional(readOnly = true) - public DiaryDetailDto detail(Long id) { - return toDetail(find(id)); - } - - @Transactional - public DiaryDetailDto update(Long id, DiarySaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsDiary d = find(id); - applySave(d, dto); - d.setUpdatedBy(actor); - d.setUpdatedAt(LocalDateTime.now()); - mapper.updateDiary(d); - attachmentService.link(AttachmentService.REF_DIARY, d.getDiaryId(), dto.attachmentIds()); - return toDetail(d); - } - - @Transactional - public void delete(Long id) { - if (!mapper.existsDiaryById(id)) { - throw new UiwsApiException(UiwsErrorCode.DIARY_NOT_FOUND); - } - attachmentService.deleteByRef(AttachmentService.REF_DIARY, id); - mapper.deleteDiaryById(id); - } - - // ================================================================== helpers - - private void applySave(UiwsDiary d, DiarySaveDto dto) { - d.setTitle(dto.title()); - d.setContent(dto.content()); - d.setScheduleId(dto.scheduleId()); - d.setDiaryDate(parseDateNullable(dto.diaryDate())); - } - - private DiaryDetailDto toDetail(UiwsDiary d) { - String writerNm = userNames(List.of(d.getWriterId())).getOrDefault(d.getWriterId(), d.getWriterId()); - List attachments = attachmentService.list(AttachmentService.REF_DIARY, d.getDiaryId()); - return new DiaryDetailDto(d.getDiaryId(), d.getTitle(), writerNm, - d.getDiaryDate() != null ? d.getDiaryDate().toString() : null, - d.getScheduleId(), d.getContent(), attachments); - } - - private UiwsDiary find(Long id) { - UiwsDiary d = mapper.findDiaryById(id); - if (d == null) { - throw new UiwsApiException(UiwsErrorCode.DIARY_NOT_FOUND); - } - return d; - } - - private Map userNames(List ids) { - List clean = ids.stream().filter(s -> s != null && !s.isBlank()).distinct().toList(); - if (clean.isEmpty()) { - return Map.of(); - } - return mapper.findUserNames(clean).stream() - .collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a)); - } - - private static int totalPages(long total, int size) { - return size <= 0 ? 0 : (int) ((total + size - 1) / size); - } - - private static LocalDate parseDateNullable(String s) { - if (s == null || s.isBlank()) { - return null; - } - try { - return LocalDate.parse(s); - } catch (Exception e) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "날짜 형식이 올바르지 않습니다(yyyy-MM-dd)."); - } - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/FileStorageService.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/FileStorageService.java deleted file mode 100644 index c70f4c0..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/FileStorageService.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.config.UiwsProperties; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.UUID; - -/** - * 첨부파일 로컬 스토리지(UIWS 이식). 경로순회 방지(파일명 정규화 + UUID 저장명), 일자별 디렉터리 분리. - * 저장 결과로 상대경로(file_path)를 반환한다. - */ -@Service -@RequiredArgsConstructor -public class FileStorageService { - - private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy/MM/dd"); - - private final UiwsProperties properties; - - public record StoredFile(String originalName, String relativePath, long size) { - } - - public StoredFile store(MultipartFile file) { - if (file == null || file.isEmpty()) { - throw new UiwsApiException(UiwsErrorCode.FILE_EMPTY); - } - String original = StringUtils.cleanPath( - file.getOriginalFilename() != null ? file.getOriginalFilename() : "file"); - original = original.replace("\\", "_").replace("/", "_"); - if (original.contains("..")) { - original = original.replace("..", "_"); - } - String ext = ""; - int dot = original.lastIndexOf('.'); - if (dot >= 0) { - ext = original.substring(dot); - } - String subDir = LocalDate.now().format(DAY); - String storedName = UUID.randomUUID().toString().replace("-", "") + ext; - String relativePath = subDir + "/" + storedName; - - try { - Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize(); - Path target = root.resolve(relativePath).normalize(); - if (!target.startsWith(root)) { - throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR, "허용되지 않은 저장 경로입니다."); - } - Files.createDirectories(target.getParent()); - file.transferTo(target.toFile()); - } catch (IOException e) { - throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR); - } - return new StoredFile(original, relativePath, file.getSize()); - } - - public Path resolve(String relativePath) { - if (relativePath == null || relativePath.isBlank()) { - throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND); - } - Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize(); - Path target = root.resolve(relativePath).normalize(); - if (!target.startsWith(root)) { - throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR, "허용되지 않은 경로입니다."); - } - return target; - } - - public void delete(String relativePath) { - if (relativePath == null || relativePath.isBlank()) { - return; - } - try { - Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize(); - Path target = root.resolve(relativePath).normalize(); - if (target.startsWith(root)) { - Files.deleteIfExists(target); - } - } catch (IOException ignore) { - // 물리파일 삭제 실패는 무시(메타 일관성 우선) - } - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/ScheduleService.java b/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/ScheduleService.java deleted file mode 100644 index 36b6d04..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/schedule/service/ScheduleService.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.zioinfo.esn.uiws.schedule.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsCurrentUser; -import com.zioinfo.esn.uiws.common.UiwsDataScope; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*; -import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper; -import com.zioinfo.esn.uiws.schedule.model.UiwsSchedule; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjusters; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 일정(모듈 02) 서비스 — 원본 com.urp.uiws.schedule.service.ScheduleService 를 MyBatis 변환 이식. - * 달력(month/week/day)·전체목록·검색팝업·CRUD. 첨부 연계(attachmentIds). - */ -@Service -@RequiredArgsConstructor -public class ScheduleService { - - private static final String TYPE_PERSONAL = "PERSONAL"; - private static final String TYPE_DEPT = "DEPT"; - private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - - private final ScheduleMapper mapper; - private final AttachmentService attachmentService; - - @Transactional(readOnly = true) - public List calendar(String type, String view, LocalDate baseDate, String deptId) { - String t = normalizeType(type); - LocalDate base = (baseDate != null) ? baseDate : LocalDate.now(); - LocalDate[] range = computeRange(view, base); - LocalDateTime from = range[0].atStartOfDay(); - LocalDateTime to = range[1].atStartOfDay(); // 배타 상한 - - String ownerId = TYPE_PERSONAL.equals(t) ? UiwsCurrentUser.id() : null; - String deptFilter = TYPE_DEPT.equals(t) ? blankToNull(deptId) : null; - - return mapper.findInRange(from, to, t, ownerId, deptFilter).stream().map(this::toDto).toList(); - } - - @Transactional(readOnly = true) - public SchedulePage all(LocalDate fromDate, LocalDate toDate, String type, int page, int size) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1); - String t = (type == null || type.isBlank()) ? null : normalizeType(type); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - LocalDateTime f = from.atStartOfDay(); - LocalDateTime tt = to.plusDays(1).atStartOfDay(); - long total = mapper.countAll(f, tt, t, scope.all(), scope.ownerIds()); - List content = mapper.searchAll(f, tt, t, scope.all(), scope.ownerIds(), size, page * size) - .stream().map(this::toDto).toList(); - return new SchedulePage(content, total, page, size, totalPages(total, size)); - } - - @Transactional(readOnly = true) - public List search(String keyword) { - UiwsDataScope.Scope scope = UiwsDataScope.current(); - return mapper.searchPopup(blankToNull(keyword), scope.all(), scope.ownerIds()) - .stream().map(this::toDto).toList(); - } - - @Transactional - public ScheduleDto create(ScheduleSaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsSchedule s = new UiwsSchedule(); - applySave(s, dto); - s.setOwnerId(actor); - s.setCreatedBy(actor); - s.setCreatedAt(LocalDateTime.now()); - mapper.insertSchedule(s); - attachmentService.link(AttachmentService.REF_SCHEDULE, s.getScheduleId(), dto.attachmentIds()); - return toDto(s); - } - - @Transactional(readOnly = true) - public ScheduleDetailDto detail(Long id) { - return toDetail(find(id)); - } - - @Transactional - public ScheduleDetailDto update(Long id, ScheduleSaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsSchedule s = find(id); - applySave(s, dto); - s.setUpdatedBy(actor); - s.setUpdatedAt(LocalDateTime.now()); - mapper.updateSchedule(s); - attachmentService.link(AttachmentService.REF_SCHEDULE, s.getScheduleId(), dto.attachmentIds()); - return toDetail(s); - } - - @Transactional - public void delete(Long id) { - if (!mapper.existsScheduleById(id)) { - throw new UiwsApiException(UiwsErrorCode.SCHEDULE_NOT_FOUND); - } - attachmentService.deleteByRef(AttachmentService.REF_SCHEDULE, id); - mapper.deleteScheduleById(id); - } - - // ================================================================== helpers - - private void applySave(UiwsSchedule s, ScheduleSaveDto dto) { - LocalDateTime start = parseDateTime(dto.startDt()); - LocalDateTime end = parseDateTime(dto.endDt()); - if (end.isBefore(start)) { - throw new UiwsApiException(UiwsErrorCode.SCHEDULE_DT_INVALID, "종료 일시가 시작 일시보다 빠릅니다."); - } - s.setScheType(normalizeType(dto.scheType())); - s.setTitle(dto.title()); - s.setScheGubunCd(blankToNull(dto.scheGubunCd())); - s.setImportanceCd(blankToNull(dto.importanceCd())); - s.setStartDt(start); - s.setEndDt(end); - s.setContent(dto.content()); - s.setDeptId(blankToNull(dto.deptId())); - s.setChargerId(blankToNull(dto.chargerId())); - } - - private ScheduleDto toDto(UiwsSchedule s) { - return new ScheduleDto(s.getScheduleId(), s.getScheType(), s.getTitle(), - s.getScheGubunCd(), s.getImportanceCd(), fmt(s.getStartDt()), fmt(s.getEndDt()), - s.getOwnerId(), s.getDeptId()); - } - - private ScheduleDetailDto toDetail(UiwsSchedule s) { - String chargerNm = null; - if (s.getChargerId() != null) { - chargerNm = userNames(List.of(s.getChargerId())).getOrDefault(s.getChargerId(), s.getChargerId()); - } - List attachments = attachmentService.list(AttachmentService.REF_SCHEDULE, s.getScheduleId()); - return new ScheduleDetailDto(s.getScheduleId(), s.getScheType(), s.getTitle(), - s.getScheGubunCd(), s.getImportanceCd(), fmt(s.getStartDt()), fmt(s.getEndDt()), - s.getOwnerId(), s.getDeptId(), s.getContent(), s.getChargerId(), chargerNm, attachments); - } - - private UiwsSchedule find(Long id) { - UiwsSchedule s = mapper.findScheduleById(id); - if (s == null) { - throw new UiwsApiException(UiwsErrorCode.SCHEDULE_NOT_FOUND); - } - return s; - } - - private Map userNames(List ids) { - List clean = ids.stream().filter(s -> s != null && !s.isBlank()).distinct().toList(); - if (clean.isEmpty()) { - return Map.of(); - } - return mapper.findUserNames(clean).stream() - .collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a)); - } - - private LocalDate[] computeRange(String view, LocalDate base) { - String v = (view == null || view.isBlank()) ? "month" : view.trim().toLowerCase(); - return switch (v) { - case "day" -> new LocalDate[]{base, base.plusDays(1)}; - case "week" -> { - LocalDate start = base.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); - yield new LocalDate[]{start, start.plusWeeks(1)}; - } - case "month" -> { - LocalDate start = base.withDayOfMonth(1); - yield new LocalDate[]{start, start.plusMonths(1)}; - } - default -> throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, - "view 는 month/week/day 중 하나여야 합니다."); - }; - } - - private String normalizeType(String type) { - String t = (type == null) ? "" : type.trim().toUpperCase(); - if (!TYPE_PERSONAL.equals(t) && !TYPE_DEPT.equals(t)) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "type 은 PERSONAL/DEPT 중 하나여야 합니다."); - } - return t; - } - - private static int totalPages(long total, int size) { - return size <= 0 ? 0 : (int) ((total + size - 1) / size); - } - - private static String blankToNull(String s) { - return (s == null || s.isBlank()) ? null : s; - } - - private static String fmt(LocalDateTime dt) { - return dt != null ? dt.format(DT) : null; - } - - private static LocalDateTime parseDateTime(String s) { - if (s == null || s.isBlank()) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "일시 값이 비어 있습니다."); - } - try { - if (s.length() <= 10) { - return LocalDate.parse(s).atStartOfDay(); - } - return LocalDateTime.parse(s); - } catch (Exception e) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, - "일시 형식이 올바르지 않습니다(yyyy-MM-ddTHH:mm:ss)."); - } - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/stats/controller/StatsController.java b/backend/src/main/java/com/zioinfo/esn/uiws/stats/controller/StatsController.java deleted file mode 100644 index cd483dd..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/stats/controller/StatsController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.zioinfo.esn.uiws.stats.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotResponse; -import com.zioinfo.esn.uiws.stats.service.StatsService; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; - -/** - * UIWS 이식 — 업무통계(모듈 04, 동적 컬럼 피벗). 인증 필수(/api/stats). - * prefix /api/stats 는 ERP 기존 라우트와 미충돌(확인). 원본 엔드포인트 보존. - */ -@RestController -@RequestMapping("/api/stats") -@RequiredArgsConstructor -public class StatsController { - - private final StatsService statsService; - - @GetMapping("/personal-work") - public ApiResponse personalWork( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String deptId, - @RequestParam(required = false) String userId, - @RequestParam(required = false) Boolean showStatus, - @RequestParam(required = false) Boolean showType) { - return ApiResponse.ok(statsService.personalWork(fromDate, toDate, deptId, userId, showStatus, showType)); - } - - @GetMapping("/company-work") - public ApiResponse companyWork( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String companyId, - @RequestParam(required = false) String userId, - @RequestParam(required = false) Boolean showType) { - return ApiResponse.ok(statsService.companyWork(fromDate, toDate, companyId, userId, showType)); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/stats/dto/StatsDtos.java b/backend/src/main/java/com/zioinfo/esn/uiws/stats/dto/StatsDtos.java deleted file mode 100644 index c2cb59b..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/stats/dto/StatsDtos.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.zioinfo.esn.uiws.stats.dto; - -import java.util.List; -import java.util.Map; - -/** - * 근무현황 통계(모듈 04) 동적 컬럼 피벗 DTO. 원본 com.urp.uiws.stats.dto.* 이식. - */ -public final class StatsDtos { - - private StatsDtos() { - } - - /** 피벗 열 메타. 고정열 예 {key:"worker",label:"근무자"}; 동적열 예 {key:"company_본사",label:"본사"}. */ - public record PivotColumn(String key, String label) { - } - - /** 동적 컬럼 피벗 응답. { fixedColumns, dynamicColumns, rows } */ - public record PivotResponse( - List fixedColumns, - List dynamicColumns, - List> rows - ) { - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/stats/mapper/StatsMapper.java b/backend/src/main/java/com/zioinfo/esn/uiws/stats/mapper/StatsMapper.java deleted file mode 100644 index 95ab5b2..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/stats/mapper/StatsMapper.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.zioinfo.esn.uiws.stats.mapper; - -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.time.LocalDate; -import java.util.List; -import java.util.Map; - -/** - * 근무현황 피벗 집계 매퍼. 원본 StatsRepository(JdbcTemplate 네이티브) → MyBatis 변환 이식. - * - * silo 정합(코어 TB_USER/TB_COMPANY/TB_CODE 미이식): - * - 근무자 라벨: esn_user (writer_id=username 라벨). 미존재 시 writer_id 노출(COALESCE). - * - 근무처(company) 라벨: company_id 직접 사용(거래처 테이블 미이식). - * - 근무상태/근무유형 라벨: 코드값 직접 사용(공통코드 미이식). - * 결과는 long-form(그룹키 + cnt). 서비스가 동적 컬럼으로 피벗한다. - */ -@Mapper -public interface StatsMapper { - - /** 개인별: 행=근무자×근무상태×근무유형, 동적열 차원=근무처. 컬럼: worker, work_status, work_type, dyn, cnt. */ - List> personalWork(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("userId") String userId, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - /** 업체별: 행=근무처×근무유형, 동적열 차원=근무자. 컬럼: company, work_type, dyn, cnt. */ - List> companyWork(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("companyId") String companyId, - @Param("userId") String userId, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/stats/service/StatsService.java b/backend/src/main/java/com/zioinfo/esn/uiws/stats/service/StatsService.java deleted file mode 100644 index 2b4c2c4..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/stats/service/StatsService.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.zioinfo.esn.uiws.stats.service; - -import com.zioinfo.esn.uiws.common.UiwsDataScope; -import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotColumn; -import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotResponse; -import com.zioinfo.esn.uiws.stats.mapper.StatsMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * 근무현황 통계(모듈 04) — 동적 컬럼 피벗 서비스. 원본 com.urp.uiws.stats.service.StatsService 이식. - * long-form 집계를 wide-form 으로 피벗. showStatus/showType 토글. 기본 조회기간 = 최근 1주일. - * 데이터 스코프(ADMIN=전체/그 외=본인)는 UiwsDataScope 로 silo 판정. - */ -@Service -@RequiredArgsConstructor -public class StatsService { - - /** 행 식별 키 구분자(데이터에 등장하지 않는 제어문자). */ - private static final char SEP = ''; - - private final StatsMapper mapper; - - @Transactional(readOnly = true) - public PivotResponse personalWork(LocalDate fromDate, LocalDate toDate, - String deptId, String userId, - Boolean showStatus, Boolean showType) { - LocalDate[] range = range(fromDate, toDate); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - // deptId 는 코어 dept 미이식으로 silo 에서 무시(파라미터 호환만 유지). - List> agg = mapper.personalWork( - range[0], range[1], emptyToNull(userId), scope.all(), scope.ownerIds()); - - boolean status = !Boolean.FALSE.equals(showStatus); - boolean type = !Boolean.FALSE.equals(showType); - - List fixed = new ArrayList<>(); - fixed.add(new PivotColumn("worker", "근무자")); - if (status) { - fixed.add(new PivotColumn("workStatus", "근무상태")); - } - if (type) { - fixed.add(new PivotColumn("workType", "근무유형")); - } - - return pivot(agg, fixed, "company", - row -> rowKey(row, status, type), - row -> { - Map base = new LinkedHashMap<>(); - base.put("worker", str(row.get("worker"))); - if (status) { - base.put("workStatus", str(row.get("work_status"))); - } - if (type) { - base.put("workType", str(row.get("work_type"))); - } - return base; - }); - } - - @Transactional(readOnly = true) - public PivotResponse companyWork(LocalDate fromDate, LocalDate toDate, - String companyId, String userId, - Boolean showType) { - LocalDate[] range = range(fromDate, toDate); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - List> agg = mapper.companyWork( - range[0], range[1], emptyToNull(companyId), emptyToNull(userId), scope.all(), scope.ownerIds()); - - boolean type = !Boolean.FALSE.equals(showType); - - List fixed = new ArrayList<>(); - fixed.add(new PivotColumn("company", "근무처")); - if (type) { - fixed.add(new PivotColumn("workType", "근무유형")); - } - - return pivot(agg, fixed, "worker", - row -> str(row.get("company")) + SEP + (type ? str(row.get("work_type")) : ""), - row -> { - Map base = new LinkedHashMap<>(); - base.put("company", str(row.get("company"))); - if (type) { - base.put("workType", str(row.get("work_type"))); - } - return base; - }); - } - - // ================================================================== 피벗 공통 - - private interface RowKeyFn { - String key(Map row); - } - - private interface BaseFn { - Map base(Map row); - } - - private PivotResponse pivot(List> agg, List fixed, - String dynPrefix, RowKeyFn keyFn, BaseFn baseFn) { - Map dynCols = new LinkedHashMap<>(); - Map> rowMap = new LinkedHashMap<>(); - - for (Map r : agg) { - String dynLabel = str(r.get("dyn")); - String dynKey = dynPrefix + "_" + dynLabel; - dynCols.putIfAbsent(dynKey, new PivotColumn(dynKey, dynLabel)); - - String rk = keyFn.key(r); - Map row = rowMap.computeIfAbsent(rk, k -> baseFn.base(r)); - long cnt = toLong(r.get("cnt")); - row.merge(dynKey, cnt, (a, b) -> toLong(a) + toLong(b)); - } - - List dynamic = new ArrayList<>(dynCols.values()); - List> rows = new ArrayList<>(); - for (Map row : rowMap.values()) { - for (PivotColumn dc : dynamic) { - row.putIfAbsent(dc.key(), 0L); - } - rows.add(row); - } - return new PivotResponse(fixed, dynamic, rows); - } - - private String rowKey(Map row, boolean status, boolean type) { - StringBuilder sb = new StringBuilder(str(row.get("worker"))); - if (status) { - sb.append(SEP).append(str(row.get("work_status"))); - } - if (type) { - sb.append(SEP).append(str(row.get("work_type"))); - } - return sb.toString(); - } - - private LocalDate[] range(LocalDate fromDate, LocalDate toDate) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusWeeks(1); - return new LocalDate[]{from, to}; - } - - private static String emptyToNull(String s) { - return (s == null || s.isBlank()) ? null : s; - } - - private static String str(Object o) { - return o == null ? "" : o.toString(); - } - - private static long toLong(Object o) { - if (o == null) { - return 0L; - } - return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString()); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/controller/WorklogController.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/controller/WorklogController.java deleted file mode 100644 index 9b4d8fa..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/controller/WorklogController.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.controller; - -import com.zioinfo.esn.common.ApiResponse; -import com.zioinfo.esn.uiws.worklog.dto.WorklogDtos.*; -import com.zioinfo.esn.uiws.worklog.service.WorklogService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; -import java.util.List; - -/** - * UIWS 이식 — 업무일지(모듈 06). 인증 필수(/api/worklogs). - * 원본 /pdf(JasperReports) 엔드포인트는 ERP 의존성 미보유로 제외(보고서는 후속 트랙 — esn_port.md 참조). - */ -@RestController -@RequestMapping("/api/worklogs") -@RequiredArgsConstructor -public class WorklogController { - - private final WorklogService worklogService; - - @GetMapping - public ApiResponse list( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate, - @RequestParam(required = false) String writerId, - @RequestParam(required = false) String progressCd, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - return ApiResponse.ok(worklogService.list(fromDate, toDate, writerId, progressCd, page, size)); - } - - @GetMapping("/dashboard/progress") - public ApiResponse> progressSummary( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate) { - return ApiResponse.ok(worklogService.progressSummary(fromDate, toDate)); - } - - @GetMapping("/calendar") - public ApiResponse> calendar( - @RequestParam(required = false) String yearMonth, - @RequestParam(required = false) String writerId) { - return ApiResponse.ok(worklogService.calendar(yearMonth, writerId)); - } - - @GetMapping("/search") - public ApiResponse> search( - @RequestParam(required = false) String keyword, - @RequestParam(required = false) String writerId) { - return ApiResponse.ok(worklogService.search(keyword, writerId)); - } - - @PostMapping - public ApiResponse create(@Valid @RequestBody WorklogSaveDto dto) { - return ApiResponse.ok(worklogService.create(dto)); - } - - @GetMapping("/{id}") - public ApiResponse detail(@PathVariable("id") Long id) { - return ApiResponse.ok(worklogService.detail(id)); - } - - @PutMapping("/{id}") - public ApiResponse update(@PathVariable("id") Long id, @Valid @RequestBody WorklogSaveDto dto) { - return ApiResponse.ok(worklogService.update(id, dto)); - } - - @DeleteMapping("/{id}") - public ApiResponse delete(@PathVariable("id") Long id) { - worklogService.delete(id); - return ApiResponse.ok(null); - } - - @PostMapping("/{id}/comments") - public ApiResponse addComment(@PathVariable("id") Long id, - @Valid @RequestBody CommentRequest req) { - return ApiResponse.ok(worklogService.addComment(id, req)); - } - - @PostMapping("/comments/{cmtId}/confirm") - public ApiResponse confirmComment(@PathVariable("cmtId") Long cmtId) { - worklogService.confirmComment(cmtId); - return ApiResponse.ok(null); - } - - @GetMapping("/comments/unconfirmed") - public ApiResponse> unconfirmedComments() { - return ApiResponse.ok(worklogService.unconfirmedByMe()); - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/dto/WorklogDtos.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/dto/WorklogDtos.java deleted file mode 100644 index 540107b..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/dto/WorklogDtos.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklogDtl; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import java.util.List; - -/** - * 업무일지(모듈 06) DTO. 원본 com.urp.uiws.worklog.dto.* 이식(shape 동일, _state 보존). - */ -public final class WorklogDtos { - - private WorklogDtos() { - } - - public record WorklogListDto( - Long worklogId, - String title, - String writerNm, - String workDate, - String progressCd, - long commentCount - ) { - } - - public record WorklogCalendarDto( - String workDate, - Long worklogId, - String workStatusNm, - boolean isHoliday, - String holidayNm - ) { - } - - public record WorklogDetailDto( - Long worklogId, - String title, - String writerId, - String writerNm, - String workDate, - String workStatusCd, - String progressCd, - String repeatYn, - String repeatStartDate, - String repeatEndDate, - List details, - List comments - ) { - } - - public record WorklogDtlDto( - Long dtlId, - Integer startHour, - Integer endHour, - String workTypeCd, - String companyId, - String workContent, - String issueContent, - Integer sortOrd, - String createdAt, - String updatedAt, - @JsonProperty("_state") String state - ) { - public static WorklogDtlDto from(UiwsWorklogDtl d) { - return new WorklogDtlDto( - d.getDtlId(), d.getStartHour(), d.getEndHour(), d.getWorkTypeCd(), d.getCompanyId(), - d.getWorkContent(), d.getIssueContent(), d.getSortOrd(), - d.getCreatedAt() != null ? d.getCreatedAt().toString() : null, - d.getUpdatedAt() != null ? d.getUpdatedAt().toString() : null, - null); - } - } - - public record WorklogSaveDto( - @NotBlank String title, - @NotBlank String writerId, - @NotNull String workDate, - @NotBlank String workStatusCd, - String progressCd, - String repeatYn, - String repeatStartDate, - String repeatEndDate, - @Valid List details - ) { - } - - public record WorklogCommentDto( - Long cmtId, - String cmtContent, - String writerNm, - String createdAt, - String kakaoSentYn, - String confirmYn - ) { - } - - public record CommentRequest(@NotBlank String cmtContent) { - } - - public record ProgressSummaryDto(String progressCd, String progressNm, long count) { - } - - public record UnconfirmedCommentDto( - Long cmtId, - Long worklogId, - String worklogTitle, - String writerNm, - String cmtContent, - String createdAt - ) { - } - - public record WorklogPage(List content, long totalElements, int page, int size, int totalPages) { - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/mapper/WorklogMapper.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/mapper/WorklogMapper.java deleted file mode 100644 index 636189f..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/mapper/WorklogMapper.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.mapper; - -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklog; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklogCmt; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklogDtl; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * 업무일지 MyBatis 매퍼. 원본 JPA Worklog/WorklogDtl/WorklogCmt Repository 변환 이식. - * 진행상태 집계는 ported tb_uiws_worklog 기반(코어 TB_CODE 미이식 → 코드값 직접 집계). - */ -@Mapper -public interface WorklogMapper { - - // ── worklog 헤더 - int insertWorklog(UiwsWorklog w); - - int updateWorklog(UiwsWorklog w); - - UiwsWorklog findWorklogById(@Param("worklogId") Long worklogId); - - boolean existsWorklogById(@Param("worklogId") Long worklogId); - - int deleteWorklogById(@Param("worklogId") Long worklogId); - - List searchList(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("writerId") String writerId, - @Param("progressCd") String progressCd, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds, - @Param("limit") int limit, - @Param("offset") int offset); - - long countList(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("writerId") String writerId, - @Param("progressCd") String progressCd, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - List findByMonth(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("writerId") String writerId, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - List searchPopup(@Param("keyword") String keyword, - @Param("writerId") String writerId, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - /** 진행상태별 건수(progress_cd, cnt). */ - List> progressSummary(@Param("from") LocalDate from, - @Param("to") LocalDate to, - @Param("scopeAll") boolean scopeAll, - @Param("ownerIds") List ownerIds); - - // ── 상세(dtl) - int insertDtl(UiwsWorklogDtl d); - - int updateDtl(UiwsWorklogDtl d); - - int deleteDtlById(@Param("dtlId") Long dtlId); - - List findDtlByWorklogId(@Param("worklogId") Long worklogId); - - // ── 댓글(cmt) - int insertCmt(UiwsWorklogCmt c); - - UiwsWorklogCmt findCmtById(@Param("cmtId") Long cmtId); - - int updateCmtKakaoSent(@Param("cmtId") Long cmtId); - - int confirmCmt(@Param("cmtId") Long cmtId, @Param("actor") String actor); - - List findCmtByWorklogId(@Param("worklogId") Long worklogId); - - List> countCommentsByWorklogIds(@Param("ids") List ids); - - List findUnconfirmedByWriter(@Param("writerId") String writerId); - - // ── 사용자명 라벨(코어 user 재사용) - List> findUserNames(@Param("ids") List ids); - - /** 댓글 알림 메일 발송 대상 이메일(username 기준). */ - String findUserEmail(@Param("username") String username); - - List findWorklogsByIds(@Param("ids") List ids); -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklog.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklog.java deleted file mode 100644 index a2cbaef..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklog.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.model; - -import lombok.Data; -import java.time.LocalDate; -import java.time.LocalDateTime; - -/** 업무일지 헤더 (tb_uiws_worklog). 원본 com.urp.uiws.domain.Worklog 이식. */ -@Data -public class UiwsWorklog { - private Long worklogId; - private String title; - private String writerId; - private LocalDate workDate; - private String workStatusCd; - private String progressCd; // ONGOING | DONE - private String repeatYn; // Y | N - private LocalDate repeatStartDate; - private LocalDate repeatEndDate; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogCmt.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogCmt.java deleted file mode 100644 index 9297aed..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogCmt.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** 업무일지 댓글 (tb_uiws_worklog_cmt). 원본 com.urp.uiws.domain.WorklogCmt 이식. */ -@Data -public class UiwsWorklogCmt { - private Long cmtId; - private Long worklogId; - private String cmtContent; - private String writerId; - private String kakaoSentYn; - private String confirmYn; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogDtl.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogDtl.java deleted file mode 100644 index 7ea7a29..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/model/UiwsWorklogDtl.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.model; - -import lombok.Data; -import java.time.LocalDateTime; - -/** 업무일지 시간대별 상세 (tb_uiws_worklog_dtl). 원본 com.urp.uiws.domain.WorklogDtl 이식. */ -@Data -public class UiwsWorklogDtl { - private Long dtlId; - private Long worklogId; - private Integer startHour; - private Integer endHour; - private String workTypeCd; - private String companyId; - private String workContent; - private String issueContent; - private Integer sortOrd; - private String createdBy; - private LocalDateTime createdAt; - private String updatedBy; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogNotifier.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogNotifier.java deleted file mode 100644 index 278a586..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogNotifier.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.service; - -import com.zioinfo.esn.uiws.common.mail.MailSender; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -/** - * 업무일지 댓글 알림 — 원본 NaverWorksNotifier(외부 메신저) 대신 MailSender 폴백으로 이식. - * 보안 불변규칙(외부 API 금지) 준수: 외부 메신저 호출 없이 메일/로그 채널로만 알림한다. - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class WorklogNotifier { - - private final MailSender mailSender; - - /** - * 일지 작성자에게 댓글 알림. 발송되면 true(=KAKAO_SENT_YN 'Y' 선반영). - * email 미보유 시 로그만 남기고 false. - */ - public boolean notifyComment(String writerId, String email, String subject, String body) { - if (email == null || email.isBlank()) { - log.info("[UIWS-WL-NOTIFY] no email for writer={} (skip mail, log only)", writerId); - return false; - } - mailSender.send(email, subject, body); - return true; - } -} diff --git a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogService.java b/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogService.java deleted file mode 100644 index 35d0068..0000000 --- a/backend/src/main/java/com/zioinfo/esn/uiws/worklog/service/WorklogService.java +++ /dev/null @@ -1,438 +0,0 @@ -package com.zioinfo.esn.uiws.worklog.service; - -import com.zioinfo.esn.uiws.common.UiwsApiException; -import com.zioinfo.esn.uiws.common.UiwsCurrentUser; -import com.zioinfo.esn.uiws.common.UiwsDataScope; -import com.zioinfo.esn.uiws.common.UiwsErrorCode; -import com.zioinfo.esn.uiws.worklog.dto.WorklogDtos.*; -import com.zioinfo.esn.uiws.worklog.mapper.WorklogMapper; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklog; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklogCmt; -import com.zioinfo.esn.uiws.worklog.model.UiwsWorklogDtl; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 업무일지(모듈 06) 서비스 — 원본 com.urp.uiws.worklog.service.WorklogService MyBatis 변환 이식. - * - * silo 단순화(코어 TB_CODE/TB_DEPT 미이식): - * - 근무상태/진행 코드 라벨: 코드값 그대로 노출(공통코드 미이식). 진행 라벨은 ONGOING/DONE 한글 매핑만 내장. - * - 진행상태 집계: ONGOING/DONE 고정 셋 0건 포함 반환(차트 범례 안정). - * - 댓글 권한: 원본 "관할 상무 이상"(부서계층+직급) → ERP RBAC MANAGER/ADMIN/CFO 로 완화 이식. - * - 댓글 알림: NaverWorks(외부) → MailSender 폴백(외부 API 금지 준수). - */ -@Service -@RequiredArgsConstructor -public class WorklogService { - - private static final String DEFAULT_PROGRESS = "ONGOING"; - private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - /** 진행 코드 → 라벨(공통코드 미이식 silo 보정). */ - private static final Map PROGRESS_LABEL = Map.of("ONGOING", "진행중", "DONE", "종료"); - - private final WorklogMapper mapper; - private final WorklogNotifier notifier; - - // ------------------------------------------------------------------ 목록(리스트형) - @Transactional(readOnly = true) - public WorklogPage list(LocalDate fromDate, LocalDate toDate, String writerId, String progressCd, int page, int size) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - String wid = blankToNull(writerId); - String pc = blankToNull(progressCd); - long total = mapper.countList(from, to, wid, pc, scope.all(), scope.ownerIds()); - List rows = mapper.searchList(from, to, wid, pc, scope.all(), scope.ownerIds(), size, page * size); - return new WorklogPage(toListDtos(rows), total, page, size, totalPages(total, size)); - } - - // ------------------------------------------------------------------ 목록(달력형) - @Transactional(readOnly = true) - public List calendar(String yearMonth, String writerId) { - YearMonth ym = parseYearMonth(yearMonth); - LocalDate from = ym.atDay(1); - LocalDate to = ym.atEndOfMonth(); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - List rows = mapper.findByMonth(from, to, blankToNull(writerId), scope.all(), scope.ownerIds()); - // 공휴일 마스터 미연동 → isHoliday=false. 근무상태 라벨은 코드값 노출(공통코드 미이식). - return rows.stream() - .map(w -> new WorklogCalendarDto(w.getWorkDate().toString(), w.getWorklogId(), - w.getWorkStatusCd(), false, null)) - .toList(); - } - - // ------------------------------------------------------------------ 조회 팝업 - @Transactional(readOnly = true) - public List search(String keyword, String writerId) { - UiwsDataScope.Scope scope = UiwsDataScope.current(); - List rows = mapper.searchPopup(blankToNull(keyword), blankToNull(writerId), scope.all(), scope.ownerIds()); - return toListDtos(rows); - } - - // ------------------------------------------------------------------ 대시보드: 진행상태별 집계 - @Transactional(readOnly = true) - public List progressSummary(LocalDate fromDate, LocalDate toDate) { - LocalDate to = (toDate != null) ? toDate : LocalDate.now(); - LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1); - UiwsDataScope.Scope scope = UiwsDataScope.current(); - Map counts = new LinkedHashMap<>(); - for (Map r : mapper.progressSummary(from, to, scope.all(), scope.ownerIds())) { - counts.put(str(r.get("progressCd")), toLong(r.get("cnt"))); - } - // 코드 정의 순서(진행중→종료)로 0건 포함 반환. - List out = new ArrayList<>(); - for (String code : List.of("ONGOING", "DONE")) { - out.add(new ProgressSummaryDto(code, PROGRESS_LABEL.getOrDefault(code, code), counts.getOrDefault(code, 0L))); - } - // 정의 외 코드값도 누락 없이 추가 - counts.forEach((k, v) -> { - if (!"ONGOING".equals(k) && !"DONE".equals(k)) { - out.add(new ProgressSummaryDto(k, k, v)); - } - }); - return out; - } - - // ------------------------------------------------------------------ 등록 - @Transactional - public WorklogDetailDto create(WorklogSaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsWorklog w = new UiwsWorklog(); - applyHeader(w, dto); - w.setWriterId((dto.writerId() != null && !dto.writerId().isBlank()) ? dto.writerId() : actor); - w.setCreatedBy(actor); - w.setCreatedAt(LocalDateTime.now()); - mapper.insertWorklog(w); - - List toInsert = (dto.details() == null) ? List.of() - : dto.details().stream().filter(d -> !"DEL".equalsIgnoreCase(safeState(d.state()))).toList(); - validateNoOverlap(toInsert); - for (WorklogDtlDto d : toInsert) { - mapper.insertDtl(newDtl(w.getWorklogId(), d, actor)); - } - return detail(w.getWorklogId()); - } - - // ------------------------------------------------------------------ 상세 - @Transactional(readOnly = true) - public WorklogDetailDto detail(Long worklogId) { - return toDetail(findWorklog(worklogId)); - } - - // ------------------------------------------------------------------ 수정(시간대별 일괄, _state) - @Transactional - public WorklogDetailDto update(Long worklogId, WorklogSaveDto dto) { - String actor = UiwsCurrentUser.id(); - UiwsWorklog w = findWorklog(worklogId); - applyHeader(w, dto); - if (dto.writerId() != null && !dto.writerId().isBlank()) { - w.setWriterId(dto.writerId()); - } - w.setUpdatedBy(actor); - w.setUpdatedAt(LocalDateTime.now()); - mapper.updateWorklog(w); - - List existing = mapper.findDtlByWorklogId(worklogId); - Map existingById = existing.stream() - .collect(Collectors.toMap(UiwsWorklogDtl::getDtlId, d -> d)); - - List details = (dto.details() == null) ? List.of() : dto.details(); - List survivors = new ArrayList<>(); - for (WorklogDtlDto d : details) { - String st = safeState(d.state()); - if ("DEL".equalsIgnoreCase(st)) { - if (d.dtlId() != null && existingById.containsKey(d.dtlId())) { - mapper.deleteDtlById(d.dtlId()); - } - continue; - } - if ("MOD".equalsIgnoreCase(st) && d.dtlId() != null) { - UiwsWorklogDtl tgt = existingById.get(d.dtlId()); - if (tgt == null) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND, "수정 대상 상세를 찾을 수 없습니다."); - } - applyDtl(tgt, d); - tgt.setUpdatedBy(actor); - tgt.setUpdatedAt(LocalDateTime.now()); - mapper.updateDtl(tgt); - survivors.add(d); - continue; - } - // _state 미표기 + 기존 dtlId 보유 = 미변경 유지행 → INSERT 금지 - if (d.dtlId() != null && existingById.containsKey(d.dtlId())) { - survivors.add(d); - continue; - } - // ADD 또는 진짜 신규 → INSERT - mapper.insertDtl(newDtl(worklogId, d, actor)); - survivors.add(d); - } - validateNoOverlap(survivors); - return detail(worklogId); - } - - // ------------------------------------------------------------------ 삭제 - @Transactional - public void delete(Long worklogId) { - if (!mapper.existsWorklogById(worklogId)) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND); - } - // 상세/댓글은 DB ON DELETE CASCADE. 헤더 삭제로 일괄 제거. - mapper.deleteWorklogById(worklogId); - } - - // ------------------------------------------------------------------ 댓글 등록(+알림) - @Transactional - public WorklogCommentDto addComment(Long worklogId, CommentRequest req) { - String actor = UiwsCurrentUser.id(); - UiwsWorklog w = findWorklog(worklogId); - assertCanComment(); - - UiwsWorklogCmt c = new UiwsWorklogCmt(); - c.setWorklogId(worklogId); - c.setCmtContent(req.cmtContent()); - c.setWriterId(actor); - c.setKakaoSentYn("N"); - c.setConfirmYn("N"); - c.setCreatedBy(actor); - c.setCreatedAt(LocalDateTime.now()); - mapper.insertCmt(c); - - // 알림: 일지 작성자에게 메일 폴백(외부 메신저 미사용). - String writerEmail = userEmail(w.getWriterId()); - String subject = "[UIWS] 업무일지 새 댓글: " + w.getTitle(); - String body = "업무일지에 새 댓글이 등록되었습니다.\n내용: " + req.cmtContent(); - boolean sent = notifier.notifyComment(w.getWriterId(), writerEmail, subject, body); - if (sent) { - mapper.updateCmtKakaoSent(c.getCmtId()); - c.setKakaoSentYn("Y"); - } - - String writerNm = userNames(List.of(actor)).getOrDefault(actor, actor); - return new WorklogCommentDto(c.getCmtId(), c.getCmtContent(), writerNm, - fmt(c.getCreatedAt()), c.getKakaoSentYn(), c.getConfirmYn()); - } - - // ------------------------------------------------------------------ 댓글 확인(작성자 전용) - @Transactional - public void confirmComment(Long cmtId) { - String actor = UiwsCurrentUser.id(); - UiwsWorklogCmt c = mapper.findCmtById(cmtId); - if (c == null) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND, "댓글을 찾을 수 없습니다."); - } - UiwsWorklog w = findWorklog(c.getWorklogId()); - if (!w.getWriterId().equals(actor)) { - throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "본인 업무일지의 댓글만 확인 처리할 수 있습니다."); - } - mapper.confirmCmt(cmtId, actor); - } - - // ------------------------------------------------------------------ 내가 단 댓글 중 작성자 미확인 목록 - @Transactional(readOnly = true) - public List unconfirmedByMe() { - String me = UiwsCurrentUser.id(); - List rows = mapper.findUnconfirmedByWriter(me); - if (rows.isEmpty()) { - return List.of(); - } - List worklogIds = rows.stream().map(UiwsWorklogCmt::getWorklogId).distinct().toList(); - Map worklogs = mapper.findWorklogsByIds(worklogIds).stream() - .collect(Collectors.toMap(UiwsWorklog::getWorklogId, wl -> wl)); - Map writerNames = userNames( - worklogs.values().stream().map(UiwsWorklog::getWriterId).toList()); - return rows.stream().map(c -> { - UiwsWorklog wl = worklogs.get(c.getWorklogId()); - return new UnconfirmedCommentDto(c.getCmtId(), c.getWorklogId(), - wl != null ? wl.getTitle() : null, - wl != null ? writerNames.getOrDefault(wl.getWriterId(), wl.getWriterId()) : null, - c.getCmtContent(), fmt(c.getCreatedAt())); - }).toList(); - } - - // ================================================================== helpers - - /** 댓글 권한(silo 완화): ERP RBAC MANAGER/ADMIN/CFO 만 작성 가능. */ - private void assertCanComment() { - if (!UiwsCurrentUser.isManagerOrAbove()) { - throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "댓글은 관리자/매니저 권한만 작성할 수 있습니다."); - } - } - - private List toListDtos(List rows) { - Map cmtCounts = commentCounts(rows.stream().map(UiwsWorklog::getWorklogId).toList()); - Map writerNames = userNames(rows.stream().map(UiwsWorklog::getWriterId).toList()); - return rows.stream() - .map(w -> new WorklogListDto(w.getWorklogId(), w.getTitle(), - writerNames.getOrDefault(w.getWriterId(), w.getWriterId()), - w.getWorkDate().toString(), w.getProgressCd(), - cmtCounts.getOrDefault(w.getWorklogId(), 0L))) - .toList(); - } - - private WorklogDetailDto toDetail(UiwsWorklog w) { - List dtls = mapper.findDtlByWorklogId(w.getWorklogId()); - List cmts = mapper.findCmtByWorklogId(w.getWorklogId()); - List nameIds = new ArrayList<>(); - nameIds.add(w.getWriterId()); - cmts.forEach(c -> nameIds.add(c.getWriterId())); - Map writerNames = userNames(nameIds); - - List details = dtls.stream().map(WorklogDtlDto::from).toList(); - List comments = cmts.stream() - .map(c -> new WorklogCommentDto(c.getCmtId(), c.getCmtContent(), - writerNames.getOrDefault(c.getWriterId(), c.getWriterId()), - fmt(c.getCreatedAt()), c.getKakaoSentYn(), c.getConfirmYn())) - .toList(); - return new WorklogDetailDto(w.getWorklogId(), w.getTitle(), w.getWriterId(), - writerNames.getOrDefault(w.getWriterId(), w.getWriterId()), w.getWorkDate().toString(), - w.getWorkStatusCd(), w.getProgressCd(), w.getRepeatYn(), - w.getRepeatStartDate() != null ? w.getRepeatStartDate().toString() : null, - w.getRepeatEndDate() != null ? w.getRepeatEndDate().toString() : null, - details, comments); - } - - private void applyHeader(UiwsWorklog w, WorklogSaveDto dto) { - w.setTitle(dto.title()); - w.setWorkDate(parseDate(dto.workDate())); - w.setWorkStatusCd(dto.workStatusCd()); - w.setProgressCd((dto.progressCd() != null && !dto.progressCd().isBlank()) ? dto.progressCd() : DEFAULT_PROGRESS); - w.setRepeatYn((dto.repeatYn() != null && !dto.repeatYn().isBlank()) ? dto.repeatYn() : "N"); - w.setRepeatStartDate(parseDateNullable(dto.repeatStartDate())); - w.setRepeatEndDate(parseDateNullable(dto.repeatEndDate())); - } - - private UiwsWorklogDtl newDtl(Long worklogId, WorklogDtlDto d, String actor) { - UiwsWorklogDtl e = new UiwsWorklogDtl(); - e.setWorklogId(worklogId); - applyDtl(e, d); - e.setCreatedBy(actor); - e.setCreatedAt(LocalDateTime.now()); - return e; - } - - private void applyDtl(UiwsWorklogDtl e, WorklogDtlDto d) { - if (d.startHour() == null || d.endHour() == null - || d.startHour() < 0 || d.endHour() > 24 || d.endHour() < d.startHour()) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_TIME_INVALID); - } - e.setStartHour(d.startHour()); - e.setEndHour(d.endHour()); - e.setWorkTypeCd(d.workTypeCd()); - e.setCompanyId(d.companyId()); - e.setWorkContent(d.workContent()); - e.setIssueContent(d.issueContent()); - e.setSortOrd(d.sortOrd() != null ? d.sortOrd() : 0); - } - - private void validateNoOverlap(List details) { - List rows = details.stream() - .filter(d -> d.startHour() != null && d.endHour() != null) - .sorted((a, b) -> Integer.compare(a.startHour(), b.startHour())) - .toList(); - for (int i = 1; i < rows.size(); i++) { - WorklogDtlDto prev = rows.get(i - 1); - WorklogDtlDto cur = rows.get(i); - if (cur.startHour() < prev.endHour()) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_TIME_OVERLAP); - } - } - } - - private UiwsWorklog findWorklog(Long id) { - UiwsWorklog w = mapper.findWorklogById(id); - if (w == null) { - throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND); - } - return w; - } - - private Map commentCounts(List worklogIds) { - if (worklogIds == null || worklogIds.isEmpty()) { - return Map.of(); - } - Map result = new LinkedHashMap<>(); - for (Map cc : mapper.countCommentsByWorklogIds(worklogIds)) { - result.put(toLong(cc.get("worklogId")), toLong(cc.get("cnt"))); - } - return result; - } - - private Map userNames(List userIds) { - List ids = userIds.stream().filter(s -> s != null && !s.isBlank()).distinct().toList(); - if (ids.isEmpty()) { - return Map.of(); - } - return mapper.findUserNames(ids).stream() - .collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a)); - } - - /** writerId(username) → email (댓글 알림 메일 발송용). 미보유 시 null. */ - private String userEmail(String writerId) { - if (writerId == null || writerId.isBlank()) { - return null; - } - return mapper.findUserEmail(writerId); - } - - private static int totalPages(long total, int size) { - return size <= 0 ? 0 : (int) ((total + size - 1) / size); - } - - private static String blankToNull(String s) { - return (s == null || s.isBlank()) ? null : s; - } - - private static String safeState(String s) { - return s == null ? "" : s.trim(); - } - - private static String str(Object o) { - return o == null ? "" : o.toString(); - } - - private static long toLong(Object o) { - if (o == null) { - return 0L; - } - return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString()); - } - - private static String fmt(LocalDateTime dt) { - return dt != null ? dt.format(DT) : null; - } - - private static LocalDate parseDate(String s) { - try { - return LocalDate.parse(s); - } catch (Exception e) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "날짜 형식이 올바르지 않습니다(yyyy-MM-dd)."); - } - } - - private static LocalDate parseDateNullable(String s) { - return (s == null || s.isBlank()) ? null : parseDate(s); - } - - private static YearMonth parseYearMonth(String s) { - try { - if (s == null || s.isBlank()) { - return YearMonth.now(); - } - return YearMonth.parse(s); - } catch (Exception e) { - throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "yearMonth 형식이 올바르지 않습니다(yyyy-MM)."); - } - } -} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index fd4c915..c866c8d 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8016 + port: 8015 spring: application: @@ -12,16 +12,6 @@ spring: hikari: maximum-pool-size: 3 connection-timeout: 30000 - # UIWS 이식: 부팅 시 schema.sql + 91_uiws_port.sql 멱등 적용 - # (둘 다 IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / ON CONFLICT DO NOTHING — 재실행 안전). - # 기존 deploy 의 psql -f schema.sql 와 중복돼도 멱등. 91_uiws_port 는 deploy 미적용분(uiws 테이블/2FA 컬럼) 보강. - sql: - init: - mode: always - continue-on-error: true - schema-locations: - - classpath:db/schema.sql - - classpath:db/91_uiws_port.sql web: resources: static-locations: classpath:/static/ @@ -29,8 +19,7 @@ spring: throw-exception-if-no-handler-found: true mybatis: - # UIWS 이식: mapper/uiws/*.xml 포함을 위해 재귀 글로브(**)로 확장(기존 mapper/*.xml 포함). - mapper-locations: classpath:mapper/**/*.xml + mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl diff --git a/backend/src/main/resources/db/91_uiws_port.sql b/backend/src/main/resources/db/91_uiws_port.sql deleted file mode 100644 index fd4cb89..0000000 --- a/backend/src/main/resources/db/91_uiws_port.sql +++ /dev/null @@ -1,243 +0,0 @@ --- ============================================================================ --- UIWS 업무 테이블 이식 (ESN) — 2026-06-20, uiws-schema-porter --- 계획서: .claude/agents/_workspace/uiws_port_plan.md (schema-porter 트랙 6.1) --- 원본: workspace/uiws/db/{03_worklog,04_schedule,05_message,02_core(2FA)}.sql --- --- 네임스페이스 격리: UIWS TB_* → 소문자 tb_uiws_ 프리픽스 (기존 tb_audit_log 등과 충돌 회피). --- 멱등: 전부 CREATE TABLE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / ON CONFLICT DO NOTHING. --- mode:always 재실행 시 완전 멱등(기존 schema.sql·시드 무영향). --- FK 정책: tb_uiws_* 내부 참조만 물리 FK. 외부 user/code/company/dept FK는 논리참조로 완화 --- (UIWS는 VARCHAR USER_ID 키 / ESN esn_user PK는 BIGSERIAL — 키 체계 불일치 → 컬럼만 유지). --- 코드값(WORK_STATUS_CD/WORK_TYPE_CD/RCV_TYPE 등): TB_CODE 미이식 → 일반 컬럼 + CHECK만 유지. --- ============================================================================ - -SET client_encoding = 'UTF8'; - --- ─────────────────────────────────────────────────────────────────────────── --- [worklog] tb_uiws_worklog — 업무일지 헤더 (원본 TB_WORKLOG) --- ─────────────────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS tb_uiws_worklog ( - worklog_id BIGINT GENERATED ALWAYS AS IDENTITY, - title VARCHAR(200) NOT NULL, - writer_id VARCHAR(20) NOT NULL, -- 논리참조(UIWS USER_ID) - work_date DATE NOT NULL, - work_status_cd VARCHAR(30) NOT NULL, -- WORK_STATUS 코드값(논리) - progress_cd VARCHAR(30) NOT NULL DEFAULT 'ONGOING', - repeat_yn CHAR(1) NOT NULL DEFAULT 'N', - repeat_start_date DATE, - repeat_end_date DATE, - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_worklog PRIMARY KEY (worklog_id), - CONSTRAINT ck_uiws_worklog_progress CHECK (progress_cd IN ('ONGOING','DONE')), - CONSTRAINT ck_uiws_worklog_repeat_yn CHECK (repeat_yn IN ('Y','N')) -); -COMMENT ON TABLE tb_uiws_worklog IS 'UIWS 이식: 업무일지 헤더 (근무일자/근무상태/반복)'; - --- tb_uiws_worklog_dtl — 시간대별 상세 (통계 집계 원천, 원본 TB_WORKLOG_DTL) -CREATE TABLE IF NOT EXISTS tb_uiws_worklog_dtl ( - dtl_id BIGINT GENERATED ALWAYS AS IDENTITY, - worklog_id BIGINT NOT NULL, - start_hour INT NOT NULL, - end_hour INT NOT NULL, - work_type_cd VARCHAR(30) NOT NULL, -- WORK_TYPE 코드값(논리) - company_id VARCHAR(20) NOT NULL, -- 논리참조(UIWS COMPANY_ID) - work_content TEXT, - issue_content TEXT, - sort_ord INT DEFAULT 0, - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_worklog_dtl PRIMARY KEY (dtl_id), - CONSTRAINT fk_uiws_dtl_worklog FOREIGN KEY (worklog_id) - REFERENCES tb_uiws_worklog (worklog_id) ON DELETE CASCADE, - CONSTRAINT ck_uiws_dtl_start_hour CHECK (start_hour BETWEEN 0 AND 24), - CONSTRAINT ck_uiws_dtl_end_hour CHECK (end_hour BETWEEN 0 AND 24), - CONSTRAINT ck_uiws_dtl_hour_order CHECK (end_hour >= start_hour) -); -COMMENT ON TABLE tb_uiws_worklog_dtl IS 'UIWS 이식: 업무일지 시간대별 상세 (헤더 삭제 시 CASCADE)'; - --- tb_uiws_worklog_cmt — 댓글 (원본 TB_WORKLOG_CMT) -CREATE TABLE IF NOT EXISTS tb_uiws_worklog_cmt ( - cmt_id BIGINT GENERATED ALWAYS AS IDENTITY, - worklog_id BIGINT NOT NULL, - cmt_content TEXT NOT NULL, - writer_id VARCHAR(20) NOT NULL, -- 논리참조 - kakao_sent_yn CHAR(1) NOT NULL DEFAULT 'N', - confirm_yn CHAR(1) NOT NULL DEFAULT 'N', - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_worklog_cmt PRIMARY KEY (cmt_id), - CONSTRAINT fk_uiws_cmt_worklog FOREIGN KEY (worklog_id) - REFERENCES tb_uiws_worklog (worklog_id) ON DELETE CASCADE, - CONSTRAINT ck_uiws_cmt_confirm_yn CHECK (confirm_yn IN ('Y','N')), - CONSTRAINT ck_uiws_cmt_kakao_yn CHECK (kakao_sent_yn IN ('Y','N')) -); -COMMENT ON TABLE tb_uiws_worklog_cmt IS 'UIWS 이식: 업무일지 댓글 (등록 시 카카오 알림톡)'; - -CREATE INDEX IF NOT EXISTS ix_uiws_worklog_date_writer ON tb_uiws_worklog (work_date, writer_id, work_status_cd); -CREATE INDEX IF NOT EXISTS ix_uiws_worklog_writer ON tb_uiws_worklog (writer_id); -CREATE INDEX IF NOT EXISTS ix_uiws_dtl_type_company ON tb_uiws_worklog_dtl (work_type_cd, company_id); -CREATE INDEX IF NOT EXISTS ix_uiws_dtl_worklog ON tb_uiws_worklog_dtl (worklog_id); -CREATE INDEX IF NOT EXISTS ix_uiws_dtl_company ON tb_uiws_worklog_dtl (company_id); -CREATE INDEX IF NOT EXISTS ix_uiws_cmt_worklog ON tb_uiws_worklog_cmt (worklog_id); - --- ─────────────────────────────────────────────────────────────────────────── --- [schedule] tb_uiws_schedule — 일정 개인/부서 (원본 TB_SCHEDULE) --- ─────────────────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS tb_uiws_schedule ( - schedule_id BIGINT GENERATED ALWAYS AS IDENTITY, - sche_type VARCHAR(10) NOT NULL, - title VARCHAR(200) NOT NULL, - sche_gubun_cd VARCHAR(30), - importance_cd VARCHAR(30), - start_dt TIMESTAMP NOT NULL, - end_dt TIMESTAMP NOT NULL, - content TEXT, - owner_id VARCHAR(20) NOT NULL, -- 논리참조 - dept_id VARCHAR(20), -- 논리참조 - charger_id VARCHAR(20), -- 논리참조 - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_schedule PRIMARY KEY (schedule_id), - CONSTRAINT ck_uiws_sche_type CHECK (sche_type IN ('PERSONAL','DEPT')), - CONSTRAINT ck_uiws_sche_dt_order CHECK (end_dt >= start_dt) -); -COMMENT ON TABLE tb_uiws_schedule IS 'UIWS 이식: 일정(개인 PERSONAL / 부서 DEPT)'; - --- tb_uiws_diary — 일지(일정 연계 선택, 원본 TB_DIARY) -CREATE TABLE IF NOT EXISTS tb_uiws_diary ( - diary_id BIGINT GENERATED ALWAYS AS IDENTITY, - title VARCHAR(200) NOT NULL, - content TEXT, - schedule_id BIGINT, - writer_id VARCHAR(20) NOT NULL, -- 논리참조 - diary_date DATE, - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_diary PRIMARY KEY (diary_id), - CONSTRAINT fk_uiws_diary_sche FOREIGN KEY (schedule_id) - REFERENCES tb_uiws_schedule (schedule_id) ON DELETE SET NULL -); -COMMENT ON TABLE tb_uiws_diary IS 'UIWS 이식: 일지 (일정 연계 선택, 일정 삭제 시 연계 해제)'; - --- tb_uiws_attach — 첨부파일 폴리모픽 (원본 TB_ATTACH, 물리 FK 미적용) -CREATE TABLE IF NOT EXISTS tb_uiws_attach ( - attach_id BIGINT GENERATED ALWAYS AS IDENTITY, - ref_type VARCHAR(20) NOT NULL, - ref_id BIGINT NOT NULL, - file_nm VARCHAR(255) NOT NULL, - file_path VARCHAR(500) NOT NULL, - file_size BIGINT, - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_attach PRIMARY KEY (attach_id), - CONSTRAINT ck_uiws_attach_ref_type CHECK (ref_type IN ('SCHEDULE','DIARY')), - CONSTRAINT ck_uiws_attach_size CHECK (file_size IS NULL OR file_size >= 0) -); -COMMENT ON TABLE tb_uiws_attach IS 'UIWS 이식: 첨부파일 (폴리모픽 REF_TYPE=SCHEDULE/DIARY)'; - -CREATE INDEX IF NOT EXISTS ix_uiws_sche_dt_range ON tb_uiws_schedule (start_dt, end_dt, sche_type); -CREATE INDEX IF NOT EXISTS ix_uiws_sche_owner ON tb_uiws_schedule (owner_id); -CREATE INDEX IF NOT EXISTS ix_uiws_sche_dept ON tb_uiws_schedule (dept_id); -CREATE INDEX IF NOT EXISTS ix_uiws_diary_sche ON tb_uiws_diary (schedule_id); -CREATE INDEX IF NOT EXISTS ix_uiws_diary_writer ON tb_uiws_diary (writer_id, diary_date); -CREATE INDEX IF NOT EXISTS ix_uiws_attach_ref ON tb_uiws_attach (ref_type, ref_id); - --- ─────────────────────────────────────────────────────────────────────────── --- [message] tb_uiws_message — 쪽지 헤더 (원본 TB_MESSAGE) --- REF_WORKLOG_ID → tb_uiws_worklog (내부 FK 유지), REPLY_TO_ID → 자기참조. --- ─────────────────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS tb_uiws_message ( - message_id BIGINT GENERATED ALWAYS AS IDENTITY, - sender_id VARCHAR(20) NOT NULL, -- 논리참조 - title VARCHAR(200) NOT NULL, - content TEXT NOT NULL, - ref_worklog_id BIGINT, - reply_to_id BIGINT, - sent_at TIMESTAMP NOT NULL DEFAULT now(), - sender_del_yn CHAR(1) NOT NULL DEFAULT 'N', - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_message PRIMARY KEY (message_id), - CONSTRAINT fk_uiws_msg_worklog FOREIGN KEY (ref_worklog_id) - REFERENCES tb_uiws_worklog (worklog_id) ON DELETE SET NULL, - CONSTRAINT fk_uiws_msg_reply FOREIGN KEY (reply_to_id) - REFERENCES tb_uiws_message (message_id), - CONSTRAINT ck_uiws_msg_sender_del_yn CHECK (sender_del_yn IN ('Y','N')) -); -COMMENT ON TABLE tb_uiws_message IS 'UIWS 이식: 쪽지 헤더 (참조 업무일지/답장 원본 자기참조)'; - --- tb_uiws_message_rcv — 수신자 (원본 TB_MESSAGE_RCV) -CREATE TABLE IF NOT EXISTS tb_uiws_message_rcv ( - rcv_id BIGINT GENERATED ALWAYS AS IDENTITY, - message_id BIGINT NOT NULL, - receiver_id VARCHAR(20) NOT NULL, -- 논리참조 - rcv_type VARCHAR(10) NOT NULL, -- MSG_RCV_TYPE 코드값(논리) - read_yn CHAR(1) NOT NULL DEFAULT 'N', - read_at TIMESTAMP, - receiver_del_yn CHAR(1) NOT NULL DEFAULT 'N', - created_by VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_message_rcv PRIMARY KEY (rcv_id), - CONSTRAINT fk_uiws_rcv_message FOREIGN KEY (message_id) - REFERENCES tb_uiws_message (message_id) ON DELETE CASCADE, - CONSTRAINT uq_uiws_rcv_msg_receiver UNIQUE (message_id, receiver_id), - CONSTRAINT ck_uiws_rcv_type CHECK (rcv_type IN ('RECV','REF')), - CONSTRAINT ck_uiws_rcv_read_yn CHECK (read_yn IN ('Y','N')), - CONSTRAINT ck_uiws_rcv_del_yn CHECK (receiver_del_yn IN ('Y','N')) -); -COMMENT ON TABLE tb_uiws_message_rcv IS 'UIWS 이식: 쪽지 수신자(수신 RECV/참조 REF, 개봉여부)'; - -CREATE INDEX IF NOT EXISTS ix_uiws_rcv_receiver_read ON tb_uiws_message_rcv (receiver_id, read_yn); -CREATE INDEX IF NOT EXISTS ix_uiws_rcv_message ON tb_uiws_message_rcv (message_id); -CREATE INDEX IF NOT EXISTS ix_uiws_msg_sender ON tb_uiws_message (sender_id, sent_at); - --- ─────────────────────────────────────────────────────────────────────────── --- [2FA] tb_uiws_login_verify — 로그인 2차 검증 코드 (원본 TB_LOGIN_VERIFY) --- USER_ID 는 논리참조(외부 user FK 미적용 — 키 체계 불일치). --- ─────────────────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS tb_uiws_login_verify ( - verify_id BIGINT GENERATED ALWAYS AS IDENTITY, - user_id VARCHAR(50) NOT NULL, -- ESN username 논리참조 - verify_method VARCHAR(20) NOT NULL DEFAULT 'EMAIL', - verify_code VARCHAR(10) NOT NULL, - expire_at TIMESTAMP NOT NULL, - verified_yn CHAR(1) NOT NULL DEFAULT 'N', - created_by VARCHAR(20) NOT NULL DEFAULT 'SYSTEM', - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_by VARCHAR(20), - updated_at TIMESTAMP, - CONSTRAINT pk_tb_uiws_login_verify PRIMARY KEY (verify_id), - CONSTRAINT ck_uiws_verify_yn CHECK (verified_yn IN ('Y','N')), - CONSTRAINT ck_uiws_verify_method CHECK (verify_method IN ('EMAIL','OTP')) -); -COMMENT ON TABLE tb_uiws_login_verify IS 'UIWS 이식(2FA): 로그인 2차 검증 코드(이메일/OTP, 만료)'; -CREATE INDEX IF NOT EXISTS ix_uiws_verify_user ON tb_uiws_login_verify (user_id, verified_yn); - --- ─────────────────────────────────────────────────────────────────────────── --- [2FA] 기존 user 테이블 컬럼 보강 (DROP/재정의 금지 — ADD COLUMN IF NOT EXISTS 멱등) --- 이메일 인증코드·만료시각, 실패 카운트(기본 0), 잠금(기본 false), OTP 시크릿. --- ─────────────────────────────────────────────────────────────────────────── -ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS email_verify_code VARCHAR(10); -ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS email_verify_expire TIMESTAMP; -ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS login_fail_count INT DEFAULT 0; -ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS locked BOOLEAN DEFAULT false; -ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS otp_secret VARCHAR(255); - --- end 91_uiws_port.sql diff --git a/backend/src/main/resources/mapper/UserAuthMapper.xml b/backend/src/main/resources/mapper/UserAuthMapper.xml index 55e3555..f492bc2 100644 --- a/backend/src/main/resources/mapper/UserAuthMapper.xml +++ b/backend/src/main/resources/mapper/UserAuthMapper.xml @@ -14,18 +14,11 @@ - - - - - - @@ -34,31 +27,4 @@ UPDATE esn_user SET last_login_at = NOW() WHERE username = #{username} - - - - UPDATE esn_user SET login_fail_count = 0 WHERE username = #{username} - - - - UPDATE esn_user - SET login_fail_count = COALESCE(login_fail_count, 0) + 1, - locked = (COALESCE(login_fail_count, 0) + 1 >= #{maxFail}) - WHERE username = #{username} - - - - UPDATE esn_user - SET email_verify_code = #{code}, email_verify_expire = #{expire}, login_fail_count = 0 - WHERE username = #{username} - - - - UPDATE esn_user SET email_verify_code = NULL, email_verify_expire = NULL WHERE username = #{username} - - - - UPDATE esn_user SET locked = false, login_fail_count = 0 WHERE username = #{username} - - diff --git a/backend/src/main/resources/mapper/uiws/LoginVerifyMapper.xml b/backend/src/main/resources/mapper/uiws/LoginVerifyMapper.xml deleted file mode 100644 index a61ab1c..0000000 --- a/backend/src/main/resources/mapper/uiws/LoginVerifyMapper.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - INSERT INTO tb_uiws_login_verify - (user_id, verify_method, verify_code, expire_at, verified_yn, created_by, created_at) - VALUES - (#{userId}, #{verifyMethod}, #{verifyCode}, #{expireAt}, #{verifiedYn}, #{createdBy}, #{createdAt}) - - - diff --git a/backend/src/main/resources/mapper/uiws/MessageMapper.xml b/backend/src/main/resources/mapper/uiws/MessageMapper.xml deleted file mode 100644 index d13e750..0000000 --- a/backend/src/main/resources/mapper/uiws/MessageMapper.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - INSERT INTO tb_uiws_message - (sender_id, title, content, ref_worklog_id, reply_to_id, sent_at, sender_del_yn, created_by, created_at) - VALUES - (#{senderId}, #{title}, #{content}, #{refWorklogId}, #{replyToId}, #{sentAt}, #{senderDelYn}, #{createdBy}, #{createdAt}) - - - - - - - - - - - - UPDATE tb_uiws_message - SET sender_del_yn = 'Y', updated_by = #{actor}, updated_at = now() - WHERE sender_id = #{senderId} - AND message_id IN - #{id} - - - - - INSERT INTO tb_uiws_message_rcv - (message_id, receiver_id, rcv_type, read_yn, receiver_del_yn, created_by, created_at) - VALUES - (#{messageId}, #{receiverId}, #{rcvType}, #{readYn}, #{receiverDelYn}, #{createdBy}, #{createdAt}) - - - - - - - - - - - - UPDATE tb_uiws_message_rcv - SET read_yn = 'Y', read_at = #{readAt}, updated_by = #{actor}, updated_at = now() - WHERE rcv_id = #{rcvId} - - - - UPDATE tb_uiws_message_rcv - SET receiver_del_yn = 'Y', updated_by = #{actor}, updated_at = now() - WHERE receiver_id = #{receiverId} - AND message_id IN - #{id} - - - - - - - - - - diff --git a/backend/src/main/resources/mapper/uiws/ScheduleMapper.xml b/backend/src/main/resources/mapper/uiws/ScheduleMapper.xml deleted file mode 100644 index d6dda54..0000000 --- a/backend/src/main/resources/mapper/uiws/ScheduleMapper.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - INSERT INTO tb_uiws_schedule - (sche_type, title, sche_gubun_cd, importance_cd, start_dt, end_dt, content, - owner_id, dept_id, charger_id, created_by, created_at) - VALUES - (#{scheType}, #{title}, #{scheGubunCd}, #{importanceCd}, #{startDt}, #{endDt}, #{content}, - #{ownerId}, #{deptId}, #{chargerId}, #{createdBy}, #{createdAt}) - - - - UPDATE tb_uiws_schedule SET - sche_type = #{scheType}, title = #{title}, sche_gubun_cd = #{scheGubunCd}, - importance_cd = #{importanceCd}, start_dt = #{startDt}, end_dt = #{endDt}, - content = #{content}, dept_id = #{deptId}, charger_id = #{chargerId}, - updated_by = #{updatedBy}, updated_at = #{updatedAt} - WHERE schedule_id = #{scheduleId} - - - - - - - - DELETE FROM tb_uiws_schedule WHERE schedule_id = #{scheduleId} - - - - - - - WHERE start_dt < #{to} - AND end_dt >= #{from} - AND sche_type = #{scheType} - - AND owner_id IN - #{oid} - - - - - - - - - - - - INSERT INTO tb_uiws_diary - (title, content, schedule_id, writer_id, diary_date, created_by, created_at) - VALUES - (#{title}, #{content}, #{scheduleId}, #{writerId}, #{diaryDate}, #{createdBy}, #{createdAt}) - - - - UPDATE tb_uiws_diary SET - title = #{title}, content = #{content}, schedule_id = #{scheduleId}, diary_date = #{diaryDate}, - updated_by = #{updatedBy}, updated_at = #{updatedAt} - WHERE diary_id = #{diaryId} - - - - - - - - DELETE FROM tb_uiws_diary WHERE diary_id = #{diaryId} - - - - - - - - - INSERT INTO tb_uiws_attach - (ref_type, ref_id, file_nm, file_path, file_size, created_by, created_at) - VALUES - (#{refType}, #{refId}, #{fileNm}, #{filePath}, #{fileSize}, #{createdBy}, #{createdAt}) - - - - - - UPDATE tb_uiws_attach - SET ref_type = #{refType}, ref_id = #{refId}, updated_by = #{actor}, updated_at = now() - WHERE attach_id = #{attachId} - - - - - - DELETE FROM tb_uiws_attach WHERE attach_id = #{attachId} - - - - DELETE FROM tb_uiws_attach WHERE ref_type = #{refType} AND ref_id = #{refId} - - - - - - diff --git a/backend/src/main/resources/mapper/uiws/StatsMapper.xml b/backend/src/main/resources/mapper/uiws/StatsMapper.xml deleted file mode 100644 index ac9dd3a..0000000 --- a/backend/src/main/resources/mapper/uiws/StatsMapper.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - diff --git a/backend/src/main/resources/mapper/uiws/WorklogMapper.xml b/backend/src/main/resources/mapper/uiws/WorklogMapper.xml deleted file mode 100644 index 42a91fa..0000000 --- a/backend/src/main/resources/mapper/uiws/WorklogMapper.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - - INSERT INTO tb_uiws_worklog - (title, writer_id, work_date, work_status_cd, progress_cd, repeat_yn, - repeat_start_date, repeat_end_date, created_by, created_at) - VALUES - (#{title}, #{writerId}, #{workDate}, #{workStatusCd}, #{progressCd}, #{repeatYn}, - #{repeatStartDate}, #{repeatEndDate}, #{createdBy}, #{createdAt}) - - - - UPDATE tb_uiws_worklog SET - title = #{title}, writer_id = #{writerId}, work_date = #{workDate}, - work_status_cd = #{workStatusCd}, progress_cd = #{progressCd}, repeat_yn = #{repeatYn}, - repeat_start_date = #{repeatStartDate}, repeat_end_date = #{repeatEndDate}, - updated_by = #{updatedBy}, updated_at = #{updatedAt} - WHERE worklog_id = #{worklogId} - - - - - - - - DELETE FROM tb_uiws_worklog WHERE worklog_id = #{worklogId} - - - - - AND writer_id IN - #{oid} - - - - - - - - - - - - - - - - INSERT INTO tb_uiws_worklog_dtl - (worklog_id, start_hour, end_hour, work_type_cd, company_id, work_content, issue_content, - sort_ord, created_by, created_at) - VALUES - (#{worklogId}, #{startHour}, #{endHour}, #{workTypeCd}, #{companyId}, #{workContent}, #{issueContent}, - #{sortOrd}, #{createdBy}, #{createdAt}) - - - - UPDATE tb_uiws_worklog_dtl SET - start_hour = #{startHour}, end_hour = #{endHour}, work_type_cd = #{workTypeCd}, - company_id = #{companyId}, work_content = #{workContent}, issue_content = #{issueContent}, - sort_ord = #{sortOrd}, updated_by = #{updatedBy}, updated_at = #{updatedAt} - WHERE dtl_id = #{dtlId} - - - - DELETE FROM tb_uiws_worklog_dtl WHERE dtl_id = #{dtlId} - - - - - - - INSERT INTO tb_uiws_worklog_cmt - (worklog_id, cmt_content, writer_id, kakao_sent_yn, confirm_yn, created_by, created_at) - VALUES - (#{worklogId}, #{cmtContent}, #{writerId}, #{kakaoSentYn}, #{confirmYn}, #{createdBy}, #{createdAt}) - - - - - - UPDATE tb_uiws_worklog_cmt SET kakao_sent_yn = 'Y' WHERE cmt_id = #{cmtId} - - - - UPDATE tb_uiws_worklog_cmt - SET confirm_yn = 'Y', updated_by = #{actor}, updated_at = now() - WHERE cmt_id = #{cmtId} - - - - - - - - - - - - - - - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 634a0f7..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,3416 +0,0 @@ -{ - "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/public/login.png b/frontend/public/login.png deleted file mode 100644 index 329b27b..0000000 Binary files a/frontend/public/login.png and /dev/null differ diff --git a/frontend/src/api/uiws.ts b/frontend/src/api/uiws.ts deleted file mode 100644 index 604aa69..0000000 --- a/frontend/src/api/uiws.ts +++ /dev/null @@ -1,65 +0,0 @@ -import api from './client' - -/** - * UIWS 이식 모듈 API 클라이언트. 기존 ESN axios 인스턴스(baseURL='', esn_token JWT 인터셉터) 재사용. - * ESN client 는 baseURL 이 비어 있으므로 전체 경로 /api/... 를 사용한다. - * 응답 봉투: { success, message, data }. 호출부는 res.data.data 로 페이로드 접근. - */ - -// ── 2FA -export const verify2fa = (verifyToken: string, code: string) => - api.post('/api/auth/verify', { verifyToken, code }) - -// ── 쪽지(message) -export const sendMessage = (body: object) => api.post('/api/messages', body) -export const listSent = (params: Record) => api.get('/api/messages/sent', { params }) -export const sentDetail = (id: number) => api.get(`/api/messages/sent/${id}`) -export const deleteSent = (ids: number[]) => api.delete('/api/messages/sent', { data: { ids } }) -export const listReceived = (params: Record) => api.get('/api/messages/received', { params }) -export const receivedDetail = (id: number) => api.get(`/api/messages/received/${id}`) -export const deleteReceived = (ids: number[]) => api.delete('/api/messages/received', { data: { ids } }) -export const unreadCount = () => api.get('/api/messages/unread-count') - -// ── 일정(schedule) -export const scheduleCalendar = (params: Record) => api.get('/api/schedules', { params }) -export const scheduleAll = (params: Record) => api.get('/api/schedules/all', { params }) -export const scheduleSearch = (keyword: string) => api.get('/api/schedules/search', { params: { keyword } }) -export const createSchedule = (body: object) => api.post('/api/schedules', body) -export const scheduleDetail = (id: number) => api.get(`/api/schedules/${id}`) -export const updateSchedule = (id: number, body: object) => api.put(`/api/schedules/${id}`, body) -export const deleteSchedule = (id: number) => api.delete(`/api/schedules/${id}`) - -// ── 일지(diary) -export const diaryList = (params: Record) => api.get('/api/diaries', { params }) -export const createDiary = (body: object) => api.post('/api/diaries', body) -export const diaryDetail = (id: number) => api.get(`/api/diaries/${id}`) -export const updateDiary = (id: number, body: object) => api.put(`/api/diaries/${id}`, body) -export const deleteDiary = (id: number) => api.delete(`/api/diaries/${id}`) - -// ── 첨부(attachment) -export const uploadAttachment = (refType: string, refId: number, file: File) => { - const fd = new FormData() - fd.append('refType', refType) - fd.append('refId', String(refId)) - fd.append('file', file) - return api.post('/api/attachments', fd, { headers: { 'Content-Type': 'multipart/form-data' } }) -} -export const deleteAttachment = (id: number) => api.delete(`/api/attachments/${id}`) -export const attachmentDownloadUrl = (id: number) => `/api/attachments/${id}/download` - -// ── 업무일지(worklog) -export const worklogList = (params: Record) => api.get('/api/worklogs', { params }) -export const worklogCalendar = (params: Record) => api.get('/api/worklogs/calendar', { params }) -export const worklogSearch = (params: Record) => api.get('/api/worklogs/search', { params }) -export const worklogProgress = (params: Record) => api.get('/api/worklogs/dashboard/progress', { params }) -export const createWorklog = (body: object) => api.post('/api/worklogs', body) -export const worklogDetail = (id: number) => api.get(`/api/worklogs/${id}`) -export const updateWorklog = (id: number, body: object) => api.put(`/api/worklogs/${id}`, body) -export const deleteWorklog = (id: number) => api.delete(`/api/worklogs/${id}`) -export const addWorklogComment = (id: number, cmtContent: string) => - api.post(`/api/worklogs/${id}/comments`, { cmtContent }) -export const confirmWorklogComment = (cmtId: number) => api.post(`/api/worklogs/comments/${cmtId}/confirm`) - -// ── 통계(stats) -export const personalWorkStats = (params: Record) => api.get('/api/stats/personal-work', { params }) -export const companyWorkStats = (params: Record) => api.get('/api/stats/company-work', { params }) diff --git a/frontend/src/components/uiws/ThemeToggle.tsx b/frontend/src/components/uiws/ThemeToggle.tsx deleted file mode 100644 index dbce4bc..0000000 --- a/frontend/src/components/uiws/ThemeToggle.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Moon, Sun } from 'lucide-react' -import { useTheme } from '../../theme/ThemeContext' - -/** 다크/라이트 테마 토글 버튼. 사이드바 하단 등에 배치. */ -export default function ThemeToggle() { - const { theme, toggle } = useTheme() - return ( - - ) -} diff --git a/frontend/src/components/uiws/ui.tsx b/frontend/src/components/uiws/ui.tsx deleted file mode 100644 index a801939..0000000 --- a/frontend/src/components/uiws/ui.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - * UIWS 이식 공통 컴포넌트 키트 — 색상 하드코딩 금지(테마 토큰 var(--uiws-*)만 사용). - * 다크/라이트 양 모드에서 대비/가독성 정상. 기존 ERP 컴포넌트와 충돌 회피 위해 components/uiws/ 네임스페이스. - */ -import { type ReactNode, type CSSProperties } from 'react' - -const card: CSSProperties = { - background: 'var(--uiws-surface)', - border: '1px solid var(--uiws-border)', - borderRadius: 12, - boxShadow: 'var(--uiws-shadow)', -} - -export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: string; actions?: ReactNode }) { - return ( -

-
-

{title}

- {subtitle &&
{subtitle}
} -
-
{actions}
-
- ) -} - -export function Panel({ children, style }: { children: ReactNode; style?: CSSProperties }) { - return
{children}
-} - -export function Button({ - children, onClick, variant = 'primary', type = 'button', disabled, -}: { - children: ReactNode; onClick?: () => void; variant?: 'primary' | 'ghost' | 'danger' - type?: 'button' | 'submit'; disabled?: boolean -}) { - const styles: Record = { - primary: { background: 'var(--uiws-primary)', color: 'var(--uiws-primary-contrast)', border: 'none' }, - ghost: { background: 'transparent', color: 'var(--uiws-text-muted)', border: '1px solid var(--uiws-border)' }, - danger: { background: 'var(--uiws-danger)', color: '#fff', border: 'none' }, - } - return ( - - ) -} - -export function Input({ - value, onChange, placeholder, type = 'text', style, -}: { - value: string; onChange: (v: string) => void; placeholder?: string; type?: string; style?: CSSProperties -}) { - return ( - onChange(e.target.value)} - style={{ padding: '9px 12px', borderRadius: 8, fontSize: 13, - background: 'var(--uiws-input-bg)', border: '1px solid var(--uiws-border)', color: 'var(--uiws-text)', ...style }} /> - ) -} - -export function FormField({ label, children }: { label: string; children: ReactNode }) { - return ( - - ) -} - -export function SearchBar({ children }: { children: ReactNode }) { - return ( - - {children} - - ) -} - -export interface Column { - key: string - header: string - render?: (row: T) => ReactNode - width?: number | string - align?: 'left' | 'center' | 'right' -} - -export function DataGrid>({ - columns, rows, rowKey, onRowClick, empty = '데이터가 없습니다.', -}: { - columns: Column[]; rows: T[]; rowKey: (row: T) => string | number - onRowClick?: (row: T) => void; empty?: string -}) { - return ( -
- - - - {columns.map(c => ( - - ))} - - - - {rows.length === 0 ? ( - - ) : rows.map(row => ( - onRowClick?.(row)} - style={{ cursor: onRowClick ? 'pointer' : 'default', borderBottom: '1px solid var(--uiws-border)' }} - onMouseEnter={e => (e.currentTarget.style.background = 'var(--uiws-row-hover)')} - onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}> - {columns.map(c => ( - - ))} - - ))} - -
{c.header}
{empty}
- {c.render ? c.render(row) : String(row[c.key] ?? '')} -
-
- ) -} - -export function Pagination({ page, totalPages, onChange }: { page: number; totalPages: number; onChange: (p: number) => void }) { - if (totalPages <= 1) return null - return ( -
- - - {page + 1} / {totalPages} - - -
- ) -} - -export function YnBadge({ yn, yes = '읽음', no = '안읽음' }: { yn: string; yes?: string; no?: string }) { - const on = yn === 'Y' - return ( - - {on ? yes : no} - - ) -} - -export function Modal({ title, onClose, children, footer }: { title: string; onClose: () => void; children: ReactNode; footer?: ReactNode }) { - return ( -
-
e.stopPropagation()} - style={{ ...card, width: 'min(560px, 92vw)', maxHeight: '88vh', overflow: 'auto', padding: 22 }}> -
-

{title}

- -
-
{children}
- {footer &&
{footer}
} -
-
- ) -} - -export function Spinner({ label = '불러오는 중...' }: { label?: string }) { - return
{label}
-} diff --git a/frontend/src/pages/AiAnalysis.tsx b/frontend/src/pages/AiAnalysis.tsx index 9d8945d..e1bfce2 100644 --- a/frontend/src/pages/AiAnalysis.tsx +++ b/frontend/src/pages/AiAnalysis.tsx @@ -36,7 +36,7 @@ export default function AiAnalysis() { value={alarmText} onChange={e => setAlarmText(e.target.value)} rows={4} - placeholder="알람 메시지를 입력하세요 예: ESL-001 배터리 부족 경고 — Store: A매장 구미, Device: ESL-A123" + placeholder="알람 메시지를 입력하세요 예: ESL-001 배터리 부족 경고 — Store: LG이노텍 구미, Device: ESL-A123" className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-gray-300 placeholder-gray-600 resize-none mb-3" /> - - ) : ( -
-

- {maskedEmail ? `${maskedEmail} 로 발송된 인증코드를 입력하세요.` : '발송된 인증코드를 입력하세요.'} -

-
- - setCode(e.target.value)} - placeholder="000000" - className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white text-center tracking-[0.4em] placeholder-gray-600 focus:border-brand focus:outline-none" - /> -
- {error &&

{error}

} - - -
- )} +
+
+ + setUsername(e.target.value)} + placeholder="admin" + className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-brand focus:outline-none" + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-brand focus:outline-none" + /> +
+ {error &&

{error}

} + +
) diff --git a/frontend/src/pages/StoreList.tsx b/frontend/src/pages/StoreList.tsx index 9227a43..410b92c 100644 --- a/frontend/src/pages/StoreList.tsx +++ b/frontend/src/pages/StoreList.tsx @@ -110,7 +110,7 @@ function StoreForm({ initial, onSave }: { initial: any; onSave: (d: any) => void
{ e.preventDefault(); onSave(f) }} className="space-y-3"> {[ { label: '테넌트', key: 'tenantCode', type: 'select', - opts: ['TENANT_A','TENANT_B','EMART','ZIOINFO'] }, + opts: ['LGINNOTEK','LGIT','EMART','ZIOINFO'] }, { label: '매장코드', key: 'storeCode' }, { label: '매장명', key: 'storeName' }, { label: '주소', key: 'address' }, diff --git a/frontend/src/pages/uiws/MessageBox.tsx b/frontend/src/pages/uiws/MessageBox.tsx deleted file mode 100644 index a2aa19b..0000000 --- a/frontend/src/pages/uiws/MessageBox.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useEffect, useState } from 'react' -import { listReceived, listSent, receivedDetail, sentDetail, sendMessage } from '../../api/uiws' -import { PageHeader, SearchBar, Input, Button, DataGrid, Pagination, Modal, FormField, YnBadge, Spinner, type Column } from '../../components/uiws/ui' - -type Tab = 'received' | 'sent' - -export default function MessageBox() { - const [tab, setTab] = useState('received') - const [rows, setRows] = useState([]) - const [page, setPage] = useState(0) - const [totalPages, setTotalPages] = useState(0) - const [keyword, setKeyword] = useState('') - const [loading, setLoading] = useState(false) - const [compose, setCompose] = useState(false) - const [detail, setDetail] = useState(null) - - const load = async () => { - setLoading(true) - try { - const fn = tab === 'received' ? listReceived : listSent - const res = await fn({ page, size: 20, titleKeyword: keyword || undefined }) - const data = res.data.data - setRows(data.content ?? []) - setTotalPages(data.totalPages ?? 0) - } finally { - setLoading(false) - } - } - useEffect(() => { load() }, [tab, page]) - - const receivedCols: Column[] = [ - { key: 'title', header: '제목' }, - { key: 'senderNm', header: '보낸사람', width: 130 }, - { key: 'sentAt', header: '받은시각', width: 170 }, - { key: 'readYn', header: '상태', width: 90, render: r => }, - ] - const sentCols: Column[] = [ - { key: 'title', header: '제목' }, - { key: 'receiverSummary', header: '받는사람', width: 160 }, - { key: 'open', header: '개봉', width: 100, align: 'center', render: r => `${r.openCount}/${r.totalCount}` }, - { key: 'sentAt', header: '보낸시각', width: 170 }, - ] - - const open = async (r: any) => { - const res = tab === 'received' ? await receivedDetail(r.messageId) : await sentDetail(r.messageId) - setDetail({ ...res.data.data, _tab: tab }) - if (tab === 'received') load() // 개봉처리 반영 - } - - return ( -
- setCompose(true)}>+ 쪽지 보내기} /> - - - - -
- - - - - {loading ? : ( - <> - r.messageId} onRowClick={open} empty="쪽지가 없습니다." /> - - - )} - - {compose && setCompose(false)} onSent={() => { setCompose(false); load() }} />} - {detail && setDetail(null)} />} -
- ) -} - -function Compose({ onClose, onSent }: { onClose: () => void; onSent: () => void }) { - const [receiverId, setReceiverId] = useState('') - const [title, setTitle] = useState('') - const [content, setContent] = useState('') - const [err, setErr] = useState('') - const send = async () => { - setErr('') - try { - await sendMessage({ title, content, receivers: [{ receiverId, rcvType: 'RECV' }] }) - onSent() - } catch (e: any) { setErr(e?.response?.data?.message || '전송 실패') } - } - return ( - }> - - - - {err &&
{err}
} -
- ) -} - -function DetailModal({ detail, onClose }: { detail: any; onClose: () => void }) { - return ( - 닫기}> -
- {detail._tab === 'received' ? `보낸사람: ${detail.senderNm}` : `개봉 ${detail.openCount}/${detail.totalCount}`} · {detail.sentAt} -
-
{detail.content}
- {detail._tab === 'sent' && detail.receivers && ( -
-
수신자 개봉현황
- {detail.receivers.map((r: any, i: number) => ( -
- {r.receiverNm} ({r.rcvType}) -
- ))} -
- )} -
- ) -} diff --git a/frontend/src/pages/uiws/ScheduleCalendar.tsx b/frontend/src/pages/uiws/ScheduleCalendar.tsx deleted file mode 100644 index 5a03426..0000000 --- a/frontend/src/pages/uiws/ScheduleCalendar.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useEffect, useState } from 'react' -import { scheduleCalendar, createSchedule, scheduleDetail, deleteSchedule } from '../../api/uiws' -import { PageHeader, Panel, Button, Modal, FormField, Input, Spinner } from '../../components/uiws/ui' - -interface Sched { scheduleId: number; title: string; startDt: string; endDt: string; scheType: string; importanceCd?: string } - -const WEEK = ['일', '월', '화', '수', '목', '금', '토'] - -export default function ScheduleCalendar() { - const [base, setBase] = useState(new Date()) - const [type, setType] = useState<'PERSONAL' | 'DEPT'>('PERSONAL') - const [items, setItems] = useState([]) - const [loading, setLoading] = useState(false) - const [showForm, setShowForm] = useState(null) // date string - const [detail, setDetail] = useState(null) - - const ym = `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, '0')}` - - const load = async () => { - setLoading(true) - try { - const res = await scheduleCalendar({ type, view: 'month', baseDate: `${ym}-01` }) - setItems(res.data.data ?? []) - } finally { - setLoading(false) - } - } - useEffect(() => { load() }, [ym, type]) - - const first = new Date(base.getFullYear(), base.getMonth(), 1) - const startPad = first.getDay() - const daysInMonth = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate() - const cells: (number | null)[] = [...Array(startPad).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)] - - const byDay = (d: number) => { - const ds = `${ym}-${String(d).padStart(2, '0')}` - return items.filter(s => (s.startDt ?? '').slice(0, 10) <= ds && ds <= (s.endDt ?? '').slice(0, 10)) - } - - const move = (delta: number) => setBase(new Date(base.getFullYear(), base.getMonth() + delta, 1)) - - return ( -
- setShowForm(`${ym}-01`)}>+ 일정 등록} /> - - - -
{ym}
- -
- - - - - {loading ? : ( -
-
- {WEEK.map((w, i) => ( -
{w}
- ))} - {cells.map((d, i) => ( -
d && setShowForm(`${ym}-${String(d).padStart(2, '0')}`)} - style={{ minHeight: 96, padding: 6, borderRight: '1px solid var(--uiws-border)', borderBottom: '1px solid var(--uiws-border)', - cursor: d ? 'pointer' : 'default' }}> - {d &&
{d}
} - {d && byDay(d).slice(0, 3).map(s => ( -
{ e.stopPropagation(); scheduleDetail(s.scheduleId).then(r => setDetail(r.data.data)) }} - style={{ fontSize: 11, padding: '2px 6px', borderRadius: 6, marginBottom: 3, - background: 'var(--uiws-primary-soft)', color: 'var(--uiws-primary)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}> - {s.title} -
- ))} -
- ))} -
-
- )} - - {showForm && setShowForm(null)} onSaved={() => { setShowForm(null); load() }} />} - {detail && setDetail(null)} onDeleted={() => { setDetail(null); load() }} />} -
- ) -} - -function ScheduleForm({ date, type, onClose, onSaved }: { date: string; type: string; onClose: () => void; onSaved: () => void }) { - const [title, setTitle] = useState('') - const [startDt, setStartDt] = useState(`${date}T09:00:00`) - const [endDt, setEndDt] = useState(`${date}T18:00:00`) - const [content, setContent] = useState('') - const [err, setErr] = useState('') - const save = async () => { - setErr('') - try { - await createSchedule({ scheType: type, title, startDt, endDt, content, attachmentIds: [] }) - onSaved() - } catch (e: any) { setErr(e?.response?.data?.message || '저장 실패') } - } - return ( - }> - - - - - {err &&
{err}
} -
- ) -} - -function ScheduleDetailModal({ detail, onClose, onDeleted }: { detail: any; onClose: () => void; onDeleted: () => void }) { - const remove = async () => { await deleteSchedule(detail.scheduleId); onDeleted() } - return ( - }> -
{detail.scheType} · {detail.startDt} ~ {detail.endDt}
-
{detail.content || '내용 없음'}
-
- ) -} diff --git a/frontend/src/pages/uiws/StatsPivot.tsx b/frontend/src/pages/uiws/StatsPivot.tsx deleted file mode 100644 index cc9f825..0000000 --- a/frontend/src/pages/uiws/StatsPivot.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useState } from 'react' -import { personalWorkStats, companyWorkStats } from '../../api/uiws' -import { PageHeader, SearchBar, Input, Button, Spinner } from '../../components/uiws/ui' - -interface PivotColumn { key: string; label: string } -interface PivotResponse { fixedColumns: PivotColumn[]; dynamicColumns: PivotColumn[]; rows: Record[] } - -type Mode = 'personal' | 'company' - -export default function StatsPivot() { - const [mode, setMode] = useState('personal') - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - const today = new Date().toISOString().slice(0, 10) - const weekAgo = new Date(Date.now() - 6 * 864e5).toISOString().slice(0, 10) - const [fromDate, setFromDate] = useState(weekAgo) - const [toDate, setToDate] = useState(today) - - const load = async () => { - setLoading(true) - try { - const fn = mode === 'personal' ? personalWorkStats : companyWorkStats - const res = await fn({ fromDate, toDate }) - setData(res.data.data) - } finally { - setLoading(false) - } - } - useEffect(() => { load() }, [mode]) - - const cols = data ? [...data.fixedColumns, ...data.dynamicColumns] : [] - - return ( -
- - - - - -
- - ~ - - - - - {loading ? : !data || data.rows.length === 0 ? ( -
집계 데이터가 없습니다.
- ) : ( -
- - - - {cols.map(c => ( - - ))} - - - - {data.rows.map((row, i) => ( - - {cols.map(c => ( - - ))} - - ))} - -
{c.label}
{String(row[c.key] ?? '')}
-
- )} -
- ) -} diff --git a/frontend/src/pages/uiws/WorklogList.tsx b/frontend/src/pages/uiws/WorklogList.tsx deleted file mode 100644 index a1c9d7b..0000000 --- a/frontend/src/pages/uiws/WorklogList.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useEffect, useState } from 'react' -import { worklogList, worklogProgress, createWorklog, worklogDetail, deleteWorklog, addWorklogComment } from '../../api/uiws' -import { PageHeader, SearchBar, Input, Button, DataGrid, Pagination, Modal, FormField, Spinner, type Column } from '../../components/uiws/ui' - -interface WorklogRow extends Record { - worklogId: number; title: string; writerNm: string; workDate: string; progressCd: string; commentCount: number -} - -const PROGRESS_LABEL: Record = { ONGOING: '진행중', DONE: '종료' } - -export default function WorklogList() { - const [rows, setRows] = useState([]) - const [page, setPage] = useState(0) - const [totalPages, setTotalPages] = useState(0) - const [progress, setProgress] = useState<{ progressCd: string; progressNm: string; count: number }[]>([]) - const [filterProgress, setFilterProgress] = useState('') - const [keyword, setKeyword] = useState('') - const [loading, setLoading] = useState(false) - const [showForm, setShowForm] = useState(false) - const [detail, setDetail] = useState(null) - - const load = async () => { - setLoading(true) - try { - const res = await worklogList({ page, size: 20, progressCd: filterProgress || undefined }) - const data = res.data.data - setRows(data.content ?? []) - setTotalPages(data.totalPages ?? 0) - const pr = await worklogProgress({}) - setProgress(pr.data.data ?? []) - } finally { - setLoading(false) - } - } - useEffect(() => { load() }, [page, filterProgress]) - - const columns: Column[] = [ - { key: 'workDate', header: '근무일자', width: 120 }, - { key: 'title', header: '제목' }, - { key: 'writerNm', header: '작성자', width: 120 }, - { key: 'progressCd', header: '진행', width: 90, render: r => PROGRESS_LABEL[r.progressCd] ?? r.progressCd }, - { key: 'commentCount', header: '댓글', width: 70, align: 'center' }, - ] - - const openDetail = async (r: WorklogRow) => { - const res = await worklogDetail(r.worklogId) - setDetail(res.data.data) - } - - return ( -
- setShowForm(true)}>+ 업무일지 작성} /> - -
- {progress.map(p => ( -
{ setFilterProgress(filterProgress === p.progressCd ? '' : p.progressCd); setPage(0) }} - style={{ cursor: 'pointer', padding: '12px 18px', borderRadius: 10, minWidth: 130, - background: 'var(--uiws-surface)', border: `1px solid ${filterProgress === p.progressCd ? 'var(--uiws-primary)' : 'var(--uiws-border)'}` }}> -
{p.progressNm}
-
{p.count}
-
- ))} -
- - - - - {filterProgress && } - - - {loading ? : ( - <> - r.worklogId} onRowClick={openDetail} empty="업무일지가 없습니다." /> - - - )} - - {showForm && setShowForm(false)} onSaved={() => { setShowForm(false); load() }} />} - {detail && setDetail(null)} onChanged={() => { setDetail(null); load() }} />} -
- ) -} - -function WorklogForm({ onClose, onSaved }: { onClose: () => void; onSaved: () => void }) { - const [title, setTitle] = useState('') - const [writerId, setWriterId] = useState(localStorage.getItem('esn_user') || 'admin') - const [workDate, setWorkDate] = useState(new Date().toISOString().slice(0, 10)) - const [workStatusCd, setWorkStatusCd] = useState('NORMAL') - const [err, setErr] = useState('') - - const save = async () => { - setErr('') - try { - await createWorklog({ title, writerId, workDate, workStatusCd, progressCd: 'ONGOING', details: [] }) - onSaved() - } catch (e: any) { - setErr(e?.response?.data?.message || '저장 실패') - } - } - return ( - }> - - - - - {err &&
{err}
} -
- ) -} - -function WorklogDetailModal({ detail, onClose, onChanged }: { detail: any; onClose: () => void; onChanged: () => void }) { - const [comment, setComment] = useState('') - const [err, setErr] = useState('') - - const remove = async () => { - await deleteWorklog(detail.worklogId) - onChanged() - } - const addCmt = async () => { - setErr('') - try { - await addWorklogComment(detail.worklogId, comment) - setComment('') - onChanged() - } catch (e: any) { - setErr(e?.response?.data?.message || '댓글 등록 실패') - } - } - return ( - }> -
- {detail.writerNm} · {detail.workDate} · {detail.progressCd} -
-
시간대별 상세 {detail.details?.length ?? 0}건
-
-
댓글
- {(detail.comments ?? []).map((c: any) => ( -
- {c.writerNm} · {c.createdAt}
{c.cmtContent} -
- ))} -
- - -
- {err &&
{err}
} -
-
- ) -} diff --git a/frontend/src/theme/ThemeContext.tsx b/frontend/src/theme/ThemeContext.tsx deleted file mode 100644 index 05c0a87..0000000 --- a/frontend/src/theme/ThemeContext.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' - -export type ThemeMode = 'dark' | 'light' - -interface ThemeCtx { - theme: ThemeMode - toggle: () => void - setTheme: (t: ThemeMode) => void -} - -const Ctx = createContext({ theme: 'dark', toggle: () => {}, setTheme: () => {} }) - -const STORAGE_KEY = 'esn_theme' - -function applyTheme(t: ThemeMode) { - document.documentElement.setAttribute('data-theme', t) -} - -export function ThemeProvider({ children }: { children: ReactNode }) { - const [theme, setThemeState] = useState(() => { - const saved = localStorage.getItem(STORAGE_KEY) - return saved === 'light' ? 'light' : 'dark' - }) - - useEffect(() => { - applyTheme(theme) - localStorage.setItem(STORAGE_KEY, theme) - }, [theme]) - - const setTheme = (t: ThemeMode) => setThemeState(t) - const toggle = () => setThemeState(prev => (prev === 'dark' ? 'light' : 'dark')) - - return {children} -} - -export function useTheme() { - return useContext(Ctx) -} diff --git a/frontend/src/theme/theme.css b/frontend/src/theme/theme.css deleted file mode 100644 index 56873b0..0000000 --- a/frontend/src/theme/theme.css +++ /dev/null @@ -1,52 +0,0 @@ -/* - * GUARDiA ESN 테마 토큰 (UIWS 이식 화면 공통). - * 다크/라이트 두 모드 지원. UIWS 화면/컴포넌트는 색상 하드코딩 금지 — 아래 CSS 변수만 사용한다. - * 기존 ESN 화면(Tailwind 토큰 다크)은 영향 없음(이 변수는 UIWS 스코프에서만 참조). - * - * 다크 기본값은 기존 ESN 팔레트(ink #0b0f17 / panel #131927 / card #1a2234 / brand #00a0c8)와 정합. - */ - -:root, -:root[data-theme='dark'] { - --uiws-bg: #0b0f17; - --uiws-surface: #1a2234; - --uiws-surface-2: #131927; - --uiws-border: #26304a; - --uiws-text: #e6edf3; - --uiws-text-muted: #8892b0; - --uiws-text-faint: #4d5568; - --uiws-primary: #00a0c8; - --uiws-primary-contrast: #ffffff; - --uiws-primary-soft: rgba(0, 160, 200, 0.14); - --uiws-danger: #e74c3c; - --uiws-success: #3ddc97; - --uiws-warning: #f1c40f; - --uiws-row-hover: rgba(255, 255, 255, 0.04); - --uiws-input-bg: #0b0f17; - --uiws-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); -} - -:root[data-theme='light'] { - --uiws-bg: #f4f6fb; - --uiws-surface: #ffffff; - --uiws-surface-2: #eef1f7; - --uiws-border: #d8deea; - --uiws-text: #14202e; - --uiws-text-muted: #5b6478; - --uiws-text-faint: #97a0b5; - --uiws-primary: #0089ab; - --uiws-primary-contrast: #ffffff; - --uiws-primary-soft: rgba(0, 137, 171, 0.10); - --uiws-danger: #d63b2b; - --uiws-success: #1f9e58; - --uiws-warning: #c79a08; - --uiws-row-hover: rgba(0, 0, 0, 0.035); - --uiws-input-bg: #ffffff; - --uiws-shadow: 0 4px 18px rgba(20, 30, 60, 0.10); -} - -/* UIWS 화면 컨테이너 — 토큰 기반 기본 타이포/배경 */ -.uiws-scope { - color: var(--uiws-text); -} -.uiws-scope a { color: var(--uiws-primary); }