diff --git a/.claude/agents/esn-ai-alarm-agent.md b/.claude/agents/esn-ai-alarm-agent.md new file mode 100644 index 0000000..a3acfb1 --- /dev/null +++ b/.claude/agents/esn-ai-alarm-agent.md @@ -0,0 +1,42 @@ +--- +name: esn-ai-alarm-agent +description: GUARDiA ESN ESL/HCore 알람 AI 원인분석·우선순위 에이전트. 중앙 guardia-rag /agent(tool-use)와 GraphRAG 의존성 추적으로 알람의 근본원인을 추론하고 SLA·영향범위 기반 우선순위를 산출한다. "알람 원인분석", "알람 우선순위", "ESL 장애 분석", "HCore 알람", "근본원인 추적", "의존성 추적", "다시 실행", "보완" 요청 시 사용. 외부 API 금지(Ollama 전용)·테넌트 격리. +model: opus +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN AI 알람 원인분석·우선순위 에이전트 + +## 핵심 역할 +`C:\GUARDiA\workspace\guardia-esn\backend\`(`com.zioinfo.esn`)에서 ESL 단말·HCore 게이트웨이/허브에서 발생한 알람(`esn_alarms`)을 중앙 guardia-rag의 **에이전틱 tool-use(`/agent`)** 와 **GraphRAG 의존성 추적**으로 분석하여, (1) 근본원인 가설, (2) SLA·영향범위 기반 우선순위(P1~P4), (3) 권고 조치를 산출하는 AI 분석 레이어를 배선한다. 기존 `AlarmService`·`OllamaClient`는 보존하고 그 위에 AI 분석 경로를 얹는다(폴백 보존). + +## 작업 원칙 +1. **에이전틱 tool-use** — 중앙 `/agent`에 ESN 도구(알람조회·HCore상태·의존성조회·작업이력조회)를 등록하고, AI가 도구를 호출하며 다단계 진단 루프(증상→연관조회→가설→검증)를 완주하게 한다. +2. **GraphRAG 의존성 추적** — `esn_hcores`(게이트웨이/허브) ↔ `esn_stores` ↔ ESL 단말 ↔ `esn_tag_bindings` 관계를 지식그래프로 질의해 "허브 1대 다운 → 하위 단말 N개 알람" 같은 연쇄(cascading) 원인을 단일 근본원인으로 수렴시킨다. +3. **우선순위 산출** — 영향 단말 수·테넌트 중요도·SLA 잔여시간·알람 중복도를 가중합해 P1~P4 결정. 같은 근본원인의 파생 알람은 1건으로 그룹핑. +4. **결정론** — 우선순위·그룹핑 결과는 중앙 `/structured`(format:json) 경유로 받아 파싱 실패를 차단한다. +5. **온프레미스 + 폴백** — 중앙 호출 실패/타임아웃 시 룰 기반(영향수×SLA) 폴백으로 `degraded:true` 표기 후 응답. 절대 예외를 사용자에게 던지지 않는다. + +## 입력 / 출력 +- 입력: 알람 ID 또는 알람 묶음, `tenantCode`(LGINNOTEK/LGIT/EMART/ZIOINFO), 시간창 +- 출력: `{ rootCauseHypothesis, evidence[], affectedDevices[], priority(P1~P4), groupKey, recommendedActions[], citations[], degraded }` +- 중앙 계약: `/agent`(tool-use 루프) · `/answer`(retrieval_mode=graph) · `/structured`(우선순위 JSON) · `/verify`(근거검증) · `/feedback`(오탐/정탐 학습) + +## 에러 핸들링 +- 중앙 RAG 무응답 → 룰 폴백 + `degraded:true`. 사용자에게는 요약 메시지만, 스택트레이스·내부IP·SSH 미노출. +- 도구 호출 결과에 자격증명/PII 포함 시 마스킹 후 컨텍스트 주입. +- 알람 데이터 없음 → `priority:null` + "데이터 부족" 사유 명시(추정 금지). + +## 팀 통신 +- 배선·기법 토글은 **esn-ai-applier** 와 협업(이 에이전트는 알람 도메인 로직, applier는 공통 배선). +- 결과 정합·근거·테넌트 격리 검증은 **esn-ai-qa** 에 의뢰. +- 백엔드 도메인/매퍼 변경은 **esn-backend-dev**, 화면 노출은 **esn-frontend-dev** 와 조율. + +## 공통 불변 (위반 불가) +- 외부 API 절대 금지 — Ollama(localhost:11434) 전용. 중앙 guardia-rag도 온프레미스만 호출. +- 자격증명·PII·스택트레이스 API/로그 미노출. `passwordHash`·`ssh_*` 응답 제외. +- 서버 RAM 제약 — 소형 모델 기본, 비전 자동로드 금지, 동시성 제한, 실패 시 `degraded` 폴백. +- 테넌트 격리 — 모든 조회·색인은 `tenant_code` 필터 + 테넌트별 컬렉션(LGInnotek/LGIT/Emart/ZIOINFO) 분리. +- `@MapperScan(annotationClass = Mapper.class)` · Hikari `maximum-pool-size: 3` 준수. diff --git a/.claude/agents/esn-ai-applier.md b/.claude/agents/esn-ai-applier.md new file mode 100644 index 0000000..5f67b2b --- /dev/null +++ b/.claude/agents/esn-ai-applier.md @@ -0,0 +1,42 @@ +--- +name: esn-ai-applier +description: GUARDiA ESN 최신 AI 기법 적용·배선 에이전트. 중앙 guardia-rag의 하이브리드/GraphRAG/리랭킹 검색·에이전틱 tool-use(/agent)·구조화 출력(/structured)·토큰 스트리밍을 ESN(Spring Boot/Java·React 단일 JAR, 멀티테넌트)에 적용하고 기법 토글 설정화면을 추가한다. "AI 기법 적용", "RAG 배선", "기법 토글", "하이브리드 검색 적용", "스트리밍 적용", "ESN AI 고도화", "다시 실행", "보완" 요청 시 사용. 외부 API 금지(Ollama 전용)·테넌트 격리. +model: opus +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN AI 기법 적용·배선 에이전트 + +## 핵심 역할 +중앙 guardia-rag에 구현된 최신 기법(**하이브리드/GraphRAG/리랭킹 검색 · 에이전틱 tool-use `/agent` · 구조화 출력 `/structured` · 토큰 스트리밍**)을 GUARDiA ESN(`C:\GUARDiA\workspace\guardia-esn\`, Spring Boot 3.5/Java 17 백엔드 + React 19 프론트, **단일 JAR**, 포트 8016, `com.zioinfo.esn`)에 적용·배선한다. 솔루션의 기존 Ollama 직접 호출을 중앙 계약 경유로 전환하고, **솔루션별 기법 토글 설정화면**(rerank·graphrag·hybrid·tool_use·structured·stream·rag_enabled)을 추가한다. 알람/POS 도메인 로직은 각 전용 에이전트가 담당하므로, 이 에이전트는 **공통 배선·계약·설정·스트리밍**에 집중한다. + +## 작업 원칙 +1. **얇은 REST 클라이언트** — Java 13벌 재구현 금지. `EsnRagClient`(Java) 하나로 중앙 `/answer`·`/verify`·`/agent`·`/structured`·`/feedback`를 호출. 기존 `OllamaClient`는 폴백으로 보존. +2. **기법 토글** — `esn_ai_setting`(테넌트별) 테이블/설정에 `retrieval_mode`(vector/hybrid/graph)·`rerank`·`tool_use`·`structured`·`stream`·`top_k`·`temperature`·`rag_enabled` 저장. 관리자 화면(React)에서 토글. 기본값=안전(소형모델·vector·stream off·비전 off). +3. **스트리밍** — 긴 답변은 SSE 토큰 스트리밍으로 단일 JAR static UI에 전달(서버 RAM 보호 위해 동시 스트림 수 제한). +4. **점진 전환** — 한 번에 전부 바꾸지 않고 알람분석→POS→일반질의 순으로 경로 전환, 각 단계 후 esn-ai-qa 검증. +5. **온프레미스 + 폴백** — 모든 기법은 중앙/Ollama 실패 시 `degraded:true` 폴백 경로 보장. + +## 입력 / 출력 +- 입력: 적용 대상 경로 목록, 활성화할 기법 토글, 테넌트 범위 +- 출력: `EsnRagClient` + 설정 테이블/설정화면 + 전환된 호출 경로 + 토글 기본값 명세 +- 중앙 계약: `/answer`(retrieval_mode) · `/verify` · `/agent` · `/structured` · `/feedback` + +## 에러 핸들링 +- 중앙 엔드포인트 4xx/5xx/타임아웃 → 기존 OllamaClient·룰 폴백 + `degraded:true`. 사용자 메시지는 요약만. +- 설정 부재/마이그레이션 전 → 안전 기본값으로 동작(기법 off). 토글 변경은 테넌트 격리. +- 스트림 중단 → 부분응답 + 재개 안내. 스택트레이스·내부IP·자격증명 미노출. + +## 팀 통신 +- 도메인 사용처는 **esn-ai-alarm-agent**(tool-use/GraphRAG)·**esn-ai-poscvt-agent**(structured)에 배선 제공. +- 배선 정합·기법 효과·결정론·폴백·외부 API 0 검증은 **esn-ai-qa** 에 의뢰. +- 스키마(`esn_ai_setting`)·매퍼는 **esn-backend-dev**, 설정화면 UI는 **esn-frontend-dev**, 배포는 **esn-devops-dev** 와 조율. + +## 공통 불변 (위반 불가) +- 외부 API 절대 금지 — Ollama(localhost:11434) 전용. 중앙 guardia-rag도 온프레미스만. +- 자격증명·PII·스택트레이스 미노출. `passwordHash`·`ssh_*` 응답 제외. +- 서버 RAM 제약 — 소형 모델 기본, 비전 자동로드 금지, 동시 스트림/요청 제한, 실패 시 `degraded` 폴백. +- 테넌트 격리 — 설정·색인·질의 모두 `tenant_code` + 테넌트별 컬렉션(LGInnotek/LGIT/Emart/ZIOINFO) 분리. +- `@MapperScan(annotationClass = Mapper.class)` · Hikari `maximum-pool-size: 3` · 단일 JAR(프론트→static 번들) 유지. diff --git a/.claude/agents/esn-ai-poscvt-agent.md b/.claude/agents/esn-ai-poscvt-agent.md new file mode 100644 index 0000000..edfdbb3 --- /dev/null +++ b/.claude/agents/esn-ai-poscvt-agent.md @@ -0,0 +1,42 @@ +--- +name: esn-ai-poscvt-agent +description: GUARDiA ESN POS 데이터 자동 분류·매핑 보조 에이전트. 중앙 guardia-rag /structured(format:json)로 POS→ESL 가격/상품 변환을 결정론적으로 분류·매핑한다. "POS 분류", "POS 변환", "PosCvt 자동 매핑", "상품 매핑 보조", "가격 변환 분류", "다시 실행", "보완" 요청 시 사용. 외부 API 금지(Ollama 전용)·결정론 JSON·테넌트 격리. +model: opus +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN AI POS 데이터 분류·매핑 에이전트 + +## 핵심 역할 +`C:\GUARDiA\workspace\guardia-esn\backend\`(`com.zioinfo.esn`)에서 POS 원천 데이터(`esn_pos_cvt`)를 ESL 표시용 상품/가격(`esn_products`·`esn_tag_bindings`)으로 변환할 때, **중앙 guardia-rag `/structured`(format:json)** 로 결정론적 분류·필드 매핑·정규화를 보조한다. 분류 결과는 항상 고정 스키마 JSON으로 받아 사람·후속 배치가 신뢰할 수 있게 한다. 기존 `PosCvtService`는 보존하고 AI 분류 경로를 옵션으로 추가한다. + +## 작업 원칙 +1. **결정론 우선** — 모든 AI 출력은 `/structured`(format:json) + 고정 JSON schema로 강제. 자유서술 금지, temperature 낮게. 동일 입력→동일 출력 보장. +2. **분류 대상** — POS 품목명/코드 → 표준 카테고리, 단위/규격 정규화, 가격 필드(정상가/행사가/단위가) 매핑, 중복/오타 품목 후보 묶음. +3. **매핑 보조(자동확정 아님)** — AI는 후보·신뢰도만 제시. 신뢰도 임계 미만은 `needsReview:true`로 사람 확인 큐에 남긴다(오매핑으로 잘못된 가격이 ESL에 표시되는 사고 방지). +4. **근거 동반** — 매핑 판단의 근거(유사 기존 매핑·규칙)를 `/answer`(retrieval_mode) 검색으로 첨부, `/verify`로 근거검증. +5. **온프레미스 + 폴백** — 중앙 실패 시 룰/사전 기반 매핑 폴백 + `degraded:true`. 미분류 항목은 버리지 않고 보류 큐로. + +## 입력 / 출력 +- 입력: POS 행(또는 배치), `tenantCode`(LGINNOTEK/LGIT/EMART/ZIOINFO), 매핑 사전/카테고리 세트 +- 출력(고정 schema): `{ items: [{ posCode, mappedProductId, category, unit, priceFields{}, confidence, needsReview }], degraded }` +- 중앙 계약: `/structured`(format:json 분류) · `/answer`(유사 매핑 검색) · `/verify`(매핑 근거) · `/feedback`(사람 교정→학습) + +## 에러 핸들링 +- JSON 파싱 실패/스키마 불일치 → 재시도 후 룰 폴백, 해당 행 `needsReview:true`. 절대 임의 추정값을 확정 매핑으로 쓰지 않는다. +- 중앙 무응답 → `degraded:true` + 룰 폴백. 사용자에게 요약 메시지만, 스택트레이스 미노출. +- 가격/금액 필드는 숫자형 검증 통과 못 하면 매핑 보류(잘못된 가격 ESL 노출 차단). + +## 팀 통신 +- 공통 배선·`/structured` 토글은 **esn-ai-applier** 와 협업(이 에이전트는 POS 도메인 규칙 담당). +- 결정론(동일입력 동일출력)·테넌트 격리·외부 API 0 검증은 **esn-ai-qa** 에 의뢰. +- 도메인/매퍼/스키마 변경은 **esn-backend-dev**, 검수 화면은 **esn-frontend-dev** 와 조율. + +## 공통 불변 (위반 불가) +- 외부 API 절대 금지 — Ollama(localhost:11434) 전용. 중앙 guardia-rag도 온프레미스만. +- 자격증명·PII·스택트레이스 미노출. `passwordHash`·`ssh_*` 응답 제외. +- 서버 RAM 제약 — 소형 모델 기본, 동시성 제한, 실패 시 `degraded` 폴백. +- 테넌트 격리 — POS·상품 색인/조회는 `tenant_code` 필터 + 테넌트별 컬렉션 분리. +- `@MapperScan(annotationClass = Mapper.class)` · Hikari `maximum-pool-size: 3` 준수. diff --git a/.claude/agents/esn-ai-qa.md b/.claude/agents/esn-ai-qa.md new file mode 100644 index 0000000..3da5266 --- /dev/null +++ b/.claude/agents/esn-ai-qa.md @@ -0,0 +1,56 @@ +--- +name: esn-ai-qa +description: GUARDiA ESN 최신 AI 기법 도입 검증 QA. 알람분석·POS분류·기법배선의 근거성·결정론·테넌트 격리·외부 API 0 경계면을 교차 검증한다. 근거 미달 시 답변 보류·인용 동반·결정론 JSON·degraded 폴백·서버 RAM 안전을 모듈 완성 직후 점진 확인하고 통과까지 반려한다. general-purpose 타입. "ESN AI 검증", "AI 기법 QA", "근거 검증", "결정론 검증", "테넌트 격리 검증", "다시 실행", "보완" 요청 시 사용. +model: opus +metadata: + type: agent + tools: [Read, Write, Edit, Bash, Glob, Grep] +--- + +# ESN AI 기법 QA 에이전트 (general-purpose) + +## 핵심 역할 +GUARDiA ESN(`C:\GUARDiA\workspace\guardia-esn\`, 포트 8016, `com.zioinfo.esn`)에 도입된 최신 AI 기법(알람 원인분석·POS 분류·기법 배선)을 **모듈 완성 직후 점진적으로** 검증한다. 중앙 guardia-rag 계약과 ESN 솔루션 측 배선의 경계면을 교차 비교하고, 근거·결정론·테넌트 격리·온프레미스(외부 API 0)·서버 RAM 안전을 확인해 통과까지 수정 지시를 반려한다. + +## 작업 원칙 (검증 항목) +1. **근거성(grounding)** — 근거 미달 시 실제로 답변이 보류/`I don't know`로 떨어지는지(환각 차단), 응답에 인용(citations)이 동반되는지. `/verify` 경유 확인. +2. **결정론** — POS 분류 등 `/structured(format:json)` 경로가 동일 입력에 동일 JSON·고정 schema를 반환하는지(2회 호출 대조). 자유서술 누출 없는지. +3. **에이전틱 완주** — `/agent` tool-use 루프가 무한루프 없이 종결하고 근본원인·우선순위를 반환하는지. GraphRAG 연쇄 알람이 단일 근본원인으로 수렴하는지. +4. **테넌트 격리** — LGINNOTEK/LGIT/EMART/ZIOINFO 간 색인·검색·설정이 누설되지 않는지(다른 테넌트 토큰으로 교차 조회 시 차단). +5. **온프레미스** — 코드·런타임에 외부 AI/HTTP 엔드포인트 호출 0. Ollama(localhost:11434)·중앙 guardia-rag(온프레미스)만. +6. **폴백·RAM 안전** — 중앙/Ollama 실패 시 `degraded:true` 폴백 동작, 소형모델 기본·비전 자동로드 금지·동시성 제한 준수. +7. **회귀·보안** — 기존 알람/POS/HCore 기능 회귀 없음, `passwordHash`·`ssh_*`·PII·스택트레이스·내부IP 미노출. + +## 입력 / 출력 +- 입력: 완성된 모듈/엔드포인트, 변경 경로, 테넌트별 테스트 토큰 +- 출력: 항목별 PASS/FAIL + 재현 절차 + 수정 지시. FAIL 잔존 시 통과 반려. + +## 검증 방법 +```bash +# 로그인(테넌트별) +curl -X POST http://localhost:8016/api/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"admin123"}' + +# 결정론 — POS 분류 2회 호출 결과 동일성 대조 +# 테넌트 격리 — A 테넌트 토큰으로 B 테넌트 데이터 조회 시 차단 확인 +curl -H 'Authorization: Bearer {tokenA}' \ + 'http://localhost:8016/api/...?tenantCode=LGIT' # 교차 차단되어야 함 + +# 외부 API 0 — 코드 정적 점검(Ollama/guardia-rag 외 외부 호출 없음) +grep -rE 'https?://(?!localhost|127\.0\.0\.1)' backend/src/main || echo 외부호출없음 +``` + +## 에러 핸들링 +- 서버 다운 시 라이브 호출 불가 → 코드/계약 경계면 정적 검증으로 대체하고 라이브 검증 보류 사실을 보고. +- 검증 과정에서 자격증명·PII 노출 발견 시 즉시 FAIL + 마스킹 수정 지시. + +## 팀 통신 +- 알람 결과 정합 지시는 **esn-ai-alarm-agent**, 분류 결정론 지시는 **esn-ai-poscvt-agent**, 배선/토글/스트리밍 지시는 **esn-ai-applier** 에 반려. +- 도메인/매퍼·UI 수정은 **esn-backend-dev**·**esn-frontend-dev**, 배포 재검증은 **esn-devops-dev** 와 조율. + +## 공통 불변 (위반 불가) +- 외부 API 절대 금지 — Ollama(localhost:11434)·중앙 guardia-rag(온프레미스) 외 호출 0 검증. +- 자격증명·PII·스택트레이스·내부IP 미노출 검증. `passwordHash`·`ssh_*` 응답 제외 확인. +- 서버 RAM 제약 — 소형모델 기본·비전 자동로드 금지·동시성 제한·`degraded` 폴백 동작 검증. +- 테넌트 격리(LGInnotek/LGIT/Emart/ZIOINFO 컬렉션 분리) 누설 0 검증. diff --git a/CLAUDE.md b/CLAUDE.md index de411e5..02ab14c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,3 +119,11 @@ workspace/guardia-esn/ - 배포 작업 - 레거시 소스 분석·통합 - 알람·POS·HCore 관련 작업 + +## AI 에이전트 (최신 기법 — 중앙 guardia-rag 연계) + +`.claude/agents/` 의 도메인 맞춤 AI 에이전트 4종(외부 API 금지·Ollama 전용·테넌트 격리·소형모델 기본·degraded 폴백, 중앙 계약 `/answer`·`/verify`·`/agent`·`/structured`·`/feedback`): +- `esn-ai-alarm-agent` — ESL/HCore 알람 원인분석·우선순위(tool-use + GraphRAG 의존성 추적) +- `esn-ai-poscvt-agent` — POS 데이터 자동 분류·매핑 보조(`/structured` format:json 결정론) +- `esn-ai-applier` — 최신 기법(hybrid/graph/rerank·tool-use·structured·스트리밍) ESN 적용·배선·기법 토글 +- `esn-ai-qa` — 근거·결정론·테넌트 격리·외부 API 0 경계면 검증(general-purpose) diff --git a/backend/src/main/java/com/zioinfo/esn/admin/AdminController.java b/backend/src/main/java/com/zioinfo/esn/admin/AdminController.java new file mode 100644 index 0000000..728369e --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/AdminController.java @@ -0,0 +1,76 @@ +package com.zioinfo.esn.admin; + +import com.zioinfo.esn.admin.dto.AuditLog; +import com.zioinfo.esn.admin.dto.EsnSetting; +import com.zioinfo.esn.common.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * GUARDiA ESN 관리자 API — 감사 로그 조회 + 시스템 설정. + * + *

RBAC (SecurityConfig requestMatchers 로 통제): + *

+ * + *

사용자/테넌트/펌웨어 CRUD 는 기존 도메인 컨트롤러(UserController/TenantController/FirmwareController) + * 가 담당한다. 이 컨트롤러는 admin 보조 영역(감사·설정)만 추가한다. + */ +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class AdminController { + + private final AuditService auditService; + private final SettingService settingService; + + // ===================== 감사 로그 (ADMIN/MANAGER) ===================== + + @GetMapping("/audit") + public ApiResponse> audit( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String username, + @RequestParam(required = false) String action, + @RequestParam(required = false) String resource, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to, + @RequestParam(defaultValue = "100") int limit) { + return ApiResponse.ok(auditService.find(tenantCode, username, action, resource, from, to, limit)); + } + + @GetMapping("/audit/count") + public ApiResponse auditCount( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String username, + @RequestParam(required = false) String action, + @RequestParam(required = false) String resource, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to) { + return ApiResponse.ok(auditService.count(tenantCode, username, action, resource, from, to)); + } + + // ===================== 시스템 설정 ===================== + + @GetMapping("/settings") + public ApiResponse> settings(@RequestParam(required = false) String category) { + return ApiResponse.ok(settingService.list(category)); + } + + @PutMapping("/settings/{key}") + public ApiResponse updateSetting(@PathVariable String key, + @RequestBody Map body) { + return ApiResponse.ok("설정 변경 완료", settingService.update(key, body.get("value"))); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/AuditService.java b/backend/src/main/java/com/zioinfo/esn/admin/AuditService.java new file mode 100644 index 0000000..7dda16a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/AuditService.java @@ -0,0 +1,99 @@ +package com.zioinfo.esn.admin; + +import com.zioinfo.esn.admin.dto.AuditLog; +import com.zioinfo.esn.admin.mapper.AuditLogMapper; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 감사 로그 서비스. + * + *

ESL 제어 운영(펌웨어 배포·POS 가격 변경·태그 큐 전파)과 관리 행위(사용자/테넌트 CRUD·설정 변경)를 + * {@code esn_audit_log} 에 기록하고 조회한다. + * + *

감사 기록 실패가 본업(펌웨어 배포 등)을 막아서는 안 되므로 insert 는 best-effort(예외 흡수)다. + * + *

보안 불변: 비밀번호/자격증명/PII 는 detail 에 절대 기록하지 않는다. 호출 측이 키/식별자/요약만 넘긴다. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AuditService { + + private final AuditLogMapper mapper; + + /** 가장 단순한 형태: 액션/리소스/식별자/요약. 현재 인증 주체로 기록. */ + public void log(String action, String resource, String resourceId, String detail) { + log(null, action, resource, resourceId, detail, true); + } + + /** 테넌트 스코프 포함. */ + public void log(String tenantCode, String action, String resource, String resourceId, + String detail, boolean success) { + try { + AuditLog entry = new AuditLog(); + entry.setTenantCode(tenantCode); + entry.setUsername(currentActor()); + entry.setAction(action); + entry.setResource(resource); + entry.setResourceId(resourceId); + entry.setDetail(detail); + entry.setSuccess(success); + HttpServletRequest req = currentRequest(); + if (req != null) { + entry.setIpAddress(clientIp(req)); + String ua = req.getHeader("User-Agent"); + entry.setUserAgent(ua != null && ua.length() > 500 ? ua.substring(0, 500) : ua); + } + mapper.insert(entry); + } catch (Exception e) { + // best-effort: 감사 기록 실패가 본업을 막지 않는다. + log.warn("audit log insert 실패 action={} resource={}: {}", action, resource, e.getMessage()); + } + } + + public List find(String tenantCode, String username, String action, String resource, + LocalDateTime from, LocalDateTime to, int limit) { + int safeLimit = (limit <= 0 || limit > 1000) ? 100 : limit; + return mapper.find(tenantCode, username, action, resource, from, to, safeLimit); + } + + public long count(String tenantCode, String username, String action, String resource, + LocalDateTime from, LocalDateTime to) { + return mapper.count(tenantCode, username, action, resource, from, to); + } + + /** SecurityContext 의 JWT subject(username). 없으면 system. */ + public static String currentActor() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getName() != null && !auth.getName().isBlank()) { + return auth.getName(); + } + return "system"; + } + + private static HttpServletRequest currentRequest() { + var attrs = RequestContextHolder.getRequestAttributes(); + if (attrs instanceof ServletRequestAttributes sra) { + return sra.getRequest(); + } + return null; + } + + private static String clientIp(HttpServletRequest req) { + String xf = req.getHeader("X-Forwarded-For"); + if (xf != null && !xf.isBlank()) { + return xf.split(",")[0].trim(); + } + return req.getRemoteAddr(); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/SettingService.java b/backend/src/main/java/com/zioinfo/esn/admin/SettingService.java new file mode 100644 index 0000000..440527d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/SettingService.java @@ -0,0 +1,43 @@ +package com.zioinfo.esn.admin; + +import com.zioinfo.esn.admin.dto.EsnSetting; +import com.zioinfo.esn.admin.mapper.SettingMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 시스템 설정 서비스. + * + *

조회는 ADMIN/MANAGER, 변경은 ADMIN (SecurityConfig requestMatchers 로 통제). + * 변경 시 감사 로그에 이전→이후 요약을 기록한다(값은 민감하지 않은 운영 설정만 다룬다). + */ +@Service +@RequiredArgsConstructor +public class SettingService { + + private final SettingMapper mapper; + private final AuditService auditService; + + public List list(String category) { + return mapper.findAll(category); + } + + public EsnSetting update(String key, String value) { + if (key == null || key.isBlank()) { + throw new IllegalArgumentException("ERR-SET-400: key 필수"); + } + if (value == null) { + throw new IllegalArgumentException("ERR-SET-400: value 필수"); + } + EsnSetting before = mapper.findByKey(key); + String category = before == null ? "GENERAL" : before.getCategory(); + String description = before == null ? null : before.getDescription(); + mapper.upsert(key, value, category, description); + String prev = before == null ? "(none)" : before.getSettingValue(); + auditService.log(null, "SETTING_CHANGE", "SETTING", key, + prev + " -> " + value, true); + return mapper.findByKey(key); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/dto/AuditLog.java b/backend/src/main/java/com/zioinfo/esn/admin/dto/AuditLog.java new file mode 100644 index 0000000..c87db2c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/dto/AuditLog.java @@ -0,0 +1,28 @@ +package com.zioinfo.esn.admin.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 감사 로그 (esn_audit_log 테이블 매핑). + * + *

ESL 제어 운영(펌웨어 배포·가격 변경·태그 큐 전파)과 관리 행위(사용자/테넌트 CRUD·설정 변경)를 + * 추적한다. 멀티테넌트 스코프(tenantCode)를 보존한다. + * + *

보안: 비밀번호/자격증명/PII 는 detail 에 절대 기록하지 않는다(키/식별자/요약만). + */ +@Data +public class AuditLog { + private Long id; + private String tenantCode; // 테넌트 스코프 (nullable) + private String username; // 작업 수행자 (JWT subject) + private String action; // USER_CREATE, FIRMWARE_DEPLOY, POS_PRICE_CHANGE, SETTING_CHANGE 등 + private String resource; // 대상 리소스 종류 (USER / FIRMWARE / POS_CVT / TENANT / SETTING ...) + private String resourceId; // 대상 식별자 (id / key 등) + private String detail; // 부가 요약 (민감정보 제외) + private String ipAddress; // 요청 IP (nullable) + private String userAgent; // User-Agent (nullable) + private Boolean success; // 성공 여부 + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/dto/EsnSetting.java b/backend/src/main/java/com/zioinfo/esn/admin/dto/EsnSetting.java new file mode 100644 index 0000000..77b1a8d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/dto/EsnSetting.java @@ -0,0 +1,20 @@ +package com.zioinfo.esn.admin.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 시스템 설정 (esn_setting 테이블 매핑). + * + *

조회는 ADMIN/MANAGER, 변경은 ADMIN (SecurityConfig requestMatchers 로 통제). + * 비밀번호/자격증명 같은 민감 설정은 저장하지 않는다(그런 값은 CryptoUtil 암호화 경로 사용). + */ +@Data +public class EsnSetting { + private String settingKey; + private String settingValue; + private String category; // GENERAL / ESL / ALARM / AI ... + private String description; + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/mapper/AuditLogMapper.java b/backend/src/main/java/com/zioinfo/esn/admin/mapper/AuditLogMapper.java new file mode 100644 index 0000000..ae52627 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/mapper/AuditLogMapper.java @@ -0,0 +1,33 @@ +package com.zioinfo.esn.admin.mapper; + +import com.zioinfo.esn.admin.dto.AuditLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 감사 로그 매퍼. @Mapper 어노테이션 필수 + * (EsnApplication 의 @MapperScan(annotationClass = Mapper.class) 패턴). + */ +@Mapper +public interface AuditLogMapper { + + int insert(AuditLog log); + + List find(@Param("tenantCode") String tenantCode, + @Param("username") String username, + @Param("action") String action, + @Param("resource") String resource, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to, + @Param("limit") int limit); + + long count(@Param("tenantCode") String tenantCode, + @Param("username") String username, + @Param("action") String action, + @Param("resource") String resource, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to); +} diff --git a/backend/src/main/java/com/zioinfo/esn/admin/mapper/SettingMapper.java b/backend/src/main/java/com/zioinfo/esn/admin/mapper/SettingMapper.java new file mode 100644 index 0000000..79355ac --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/admin/mapper/SettingMapper.java @@ -0,0 +1,25 @@ +package com.zioinfo.esn.admin.mapper; + +import com.zioinfo.esn.admin.dto.EsnSetting; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 시스템 설정 매퍼. @Mapper 어노테이션 필수 + * (EsnApplication 의 @MapperScan(annotationClass = Mapper.class) 패턴). + */ +@Mapper +public interface SettingMapper { + + List findAll(@Param("category") String category); + + EsnSetting findByKey(@Param("settingKey") String settingKey); + + /** upsert (ON CONFLICT). description/category 는 신규 INSERT 시에만 사용. */ + int upsert(@Param("settingKey") String settingKey, + @Param("settingValue") String settingValue, + @Param("category") String category, + @Param("description") String description); +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java index df39263..e854de9 100644 --- a/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java +++ b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java @@ -44,6 +44,10 @@ public class SecurityConfig { .requestMatchers("/api/tenants/**").hasRole("ADMIN") .requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER") .requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER") + // admin 보조: 감사로그 조회·설정 조회 = ADMIN/MANAGER, 설정 변경 = ADMIN + // (아래 일반 PUT /api/** 규칙보다 먼저 평가되도록 상단 배치) + .requestMatchers(HttpMethod.PUT, "/api/admin/settings/**").hasRole("ADMIN") + .requestMatchers("/api/admin/**").hasAnyRole("ADMIN", "MANAGER") .requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MANAGER") .requestMatchers(HttpMethod.POST, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER") .requestMatchers(HttpMethod.PUT, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER") diff --git a/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java index f5a0944..815caf4 100644 --- a/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java +++ b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java @@ -1,5 +1,6 @@ package com.zioinfo.esn.controller; +import com.zioinfo.esn.admin.AuditService; import com.zioinfo.esn.common.ApiResponse; import com.zioinfo.esn.domain.FirmwareVo; import com.zioinfo.esn.service.FirmwareService; @@ -12,6 +13,7 @@ import java.util.List; @RequiredArgsConstructor public class FirmwareController { private final FirmwareService service; + private final AuditService audit; @GetMapping public ApiResponse> list( @@ -27,17 +29,27 @@ public class FirmwareController { @PostMapping public ApiResponse create(@RequestBody FirmwareVo v) { - return ApiResponse.ok("펌웨어 등록 완료", service.create(v)); + FirmwareVo created = service.create(v); + // ESL 펌웨어 배포 — 추적 가치 높음 + audit.log(created.getTenantCode(), "FIRMWARE_DEPLOY", "FIRMWARE", String.valueOf(created.getId()), + "deviceType=" + created.getDeviceType() + ", version=" + created.getFirmwareVersion() + + ", latest=" + created.isLatest(), true); + return ApiResponse.ok("펌웨어 등록 완료", created); } @PutMapping("/{id}") public ApiResponse update(@PathVariable Long id, @RequestBody FirmwareVo v) { - return ApiResponse.ok("펌웨어 수정 완료", service.update(id, v)); + FirmwareVo updated = service.update(id, v); + audit.log(updated.getTenantCode(), "FIRMWARE_UPDATE", "FIRMWARE", String.valueOf(id), + "deviceType=" + updated.getDeviceType() + ", version=" + updated.getFirmwareVersion() + + ", latest=" + updated.isLatest(), true); + return ApiResponse.ok("펌웨어 수정 완료", updated); } @DeleteMapping("/{id}") public ApiResponse delete(@PathVariable Long id) { service.delete(id); + audit.log(null, "FIRMWARE_DELETE", "FIRMWARE", String.valueOf(id), "펌웨어 삭제", true); return ApiResponse.ok("펌웨어 삭제 완료", null); } } diff --git a/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java index 89c3b21..f25a2e0 100644 --- a/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java +++ b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java @@ -1,5 +1,6 @@ package com.zioinfo.esn.controller; +import com.zioinfo.esn.admin.AuditService; import com.zioinfo.esn.common.ApiResponse; import com.zioinfo.esn.domain.PosCvtVo; import com.zioinfo.esn.service.PosCvtService; @@ -13,6 +14,7 @@ import java.util.Map; @RequiredArgsConstructor public class PosCvtController { private final PosCvtService service; + private final AuditService audit; @GetMapping public ApiResponse> list( @@ -30,7 +32,13 @@ public class PosCvtController { @PutMapping("/{id}/process") public ApiResponse process(@PathVariable Long id) { + PosCvtVo pos = service.get(id); int queued = service.process(id); + // ESL 가격 변경 전파 — 추적 가치 높음 (가격은 영업정보이나 민감개인정보 아님) + String detail = pos == null ? ("queued=" + queued) + : ("product=" + pos.getProductCode() + ", price=" + pos.getPrice() + ", queued=" + queued); + audit.log(pos != null ? pos.getTenantCode() : null, "POS_PRICE_CHANGE", "POS_CVT", + String.valueOf(id), detail, true); return ApiResponse.ok("처리 완료 (태그 큐 " + queued + "건)", queued); } diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java index 7e5007f..79653ae 100644 --- a/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java +++ b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java @@ -1,5 +1,6 @@ package com.zioinfo.esn.controller; +import com.zioinfo.esn.admin.AuditService; import com.zioinfo.esn.common.ApiResponse; import com.zioinfo.esn.domain.TenantVo; import com.zioinfo.esn.service.TenantService; @@ -12,6 +13,7 @@ import java.util.List; @RequiredArgsConstructor public class TenantController { private final TenantService service; + private final AuditService audit; @GetMapping public ApiResponse> list() { @@ -25,17 +27,24 @@ public class TenantController { @PostMapping public ApiResponse create(@RequestBody TenantVo v) { - return ApiResponse.ok("테넌트 생성 완료", service.create(v)); + TenantVo created = service.create(v); + audit.log(created.getTenantCode(), "TENANT_CREATE", "TENANT", String.valueOf(created.getId()), + "name=" + created.getTenantName(), true); + return ApiResponse.ok("테넌트 생성 완료", created); } @PutMapping("/{id}") public ApiResponse update(@PathVariable Long id, @RequestBody TenantVo v) { - return ApiResponse.ok("테넌트 수정 완료", service.update(id, v)); + TenantVo updated = service.update(id, v); + audit.log(updated.getTenantCode(), "TENANT_UPDATE", "TENANT", String.valueOf(id), + "name=" + updated.getTenantName(), true); + return ApiResponse.ok("테넌트 수정 완료", updated); } @DeleteMapping("/{id}") public ApiResponse delete(@PathVariable Long id) { service.delete(id); + audit.log(null, "TENANT_DELETE", "TENANT", String.valueOf(id), "테넌트 삭제", true); return ApiResponse.ok("테넌트 삭제 완료", null); } } diff --git a/backend/src/main/java/com/zioinfo/esn/controller/UserController.java b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java index fd67054..4ad8205 100644 --- a/backend/src/main/java/com/zioinfo/esn/controller/UserController.java +++ b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java @@ -1,5 +1,6 @@ package com.zioinfo.esn.controller; +import com.zioinfo.esn.admin.AuditService; import com.zioinfo.esn.auth.EsnUser; import com.zioinfo.esn.common.ApiResponse; import com.zioinfo.esn.service.UserService; @@ -13,6 +14,7 @@ import java.util.Map; @RequiredArgsConstructor public class UserController { private final UserService service; + private final AuditService audit; @GetMapping public ApiResponse> list( @@ -36,23 +38,42 @@ public class UserController { user.setPhone((String) body.get("phone")); user.setActive(true); String rawPassword = (String) body.getOrDefault("password", "changeme123"); - return ApiResponse.ok("사용자 생성 완료", service.create(user, rawPassword)); + EsnUser created = service.create(user, rawPassword); + // 보안: 비밀번호는 감사 detail 에 절대 기록하지 않는다(username/role/tenant 요약만). + audit.log(created.getTenantCode(), "USER_CREATE", "USER", String.valueOf(created.getId()), + "username=" + created.getUsername() + ", role=" + created.getRole(), true); + return ApiResponse.ok("사용자 생성 완료", created); } @PutMapping("/{id}") public ApiResponse update(@PathVariable Long id, @RequestBody EsnUser v) { - return ApiResponse.ok("사용자 수정 완료", service.update(id, v)); + EsnUser updated = service.update(id, v); + audit.log(updated.getTenantCode(), "USER_UPDATE", "USER", String.valueOf(id), + "role=" + updated.getRole() + ", active=" + updated.isActive(), true); + return ApiResponse.ok("사용자 수정 완료", updated); } @PutMapping("/{id}/password") public ApiResponse changePassword(@PathVariable Long id, @RequestBody Map body) { service.changePassword(id, body.getOrDefault("password", "changeme123")); + // 보안: 새 비밀번호 값은 기록하지 않는다(대상 id 만). + audit.log(null, "USER_PASSWORD_RESET", "USER", String.valueOf(id), "비밀번호 초기화", true); return ApiResponse.ok("비밀번호 변경 완료", null); } + @PutMapping("/{id}/active") + public ApiResponse setActive(@PathVariable Long id, @RequestBody Map body) { + boolean active = Boolean.TRUE.equals(body.get("active")); + EsnUser u = service.setActive(id, active); + audit.log(u != null ? u.getTenantCode() : null, "USER_ACTIVE_TOGGLE", "USER", + String.valueOf(id), "active=" + active, true); + return ApiResponse.ok(active ? "활성화 완료" : "비활성화 완료", u); + } + @DeleteMapping("/{id}") public ApiResponse delete(@PathVariable Long id) { service.delete(id); + audit.log(null, "USER_DELETE", "USER", String.valueOf(id), "사용자 삭제", true); return ApiResponse.ok("사용자 삭제 완료", null); } } diff --git a/backend/src/main/java/com/zioinfo/esn/service/UserService.java b/backend/src/main/java/com/zioinfo/esn/service/UserService.java index f930f76..07670df 100644 --- a/backend/src/main/java/com/zioinfo/esn/service/UserService.java +++ b/backend/src/main/java/com/zioinfo/esn/service/UserService.java @@ -35,5 +35,13 @@ public class UserService { mapper.updatePassword(id, passwordEncoder.encode(rawPassword)); } + public EsnUser setActive(Long id, boolean active) { + EsnUser u = mapper.findById(id); + if (u == null) return null; + u.setActive(active); + mapper.update(u); + return mapper.findById(id); + } + public void delete(Long id) { mapper.delete(id); } } diff --git a/backend/src/main/resources/db/schema.sql b/backend/src/main/resources/db/schema.sql index fad17b9..a97e4d0 100644 --- a/backend/src/main/resources/db/schema.sql +++ b/backend/src/main/resources/db/schema.sql @@ -331,3 +331,25 @@ CREATE TABLE IF NOT EXISTS esn_audit_log ( ); CREATE INDEX IF NOT EXISTS idx_audit_created ON esn_audit_log(created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_tenant ON esn_audit_log(tenant_code, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_audit_action ON esn_audit_log(action, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_audit_user ON esn_audit_log(username, created_at DESC); + +-- ── 시스템 설정 (admin 보조) ────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS esn_setting ( + setting_key VARCHAR(100) PRIMARY KEY, + setting_value TEXT, + category VARCHAR(50) DEFAULT 'GENERAL', + description VARCHAR(300), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- 기본 운영 설정 시드 (멱등 — ON CONFLICT DO NOTHING) +INSERT INTO esn_setting (setting_key, setting_value, category, description) VALUES + ('system.name', 'GUARDiA ESN', 'GENERAL', '시스템 표시명'), + ('esl.update.poll.limit', '50', 'ESL', 'Gateway 폴링 1회 최대 큐 건수'), + ('esl.update.retry.max', '3', 'ESL', '태그 전송 실패 재시도 횟수'), + ('alarm.retention.days', '90', 'ALARM', '알람 이력 보존 일수'), + ('audit.retention.days', '365', 'GENERAL', '감사 로그 보존 일수'), + ('ai.ollama.text.model', 'llama3.2:1b', 'AI', 'Ollama 텍스트 모델'), + ('ai.ollama.enabled', 'true', 'AI', 'AI 분석 기능 사용 여부') +ON CONFLICT (setting_key) DO NOTHING; diff --git a/backend/src/main/resources/mapper/AuditLogMapper.xml b/backend/src/main/resources/mapper/AuditLogMapper.xml new file mode 100644 index 0000000..0c943bf --- /dev/null +++ b/backend/src/main/resources/mapper/AuditLogMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_audit_log + (tenant_code, username, action, resource, resource_id, detail, ip_address, user_agent, success) + VALUES + (#{tenantCode}, #{username}, #{action}, #{resource}, #{resourceId}, #{detail}, + #{ipAddress}, #{userAgent}, #{success}) + + + + + AND tenant_code = #{tenantCode} + AND username = #{username} + AND action = #{action} + AND resource = #{resource} + AND created_at >= #{from} + AND created_at <= #{to} + + + + + + + + diff --git a/backend/src/main/resources/mapper/SettingMapper.xml b/backend/src/main/resources/mapper/SettingMapper.xml new file mode 100644 index 0000000..f4a1e39 --- /dev/null +++ b/backend/src/main/resources/mapper/SettingMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + INSERT INTO esn_setting (setting_key, setting_value, category, description, updated_at) + VALUES (#{settingKey}, #{settingValue}, #{category}, #{description}, NOW()) + ON CONFLICT (setting_key) DO UPDATE + SET setting_value = EXCLUDED.setting_value, + updated_at = NOW() + + + diff --git a/backend/src/main/resources/static/assets/index-B4CAJSnh.js b/backend/src/main/resources/static/assets/index-B4CAJSnh.js new file mode 100644 index 0000000..46cece2 --- /dev/null +++ b/backend/src/main/resources/static/assets/index-B4CAJSnh.js @@ -0,0 +1,352 @@ +var Jx=e=>{throw TypeError(e)};var sy=(e,t,n)=>t.has(e)||Jx("Cannot "+n);var T=(e,t,n)=>(sy(e,t,"read from private field"),n?n.call(e):t.get(e)),te=(e,t,n)=>t.has(e)?Jx("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),Y=(e,t,n,r)=>(sy(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),fe=(e,t,n)=>(sy(e,t,"access private method"),n);var Qc=(e,t,n,r)=>({set _(a){Y(e,t,a,n)},get _(){return T(e,t,r)}});(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))r(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(a){if(a.ep)return;a.ep=!0;const i=n(a);fetch(a.href,i)}})();var Wc=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function ze(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var yE={exports:{}},kh={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var yR=Symbol.for("react.transitional.element"),mR=Symbol.for("react.fragment");function mE(e,t,n){var r=null;if(n!==void 0&&(r=""+n),t.key!==void 0&&(r=""+t.key),"key"in t){n={};for(var a in t)a!=="key"&&(n[a]=t[a])}else n=t;return t=n.ref,{$$typeof:yR,type:e,key:r,ref:t!==void 0?t:null,props:n}}kh.Fragment=mR;kh.jsx=mE;kh.jsxs=mE;yE.exports=kh;var h=yE.exports,vE={exports:{}},le={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Sb=Symbol.for("react.transitional.element"),vR=Symbol.for("react.portal"),gR=Symbol.for("react.fragment"),bR=Symbol.for("react.strict_mode"),xR=Symbol.for("react.profiler"),SR=Symbol.for("react.consumer"),wR=Symbol.for("react.context"),OR=Symbol.for("react.forward_ref"),AR=Symbol.for("react.suspense"),jR=Symbol.for("react.memo"),gE=Symbol.for("react.lazy"),ER=Symbol.for("react.activity"),e1=Symbol.iterator;function _R(e){return e===null||typeof e!="object"?null:(e=e1&&e[e1]||e["@@iterator"],typeof e=="function"?e:null)}var bE={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},xE=Object.assign,SE={};function Jl(e,t,n){this.props=e,this.context=t,this.refs=SE,this.updater=n||bE}Jl.prototype.isReactComponent={};Jl.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Jl.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function wE(){}wE.prototype=Jl.prototype;function wb(e,t,n){this.props=e,this.context=t,this.refs=SE,this.updater=n||bE}var Ob=wb.prototype=new wE;Ob.constructor=wb;xE(Ob,Jl.prototype);Ob.isPureReactComponent=!0;var t1=Array.isArray;function Cm(){}var We={H:null,A:null,T:null,S:null},OE=Object.prototype.hasOwnProperty;function Ab(e,t,n){var r=n.ref;return{$$typeof:Sb,type:e,key:t,ref:r!==void 0?r:null,props:n}}function TR(e,t){return Ab(e.type,t,e.props)}function jb(e){return typeof e=="object"&&e!==null&&e.$$typeof===Sb}function CR(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return t[n]})}var n1=/\/+/g;function uy(e,t){return typeof e=="object"&&e!==null&&e.key!=null?CR(""+e.key):t.toString(36)}function NR(e){switch(e.status){case"fulfilled":return e.value;case"rejected":throw e.reason;default:switch(typeof e.status=="string"?e.then(Cm,Cm):(e.status="pending",e.then(function(t){e.status==="pending"&&(e.status="fulfilled",e.value=t)},function(t){e.status==="pending"&&(e.status="rejected",e.reason=t)})),e.status){case"fulfilled":return e.value;case"rejected":throw e.reason}}throw e}function jo(e,t,n,r,a){var i=typeof e;(i==="undefined"||i==="boolean")&&(e=null);var o=!1;if(e===null)o=!0;else switch(i){case"bigint":case"string":case"number":o=!0;break;case"object":switch(e.$$typeof){case Sb:case vR:o=!0;break;case gE:return o=e._init,jo(o(e._payload),t,n,r,a)}}if(o)return a=a(e),o=r===""?"."+uy(e,0):r,t1(a)?(n="",o!=null&&(n=o.replace(n1,"$&/")+"/"),jo(a,t,n,"",function(u){return u})):a!=null&&(jb(a)&&(a=TR(a,n+(a.key==null||e&&e.key===a.key?"":(""+a.key).replace(n1,"$&/")+"/")+o)),t.push(a)),1;o=0;var l=r===""?".":r+":";if(t1(e))for(var s=0;s>>1,F=R[q];if(0>>1;qa(ee,U))dea(Re,ee)?(R[q]=Re,R[de]=U,q=de):(R[q]=ee,R[G]=U,q=G);else if(dea(Re,U))R[q]=Re,R[de]=U,q=de;else break e}}return k}function a(R,k){var U=R.sortIndex-k.sortIndex;return U!==0?U:R.id-k.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,l=o.now();e.unstable_now=function(){return o.now()-l}}var s=[],u=[],f=1,c=null,d=3,p=!1,b=!1,m=!1,g=!1,y=typeof setTimeout=="function"?setTimeout:null,v=typeof clearTimeout=="function"?clearTimeout:null,x=typeof setImmediate<"u"?setImmediate:null;function w(R){for(var k=n(u);k!==null;){if(k.callback===null)r(u);else if(k.startTime<=R)r(u),k.sortIndex=k.expirationTime,t(s,k);else break;k=n(u)}}function S(R){if(m=!1,w(R),!b)if(n(s)!==null)b=!0,O||(O=!0,P());else{var k=n(u);k!==null&&D(S,k.startTime-R)}}var O=!1,A=-1,E=5,_=-1;function N(){return g?!0:!(e.unstable_now()-_R&&N());){var q=c.callback;if(typeof q=="function"){c.callback=null,d=c.priorityLevel;var F=q(c.expirationTime<=R);if(R=e.unstable_now(),typeof F=="function"){c.callback=F,w(R),k=!0;break t}c===n(s)&&r(s),w(R)}else r(s);c=n(s)}if(c!==null)k=!0;else{var V=n(u);V!==null&&D(S,V.startTime-R),k=!1}}break e}finally{c=null,d=U,p=!1}k=void 0}}finally{k?P():O=!1}}}var P;if(typeof x=="function")P=function(){x($)};else if(typeof MessageChannel<"u"){var B=new MessageChannel,L=B.port2;B.port1.onmessage=$,P=function(){L.postMessage(null)}}else P=function(){y($,0)};function D(R,k){A=y(function(){R(e.unstable_now())},k)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(R){R.callback=null},e.unstable_forceFrameRate=function(R){0>R||125q?(R.sortIndex=U,t(u,R),n(s)===null&&R===n(u)&&(m?(v(A),A=-1):m=!0,D(S,U-q))):(R.sortIndex=F,t(s,R),b||p||(b=!0,O||(O=!0,P()))),R},e.unstable_shouldYield=N,e.unstable_wrapCallback=function(R){var k=d;return function(){var U=d;d=k;try{return R.apply(this,arguments)}finally{d=U}}}})(EE);jE.exports=EE;var RR=jE.exports,_E={exports:{}},en={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var $R=j;function TE(e){var t="https://react.dev/errors/"+e;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(CE)}catch(e){console.error(e)}}CE(),_E.exports=en;var LR=_E.exports;/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ot=RR,NE=j,zR=LR;function I(e){var t="https://react.dev/errors/"+e;if(1No||(e.current=Dm[No],Dm[No]=null,No--)}function Fe(e,t){No++,Dm[No]=e.current,e.current=t}var wr=Er(null),xu=Er(null),qa=Er(null),ed=Er(null);function td(e,t){switch(Fe(qa,t),Fe(xu,e),Fe(wr,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?fS(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=fS(t),e=nT(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}Nt(wr),Fe(wr,e)}function ml(){Nt(wr),Nt(xu),Nt(qa)}function km(e){e.memoizedState!==null&&Fe(ed,e);var t=wr.current,n=nT(t,e.type);t!==n&&(Fe(xu,e),Fe(wr,n))}function nd(e){xu.current===e&&(Nt(wr),Nt(xu)),ed.current===e&&(Nt(ed),Mu._currentValue=Ui)}var cy,o1;function vi(e){if(cy===void 0)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);cy=t&&t[1]||"",o1=-1)":-1a||s[r]!==u[a]){var f=` +`+s[r].replace(" at new "," at ");return e.displayName&&f.includes("")&&(f=f.replace("",e.displayName)),f}while(1<=r&&0<=a);break}}}finally{fy=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?vi(n):""}function qR(e,t){switch(e.tag){case 26:case 27:case 5:return vi(e.type);case 16:return vi("Lazy");case 13:return e.child!==t&&t!==null?vi("Suspense Fallback"):vi("Suspense");case 19:return vi("SuspenseList");case 0:case 15:return dy(e.type,!1);case 11:return dy(e.type.render,!1);case 1:return dy(e.type,!0);case 31:return vi("Activity");default:return""}}function l1(e){try{var t="",n=null;do t+=qR(e,n),n=e,e=e.return;while(e);return t}catch(r){return` +Error generating stack: `+r.message+` +`+r.stack}}var Lm=Object.prototype.hasOwnProperty,Tb=Ot.unstable_scheduleCallback,hy=Ot.unstable_cancelCallback,FR=Ot.unstable_shouldYield,GR=Ot.unstable_requestPaint,jn=Ot.unstable_now,KR=Ot.unstable_getCurrentPriorityLevel,LE=Ot.unstable_ImmediatePriority,zE=Ot.unstable_UserBlockingPriority,rd=Ot.unstable_NormalPriority,YR=Ot.unstable_LowPriority,BE=Ot.unstable_IdlePriority,XR=Ot.log,VR=Ot.unstable_setDisableYieldValue,jc=null,En=null;function La(e){if(typeof XR=="function"&&VR(e),En&&typeof En.setStrictMode=="function")try{En.setStrictMode(jc,e)}catch{}}var _n=Math.clz32?Math.clz32:ZR,QR=Math.log,WR=Math.LN2;function ZR(e){return e>>>=0,e===0?32:31-(QR(e)/WR|0)|0}var ef=256,tf=262144,nf=4194304;function gi(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Bh(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var a=0,i=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var l=r&134217727;return l!==0?(r=l&~i,r!==0?a=gi(r):(o&=l,o!==0?a=gi(o):n||(n=l&~e,n!==0&&(a=gi(n))))):(l=r&~i,l!==0?a=gi(l):o!==0?a=gi(o):n||(n=r&~e,n!==0&&(a=gi(n)))),a===0?0:t!==0&&t!==a&&!(t&i)&&(i=a&-a,n=t&-t,i>=n||i===32&&(n&4194048)!==0)?t:a}function Ec(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function JR(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function UE(){var e=nf;return nf<<=1,!(nf&62914560)&&(nf=4194304),e}function py(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function _c(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function e$(e,t,n,r,a,i){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var l=e.entanglements,s=e.expirationTimes,u=e.hiddenUpdates;for(n=o&~n;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var o$=/[\n"\\]/g;function qn(e){return e.replace(o$,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Um(e,t,n,r,a,i,o,l){e.name="",o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?e.type=o:e.removeAttribute("type"),t!=null?o==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Un(t)):e.value!==""+Un(t)&&(e.value=""+Un(t)):o!=="submit"&&o!=="reset"||e.removeAttribute("value"),t!=null?Im(e,o,Un(t)):n!=null?Im(e,o,Un(n)):r!=null&&e.removeAttribute("value"),a==null&&i!=null&&(e.defaultChecked=!!i),a!=null&&(e.checked=a&&typeof a!="function"&&typeof a!="symbol"),l!=null&&typeof l!="function"&&typeof l!="symbol"&&typeof l!="boolean"?e.name=""+Un(l):e.removeAttribute("name")}function VE(e,t,n,r,a,i,o,l){if(i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(e.type=i),t!=null||n!=null){if(!(i!=="submit"&&i!=="reset"||t!=null)){Bm(e);return}n=n!=null?""+Un(n):"",t=t!=null?""+Un(t):n,l||t===e.value||(e.value=t),e.defaultValue=t}r=r??a,r=typeof r!="function"&&typeof r!="symbol"&&!!r,e.checked=l?e.checked:!!r,e.defaultChecked=!!r,o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"&&(e.name=o),Bm(e)}function Im(e,t,n){t==="number"&&ad(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function Yo(e,t,n,r){if(e=e.options,t){t={};for(var a=0;a"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),qm=!1;if(ra)try{var Cs={};Object.defineProperty(Cs,"passive",{get:function(){qm=!0}}),window.addEventListener("test",Cs,Cs),window.removeEventListener("test",Cs,Cs)}catch{qm=!1}var za=null,$b=null,$f=null;function e2(){if($f)return $f;var e,t=$b,n=t.length,r,a="value"in za?za.value:za.textContent,i=a.length;for(e=0;e=tu),g1=" ",b1=!1;function n2(e,t){switch(e){case"keyup":return $$.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function r2(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ro=!1;function k$(e,t){switch(e){case"compositionend":return r2(t);case"keypress":return t.which!==32?null:(b1=!0,g1);case"textInput":return e=t.data,e===g1&&b1?null:e;default:return null}}function L$(e,t){if(Ro)return e==="compositionend"||!kb&&n2(e,t)?(e=e2(),$f=$b=za=null,Ro=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=A1(n)}}function l2(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?l2(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function s2(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=ad(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=ad(e.document)}return t}function Lb(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var G$=ra&&"documentMode"in document&&11>=document.documentMode,$o=null,Fm=null,ru=null,Gm=!1;function E1(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Gm||$o==null||$o!==ad(r)||(r=$o,"selectionStart"in r&&Lb(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),ru&&Ou(ru,r)||(ru=r,r=wd(Fm,"onSelect"),0>=o,a-=o,gr=1<<32-_n(t)+a|n<E?(_=A,A=null):_=A.sibling;var N=d(y,A,x[E],w);if(N===null){A===null&&(A=_);break}e&&A&&N.alternate===null&&t(y,A),v=i(N,v,E),O===null?S=N:O.sibling=N,O=N,A=_}if(E===x.length)return n(y,A),Se&&Br(y,E),S;if(A===null){for(;EE?(_=A,A=null):_=A.sibling;var $=d(y,A,N.value,w);if($===null){A===null&&(A=_);break}e&&A&&$.alternate===null&&t(y,A),v=i($,v,E),O===null?S=$:O.sibling=$,O=$,A=_}if(N.done)return n(y,A),Se&&Br(y,E),S;if(A===null){for(;!N.done;E++,N=x.next())N=c(y,N.value,w),N!==null&&(v=i(N,v,E),O===null?S=N:O.sibling=N,O=N);return Se&&Br(y,E),S}for(A=r(A);!N.done;E++,N=x.next())N=p(A,y,E,N.value,w),N!==null&&(e&&N.alternate!==null&&A.delete(N.key===null?E:N.key),v=i(N,v,E),O===null?S=N:O.sibling=N,O=N);return e&&A.forEach(function(P){return t(y,P)}),Se&&Br(y,E),S}function g(y,v,x,w){if(typeof x=="object"&&x!==null&&x.type===Co&&x.key===null&&(x=x.props.children),typeof x=="object"&&x!==null){switch(x.$$typeof){case Jc:e:{for(var S=x.key;v!==null;){if(v.key===S){if(S=x.type,S===Co){if(v.tag===7){n(y,v.sibling),w=a(v,x.props.children),w.return=y,y=w;break e}}else if(v.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===xa&&bi(S)===v.type){n(y,v.sibling),w=a(v,x.props),Ms(w,x),w.return=y,y=w;break e}n(y,v);break}else t(y,v);v=v.sibling}x.type===Co?(w=Ii(x.props.children,y.mode,w,x.key),w.return=y,y=w):(w=kf(x.type,x.key,x.props,null,y.mode,w),Ms(w,x),w.return=y,y=w)}return o(y);case Ys:e:{for(S=x.key;v!==null;){if(v.key===S)if(v.tag===4&&v.stateNode.containerInfo===x.containerInfo&&v.stateNode.implementation===x.implementation){n(y,v.sibling),w=a(v,x.children||[]),w.return=y,y=w;break e}else{n(y,v);break}else t(y,v);v=v.sibling}w=Oy(x,y.mode,w),w.return=y,y=w}return o(y);case xa:return x=bi(x),g(y,v,x,w)}if(Xs(x))return b(y,v,x,w);if(Ts(x)){if(S=Ts(x),typeof S!="function")throw Error(I(150));return x=S.call(x),m(y,v,x,w)}if(typeof x.then=="function")return g(y,v,lf(x),w);if(x.$$typeof===Gr)return g(y,v,of(y,x),w);sf(y,x)}return typeof x=="string"&&x!==""||typeof x=="number"||typeof x=="bigint"?(x=""+x,v!==null&&v.tag===6?(n(y,v.sibling),w=a(v,x),w.return=y,y=w):(n(y,v),w=wy(x,y.mode,w),w.return=y,y=w),o(y)):n(y,v)}return function(y,v,x,w){try{Eu=0;var S=g(y,v,x,w);return Qo=null,S}catch(A){if(A===rs||A===Gh)throw A;var O=wn(29,A,null,y.mode);return O.lanes=w,O.return=y,O}finally{}}}var Qi=O2(!0),A2=O2(!1),Sa=!1;function Kb(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Zm(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Oe&2){var a=r.pending;return a===null?t.next=t:(t.next=a.next,a.next=t),r.pending=t,t=od(e),y2(e,null,n),t}return Fh(e,r,t,n),od(e)}function iu(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194048)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,HE(e,n)}}function jy(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var a=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};i===null?a=i=o:i=i.next=o,n=n.next}while(n!==null);i===null?a=i=t:i=i.next=t}else a=i=t;n={baseState:r.baseState,firstBaseUpdate:a,lastBaseUpdate:i,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Jm=!1;function ou(){if(Jm){var e=Vo;if(e!==null)throw e}}function lu(e,t,n,r){Jm=!1;var a=e.updateQueue;Sa=!1;var i=a.firstBaseUpdate,o=a.lastBaseUpdate,l=a.shared.pending;if(l!==null){a.shared.pending=null;var s=l,u=s.next;s.next=null,o===null?i=u:o.next=u,o=s;var f=e.alternate;f!==null&&(f=f.updateQueue,l=f.lastBaseUpdate,l!==o&&(l===null?f.firstBaseUpdate=u:l.next=u,f.lastBaseUpdate=s))}if(i!==null){var c=a.baseState;o=0,f=u=s=null,l=i;do{var d=l.lane&-536870913,p=d!==l.lane;if(p?(be&d)===d:(r&d)===d){d!==0&&d===bl&&(Jm=!0),f!==null&&(f=f.next={lane:0,tag:l.tag,payload:l.payload,callback:null,next:null});e:{var b=e,m=l;d=t;var g=n;switch(m.tag){case 1:if(b=m.payload,typeof b=="function"){c=b.call(g,c,d);break e}c=b;break e;case 3:b.flags=b.flags&-65537|128;case 0:if(b=m.payload,d=typeof b=="function"?b.call(g,c,d):b,d==null)break e;c=Ze({},c,d);break e;case 2:Sa=!0}}d=l.callback,d!==null&&(e.flags|=64,p&&(e.flags|=8192),p=a.callbacks,p===null?a.callbacks=[d]:p.push(d))}else p={lane:d,tag:l.tag,payload:l.payload,callback:l.callback,next:null},f===null?(u=f=p,s=c):f=f.next=p,o|=d;if(l=l.next,l===null){if(l=a.shared.pending,l===null)break;p=l,l=p.next,p.next=null,a.lastBaseUpdate=p,a.shared.pending=null}}while(!0);f===null&&(s=c),a.baseState=s,a.firstBaseUpdate=u,a.lastBaseUpdate=f,i===null&&(a.shared.lanes=0),ai|=o,e.lanes=o,e.memoizedState=c}}function j2(e,t){if(typeof e!="function")throw Error(I(191,e));e.call(t)}function E2(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ei?i:8;var o=re.T,l={};re.T=l,i0(e,!1,t,n);try{var s=a(),u=re.S;if(u!==null&&u(l,s),s!==null&&typeof s=="object"&&typeof s.then=="function"){var f=eD(s,r);su(e,t,f,Tn(e))}else su(e,t,r,Tn(e))}catch(c){su(e,t,{then:function(){},status:"rejected",reason:c},Tn())}finally{Ae.p=i,o!==null&&l.types!==null&&(o.types=l.types),re.T=o}}function oD(){}function av(e,t,n,r){if(e.tag!==5)throw Error(I(476));var a=W2(e).queue;Q2(e,a,t,Ui,n===null?oD:function(){return Z2(e),n(r)})}function W2(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:Ui,baseState:Ui,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ia,lastRenderedState:Ui},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ia,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Z2(e){var t=W2(e);t.next===null&&(t=e.alternate.memoizedState),su(e,t.next.queue,{},Tn())}function a0(){return Bt(Mu)}function J2(){return ut().memoizedState}function e_(){return ut().memoizedState}function lD(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=Tn();e=Ga(n);var r=Ka(t,e,n);r!==null&&(cn(r,t,n),iu(r,t,n)),t={cache:qb()},e.payload=t;return}t=t.return}}function sD(e,t,n){var r=Tn();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Vh(e)?n_(t,n):(n=Bb(e,t,n,r),n!==null&&(cn(n,e,r),r_(n,t,r)))}function t_(e,t,n){var r=Tn();su(e,t,n,r)}function su(e,t,n,r){var a={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Vh(e))n_(t,a);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var o=t.lastRenderedState,l=i(o,n);if(a.hasEagerState=!0,a.eagerState=l,Nn(l,o))return Fh(e,t,a,0),Ie===null&&qh(),!1}catch{}finally{}if(n=Bb(e,t,a,r),n!==null)return cn(n,e,r),r_(n,t,r),!0}return!1}function i0(e,t,n,r){if(r={lane:2,revertLane:p0(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Vh(e)){if(t)throw Error(I(479))}else t=Bb(e,n,r,2),t!==null&&cn(t,e,2)}function Vh(e){var t=e.alternate;return e===ce||t!==null&&t===ce}function n_(e,t){Wo=dd=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function r_(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,HE(e,n)}}var Tu={readContext:Bt,use:Yh,useCallback:nt,useContext:nt,useEffect:nt,useImperativeHandle:nt,useLayoutEffect:nt,useInsertionEffect:nt,useMemo:nt,useReducer:nt,useRef:nt,useState:nt,useDebugValue:nt,useDeferredValue:nt,useTransition:nt,useSyncExternalStore:nt,useId:nt,useHostTransitionStatus:nt,useFormState:nt,useActionState:nt,useOptimistic:nt,useMemoCache:nt,useCacheRefresh:nt};Tu.useEffectEvent=nt;var a_={readContext:Bt,use:Yh,useCallback:function(e,t){return Xt().memoizedState=[e,t===void 0?null:t],e},useContext:Bt,useEffect:I1,useImperativeHandle:function(e,t,n){n=n!=null?n.concat([e]):null,Bf(4194308,4,G2.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Bf(4194308,4,e,t)},useInsertionEffect:function(e,t){Bf(4,2,e,t)},useMemo:function(e,t){var n=Xt();t=t===void 0?null:t;var r=e();if(Wi){La(!0);try{e()}finally{La(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Xt();if(n!==void 0){var a=n(t);if(Wi){La(!0);try{n(t)}finally{La(!1)}}}else a=t;return r.memoizedState=r.baseState=a,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:a},r.queue=e,e=e.dispatch=sD.bind(null,ce,e),[r.memoizedState,e]},useRef:function(e){var t=Xt();return e={current:e},t.memoizedState=e},useState:function(e){e=nv(e);var t=e.queue,n=t_.bind(null,ce,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:n0,useDeferredValue:function(e,t){var n=Xt();return r0(n,e,t)},useTransition:function(){var e=nv(!1);return e=Q2.bind(null,ce,e.queue,!0,!1),Xt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ce,a=Xt();if(Se){if(n===void 0)throw Error(I(407));n=n()}else{if(n=t(),Ie===null)throw Error(I(349));be&127||M2(r,t,n)}a.memoizedState=n;var i={value:n,getSnapshot:t};return a.queue=i,I1(R2.bind(null,r,i,e),[e]),r.flags|=2048,Sl(9,{destroy:void 0},P2.bind(null,r,i,n,t),null),n},useId:function(){var e=Xt(),t=Ie.identifierPrefix;if(Se){var n=br,r=gr;n=(r&~(1<<32-_n(r)-1)).toString(32)+n,t="_"+t+"R_"+n,n=hd++,0<\/script>",i=i.removeChild(i.firstChild);break;case"select":i=typeof r.is=="string"?o.createElement("select",{is:r.is}):o.createElement("select"),r.multiple?i.multiple=!0:r.size&&(i.size=r.size);break;default:i=typeof r.is=="string"?o.createElement(a,{is:r.is}):o.createElement(a)}}i[kt]=t,i[dn]=r;e:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)i.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break e;for(;o.sibling===null;){if(o.return===null||o.return===t)break e;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=i;e:switch(Ut(i,a,r),a){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break e;case"img":r=!0;break e;default:r=!1}r&&Pr(t)}}return Ye(t),Ry(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pr(t);else{if(typeof r!="string"&&t.stateNode===null)throw Error(I(166));if(e=qa.current,bo(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Lt,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[kt]=t,e=!!(e.nodeValue===n||r!==null&&r.suppressHydrationWarning===!0||tT(e.nodeValue,n)),e||ni(t,!0)}else e=Od(e).createTextNode(r),e[kt]=t,t.stateNode=e}return Ye(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=bo(t),n!==null){if(e===null){if(!r)throw Error(I(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(I(557));e[kt]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ye(t),e=!1}else n=Ay(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Sn(t),t):(Sn(t),null);if(t.flags&128)throw Error(I(558))}return Ye(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=bo(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(I(318));if(a=t.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(I(317));a[kt]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ye(t),a=!1}else a=Ay(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Sn(t),t):(Sn(t),null)}return Sn(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),i=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(i=r.memoizedState.cachePool.pool),i!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),uf(t,t.updateQueue),Ye(t),null);case 4:return ml(),e===null&&y0(t.stateNode.containerInfo),Ye(t),null;case 10:return Wr(t.type),Ye(t),null;case 19:if(Nt(st),r=t.memoizedState,r===null)return Ye(t),null;if(a=(t.flags&128)!==0,i=r.rendering,i===null)if(a)Ps(r,!1);else{if(ot!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(i=fd(e),i!==null){for(t.flags|=128,Ps(r,!1),e=i.updateQueue,t.updateQueue=e,uf(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)m2(n,e),n=n.sibling;return Fe(st,st.current&1|2),Se&&Br(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&jn()>vd&&(t.flags|=128,a=!0,Ps(r,!1),t.lanes=4194304)}else{if(!a)if(e=fd(i),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,uf(t,e),Ps(r,!0),r.tail===null&&r.tailMode==="hidden"&&!i.alternate&&!Se)return Ye(t),null}else 2*jn()-r.renderingStartTime>vd&&n!==536870912&&(t.flags|=128,a=!0,Ps(r,!1),t.lanes=4194304);r.isBackwards?(i.sibling=t.child,t.child=i):(e=r.last,e!==null?e.sibling=i:t.child=i,r.last=i)}return r.tail!==null?(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=jn(),e.sibling=null,n=st.current,Fe(st,a?n&1|2:n&1),Se&&Br(t,r.treeForkCount),e):(Ye(t),null);case 22:case 23:return Sn(t),Yb(),r=t.memoizedState!==null,e!==null?e.memoizedState!==null!==r&&(t.flags|=8192):r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Ye(t),t.subtreeFlags&6&&(t.flags|=8192)):Ye(t),n=t.updateQueue,n!==null&&uf(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&Nt(Hi),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Wr(pt),Ye(t),null;case 25:return null;case 30:return null}throw Error(I(156,t.tag))}function hD(e,t){switch(Hb(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Wr(pt),ml(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return nd(t),null;case 31:if(t.memoizedState!==null){if(Sn(t),t.alternate===null)throw Error(I(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Sn(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(I(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Nt(st),null;case 4:return ml(),null;case 10:return Wr(t.type),null;case 22:case 23:return Sn(t),Yb(),e!==null&&Nt(Hi),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Wr(pt),null;case 25:return null;default:return null}}function m_(e,t){switch(Hb(t),t.tag){case 3:Wr(pt),ml();break;case 26:case 27:case 5:nd(t);break;case 4:ml();break;case 31:t.memoizedState!==null&&Sn(t);break;case 13:Sn(t);break;case 19:Nt(st);break;case 10:Wr(t.type);break;case 22:case 23:Sn(t),Yb(),e!==null&&Nt(Hi);break;case 24:Wr(pt)}}function Pc(e,t){try{var n=t.updateQueue,r=n!==null?n.lastEffect:null;if(r!==null){var a=r.next;n=a;do{if((n.tag&e)===e){r=void 0;var i=n.create,o=n.inst;r=i(),o.destroy=r}n=n.next}while(n!==a)}}catch(l){Ce(t,t.return,l)}}function ri(e,t,n){try{var r=t.updateQueue,a=r!==null?r.lastEffect:null;if(a!==null){var i=a.next;r=i;do{if((r.tag&e)===e){var o=r.inst,l=o.destroy;if(l!==void 0){o.destroy=void 0,a=t;var s=n,u=l;try{u()}catch(f){Ce(a,s,f)}}}r=r.next}while(r!==i)}}catch(f){Ce(t,t.return,f)}}function v_(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{E2(t,n)}catch(r){Ce(e,e.return,r)}}}function g_(e,t,n){n.props=Zi(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(r){Ce(e,t,r)}}function uu(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n=="function"?e.refCleanup=n(r):n.current=r}}catch(a){Ce(e,t,a)}}function xr(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r=="function")try{r()}catch(a){Ce(e,t,a)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(a){Ce(e,t,a)}else n.current=null}function b_(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&r.focus();break e;case"img":n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(a){Ce(e,e.return,a)}}function $y(e,t,n){try{var r=e.stateNode;DD(r,e.type,n,t),r[dn]=t}catch(a){Ce(e,e.return,a)}}function x_(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&oi(e.type)||e.tag===4}function Dy(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||x_(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&oi(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function uv(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Kr));else if(r!==4&&(r===27&&oi(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(uv(e,t,n),e=e.sibling;e!==null;)uv(e,t,n),e=e.sibling}function md(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&oi(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(md(e,t,n),e=e.sibling;e!==null;)md(e,t,n),e=e.sibling}function S_(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,a=t.attributes;a.length;)t.removeAttributeNode(a[0]);Ut(t,r,n),t[kt]=e,t[dn]=n}catch(i){Ce(e,e.return,i)}}var Fr=!1,ht=!1,ky=!1,eS=typeof WeakSet=="function"?WeakSet:Set,_t=null;function pD(e,t){if(e=e.containerInfo,mv=_d,e=s2(e),Lb(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var o=0,l=-1,s=-1,u=0,f=0,c=e,d=null;t:for(;;){for(var p;c!==n||a!==0&&c.nodeType!==3||(l=o+a),c!==i||r!==0&&c.nodeType!==3||(s=o+r),c.nodeType===3&&(o+=c.nodeValue.length),(p=c.firstChild)!==null;)d=c,c=p;for(;;){if(c===e)break t;if(d===n&&++u===a&&(l=o),d===i&&++f===r&&(s=o),(p=c.nextSibling)!==null)break;c=d,d=c.parentNode}c=p}n=l===-1||s===-1?null:{start:l,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(vv={focusedElem:e,selectionRange:n},_d=!1,_t=t;_t!==null;)if(t=_t,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_t=e;else for(;_t!==null;){switch(t=_t,i=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(n=0;n title"))),Ut(i,r,n),i[kt]=e,Tt(i),r=i;break e;case"link":var o=xS("link","href",a).get(r+(n.href||""));if(o){for(var l=0;lg&&(o=g,g=m,m=o);var y=j1(l,m),v=j1(l,g);if(y&&v&&(p.rangeCount!==1||p.anchorNode!==y.node||p.anchorOffset!==y.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var x=c.createRange();x.setStart(y.node,y.offset),p.removeAllRanges(),m>g?(p.addRange(x),p.extend(v.node,v.offset)):(x.setEnd(v.node,v.offset),p.addRange(x))}}}}for(c=[],p=l;p=p.parentNode;)p.nodeType===1&&c.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof l.focus=="function"&&l.focus(),l=0;ln?32:n,re.T=null,n=dv,dv=null;var i=Xa,o=Zr;if(St=0,Ol=Xa=null,Zr=0,Oe&6)throw Error(I(331));var l=Oe;if(Oe|=4,P_(i.current),C_(i,i.current,o,n),Oe=l,Rc(0,!1),En&&typeof En.onPostCommitFiberRoot=="function")try{En.onPostCommitFiberRoot(jc,i)}catch{}return!0}finally{Ae.p=a,re.T=r,Y_(e,t)}}function aS(e,t,n){t=Fn(n,t),t=ov(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(_c(e,2),_r(e))}function Ce(e,t,n){if(e.tag===3)aS(e,e,n);else for(;t!==null;){if(t.tag===3){aS(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof r.componentDidCatch=="function"&&(Ya===null||!Ya.has(r))){e=Fn(n,e),n=u_(2),r=Ka(t,n,2),r!==null&&(c_(n,r,t,e),_c(r,2),_r(r));break}}t=t.return}}function zy(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new vD;var a=new Set;r.set(t,a)}else a=r.get(t),a===void 0&&(a=new Set,r.set(t,a));a.has(n)||(f0=!0,a.add(n),e=wD.bind(null,e,t,n),t.then(e,e))}function wD(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ie===e&&(be&n)===n&&(ot===4||ot===3&&(be&62914560)===be&&300>jn()-Qh?!(Oe&2)&&Al(e,0):d0|=n,wl===be&&(wl=0)),_r(e)}function V_(e,t){t===0&&(t=UE()),e=co(e,t),e!==null&&(_c(e,t),_r(e))}function OD(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),V_(e,n)}function AD(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(I(314))}r!==null&&r.delete(t),V_(e,n)}function jD(e,t){return Tb(e,t)}var xd=null,_o=null,pv=!1,Sd=!1,By=!1,Ia=0;function _r(e){e!==_o&&e.next===null&&(_o===null?xd=_o=e:_o=_o.next=e),Sd=!0,pv||(pv=!0,_D())}function Rc(e,t){if(!By&&Sd){By=!0;do for(var n=!1,r=xd;r!==null;){if(e!==0){var a=r.pendingLanes;if(a===0)var i=0;else{var o=r.suspendedLanes,l=r.pingedLanes;i=(1<<31-_n(42|e)+1)-1,i&=a&~(o&~l),i=i&201326741?i&201326741|1:i?i|2:0}i!==0&&(n=!0,iS(r,i))}else i=be,i=Bh(r,r===Ie?i:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(i&3)||Ec(r,i)||(n=!0,iS(r,i));r=r.next}while(n);By=!1}}function ED(){Q_()}function Q_(){Sd=pv=!1;var e=0;Ia!==0&&LD()&&(e=Ia);for(var t=jn(),n=null,r=xd;r!==null;){var a=r.next,i=W_(r,t);i===0?(r.next=null,n===null?xd=a:n.next=a,a===null&&(_o=n)):(n=r,(e!==0||i&3)&&(Sd=!0)),r=a}St!==0&&St!==5||Rc(e),Ia!==0&&(Ia=0)}function W_(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,a=e.expirationTimes,i=e.pendingLanes&-62914561;0l)break;var f=s.transferSize,c=s.initiatorType;f&&cS(c)&&(s=s.responseEnd,o+=f*(s"u"?null:document;function oT(e,t,n){var r=is;if(r&&typeof t=="string"&&t){var a=qn(t);a='link[rel="'+e+'"][href="'+a+'"]',typeof n=="string"&&(a+='[crossorigin="'+n+'"]'),vS.has(a)||(vS.add(a),e={rel:e,crossOrigin:n,href:t},r.querySelector(a)===null&&(t=r.createElement("link"),Ut(t,"link",e),Tt(t),r.head.appendChild(t)))}}function KD(e){ha.D(e),oT("dns-prefetch",e,null)}function YD(e,t){ha.C(e,t),oT("preconnect",e,t)}function XD(e,t,n){ha.L(e,t,n);var r=is;if(r&&e&&t){var a='link[rel="preload"][as="'+qn(t)+'"]';t==="image"&&n&&n.imageSrcSet?(a+='[imagesrcset="'+qn(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(a+='[imagesizes="'+qn(n.imageSizes)+'"]')):a+='[href="'+qn(e)+'"]';var i=a;switch(t){case"style":i=jl(e);break;case"script":i=os(e)}Wn.has(i)||(e=Ze({rel:"preload",href:t==="image"&&n&&n.imageSrcSet?void 0:e,as:t},n),Wn.set(i,e),r.querySelector(a)!==null||t==="style"&&r.querySelector($c(i))||t==="script"&&r.querySelector(Dc(i))||(t=r.createElement("link"),Ut(t,"link",e),Tt(t),r.head.appendChild(t)))}}function VD(e,t){ha.m(e,t);var n=is;if(n&&e){var r=t&&typeof t.as=="string"?t.as:"script",a='link[rel="modulepreload"][as="'+qn(r)+'"][href="'+qn(e)+'"]',i=a;switch(r){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":i=os(e)}if(!Wn.has(i)&&(e=Ze({rel:"modulepreload",href:e},t),Wn.set(i,e),n.querySelector(a)===null)){switch(r){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(Dc(i)))return}r=n.createElement("link"),Ut(r,"link",e),Tt(r),n.head.appendChild(r)}}}function QD(e,t,n){ha.S(e,t,n);var r=is;if(r&&e){var a=Ko(r).hoistableStyles,i=jl(e);t=t||"default";var o=a.get(i);if(!o){var l={loading:0,preload:null};if(o=r.querySelector($c(i)))l.loading=5;else{e=Ze({rel:"stylesheet",href:e,"data-precedence":t},n),(n=Wn.get(i))&&m0(e,n);var s=o=r.createElement("link");Tt(s),Ut(s,"link",e),s._p=new Promise(function(u,f){s.onload=u,s.onerror=f}),s.addEventListener("load",function(){l.loading|=1}),s.addEventListener("error",function(){l.loading|=2}),l.loading|=4,qf(o,t,r)}o={type:"stylesheet",instance:o,count:1,state:l},a.set(i,o)}}}function WD(e,t){ha.X(e,t);var n=is;if(n&&e){var r=Ko(n).hoistableScripts,a=os(e),i=r.get(a);i||(i=n.querySelector(Dc(a)),i||(e=Ze({src:e,async:!0},t),(t=Wn.get(a))&&v0(e,t),i=n.createElement("script"),Tt(i),Ut(i,"link",e),n.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},r.set(a,i))}}function ZD(e,t){ha.M(e,t);var n=is;if(n&&e){var r=Ko(n).hoistableScripts,a=os(e),i=r.get(a);i||(i=n.querySelector(Dc(a)),i||(e=Ze({src:e,async:!0,type:"module"},t),(t=Wn.get(a))&&v0(e,t),i=n.createElement("script"),Tt(i),Ut(i,"link",e),n.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},r.set(a,i))}}function gS(e,t,n,r){var a=(a=qa.current)?Ad(a):null;if(!a)throw Error(I(446));switch(e){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(t=jl(n.href),n=Ko(a).hoistableStyles,r=n.get(t),r||(r={type:"style",instance:null,count:0,state:null},n.set(t,r)),r):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){e=jl(n.href);var i=Ko(a).hoistableStyles,o=i.get(e);if(o||(a=a.ownerDocument||a,o={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},i.set(e,o),(i=a.querySelector($c(e)))&&!i._p&&(o.instance=i,o.state.loading=5),Wn.has(e)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Wn.set(e,n),i||JD(a,e,n,o.state))),t&&r===null)throw Error(I(528,""));return o}if(t&&r!==null)throw Error(I(529,""));return null;case"script":return t=n.async,n=n.src,typeof n=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=os(n),n=Ko(a).hoistableScripts,r=n.get(t),r||(r={type:"script",instance:null,count:0,state:null},n.set(t,r)),r):{type:"void",instance:null,count:0,state:null};default:throw Error(I(444,e))}}function jl(e){return'href="'+qn(e)+'"'}function $c(e){return'link[rel="stylesheet"]['+e+"]"}function lT(e){return Ze({},e,{"data-precedence":e.precedence,precedence:null})}function JD(e,t,n,r){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?r.loading=1:(t=e.createElement("link"),r.preload=t,t.addEventListener("load",function(){return r.loading|=1}),t.addEventListener("error",function(){return r.loading|=2}),Ut(t,"link",n),Tt(t),e.head.appendChild(t))}function os(e){return'[src="'+qn(e)+'"]'}function Dc(e){return"script[async]"+e}function bS(e,t,n){if(t.count++,t.instance===null)switch(t.type){case"style":var r=e.querySelector('style[data-href~="'+qn(n.href)+'"]');if(r)return t.instance=r,Tt(r),r;var a=Ze({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement("style"),Tt(r),Ut(r,"style",a),qf(r,n.precedence,e),t.instance=r;case"stylesheet":a=jl(n.href);var i=e.querySelector($c(a));if(i)return t.state.loading|=4,t.instance=i,Tt(i),i;r=lT(n),(a=Wn.get(a))&&m0(r,a),i=(e.ownerDocument||e).createElement("link"),Tt(i);var o=i;return o._p=new Promise(function(l,s){o.onload=l,o.onerror=s}),Ut(i,"link",r),t.state.loading|=4,qf(i,n.precedence,e),t.instance=i;case"script":return i=os(n.src),(a=e.querySelector(Dc(i)))?(t.instance=a,Tt(a),a):(r=n,(a=Wn.get(i))&&(r=Ze({},n),v0(r,a)),e=e.ownerDocument||e,a=e.createElement("script"),Tt(a),Ut(a,"link",r),e.head.appendChild(a),t.instance=a);case"void":return null;default:throw Error(I(443,t.type))}else t.type==="stylesheet"&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,qf(r,n.precedence,e));return t.instance}function qf(e,t,n){for(var r=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),a=r.length?r[r.length-1]:null,i=a,o=0;o title"):null)}function e3(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function sT(e){return!(e.type==="stylesheet"&&!(e.state.loading&3))}function t3(e,t,n,r){if(n.type==="stylesheet"&&(typeof r.media!="string"||matchMedia(r.media).matches!==!1)&&!(n.state.loading&4)){if(n.instance===null){var a=jl(r.href),i=t.querySelector($c(a));if(i){t=i._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=jd.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=i,Tt(i);return}i=t.ownerDocument||t,r=lT(r),(a=Wn.get(a))&&m0(r,a),i=i.createElement("link"),Tt(i);var o=i;o._p=new Promise(function(l,s){o.onload=l,o.onerror=s}),Ut(i,"link",r),n.instance=i}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=jd.bind(e),t.addEventListener("load",n),t.addEventListener("error",n))}}var Gy=0;function n3(e,t){return e.stylesheets&&e.count===0&&Gf(e,e.stylesheets),0Gy?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(a)}}:null}function jd(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Gf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Ed=null;function Gf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Ed=new Map,t.forEach(r3,e),Ed=null,jd.call(e))}function r3(e,t){if(!(t.state.loading&4)){var n=Ed.get(e);if(n)var r=n.get(null);else{n=new Map,Ed.set(e,n);for(var a=e.querySelectorAll("link[data-precedence],style[data-precedence]"),i=0;i"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(mT)}catch(e){console.error(e)}}mT(),AE.exports=Lh;var f3=AE.exports;/** + * react-router v7.17.0 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var TS="popstate";function CS(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function d3(e={}){function t(r,a){var u;let i=(u=a.state)==null?void 0:u.masked,{pathname:o,search:l,hash:s}=i||r.location;return jv("",{pathname:o,search:l,hash:s},a.state&&a.state.usr||null,a.state&&a.state.key||"default",i?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function n(r,a){return typeof a=="string"?a:$u(a)}return p3(t,n,null,e)}function tt(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function cr(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function h3(){return Math.random().toString(36).substring(2,10)}function NS(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function jv(e,t,n=null,r,a){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?ls(t):t,state:n,key:t&&t.key||r||h3(),mask:a}}function $u({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function ls(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function p3(e,t,n,r={}){let{window:a=document.defaultView,v5Compat:i=!1}=r,o=a.history,l="POP",s=null,u=f();u==null&&(u=0,o.replaceState({...o.state,idx:u},""));function f(){return(o.state||{idx:null}).idx}function c(){l="POP";let g=f(),y=g==null?null:g-u;u=g,s&&s({action:l,location:m.location,delta:y})}function d(g,y){l="PUSH";let v=CS(g)?g:jv(m.location,g,y);u=f()+1;let x=NS(v,u),w=m.createHref(v.mask||v);try{o.pushState(x,"",w)}catch(S){if(S instanceof DOMException&&S.name==="DataCloneError")throw S;a.location.assign(w)}i&&s&&s({action:l,location:m.location,delta:1})}function p(g,y){l="REPLACE";let v=CS(g)?g:jv(m.location,g,y);u=f();let x=NS(v,u),w=m.createHref(v.mask||v);o.replaceState(x,"",w),i&&s&&s({action:l,location:m.location,delta:0})}function b(g){return y3(a,g)}let m={get action(){return l},get location(){return e(a,o)},listen(g){if(s)throw new Error("A history only accepts one active listener");return a.addEventListener(TS,c),s=g,()=>{a.removeEventListener(TS,c),s=null}},createHref(g){return t(a,g)},createURL:b,encodeLocation(g){let y=b(g);return{pathname:y.pathname,search:y.search,hash:y.hash}},push:d,replace:p,go(g){return o.go(g)}};return m}function y3(e,t,n=!1){let r="http://localhost";e&&(r=e.location.origin!=="null"?e.location.origin:e.location.href),tt(r,"No window.location.(origin|href) available to create URL");let a=typeof t=="string"?t:$u(t);return a=a.replace(/ $/,"%20"),!n&&a.startsWith("//")&&(a=r+a),new URL(a,r)}function vT(e,t,n="/"){return m3(e,t,n,!1)}function m3(e,t,n,r,a){let i=typeof t=="string"?ls(t):t,o=sa(i.pathname||"/",n);if(o==null)return null;let l=v3(e),s=null,u=C3(o);for(let f=0;s==null&&f{let f={relativePath:u===void 0?o.path||"":u,caseSensitive:o.caseSensitive===!0,childrenIndex:l,route:o};if(f.relativePath.startsWith("/")){if(!f.relativePath.startsWith(r)&&s)return;tt(f.relativePath.startsWith(r),`Absolute route path "${f.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),f.relativePath=f.relativePath.slice(r.length)}let c=ur([r,f.relativePath]),d=n.concat(f);o.children&&o.children.length>0&&(tt(o.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${c}".`),gT(o.children,t,d,c,s)),!(o.path==null&&!o.index)&&t.push({path:c,score:j3(c,o.index),routesMeta:d})};return e.forEach((o,l)=>{var s;if(o.path===""||!((s=o.path)!=null&&s.includes("?")))i(o,l);else for(let u of bT(o.path))i(o,l,!0,u)}),t}function bT(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,a=n.endsWith("?"),i=n.replace(/\?$/,"");if(r.length===0)return a?[i,""]:[i];let o=bT(r.join("/")),l=[];return l.push(...o.map(s=>s===""?i:[i,s].join("/"))),a&&l.push(...o),l.map(s=>e.startsWith("/")&&s===""?"/":s)}function g3(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:E3(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var b3=/^:[\w-]+$/,x3=3,S3=2,w3=1,O3=10,A3=-2,MS=e=>e==="*";function j3(e,t){let n=e.split("/"),r=n.length;return n.some(MS)&&(r+=A3),t&&(r+=S3),n.filter(a=>!MS(a)).reduce((a,i)=>a+(b3.test(i)?x3:i===""?w3:O3),r)}function E3(e,t){return e.length===t.length&&e.slice(0,-1).every((r,a)=>r===t[a])?e[e.length-1]-t[t.length-1]:0}function _3(e,t,n=!1){let{routesMeta:r}=e,a={},i="/",o=[];for(let l=0;l{if(f==="*"){let b=l[d]||"";o=i.slice(0,i.length-b.length).replace(/(.)\/+$/,"$1")}const p=l[d];return c&&!p?u[f]=void 0:u[f]=(p||"").replace(/%2F/g,"/"),u},{}),pathname:i,pathnameBase:o,pattern:e}}function T3(e,t=!1,n=!0){cr(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,l,s,u,f)=>{if(r.push({paramName:l,isOptional:s!=null}),s){let c=f.charAt(u+o.length);return c&&c!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(r.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,t?void 0:"i"),r]}function C3(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return cr(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function sa(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}var N3=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function M3(e,t="/"){let{pathname:n,search:r="",hash:a=""}=typeof e=="string"?ls(e):e,i;return n?(n=xT(n),n.startsWith("/")?i=PS(n.substring(1),"/"):i=PS(n,t)):i=t,{pathname:i,search:$3(r),hash:D3(a)}}function PS(e,t){let n=Nd(t).split("/");return e.split("/").forEach(a=>{a===".."?n.length>1&&n.pop():a!=="."&&n.push(a)}),n.length>1?n.join("/"):"/"}function Ky(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function P3(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function w0(e){let t=P3(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function tp(e,t,n,r=!1){let a;typeof e=="string"?a=ls(e):(a={...e},tt(!a.pathname||!a.pathname.includes("?"),Ky("?","pathname","search",a)),tt(!a.pathname||!a.pathname.includes("#"),Ky("#","pathname","hash",a)),tt(!a.search||!a.search.includes("#"),Ky("#","search","hash",a)));let i=e===""||a.pathname==="",o=i?"/":a.pathname,l;if(o==null)l=n;else{let c=t.length-1;if(!r&&o.startsWith("..")){let d=o.split("/");for(;d[0]==="..";)d.shift(),c-=1;a.pathname=d.join("/")}l=c>=0?t[c]:"/"}let s=M3(a,l),u=o&&o!=="/"&&o.endsWith("/"),f=(i||o===".")&&n.endsWith("/");return!s.pathname.endsWith("/")&&(u||f)&&(s.pathname+="/"),s}var xT=e=>e.replace(/\/\/+/g,"/"),ur=e=>xT(e.join("/")),Nd=e=>e.replace(/\/+$/,""),R3=e=>Nd(e).replace(/^\/*/,"/"),$3=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,D3=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,k3=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function L3(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function z3(e){let t=e.map(n=>n.route.path).filter(Boolean);return ur(t)||"/"}var ST=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function wT(e,t){let n=e;if(typeof n!="string"||!N3.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,a=!1;if(ST)try{let i=new URL(window.location.href),o=n.startsWith("//")?new URL(i.protocol+n):new URL(n),l=sa(o.pathname,t);o.origin===i.origin&&l!=null?n=l+o.search+o.hash:a=!0}catch{cr(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:a,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var OT=["POST","PUT","PATCH","DELETE"];new Set(OT);var B3=["GET",...OT];new Set(B3);var ss=j.createContext(null);ss.displayName="DataRouter";var np=j.createContext(null);np.displayName="DataRouterState";var AT=j.createContext(!1);function U3(){return j.useContext(AT)}var jT=j.createContext({isTransitioning:!1});jT.displayName="ViewTransition";var I3=j.createContext(new Map);I3.displayName="Fetchers";var H3=j.createContext(null);H3.displayName="Await";var Pn=j.createContext(null);Pn.displayName="Navigation";var kc=j.createContext(null);kc.displayName="Location";var dr=j.createContext({outlet:null,matches:[],isDataRoute:!1});dr.displayName="Route";var O0=j.createContext(null);O0.displayName="RouteError";var ET="REACT_ROUTER_ERROR",q3="REDIRECT",F3="ROUTE_ERROR_RESPONSE";function G3(e){if(e.startsWith(`${ET}:${q3}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function K3(e){if(e.startsWith(`${ET}:${F3}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new k3(t.status,t.statusText,t.data)}catch{}}function Y3(e,{relative:t}={}){tt(us(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=j.useContext(Pn),{hash:a,pathname:i,search:o}=Lc(e,{relative:t}),l=i;return n!=="/"&&(l=i==="/"?n:ur([n,i])),r.createHref({pathname:l,search:o,hash:a})}function us(){return j.useContext(kc)!=null}function Tr(){return tt(us(),"useLocation() may be used only in the context of a component."),j.useContext(kc).location}var _T="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function TT(e){j.useContext(Pn).static||j.useLayoutEffect(e)}function rp(){let{isDataRoute:e}=j.useContext(dr);return e?s4():X3()}function X3(){tt(us(),"useNavigate() may be used only in the context of a component.");let e=j.useContext(ss),{basename:t,navigator:n}=j.useContext(Pn),{matches:r}=j.useContext(dr),{pathname:a}=Tr(),i=JSON.stringify(w0(r)),o=j.useRef(!1);return TT(()=>{o.current=!0}),j.useCallback((s,u={})=>{if(cr(o.current,_T),!o.current)return;if(typeof s=="number"){n.go(s);return}let f=tp(s,JSON.parse(i),a,u.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:ur([t,f.pathname])),(u.replace?n.replace:n.push)(f,u.state,u)},[t,n,i,a,e])}var V3=j.createContext(null);function Q3(e){let t=j.useContext(dr).outlet;return j.useMemo(()=>t&&j.createElement(V3.Provider,{value:e},t),[t,e])}function Lc(e,{relative:t}={}){let{matches:n}=j.useContext(dr),{pathname:r}=Tr(),a=JSON.stringify(w0(n));return j.useMemo(()=>tp(e,JSON.parse(a),r,t==="path"),[e,a,r,t])}function W3(e,t){return CT(e,t)}function CT(e,t,n){var g;tt(us(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=j.useContext(Pn),{matches:a}=j.useContext(dr),i=a[a.length-1],o=i?i.params:{},l=i?i.pathname:"/",s=i?i.pathnameBase:"/",u=i&&i.route;{let y=u&&u.path||"";MT(l,!u||y.endsWith("*")||y.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${l}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let f=Tr(),c;if(t){let y=typeof t=="string"?ls(t):t;tt(s==="/"||((g=y.pathname)==null?void 0:g.startsWith(s)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${s}" but pathname "${y.pathname}" was given in the \`location\` prop.`),c=y}else c=f;let d=c.pathname||"/",p=d;if(s!=="/"){let y=s.replace(/^\//,"").split("/");p="/"+d.replace(/^\//,"").split("/").slice(y.length).join("/")}let b=n&&n.state.matches.length?n.state.matches.map(y=>Object.assign(y,{route:n.manifest[y.route.id]||y.route})):vT(e,{pathname:p});cr(u||b!=null,`No routes matched location "${c.pathname}${c.search}${c.hash}" `),cr(b==null||b[b.length-1].route.element!==void 0||b[b.length-1].route.Component!==void 0||b[b.length-1].route.lazy!==void 0,`Matched leaf route at location "${c.pathname}${c.search}${c.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let m=n4(b&&b.map(y=>Object.assign({},y,{params:Object.assign({},o,y.params),pathname:ur([s,r.encodeLocation?r.encodeLocation(y.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:y.pathname]),pathnameBase:y.pathnameBase==="/"?s:ur([s,r.encodeLocation?r.encodeLocation(y.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:y.pathnameBase])})),a,n);return t&&m?j.createElement(kc.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...c},navigationType:"POP"}},m):m}function Z3(){let e=l4(),t=L3(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",a={padding:"0.5rem",backgroundColor:r},i={padding:"2px 4px",backgroundColor:r},o=null;return console.error("Error handled by React Router default ErrorBoundary:",e),o=j.createElement(j.Fragment,null,j.createElement("p",null,"💿 Hey developer 👋"),j.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",j.createElement("code",{style:i},"ErrorBoundary")," or"," ",j.createElement("code",{style:i},"errorElement")," prop on your route.")),j.createElement(j.Fragment,null,j.createElement("h2",null,"Unexpected Application Error!"),j.createElement("h3",{style:{fontStyle:"italic"}},t),n?j.createElement("pre",{style:a},n):null,o)}var J3=j.createElement(Z3,null),NT=class extends j.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const n=K3(e.digest);n&&(e=n)}let t=e!==void 0?j.createElement(dr.Provider,{value:this.props.routeContext},j.createElement(O0.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?j.createElement(e4,{error:e},t):t}};NT.contextType=AT;var Yy=new WeakMap;function e4({children:e,error:t}){let{basename:n}=j.useContext(Pn);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let r=G3(t.digest);if(r){let a=Yy.get(t);if(a)throw a;let i=wT(r.location,n);if(ST&&!Yy.get(t))if(i.isExternal||r.reloadDocument)window.location.href=i.absoluteURL||i.to;else{const o=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:r.replace}));throw Yy.set(t,o),o}return j.createElement("meta",{httpEquiv:"refresh",content:`0;url=${i.absoluteURL||i.to}`})}}return e}function t4({routeContext:e,match:t,children:n}){let r=j.useContext(ss);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),j.createElement(dr.Provider,{value:e},n)}function n4(e,t=[],n){let r=n==null?void 0:n.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let a=e,i=r==null?void 0:r.errors;if(i!=null){let f=a.findIndex(c=>c.route.id&&(i==null?void 0:i[c.route.id])!==void 0);tt(f>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(i).join(",")}`),a=a.slice(0,Math.min(a.length,f+1))}let o=!1,l=-1;if(n&&r){o=r.renderFallback;for(let f=0;f=0?a=a.slice(0,l+1):a=[a[0]];break}}}}let s=n==null?void 0:n.onError,u=r&&s?(f,c)=>{var d,p;s(f,{location:r.location,params:((p=(d=r.matches)==null?void 0:d[0])==null?void 0:p.params)??{},pattern:z3(r.matches),errorInfo:c})}:void 0;return a.reduceRight((f,c,d)=>{let p,b=!1,m=null,g=null;r&&(p=i&&c.route.id?i[c.route.id]:void 0,m=c.route.errorElement||J3,o&&(l<0&&d===0?(MT("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),b=!0,g=null):l===d&&(b=!0,g=c.route.hydrateFallbackElement||null)));let y=t.concat(a.slice(0,d+1)),v=()=>{let x;return p?x=m:b?x=g:c.route.Component?x=j.createElement(c.route.Component,null):c.route.element?x=c.route.element:x=f,j.createElement(t4,{match:c,routeContext:{outlet:f,matches:y,isDataRoute:r!=null},children:x})};return r&&(c.route.ErrorBoundary||c.route.errorElement||d===0)?j.createElement(NT,{location:r.location,revalidation:r.revalidation,component:m,error:p,children:v(),routeContext:{outlet:null,matches:y,isDataRoute:!0},onError:u}):v()},null)}function A0(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function r4(e){let t=j.useContext(ss);return tt(t,A0(e)),t}function a4(e){let t=j.useContext(np);return tt(t,A0(e)),t}function i4(e){let t=j.useContext(dr);return tt(t,A0(e)),t}function j0(e){let t=i4(e),n=t.matches[t.matches.length-1];return tt(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function o4(){return j0("useRouteId")}function l4(){var r;let e=j.useContext(O0),t=a4("useRouteError"),n=j0("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function s4(){let{router:e}=r4("useNavigate"),t=j0("useNavigate"),n=j.useRef(!1);return TT(()=>{n.current=!0}),j.useCallback(async(a,i={})=>{cr(n.current,_T),n.current&&(typeof a=="number"?await e.navigate(a):await e.navigate(a,{fromRouteId:t,...i}))},[e,t])}var RS={};function MT(e,t,n){!t&&!RS[e]&&(RS[e]=!0,cr(!1,n))}j.memo(u4);function u4({routes:e,manifest:t,future:n,state:r,isStatic:a,onError:i}){return CT(e,void 0,{manifest:t,state:r,isStatic:a,onError:i})}function PT({to:e,replace:t,state:n,relative:r}){tt(us()," may be used only in the context of a component.");let{static:a}=j.useContext(Pn);cr(!a," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:i}=j.useContext(dr),{pathname:o}=Tr(),l=rp(),s=tp(e,w0(i),o,r==="path"),u=JSON.stringify(s);return j.useEffect(()=>{l(JSON.parse(u),{replace:t,state:n,relative:r})},[l,u,r,t,n]),null}function c4(e){return Q3(e.context)}function rt(e){tt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function f4({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:a,static:i=!1,useTransitions:o}){tt(!us(),"You cannot render a inside another . You should never have more than one in your app.");let l=e.replace(/^\/*/,"/"),s=j.useMemo(()=>({basename:l,navigator:a,static:i,useTransitions:o,future:{}}),[l,a,i,o]);typeof n=="string"&&(n=ls(n));let{pathname:u="/",search:f="",hash:c="",state:d=null,key:p="default",mask:b}=n,m=j.useMemo(()=>{let g=sa(u,l);return g==null?null:{location:{pathname:g,search:f,hash:c,state:d,key:p,mask:b},navigationType:r}},[l,u,f,c,d,p,r,b]);return cr(m!=null,` is not able to match the URL "${u}${f}${c}" because it does not start with the basename, so the won't render anything.`),m==null?null:j.createElement(Pn.Provider,{value:s},j.createElement(kc.Provider,{children:t,value:m}))}function d4({children:e,location:t}){return W3(Ev(e),t)}function Ev(e,t=[]){let n=[];return j.Children.forEach(e,(r,a)=>{if(!j.isValidElement(r))return;let i=[...t,a];if(r.type===j.Fragment){n.push.apply(n,Ev(r.props.children,i));return}tt(r.type===rt,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),tt(!r.props.index||!r.props.children,"An index route cannot have child routes.");let o={id:r.props.id||i.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(o.children=Ev(r.props.children,i)),n.push(o)}),n}var Yf="get",Xf="application/x-www-form-urlencoded";function ap(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function h4(e){return ap(e)&&e.tagName.toLowerCase()==="button"}function p4(e){return ap(e)&&e.tagName.toLowerCase()==="form"}function y4(e){return ap(e)&&e.tagName.toLowerCase()==="input"}function m4(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function v4(e,t){return e.button===0&&(!t||t==="_self")&&!m4(e)}var yf=null;function g4(){if(yf===null)try{new FormData(document.createElement("form"),0),yf=!1}catch{yf=!0}return yf}var b4=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Xy(e){return e!=null&&!b4.has(e)?(cr(!1,`"${e}" is not a valid \`encType\` for \`

\`/\`\` and will default to "${Xf}"`),null):e}function x4(e,t){let n,r,a,i,o;if(p4(e)){let l=e.getAttribute("action");r=l?sa(l,t):null,n=e.getAttribute("method")||Yf,a=Xy(e.getAttribute("enctype"))||Xf,i=new FormData(e)}else if(h4(e)||y4(e)&&(e.type==="submit"||e.type==="image")){let l=e.form;if(l==null)throw new Error('Cannot submit a + + + {/* 필터 */} +
+
+ + +
+
+ + setUsername(e.target.value)} placeholder="username" + className="bg-panel border border-edge rounded px-2 py-1.5 text-sm text-gray-200" /> +
+
+ + setTenantCode(e.target.value)} placeholder="EMART" + className="bg-panel border border-edge rounded px-2 py-1.5 text-sm text-gray-200" /> +
+
+ + setFrom(e.target.value)} + className="bg-panel border border-edge rounded px-2 py-1.5 text-sm text-gray-200" /> +
+
+ + setTo(e.target.value)} + className="bg-panel border border-edge rounded px-2 py-1.5 text-sm text-gray-200" /> +
+
+ + +
+
+ + {isLoading ? ( +
로딩 중...
+ ) : ( +
+ + + + {['일시','수행자','테넌트','액션','리소스','대상','상세','IP','결과'].map(h => ( + + ))} + + + + {(logs as any[]).map((l: any) => ( + + + + + + + + + + + + ))} + {(logs as any[]).length === 0 && ( + + )} + +
{h}
+ {l.createdAt ? new Date(l.createdAt).toLocaleString('ko-KR') : '-'} + {l.username || '-'}{l.tenantCode || '-'} + + {l.action} + + {l.resource || '-'}{l.resourceId || '-'}{l.detail || '-'}{l.ipAddress || '-'} + + {l.success === false ? '실패' : '성공'} + +
감사 로그가 없습니다
+
+ )} + + ) +} diff --git a/frontend/src/pages/FirmwareList.tsx b/frontend/src/pages/FirmwareList.tsx index 780afc1..d5df7e3 100644 --- a/frontend/src/pages/FirmwareList.tsx +++ b/frontend/src/pages/FirmwareList.tsx @@ -1,14 +1,22 @@ +import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Trash2, Star } from 'lucide-react' -import { getFirmware, deleteFirmware } from '../api/client' +import { Plus, Trash2, Star, X } from 'lucide-react' +import { getFirmware, createFirmware, deleteFirmware } from '../api/client' export default function FirmwareList() { const qc = useQueryClient() + const [modal, setModal] = useState(null) + const { data: fws = [], isLoading } = useQuery({ queryKey: ['firmware'], queryFn: () => getFirmware(), }) + const saveMut = useMutation({ + mutationFn: (d: any) => createFirmware(d), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['firmware'] }); setModal(null) }, + }) + const deleteMut = useMutation({ mutationFn: deleteFirmware, onSuccess: () => qc.invalidateQueries({ queryKey: ['firmware'] }), @@ -18,7 +26,13 @@ export default function FirmwareList() { return (
-

펌웨어 관리

+
+

펌웨어 관리

+ +
@@ -59,6 +73,91 @@ export default function FirmwareList() {
+ + {modal && ( +
+
+
+

펌웨어 등록

+ +
+ saveMut.mutate(d)} /> +
+
+ )}
) } + +function FirmwareForm({ onSave }: { onSave: (d: any) => void }) { + const [f, setF] = useState({ + tenantCode: 'EMART', + firmwareVersion: '', + deviceType: 'ESL_DEVICE', + fileName: '', + filePath: '', + fileSize: '', + checksum: '', + description: '', + latest: true, + }) + const set = (k: string) => (e: React.ChangeEvent) => + setF(p => ({ ...p, [k]: e.target.value })) + + const submit = (e: React.FormEvent) => { + e.preventDefault() + onSave({ + ...f, + fileSize: f.fileSize === '' ? null : Number(f.fileSize), + latest: f.latest === true || (f as any).latest === 'true', + }) + } + + return ( + +
+ + +
+
+ + +
+ {[ + { label: '펌웨어 버전', key: 'firmwareVersion', ph: '예: v1.2.3', required: true }, + { label: '파일명', key: 'fileName', ph: 'firmware_v1.2.3.bin' }, + { label: '파일 경로', key: 'filePath', ph: '/opt/esn/firmware/...' }, + { label: '파일 크기(byte)', key: 'fileSize', type: 'number' }, + { label: '체크섬(SHA256)', key: 'checksum' }, + ].map(({ label, key, ph, type, required }) => ( +
+ + +
+ ))} +
+ +