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 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 조회는 ADMIN/MANAGER, 변경은 ADMIN (SecurityConfig requestMatchers 로 통제).
+ * 변경 시 감사 로그에 이전→이후 요약을 기록한다(값은 민감하지 않은 운영 설정만 다룬다).
+ */
+@Service
+@RequiredArgsConstructor
+public class SettingService {
+
+ private final SettingMapper mapper;
+ private final AuditService auditService;
+
+ public List 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> 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
> settings(@RequestParam(required = false) String category) {
+ return ApiResponse.ok(settingService.list(category));
+ }
+
+ @PutMapping("/settings/{key}")
+ public ApiResponse
> list(
@@ -27,17 +29,27 @@ public class FirmwareController {
@PostMapping
public ApiResponse
> list(
@@ -30,7 +32,13 @@ public class PosCvtController {
@PutMapping("/{id}/process")
public ApiResponse
> list() {
@@ -25,17 +27,24 @@ public class TenantController {
@PostMapping
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