Initial guardia-esn import for deployment

This commit is contained in:
ops-agent 2026-06-17 07:00:42 +09:00
commit db76174931
125 changed files with 9608 additions and 0 deletions

View File

@ -0,0 +1,54 @@
---
name: esn-backend-dev
description: GUARDiA ESN 백엔드 구현 에이전트. Spring Boot 3.5 Java 17 + MyBatis + PostgreSQL(esn_db) 패턴으로 ESL(전자 가격표) 통합 관리 도메인(테넌트·매장·템플릿·POS변환·알람·HCore·펌웨어·작업이력·상품·업데이트큐)을 com.zioinfo.esn 패키지에 구현한다. Ollama 온프레미스 AI(알람 원인 분석·POS 데이터 자동 분류)와 ITSM 연계 담당.
metadata:
type: agent
tools: [Read, Write, Edit, Bash, Glob, Grep]
---
# ESN 백엔드 개발 에이전트
## 역할
`C:\GUARDiA\workspace\guardia-esn\backend\` 에서 Spring Boot 3.5 + Java 17 + MyBatis + PostgreSQL ESN 플랫폼 백엔드를 개발·유지한다.
## 기술 스택
- Spring Boot 3.5 / Java 17
- MyBatis 3.0.3 + PostgreSQL(esn_db/esn_user)
- JWT(jjwt 0.12.6) + Spring Security
- Ollama 온프레미스(폴백 필수) + ITSM 연계
## 불변 규칙
1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 절대 금지
2. Hikari `maximum-pool-size: 3` — 공유 PG 서버
3. Ollama 호출 실패 시 fallback 응답 반환 (예외 던지지 않음)
4. API 응답에서 `passwordHash`, 자격증명 필드 완전 제외
5. AES-256-GCM 자격증명 암호화 저장
6. admin 시드: bcrypt `$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm` (admin/admin123)
## 도메인 구조
```
com.zioinfo.esn/
├── EsnApplication.java (@MapperScan(annotationClass=Mapper.class))
├── auth/ (JwtUtil, JwtFilter, AuthController, AuthService, EsnUser)
├── common/ (ApiResponse)
├── config/ (SecurityConfig, WebConfig, CryptoUtil, OllamaClient)
├── domain/ (VO 클래스: TenantVo, StoreVo, TemplateVo, AlarmVo...)
├── mapper/ (@Mapper 인터페이스: TenantMapper, StoreMapper...)
├── service/ (TenantService, StoreService, AlarmService...)
└── controller/ (TenantController, StoreController, AlarmController...)
```
## 멀티테넌트 핵심
모든 데이터는 `tenant_code VARCHAR(20)` 컬럼으로 격리:
- LGINNOTEK — LG 이노텍
- LGIT — LG IT
- EMART — 이마트
- ZIOINFO — 지오정보기술 (기본)
## 엔드포인트 패턴
- `GET /api/{도메인}s` — 목록 (tenantCode 파라미터)
- `POST /api/{도메인}s` — 생성
- `PUT /api/{도메인}s/{id}` — 수정
- `DELETE /api/{도메인}s/{id}` — 삭제
- `GET /api/ai/alarm-analysis` — Ollama AI 알람 분석
- `POST /api/ai/pos-classify` — POS 데이터 자동 분류

View File

@ -0,0 +1,39 @@
---
name: esn-devops-dev
description: GUARDiA ESN 인프라·배포·CI/CD 에이전트. Gitea 저장소(zio/guardia-esn) 관리, deploy_server.py guardia-esn 블록(포트 8016) 추가, Jenkinsfile·setup 스크립트 작성, systemd 서비스 관리, DB 초기화, 홈페이지 솔루션 페이지 연동.
metadata:
type: agent
tools: [Read, Write, Edit, Bash, Glob, Grep]
---
# ESN DevOps 에이전트
## 역할
`C:\GUARDiA\workspace\guardia-esn\` 프로젝트의 배포·인프라·CI/CD 전체를 담당한다.
## 서버 정보
- 서버: `101.79.17.164` root / `1q2w3e!Q`
- Gitea: `http://127.0.0.1:9003` (내부), `https://git.zioinfo.co.kr` (외부)
- Gitea 계정: `zio` / `1q2w3e!Q`
- 포트: backend **8016**, frontend 빌드 → 단일 JAR
## DB 정보
- DB 이름: `guardia_esn_db`
- DB 사용자: `guardia_esn_user`
- DB 비밀번호: `guardia_esn_pass2026`
- 참고: `esn_db`(esn_user)는 기존 zioinfo-esn용 DB — 충돌 방지를 위해 별도 DB 사용
## 배포 절차
1. repos에 `fresh git init` (모노레포 .git 상속 금지)
2. 프론트 빌드: `npm run build` → static/ 복사
3. 백엔드 빌드: `mvn clean package -DskipTests`
4. JAR → `/opt/guardia-esn/app.jar`
5. systemd `guardia-esn.service``spring.sql.init.mode=never`
6. Gitea 저장소 생성 → push
## 함정 목록
- repos에 fresh `git init` 필수 (모노레포 bundle 1.5GB 방지)
- deploy_server.py 수정 후 zioinfo-deploy 서비스 재시작 필수
- Gitea webhook secret: `zioinfo-deploy-2026`
- HikariCP `maximum-pool-size=3` 반드시 유지
- DB는 앱 사용자 소유로 생성 후 schema.sql 적용

View File

@ -0,0 +1,42 @@
---
name: esn-frontend-dev
description: GUARDiA ESN 프론트엔드 구현 에이전트. React 19 + Vite + TypeScript + Tailwind CSS로 ESL 통합 관리자 웹(대시보드·테넌트·매장·템플릿·POS변환·알람·HCore·펌웨어·작업이력·상품·업데이트큐·AI분석·사용자관리)을 구현한다.
metadata:
type: agent
tools: [Read, Write, Edit, Bash, Glob, Grep]
---
# ESN 프론트엔드 개발 에이전트
## 역할
`C:\GUARDiA\workspace\guardia-esn\frontend\` 에서 React 19 + Vite + TypeScript + Tailwind CSS ESN 관리자 웹을 개발·유지한다.
## 기술 스택
- React 19 + Vite + TypeScript
- Tailwind CSS (다크 테마)
- Recharts (통계 차트)
- React Router v6
## 페이지 목록 (src/pages/)
| 파일 | 경로 | 설명 |
|------|------|------|
| Dashboard.tsx | / | 전체 현황 차트 |
| TenantAdmin.tsx | /tenants | 테넌트 관리 |
| StoreList.tsx | /stores | 매장 관리 |
| TemplateList.tsx | /templates | ESL 템플릿 |
| PosCvtList.tsx | /pos-cvt | POS 변환 데이터 |
| ProductList.tsx | /products | 상품/가격 |
| AlarmList.tsx | /alarms | 알람 모니터링 |
| HCoreStatus.tsx | /hcore | HCore 게이트웨이 |
| FirmwareList.tsx | /firmware | 펌웨어 관리 |
| WorkHistory.tsx | /work | 작업 이력 |
| TagBindingList.tsx | /tags | 태그 바인딩 |
| UpdateQueueList.tsx | /queue | 업데이트 큐 |
| UserList.tsx | /users | 사용자 관리 |
| AiAnalysis.tsx | /ai | AI 분석 |
| Login.tsx | /login | 로그인 |
## 빌드 원칙
- Vite build → `backend/src/main/resources/static/` 복사 → `mvn package` (단일 JAR)
- `vite.config.ts` outDir: `../backend/src/main/resources/static`
- API 호출: `src/api/client.ts` (axios 기반, JWT Bearer 헤더)

29
.claude/agents/esn-qa.md Normal file
View File

@ -0,0 +1,29 @@
---
name: esn-qa
description: GUARDiA ESN QA 에이전트. 백엔드 API 응답 shape와 프론트엔드 호출을 대조 검증한다. 테넌트 격리, 알람 이력, HCore 상태, 펌웨어 배포 경계면의 필드 불일치·인증 흐름·민감정보(SSH·자격증명) 노출·RBAC 가드 누락·AI 온프레미스 준수를 검증한다.
metadata:
type: agent
tools: [Read, Write, Edit, Bash, Glob, Grep]
---
# ESN QA 에이전트
## 검증 항목
1. **테넌트 격리** — tenant_code 필터 누락 시 크로스 테넌트 데이터 노출 여부
2. **인증 흐름** — JWT 만료·무효 토큰 시 401 반환 확인
3. **민감정보 제외** — API 응답에 `passwordHash`, SSH 자격증명 필드 미포함
4. **AI 온프레미스** — Ollama 외 외부 AI API 호출 없음
5. **알람 통계** — Dashboard 집계 쿼리 정합성
6. **HCore 상태** — 연결/비연결 상태 전이 정확성
## 테스트 방법
```bash
# 서버에서 ESN API 테스트
curl -X POST http://localhost:8016/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}'
# 테넌트 격리 검증
curl -H 'Authorization: Bearer {token}' \
'http://localhost:8016/api/stores?tenantCode=LGINNOTEK'
```

View File

@ -0,0 +1,118 @@
# guardia-esn-orchestrator
## 역할
레거시 ESL(전자 가격표) 관리 시스템(`workspace/esn/` 6개 프로젝트 — Spring Boot 1.5 + Java 8 + Thymeleaf)을 Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL 단일 플랫폼으로 통합한 `workspace/guardia-esn/` 프로젝트를 운영·확장하는 오케스트레이터 스킬.
## 트리거
"guardia-esn", "ESN 통합", "ESL 플랫폼", "전자가격표", "HCore", "POS 변환", "PosCvt", "ESN 알람", "ESL 템플릿", "업데이트 큐", "다시 실행", "업데이트" 요청 시 이 스킬을 사용하라.
## 프로젝트 정보
- **위치:** `C:\GUARDiA\workspace\guardia-esn\`
- **포트:** backend:8016 (단일 JAR, Vite 번들)
- **DB:** `guardia_esn_db` / `guardia_esn_user` / `guardia_esn_pass2026`
- **패키지:** `com.zioinfo.esn`
- **멀티테넌트:** `tenant_code VARCHAR(20)` — LGINNOTEK / LGIT / EMART / ZIOINFO
- **Gitea:** `http://git.zioinfo.co.kr/zio/guardia-esn`
- **서버:** `101.79.17.164` `/opt/guardia-esn/`
## 레거시 소스 (C:\GUARDiA\workspace\esn\)
| 레거시 프로젝트 | 용도 | 현대화 대상 |
|----------------|------|------------|
| ESN_WEB_ZIOINFO | ZIOINFO ESL 웹 관리 (Thymeleaf) | → React 19 관리자 웹 |
| ESN_WEB_LGInnotek | LG Innotek ESL 웹 (Thymeleaf) | → 멀티테넌트 LGINNOTEK |
| ESN_DAEMON_EMART | 이마트 ESL 데몬 | → EMART 테넌트 |
| ESN_DAEMON_LGIT | LG IT ESL 데몬 | → LGIT 테넌트 |
| esn | ESN 코어 (Maven) | → 도메인 모델 |
| ESL_AUTO_UPDATE | 펌웨어 자동 업데이트 | → firmware 도메인 |
| restapi_lgit | LGIT REST API | → API 통합 |
| UCS_LGU | UCS 시스템 | → HCore 통합 |
## 아키텍처
```
레거시 ESL 8개 프로젝트 (Spring Boot 1.5 + Java 8 + Thymeleaf)
↓ 현대화
guardia-esn (Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL)
backend:8016 ←→ frontend static bundle (단일 JAR)
ITSM:9001 · Ollama:11434
```
## 도메인 개념
| 도메인 | 테이블 | 설명 |
|--------|--------|------|
| Tenant | esn_tenants | 멀티테넌트 — LGINNOTEK·LGIT·EMART·ZIOINFO |
| StoreGroup | esn_store_groups | 매장 그룹 |
| Store | esn_stores | 매장 (tenantCode 격리) |
| Template | esn_templates | ESL 표시 템플릿 |
| PosCvt | esn_pos_cvt | POS→ESL 가격 변환 데이터 |
| Alarm / AlarmHistory | esn_alarms | 장치 알람·이력 |
| HCore | esn_hcores | 게이트웨이/허브 장치 |
| WorkHistory | esn_work_history | 작업 이력 |
| Firmware | esn_firmware | 장치 펌웨어 |
| User | esn_users | 사용자 (멀티테넌트 RBAC) |
| Product | esn_products | 상품 가격 |
| TagBinding | esn_tag_bindings | ESL 태그↔상품 바인딩 |
| UpdateQueue | esn_update_queue | 업데이트 큐 |
## 에이전트 팀
| 에이전트 | 역할 |
|----------|------|
| `esn-backend-dev` | Spring Boot 3.5 백엔드 — 도메인·매퍼·서비스·컨트롤러 |
| `esn-frontend-dev` | React 19 + Vite + TypeScript — 관리자 웹 |
| `esn-devops-dev` | Gitea·deploy·Jenkinsfile·setup·홈페이지 |
| `esn-qa` | API 통합 테스트 + 크로스 경계면 검증 |
## 구현 원칙 (GUARDiA 불변 규칙)
1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 아님
2. Hikari `maximum-pool-size=3` — 공유 PG 서버
3. repos에 `fresh git init` — 모노레포 .git 상속 금지
4. 단일 JAR: 프론트 빌드 → `backend/src/main/resources/static/` → mvn package
5. Ollama 폴백: 연결 실패 시 기본 응답 반환 (예외 던지지 않음)
6. admin 시드: bcrypt `$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm` (admin/admin123)
7. API 응답에 `password_hash`, 자격증명 필드 절대 미포함
8. AES-256-GCM 자격증명 암호화 저장
9. SecurityConfig에 static 리소스 `permitAll` 필수 (SPA 403 방지)
## 파일 구조
```
workspace/guardia-esn/
├── .claude/
│ ├── agents/
│ │ ├── esn-backend-dev.md
│ │ ├── esn-frontend-dev.md
│ │ ├── esn-devops-dev.md
│ │ └── esn-qa.md
│ └── skills/guardia-esn-orchestrator/SKILL.md
├── CLAUDE.md
├── Jenkinsfile
├── backend/
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/zioinfo/esn/
│ │ ├── EsnApplication.java (@MapperScan(annotationClass=Mapper.class))
│ │ ├── auth/
│ │ ├── common/
│ │ ├── config/ (SecurityConfig, WebConfig, CryptoUtil, OllamaClient)
│ │ ├── domain/ (VO 클래스)
│ │ ├── mapper/ (@Mapper 인터페이스)
│ │ ├── service/
│ │ └── controller/
│ └── resources/
│ ├── application.yml
│ ├── mapper/*.xml
│ ├── db/schema.sql
│ └── static/ (Vite 빌드 산출물)
└── frontend/
├── package.json
├── vite.config.ts
└── src/
├── App.tsx
├── api/client.ts
├── components/
└── pages/ (15개 관리자 화면)
```
## 변경 이력
| 날짜 | 내용 | 사유 |
|------|------|------|
| 2026-06-16 | 초기 하네스 구성 + 통합 소스 생성 | 레거시 ESN 8개 프로젝트 → 단일 현대화 플랫폼 |

121
CLAUDE.md Normal file
View File

@ -0,0 +1,121 @@
# GUARDiA ESN — ESL 통합 관리 플랫폼
> 레거시 ESL(전자 가격표) 8개 Spring Boot 1.5 프로젝트를 단일 현대화 플랫폼으로 통합한 프로젝트
---
## 프로젝트 개요
`C:\GUARDiA\workspace\esn\` 의 레거시 ESN 시스템(ESN_WEB_ZIOINFO·ESN_WEB_LGInnotek·ESN_DAEMON_EMART·ESN_DAEMON_LGIT·esn·ESL_AUTO_UPDATE·restapi_lgit·UCS_LGU)을
**Spring Boot 3.5 + Java 17 + React 19 + PostgreSQL** 단일 플랫폼으로 재구현.
멀티테넌트 (TENANT_A / TENANT_B / EMART / ZIOINFO), Ollama 온프레미스 AI 알람 분석·POS 자동 분류.
---
## 기술 스택
| 레이어 | 기술 |
|--------|------|
| Backend | Spring Boot 3.5 · Java 17 · MyBatis 3.0.3 |
| Database | PostgreSQL (`guardia_esn_db` / `guardia_esn_user`) |
| Frontend | React 19 · Vite · TypeScript · Tailwind CSS |
| AI | Ollama 온프레미스 (llama3, llava) |
| 인증 | Spring Security · JWT (jjwt 0.12.6) |
| 배포 | 단일 JAR (Vite 번들 → static/) |
---
## 실행 명령어
```bash
# 프론트엔드 빌드 (필수 — 백엔드 static/ 번들)
cd frontend && npm install && npm run build
# 백엔드 빌드 + 실행
cd backend && mvn clean package -DskipTests
java -jar target/zioinfo-esn-1.0.0.jar \
--spring.datasource.url=jdbc:postgresql://localhost:5432/guardia_esn_db \
--spring.datasource.username=guardia_esn_user \
--spring.datasource.password=guardia_esn_pass2026 \
--server.port=8016
```
---
## 환경변수
| 변수 | 기본값 | 설명 |
|------|--------|------|
| DB_URL | jdbc:postgresql://localhost:5432/esn_db | 운영에서는 guardia_esn_db |
| DB_USER | esn_user | 운영에서는 guardia_esn_user |
| DB_PASS | esn_pass2026 | 운영에서는 guardia_esn_pass2026 |
| OLLAMA_URL | http://localhost:11434 | Ollama 서버 |
| JWT_SECRET | ZioInfoEsnJwtSecret... | JWT 서명 키 |
---
## 디렉터리 구조
```
workspace/guardia-esn/
├── .claude/
│ ├── agents/ (에이전트 4종)
│ └── skills/guardia-esn-orchestrator/SKILL.md
├── CLAUDE.md ← 이 파일
├── Jenkinsfile
├── backend/
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/zioinfo/esn/ (67개 Java 파일)
│ └── resources/
│ ├── application.yml
│ ├── db/schema.sql (13개 테이블)
│ └── mapper/*.xml (15개 MyBatis XML)
└── frontend/
└── src/
├── App.tsx
├── api/client.ts
├── components/ (Layout, Sidebar, Header...)
└── pages/ (15개 관리자 화면)
```
---
## 도메인 (13개)
| 도메인 | 테이블 | 설명 |
|--------|--------|------|
| Tenant | esn_tenants | 멀티테넌트 (LGINNOTEK·LGIT·EMART·ZIOINFO) |
| StoreGroup | esn_store_groups | 매장 그룹 |
| Store | esn_stores | 매장 (tenantCode 격리) |
| Template | esn_templates | ESL 표시 템플릿 |
| PosCvt | esn_pos_cvt | POS→ESL 가격 변환 |
| Alarm | esn_alarms | 장치 알람 |
| HCore | esn_hcores | 게이트웨이/허브 |
| WorkHistory | esn_work_history | 작업 이력 |
| Firmware | esn_firmware | 펌웨어 버전 |
| User | esn_users | 사용자 (RBAC) |
| Product | esn_products | 상품/가격 |
| TagBinding | esn_tag_bindings | ESL↔상품 바인딩 |
| UpdateQueue | esn_update_queue | 업데이트 큐 |
---
## 보안 제약 (불변)
1. `@MapperScan(annotationClass = Mapper.class)` — basePackages 절대 금지
2. Hikari `maximum-pool-size=3` — 공유 PG 서버 보호
3. 외부 AI API 절대 금지 — Ollama localhost:11434만 허용
4. API 응답에서 `passwordHash`, `ssh_*` 필드 완전 제외
5. SecurityConfig에 static 리소스 `permitAll` 필수 (SPA 403 방지)
---
## 하네스 트리거
`guardia-esn-orchestrator` 스킬을 사용하라:
- ESN 기능 추가·수정
- 배포 작업
- 레거시 소스 분석·통합
- 알람·POS·HCore 관련 작업

65
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,65 @@
pipeline {
agent any
environment {
APP_NAME = 'guardia-esn'
DEPLOY_DIR = '/opt/guardia-esn'
SERVICE = 'guardia-esn'
JAR_NAME = 'zioinfo-esn-1.0.0.jar'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Frontend') {
steps {
dir('frontend') {
sh 'npm ci --legacy-peer-deps'
sh 'npm run build'
}
}
}
stage('Build Backend') {
steps {
dir('backend') {
sh 'mvn clean package -DskipTests -q'
}
}
}
stage('Deploy') {
steps {
sh """
sudo systemctl stop ${SERVICE} || true
sudo mkdir -p ${DEPLOY_DIR}
sudo cp backend/target/${JAR_NAME} ${DEPLOY_DIR}/app.jar
sudo systemctl start ${SERVICE}
"""
}
}
stage('Health Check') {
steps {
sh """
sleep 20
curl -sf http://localhost:8016/actuator/health | grep -q UP || exit 1
"""
}
}
}
post {
success {
echo "guardia-esn 배포 성공"
}
failure {
sh "sudo systemctl stop ${SERVICE} || true"
echo "guardia-esn 배포 실패"
}
}
}

77
backend/pom.xml Normal file
View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
</parent>
<groupId>com.zioinfo</groupId>
<artifactId>zioinfo-esn</artifactId>
<version>1.0.0</version>
<name>zioinfo-esn</name>
<description>ESL(전자 가격표) 통합 관리 플랫폼 — 레거시 Spring Boot 1.5 현대화 (Spring Boot 3.5 + Java 17 + React 19)</description>
<properties>
<java.version>17</java.version>
<jjwt.version>0.12.6</jjwt.version>
<mybatis.version>3.0.3</mybatis.version>
<postgresql.version>42.7.3</postgresql.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<!-- JWT -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jjwt.version}</version></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jjwt.version}</version><scope>runtime</scope></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jjwt.version}</version><scope>runtime</scope></dependency>
<!-- Lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
<!-- Test -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<finalName>esn-1.0.0</finalName>
</build>
</project>

View File

@ -0,0 +1,30 @@
package com.zioinfo.esn;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* zioinfo-esn ESL 통합 관리 플랫폼.
*
* <p>레거시 Spring Boot 1.5 / Java 8 ESL 시스템 6개를 Spring Boot 3.5 / Java 17 단일 플랫폼으로 재구현.
* 멀티테넌트(LGINNOTEK·LGIT·EMART·ZIOINFO), 매장/HCore 장치 관리, POS 가격 연동,
* 알람 모니터링, 펌웨어 관리, Ollama 온프레미스 AI 이상 감지.
*
* <p>보안 불변 규칙: 외부 AI API 절대 금지 (Ollama localhost:11434만 허용),
* 자격증명은 AES-256-GCM 암호화 저장, API 응답에서 password_hash 완전 제외.
*
* <p>CRITICAL: @MapperScan(annotationClass = Mapper.class) 패턴 사용 basePackages 아님.
*/
@SpringBootApplication
@MapperScan(annotationClass = Mapper.class)
@EnableScheduling
@EnableAsync
public class EsnApplication {
public static void main(String[] args) {
SpringApplication.run(EsnApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
package com.zioinfo.esn.auth;
import com.zioinfo.esn.common.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ApiResponse<Map<String, String>> login(@RequestBody LoginRequest req) {
String token = authService.login(req.username(), req.password());
return ApiResponse.ok(Map.of("token", token, "type", "Bearer"));
}
@GetMapping("/me")
public ApiResponse<Map<String, String>> me(@RequestHeader("Authorization") String header) {
String token = header.replace("Bearer ", "");
return ApiResponse.ok(authService.me(token));
}
@PostMapping("/logout")
public ApiResponse<Void> logout() {
// JWT stateless 클라이언트에서 토큰 삭제
return ApiResponse.ok("로그아웃 성공", null);
}
record LoginRequest(String username, String password) {}
}

View File

@ -0,0 +1,37 @@
package com.zioinfo.esn.auth;
import com.zioinfo.esn.auth.mapper.UserAuthMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserAuthMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
public String login(String username, String password) {
EsnUser user = userMapper.findByUsername(username);
if (user == null || !user.isActive()) {
throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정");
}
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치");
}
userMapper.updateLastLogin(username);
return jwtUtil.generate(username, user.getRole(), user.getTenantCode());
}
public Map<String, String> me(String token) {
return Map.of(
"username", jwtUtil.getUsername(token),
"role", jwtUtil.getRole(token),
"tenant", jwtUtil.getTenant(token) != null ? jwtUtil.getTenant(token) : ""
);
}
}

View File

@ -0,0 +1,21 @@
package com.zioinfo.esn.auth;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class EsnUser {
private Long id;
private String tenantCode;
private String username;
@JsonIgnore
private String passwordHash; // API 응답 미노출
private String role; // ADMIN, MANAGER, USER
private String email;
private String phone;
private boolean active;
private LocalDateTime lastLoginAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,40 @@
package com.zioinfo.esn.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtUtil.isValid(token)) {
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
var auth = new UsernamePasswordAuthenticationToken(
username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role))
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res);
}
}

View File

@ -0,0 +1,56 @@
package com.zioinfo.esn.auth;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Slf4j
@Component
public class JwtUtil {
@Value("${guardia.jwt.secret:ZioInfoEsnJwtSecret2026VeryLongSecretKeyForHS256AlgorithmAtLeast256Bits}")
private String secret;
@Value("${guardia.jwt.expiration:86400000}")
private long expirationMs;
private SecretKey key() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generate(String username, String role, String tenantCode) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.claim("tenant", tenantCode)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(key())
.compact();
}
public Claims parse(String token) {
return Jwts.parser().verifyWith(key()).build()
.parseSignedClaims(token).getPayload();
}
public boolean isValid(String token) {
try {
parse(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.debug("JWT 검증 실패: {}", e.getMessage());
return false;
}
}
public String getUsername(String token) { return parse(token).getSubject(); }
public String getRole(String token) { return parse(token).get("role", String.class); }
public String getTenant(String token) { return parse(token).get("tenant", String.class); }
}

View File

@ -0,0 +1,11 @@
package com.zioinfo.esn.auth.mapper;
import com.zioinfo.esn.auth.EsnUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserAuthMapper {
EsnUser findByUsername(@Param("username") String username);
int updateLastLogin(@Param("username") String username);
}

View File

@ -0,0 +1,27 @@
package com.zioinfo.esn.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private LocalDateTime timestamp;
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, "OK", data, LocalDateTime.now());
}
public static <T> ApiResponse<T> ok(String message, T data) {
return new ApiResponse<>(true, message, data, LocalDateTime.now());
}
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, message, null, LocalDateTime.now());
}
}

View File

@ -0,0 +1,68 @@
package com.zioinfo.esn.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* AES-256-GCM 자격증명 암호화 유틸리티.
* 보안 불변 규칙: 서버 자격증명은 암호화 DB에만 저장, API 응답에 절대 노출 금지.
*/
@Slf4j
@Component
public class CryptoUtil {
private static final String ALGO = "AES/GCM/NoPadding";
private static final int GCM_IV_LEN = 12;
private static final int GCM_TAG_LEN = 128;
// 운영 환경변수로 주입 권장
private static final String KEY_HEX = "7a696f696e666f65736e6b657932303236736563726574";
private SecretKey secretKey() {
byte[] key = new byte[32];
byte[] src = KEY_HEX.getBytes(StandardCharsets.UTF_8);
System.arraycopy(src, 0, key, 0, Math.min(src.length, 32));
return new SecretKeySpec(key, "AES");
}
public String encrypt(String plain) {
if (plain == null) return null;
try {
byte[] iv = new byte[GCM_IV_LEN];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.ENCRYPT_MODE, secretKey(), new GCMParameterSpec(GCM_TAG_LEN, iv));
byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
byte[] result = new byte[iv.length + enc.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(enc, 0, result, iv.length, enc.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
log.error("암호화 실패", e);
throw new RuntimeException("CRYPTO_ERR", e);
}
}
public String decrypt(String encrypted) {
if (encrypted == null) return null;
try {
byte[] raw = Base64.getDecoder().decode(encrypted);
byte[] iv = new byte[GCM_IV_LEN];
System.arraycopy(raw, 0, iv, 0, GCM_IV_LEN);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.DECRYPT_MODE, secretKey(), new GCMParameterSpec(GCM_TAG_LEN, iv));
byte[] dec = cipher.doFinal(raw, GCM_IV_LEN, raw.length - GCM_IV_LEN);
return new String(dec, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("복호화 실패", e);
throw new RuntimeException("CRYPTO_ERR", e);
}
}
}

View File

@ -0,0 +1,77 @@
package com.zioinfo.esn.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
/**
* Ollama 온프레미스 AI 클라이언트.
* 보안 불변 규칙: 외부 AI API 절대 금지 Ollama localhost:11434만 허용.
* 연결 실패 폴백 응답 반환(예외 미전파).
*/
@Slf4j
@Component
public class OllamaClient {
@Value("${guardia.ollama-url:http://localhost:11434}")
private String baseUrl;
@Value("${guardia.ollama-text-model:llama3}")
private String model;
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private final ObjectMapper mapper = new ObjectMapper();
public String chat(String prompt) {
return chat(prompt, "ESN AI 이상 분석 전문가 역할입니다.");
}
public String chat(String prompt, String systemPrompt) {
try {
var body = Map.of(
"model", model,
"messages", new Object[]{
Map.of("role", "system", "content", systemPrompt),
Map.of("role", "user", "content", prompt)
},
"stream", false
);
String json = mapper.writeValueAsString(body);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/chat"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.timeout(Duration.ofSeconds(30))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
var res = mapper.readTree(resp.body());
return res.path("message").path("content").asText("분석 결과를 생성했습니다.");
}
return fallback(prompt);
} catch (Exception e) {
log.warn("Ollama 연결 실패 — 폴백 응답 반환: {}", e.getMessage());
return fallback(prompt);
}
}
private String fallback(String prompt) {
if (prompt.toLowerCase().contains("alarm") || prompt.contains("알람")) {
return "알람 분석: 장치 연결 상태를 확인하고 네트워크 환경을 점검하세요. 지속 발생 시 현장 엔지니어 파견이 필요합니다.";
}
if (prompt.toLowerCase().contains("pos") || prompt.contains("가격")) {
return "POS 데이터 분류: 정상 가격 변환 데이터입니다. 이상 감지된 항목은 수동 검토가 필요합니다.";
}
return "AI 분석 결과: 현재 시스템 상태를 검토하고 운영 매뉴얼을 참조하세요.";
}
}

View File

@ -0,0 +1,67 @@
package com.zioinfo.esn.config;
import com.zioinfo.esn.auth.JwtFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 6 JWT 무상태 인증 + 멀티테넌트 RBAC.
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> {})
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 인증 불필요
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
// 정적 리소스 (React SPA)
.requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll()
// 관리자 전용
.requestMatchers("/api/tenants/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers(HttpMethod.POST, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
.requestMatchers(HttpMethod.PUT, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
// GET은 인증 사용자 허용
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

View File

@ -0,0 +1,23 @@
package com.zioinfo.esn.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class WebConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,73 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.config.OllamaClient;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class AiController {
private final OllamaClient ollama;
/**
* 알람 AI 이상 분석 Ollama 온프레미스만 허용.
*/
@PostMapping("/analyze-alarm")
public ApiResponse<Map<String, String>> analyzeAlarm(@RequestBody Map<String, Object> body) {
String alarmType = (String) body.getOrDefault("alarmType", "UNKNOWN");
String message = (String) body.getOrDefault("message", "");
String severity = (String) body.getOrDefault("severity", "LOW");
String prompt = String.format(
"ESL 장치 알람 분석:\n유형: %s\n심각도: %s\n메시지: %s\n\n" +
"원인 분석 및 조치 방안을 3줄 이내로 간결하게 제시하세요.",
alarmType, severity, message
);
String result = ollama.chat(prompt,
"당신은 ESL(전자 가격표) 시스템 전문 AI 엔지니어입니다. 알람을 분석하고 실용적인 조치를 제안하세요.");
return ApiResponse.ok(Map.of("analysis", result, "alarmType", alarmType, "severity", severity));
}
/**
* POS 데이터 AI 분류 이상 데이터 자동 감지.
*/
@PostMapping("/classify-pos")
public ApiResponse<Map<String, String>> classifyPos(@RequestBody Map<String, Object> body) {
String productCode = (String) body.getOrDefault("productCode", "");
String price = String.valueOf(body.getOrDefault("price", "0"));
String salePrice = String.valueOf(body.getOrDefault("salePrice", "0"));
String prompt = String.format(
"POS 가격 데이터 검증:\n상품코드: %s\n정가: %s\n판매가: %s\n\n" +
"이 데이터가 정상인지 이상(오류/이상값)인지 판단하고, 분류(NORMAL/ABNORMAL)와 이유를 2줄 이내로 제시하세요.",
productCode, price, salePrice
);
String result = ollama.chat(prompt,
"당신은 POS 데이터 품질 관리 AI입니다. 가격 데이터의 이상을 감지하세요.");
String classification = result.toUpperCase().contains("ABNORMAL") ? "ABNORMAL" : "NORMAL";
return ApiResponse.ok(Map.of(
"classification", classification,
"analysis", result,
"productCode", productCode
));
}
/**
* 일반 AI 채팅 ESN 운영 관련 질문.
*/
@PostMapping("/chat")
public ApiResponse<Map<String, String>> chat(@RequestBody Map<String, String> body) {
String message = body.getOrDefault("message", "");
String result = ollama.chat(message, "ESL 전자 가격표 통합 관리 플랫폼 운영 전문가입니다.");
return ApiResponse.ok(Map.of("response", result));
}
}

View File

@ -0,0 +1,56 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.AlarmVo;
import com.zioinfo.esn.service.AlarmService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/alarms")
@RequiredArgsConstructor
public class AlarmController {
private final AlarmService service;
@GetMapping
public ApiResponse<List<AlarmVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String severity,
@RequestParam(required = false) String status) {
return ApiResponse.ok(service.list(tenantCode, storeId, severity, status));
}
@GetMapping("/{id}")
public ApiResponse<AlarmVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<AlarmVo> create(@RequestBody AlarmVo v) {
return ApiResponse.ok("알람 생성 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<AlarmVo> update(@PathVariable Long id, @RequestBody AlarmVo v) {
return ApiResponse.ok("알람 수정 완료", service.update(id, v));
}
@PutMapping("/{id}/resolve")
public ApiResponse<Void> resolve(@PathVariable Long id,
@RequestBody Map<String, String> body,
Authentication auth) {
String resolvedBy = auth != null ? auth.getName() : "system";
service.resolve(id, resolvedBy, body.getOrDefault("resolution", ""));
return ApiResponse.ok("알람 해결 완료", null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("알람 삭제 완료", null);
}
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.DashboardVo;
import com.zioinfo.esn.mapper.DashboardMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardMapper mapper;
@GetMapping
public ApiResponse<DashboardVo> summary(@RequestParam(required = false) String tenantCode) {
return ApiResponse.ok(mapper.getSummary(tenantCode));
}
}

View File

@ -0,0 +1,43 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.FirmwareVo;
import com.zioinfo.esn.service.FirmwareService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/firmware")
@RequiredArgsConstructor
public class FirmwareController {
private final FirmwareService service;
@GetMapping
public ApiResponse<List<FirmwareVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) String deviceType) {
return ApiResponse.ok(service.list(tenantCode, deviceType));
}
@GetMapping("/{id}")
public ApiResponse<FirmwareVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<FirmwareVo> create(@RequestBody FirmwareVo v) {
return ApiResponse.ok("펌웨어 등록 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<FirmwareVo> update(@PathVariable Long id, @RequestBody FirmwareVo v) {
return ApiResponse.ok("펌웨어 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("펌웨어 삭제 완료", null);
}
}

View File

@ -0,0 +1,52 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.HCoreVo;
import com.zioinfo.esn.service.HCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/hcore")
@RequiredArgsConstructor
public class HCoreController {
private final HCoreService service;
@GetMapping
public ApiResponse<List<HCoreVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String deviceType,
@RequestParam(required = false) String status) {
return ApiResponse.ok(service.list(tenantCode, storeId, deviceType, status));
}
@GetMapping("/{id}")
public ApiResponse<HCoreVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<HCoreVo> create(@RequestBody HCoreVo v) {
return ApiResponse.ok("장치 등록 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<HCoreVo> update(@PathVariable Long id, @RequestBody HCoreVo v) {
return ApiResponse.ok("장치 수정 완료", service.update(id, v));
}
@PutMapping("/{id}/status")
public ApiResponse<Void> updateStatus(@PathVariable Long id, @RequestBody Map<String, String> body) {
service.updateStatus(id, body.getOrDefault("status", "OFFLINE"));
return ApiResponse.ok("상태 업데이트 완료", null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("장치 삭제 완료", null);
}
}

View File

@ -0,0 +1,48 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.PosCvtVo;
import com.zioinfo.esn.service.PosCvtService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/pos-cvt")
@RequiredArgsConstructor
public class PosCvtController {
private final PosCvtService service;
@GetMapping
public ApiResponse<List<PosCvtVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
return ApiResponse.ok(service.list(tenantCode, storeId, status, keyword));
}
@GetMapping("/{id}")
public ApiResponse<PosCvtVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PutMapping("/{id}/process")
public ApiResponse<Integer> process(@PathVariable Long id) {
int queued = service.process(id);
return ApiResponse.ok("처리 완료 (태그 큐 " + queued + "건)", queued);
}
@PutMapping("/{id}/ignore")
public ApiResponse<Void> ignore(@PathVariable Long id) {
service.ignore(id);
return ApiResponse.ok("무시 처리 완료", null);
}
@PutMapping("/{id}/error")
public ApiResponse<Void> error(@PathVariable Long id, @RequestBody Map<String, String> body) {
service.markError(id, body.getOrDefault("message", "처리 오류"));
return ApiResponse.ok("오류 처리 완료", null);
}
}

View File

@ -0,0 +1,44 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.ProductVo;
import com.zioinfo.esn.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@GetMapping
public ApiResponse<List<ProductVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String keyword) {
return ApiResponse.ok(service.list(tenantCode, storeId, keyword));
}
@GetMapping("/{id}")
public ApiResponse<ProductVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<ProductVo> create(@RequestBody ProductVo v) {
return ApiResponse.ok("상품 등록 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<ProductVo> update(@PathVariable Long id, @RequestBody ProductVo v) {
return ApiResponse.ok("상품 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("상품 삭제 완료", null);
}
}

View File

@ -0,0 +1,44 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.StoreVo;
import com.zioinfo.esn.service.StoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/stores")
@RequiredArgsConstructor
public class StoreController {
private final StoreService service;
@GetMapping
public ApiResponse<List<StoreVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeGroupId,
@RequestParam(required = false) String keyword) {
return ApiResponse.ok(service.list(tenantCode, storeGroupId, keyword));
}
@GetMapping("/{id}")
public ApiResponse<StoreVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<StoreVo> create(@RequestBody StoreVo v) {
return ApiResponse.ok("매장 생성 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<StoreVo> update(@PathVariable Long id, @RequestBody StoreVo v) {
return ApiResponse.ok("매장 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("매장 삭제 완료", null);
}
}

View File

@ -0,0 +1,41 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.StoreGroupVo;
import com.zioinfo.esn.service.StoreGroupService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/store-groups")
@RequiredArgsConstructor
public class StoreGroupController {
private final StoreGroupService service;
@GetMapping
public ApiResponse<List<StoreGroupVo>> list(@RequestParam(required = false) String tenantCode) {
return ApiResponse.ok(service.list(tenantCode));
}
@GetMapping("/{id}")
public ApiResponse<StoreGroupVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<StoreGroupVo> create(@RequestBody StoreGroupVo v) {
return ApiResponse.ok("매장그룹 생성 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<StoreGroupVo> update(@PathVariable Long id, @RequestBody StoreGroupVo v) {
return ApiResponse.ok("매장그룹 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("매장그룹 삭제 완료", null);
}
}

View File

@ -0,0 +1,57 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.TagBindingVo;
import com.zioinfo.esn.service.TagBindingService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* e-paper ESL 태그 선반 위치 상품 바인딩 관리
* 핵심 플로우: 태그 스캔 상품 선택 위치 지정 bind() 가격 변경 자동
*/
@RestController
@RequestMapping("/api/tag-binding")
@RequiredArgsConstructor
public class TagBindingController {
private final TagBindingService service;
@GetMapping
public ApiResponse<List<TagBindingVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String bindStatus,
@RequestParam(required = false) String keyword) {
return ApiResponse.ok(service.list(tenantCode, storeId, bindStatus, keyword));
}
@GetMapping("/{id}")
public ApiResponse<TagBindingVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@GetMapping("/device/{deviceId}")
public ApiResponse<TagBindingVo> getByDevice(@PathVariable String deviceId) {
return ApiResponse.ok(service.getByDeviceId(deviceId));
}
/** 태그에 상품+위치 바인딩 */
@PostMapping
public ApiResponse<TagBindingVo> bind(@RequestBody TagBindingVo vo) {
return ApiResponse.ok(service.bind(vo));
}
@PutMapping("/{id}")
public ApiResponse<TagBindingVo> update(@PathVariable Long id, @RequestBody TagBindingVo vo) {
vo.setId(id);
return ApiResponse.ok(service.bind(vo));
}
/** 태그 언바인딩 (선반에서 태그 제거 시) */
@PutMapping("/{id}/unbind")
public ApiResponse<Void> unbind(@PathVariable Long id) {
service.unbind(id);
return ApiResponse.ok("언바인딩 완료", null);
}
}

View File

@ -0,0 +1,43 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.TemplateVo;
import com.zioinfo.esn.service.TemplateService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/templates")
@RequiredArgsConstructor
public class TemplateController {
private final TemplateService service;
@GetMapping
public ApiResponse<List<TemplateVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) String templateType) {
return ApiResponse.ok(service.list(tenantCode, templateType));
}
@GetMapping("/{id}")
public ApiResponse<TemplateVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<TemplateVo> create(@RequestBody TemplateVo v) {
return ApiResponse.ok("템플릿 생성 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<TemplateVo> update(@PathVariable Long id, @RequestBody TemplateVo v) {
return ApiResponse.ok("템플릿 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("템플릿 삭제 완료", null);
}
}

View File

@ -0,0 +1,41 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.TenantVo;
import com.zioinfo.esn.service.TenantService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tenants")
@RequiredArgsConstructor
public class TenantController {
private final TenantService service;
@GetMapping
public ApiResponse<List<TenantVo>> list() {
return ApiResponse.ok(service.list());
}
@GetMapping("/{id}")
public ApiResponse<TenantVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<TenantVo> create(@RequestBody TenantVo v) {
return ApiResponse.ok("테넌트 생성 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<TenantVo> update(@PathVariable Long id, @RequestBody TenantVo v) {
return ApiResponse.ok("테넌트 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("테넌트 삭제 완료", null);
}
}

View File

@ -0,0 +1,75 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.UpdateQueueVo;
import com.zioinfo.esn.service.UpdateQueueService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* e-paper 태그 가격 업데이트 전파
* Gateway가 API를 주기적으로 폴링 태그에 RF 전송 confirm 호출
*
* Gateway 연동 흐름:
* GET /api/update-queue/poll?tenantCode=EMART&limit=50 PENDING 목록 수신
* PUT /api/update-queue/{id}/sent RF 전송 완료
* PUT /api/update-queue/confirm?deviceId=ESL-001 태그 ACK 수신
*/
@RestController
@RequestMapping("/api/update-queue")
@RequiredArgsConstructor
public class UpdateQueueController {
private final UpdateQueueService service;
@GetMapping
public ApiResponse<List<UpdateQueueVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String status) {
return ApiResponse.ok(service.list(tenantCode, storeId, status));
}
@GetMapping("/{id}")
public ApiResponse<UpdateQueueVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
/** Gateway 폴링 엔드포인트: PENDING → SENDING 전환 후 반환 */
@GetMapping("/poll")
public ApiResponse<List<UpdateQueueVo>> poll(
@RequestParam(required = false) String tenantCode,
@RequestParam(defaultValue = "50") int limit) {
return ApiResponse.ok(service.pollPending(tenantCode, limit));
}
/** Gateway가 RF 전송 완료 후 호출 */
@PutMapping("/{id}/sent")
public ApiResponse<Void> sent(@PathVariable Long id) {
service.markSent(id);
return ApiResponse.ok("전송 완료", null);
}
/** 태그로부터 확인 응답 수신 (device_id 기반 일괄 확인) */
@PutMapping("/confirm")
public ApiResponse<Void> confirm(@RequestParam String deviceId) {
service.confirm(deviceId);
return ApiResponse.ok("확인 완료", null);
}
/** 전송 실패 처리 */
@PutMapping("/{id}/failed")
public ApiResponse<Void> failed(@PathVariable Long id,
@RequestBody Map<String, String> body) {
service.markFailed(id, body.getOrDefault("message", "전송 실패"));
return ApiResponse.ok("실패 처리 완료", null);
}
/** 큐 항목 취소 */
@PutMapping("/{id}/cancel")
public ApiResponse<Void> cancel(@PathVariable Long id) {
service.cancel(id);
return ApiResponse.ok("취소 완료", null);
}
}

View File

@ -0,0 +1,58 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.auth.EsnUser;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@GetMapping
public ApiResponse<List<EsnUser>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) String role) {
return ApiResponse.ok(service.list(tenantCode, role));
}
@GetMapping("/{id}")
public ApiResponse<EsnUser> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<EsnUser> create(@RequestBody Map<String, Object> body) {
EsnUser user = new EsnUser();
user.setUsername((String) body.get("username"));
user.setRole((String) body.getOrDefault("role", "USER"));
user.setTenantCode((String) body.get("tenantCode"));
user.setEmail((String) body.get("email"));
user.setPhone((String) body.get("phone"));
user.setActive(true);
String rawPassword = (String) body.getOrDefault("password", "changeme123");
return ApiResponse.ok("사용자 생성 완료", service.create(user, rawPassword));
}
@PutMapping("/{id}")
public ApiResponse<EsnUser> update(@PathVariable Long id, @RequestBody EsnUser v) {
return ApiResponse.ok("사용자 수정 완료", service.update(id, v));
}
@PutMapping("/{id}/password")
public ApiResponse<Void> changePassword(@PathVariable Long id, @RequestBody Map<String, String> body) {
service.changePassword(id, body.getOrDefault("password", "changeme123"));
return ApiResponse.ok("비밀번호 변경 완료", null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("사용자 삭제 완료", null);
}
}

View File

@ -0,0 +1,45 @@
package com.zioinfo.esn.controller;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.domain.WorkHistoryVo;
import com.zioinfo.esn.service.WorkService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/works")
@RequiredArgsConstructor
public class WorkController {
private final WorkService service;
@GetMapping
public ApiResponse<List<WorkHistoryVo>> list(
@RequestParam(required = false) String tenantCode,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String workType) {
return ApiResponse.ok(service.list(tenantCode, storeId, status, workType));
}
@GetMapping("/{id}")
public ApiResponse<WorkHistoryVo> get(@PathVariable Long id) {
return ApiResponse.ok(service.get(id));
}
@PostMapping
public ApiResponse<WorkHistoryVo> create(@RequestBody WorkHistoryVo v) {
return ApiResponse.ok("작업 등록 완료", service.create(v));
}
@PutMapping("/{id}")
public ApiResponse<WorkHistoryVo> update(@PathVariable Long id, @RequestBody WorkHistoryVo v) {
return ApiResponse.ok("작업 수정 완료", service.update(id, v));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
service.delete(id);
return ApiResponse.ok("작업 삭제 완료", null);
}
}

View File

@ -0,0 +1,21 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class AlarmVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String alarmType; // DEVICE, NETWORK, BATTERY, FIRMWARE, SYSTEM
private String alarmCode;
private String severity; // CRITICAL, HIGH, MEDIUM, LOW
private String message;
private String status; // OPEN, ACKNOWLEDGED, RESOLVED
private String resolvedBy;
private String resolution;
private LocalDateTime resolvedAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.domain;
import lombok.Data;
@Data
public class DashboardVo {
private long totalStores;
private long activeStores;
private long totalAlarms;
private long unresolvedAlarms;
private long criticalAlarms;
private long totalDevices;
private long onlineDevices;
private long offlineDevices;
private long totalWorkToday;
private long completedWorkToday;
private long pendingPosCvt;
private long processedPosCvtToday;
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class FirmwareVo {
private Long id;
private String tenantCode;
private String firmwareVersion;
private String deviceType; // GATEWAY, HUB, ESL_DEVICE
private String fileName;
private String filePath;
private Long fileSize;
private String checksum;
private String description;
private boolean latest;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,22 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class HCoreVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String deviceType; // GATEWAY, HUB, ESL_DEVICE
private String deviceId;
private String ipAddress;
private String macAddress;
private String firmwareVersion;
private String status; // ONLINE, OFFLINE, ERROR, UPDATING
private Integer batteryLevel;
private String signalStrength;
private LocalDateTime lastSeenAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,23 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class PosCvtVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String posCode;
private String productCode;
private String productName;
private BigDecimal price;
private BigDecimal salePrice;
private String currency;
private String status; // PENDING, PROCESSED, ERROR, IGNORED
private String errorMessage;
private LocalDateTime processedAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,22 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ProductVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String productCode;
private String productName;
private String category;
private BigDecimal price;
private BigDecimal salePrice;
private String currency;
private boolean active;
private LocalDateTime updatedAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,15 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class StoreGroupVo {
private Long id;
private String tenantCode;
private String groupCode;
private String groupName;
private String description;
private boolean active;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class StoreVo {
private Long id;
private String tenantCode;
private String storeCode;
private String storeName;
private String address;
private String phone;
private Long storeGroupId;
private String storeGroupName;
private String managerName;
private boolean active;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,27 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class TagBindingVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String deviceId; // ESL_DEVICE의 device_id
private String productCode;
private String productName;
private String locationCode; // 선반 위치 (: A01-03)
private String templateCode;
private String displayData; // 현재 화면 렌더링 데이터 (JSON)
private BigDecimal lastPrice;
private LocalDateTime lastUpdatedAt;
private String bindStatus; // ACTIVE, UNBOUND, ERROR
// HCore 장치 정보 (join)
private String deviceStatus; // ONLINE, OFFLINE
private Integer batteryLevel;
private String signalStrength;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TemplateVo {
private Long id;
private String tenantCode;
private String templateCode;
private String templateName;
private String templateType; // PRICE, INFO, PROMO, CUSTOM
private Integer width;
private Integer height;
private String description;
private String layoutJson; // JSON 레이아웃 정의
private boolean active;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,14 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TenantVo {
private Long id;
private String tenantCode;
private String tenantName;
private String description;
private boolean active;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,31 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class UpdateQueueVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private Long bindingId;
private String deviceId;
private String locationCode;
private String productCode;
private String productName;
private BigDecimal newPrice;
private BigDecimal newSalePrice;
private String templateCode;
private String renderData;
private Integer priority; // 1(긴급)~10(일반)
private String status; // PENDING, SENDING, SENT, CONFIRMED, FAILED, CANCELLED
private Integer retryCount;
private Integer maxRetry;
private String errorMessage;
private Long posCvtId;
private LocalDateTime sentAt;
private LocalDateTime confirmedAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,20 @@
package com.zioinfo.esn.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class WorkHistoryVo {
private Long id;
private String tenantCode;
private Long storeId;
private String storeName;
private String workType; // INSTALL, MAINTENANCE, FIRMWARE_UPDATE, REPLACEMENT, INSPECTION
private String workContent;
private String workerName;
private String status; // SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED
private String remarks;
private LocalDateTime startedAt;
private LocalDateTime completedAt;
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,23 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.AlarmVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AlarmMapper {
List<AlarmVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("severity") String severity,
@Param("status") String status);
AlarmVo findById(@Param("id") Long id);
int insert(AlarmVo alarm);
int update(AlarmVo alarm);
int resolve(@Param("id") Long id, @Param("resolvedBy") String resolvedBy,
@Param("resolution") String resolution);
int delete(@Param("id") Long id);
long countUnresolved(@Param("tenantCode") String tenantCode);
long countCritical(@Param("tenantCode") String tenantCode);
long countAll(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,10 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.DashboardVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface DashboardMapper {
DashboardVo getSummary(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.FirmwareVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface FirmwareMapper {
List<FirmwareVo> findAll(@Param("tenantCode") String tenantCode,
@Param("deviceType") String deviceType);
FirmwareVo findById(@Param("id") Long id);
FirmwareVo findLatest(@Param("deviceType") String deviceType,
@Param("tenantCode") String tenantCode);
int insert(FirmwareVo firmware);
int update(FirmwareVo firmware);
int clearLatest(@Param("deviceType") String deviceType, @Param("tenantCode") String tenantCode);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,23 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.HCoreVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface HCoreMapper {
List<HCoreVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("deviceType") String deviceType,
@Param("status") String status);
HCoreVo findById(@Param("id") Long id);
HCoreVo findByDeviceId(@Param("deviceId") String deviceId);
int insert(HCoreVo hcore);
int update(HCoreVo hcore);
int updateStatus(@Param("id") Long id, @Param("status") String status);
int delete(@Param("id") Long id);
long countOnline(@Param("tenantCode") String tenantCode);
long countOffline(@Param("tenantCode") String tenantCode);
long countAll(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,20 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.PosCvtVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PosCvtMapper {
List<PosCvtVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("status") String status,
@Param("keyword") String keyword);
PosCvtVo findById(@Param("id") Long id);
int insert(PosCvtVo posCvt);
int updateStatus(@Param("id") Long id, @Param("status") String status,
@Param("errorMessage") String errorMessage);
long countPending(@Param("tenantCode") String tenantCode);
long countProcessedToday(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,19 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.ProductVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ProductMapper {
List<ProductVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("keyword") String keyword);
ProductVo findById(@Param("id") Long id);
ProductVo findByCode(@Param("productCode") String productCode,
@Param("storeId") Long storeId);
int insert(ProductVo product);
int update(ProductVo product);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,15 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.StoreGroupVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface StoreGroupMapper {
List<StoreGroupVo> findAll(@Param("tenantCode") String tenantCode);
StoreGroupVo findById(@Param("id") Long id);
int insert(StoreGroupVo group);
int update(StoreGroupVo group);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,20 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.StoreVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface StoreMapper {
List<StoreVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeGroupId") Long storeGroupId,
@Param("keyword") String keyword);
StoreVo findById(@Param("id") Long id);
StoreVo findByCode(@Param("storeCode") String storeCode, @Param("tenantCode") String tenantCode);
int insert(StoreVo store);
int update(StoreVo store);
int delete(@Param("id") Long id);
long countActive(@Param("tenantCode") String tenantCode);
long countAll(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,25 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.TagBindingVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface TagBindingMapper {
List<TagBindingVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("bindStatus") String bindStatus,
@Param("keyword") String keyword);
TagBindingVo findById(@Param("id") Long id);
TagBindingVo findByDeviceId(@Param("deviceId") String deviceId);
List<TagBindingVo> findByProductCode(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("productCode") String productCode);
int insert(TagBindingVo vo);
int update(TagBindingVo vo);
int unbind(@Param("id") Long id);
int delete(@Param("id") Long id);
long countAll(@Param("tenantCode") String tenantCode);
long countActive(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,16 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.TemplateVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface TemplateMapper {
List<TemplateVo> findAll(@Param("tenantCode") String tenantCode,
@Param("templateType") String templateType);
TemplateVo findById(@Param("id") Long id);
int insert(TemplateVo template);
int update(TemplateVo template);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,16 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.TenantVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface TenantMapper {
List<TenantVo> findAll();
TenantVo findById(@Param("id") Long id);
TenantVo findByCode(@Param("tenantCode") String tenantCode);
int insert(TenantVo tenant);
int update(TenantVo tenant);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,24 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.UpdateQueueVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UpdateQueueMapper {
List<UpdateQueueVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("status") String status);
List<UpdateQueueVo> findPending(@Param("tenantCode") String tenantCode,
@Param("limit") int limit);
UpdateQueueVo findById(@Param("id") Long id);
int insert(UpdateQueueVo vo);
int updateStatus(@Param("id") Long id, @Param("status") String status,
@Param("errorMessage") String errorMessage);
int confirmByDeviceId(@Param("deviceId") String deviceId);
int incrementRetry(@Param("id") Long id);
long countPending(@Param("tenantCode") String tenantCode);
long countFailed(@Param("tenantCode") String tenantCode);
long countByStatus(@Param("tenantCode") String tenantCode, @Param("status") String status);
}

View File

@ -0,0 +1,18 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.auth.EsnUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
List<EsnUser> findAll(@Param("tenantCode") String tenantCode,
@Param("role") String role);
EsnUser findById(@Param("id") Long id);
EsnUser findByUsername(@Param("username") String username);
int insert(EsnUser user);
int update(EsnUser user);
int updatePassword(@Param("id") Long id, @Param("passwordHash") String passwordHash);
int delete(@Param("id") Long id);
}

View File

@ -0,0 +1,20 @@
package com.zioinfo.esn.mapper;
import com.zioinfo.esn.domain.WorkHistoryVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface WorkMapper {
List<WorkHistoryVo> findAll(@Param("tenantCode") String tenantCode,
@Param("storeId") Long storeId,
@Param("status") String status,
@Param("workType") String workType);
WorkHistoryVo findById(@Param("id") Long id);
int insert(WorkHistoryVo work);
int update(WorkHistoryVo work);
int delete(@Param("id") Long id);
long countToday(@Param("tenantCode") String tenantCode);
long countCompletedToday(@Param("tenantCode") String tenantCode);
}

View File

@ -0,0 +1,36 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.AlarmVo;
import com.zioinfo.esn.mapper.AlarmMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AlarmService {
private final AlarmMapper mapper;
public List<AlarmVo> list(String tenantCode, Long storeId, String severity, String status) {
return mapper.findAll(tenantCode, storeId, severity, status);
}
public AlarmVo get(Long id) { return mapper.findById(id); }
public AlarmVo create(AlarmVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public AlarmVo update(Long id, AlarmVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void resolve(Long id, String resolvedBy, String resolution) {
mapper.resolve(id, resolvedBy, resolution);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,38 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.FirmwareVo;
import com.zioinfo.esn.mapper.FirmwareMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class FirmwareService {
private final FirmwareMapper mapper;
public List<FirmwareVo> list(String tenantCode, String deviceType) {
return mapper.findAll(tenantCode, deviceType);
}
public FirmwareVo get(Long id) { return mapper.findById(id); }
public FirmwareVo create(FirmwareVo v) {
if (v.isLatest()) {
mapper.clearLatest(v.getDeviceType(), v.getTenantCode());
}
mapper.insert(v);
return mapper.findById(v.getId());
}
public FirmwareVo update(Long id, FirmwareVo v) {
v.setId(id);
if (v.isLatest()) {
mapper.clearLatest(v.getDeviceType(), v.getTenantCode());
}
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,36 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.HCoreVo;
import com.zioinfo.esn.mapper.HCoreMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class HCoreService {
private final HCoreMapper mapper;
public List<HCoreVo> list(String tenantCode, Long storeId, String deviceType, String status) {
return mapper.findAll(tenantCode, storeId, deviceType, status);
}
public HCoreVo get(Long id) { return mapper.findById(id); }
public HCoreVo create(HCoreVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public HCoreVo update(Long id, HCoreVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void updateStatus(Long id, String status) {
mapper.updateStatus(id, status);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,51 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.PosCvtVo;
import com.zioinfo.esn.mapper.PosCvtMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class PosCvtService {
private final PosCvtMapper mapper;
private final TagBindingService tagBindingService;
public List<PosCvtVo> list(String tenantCode, Long storeId, String status, String keyword) {
return mapper.findAll(tenantCode, storeId, status, keyword);
}
public PosCvtVo get(Long id) { return mapper.findById(id); }
/**
* POS 가격변경 처리 바인딩된 e-paper 태그에 업데이트 자동 생성
*/
@Transactional
public int process(Long id) {
PosCvtVo pos = mapper.findById(id);
if (pos == null) return 0;
mapper.updateStatus(id, "PROCESSED", null);
// 상품에 바인딩된 e-paper 태그들에 가격 업데이트 생성
int queued = tagBindingService.enqueueByProductCode(
pos.getTenantCode(), pos.getStoreId(),
pos.getProductCode(), pos.getPrice(), pos.getSalePrice(), id);
log.info("POS 처리 완료: posCvtId={}, product={}, 큐생성={}개",
id, pos.getProductCode(), queued);
return queued;
}
public void markError(Long id, String errorMessage) {
mapper.updateStatus(id, "ERROR", errorMessage);
}
public void ignore(Long id) {
mapper.updateStatus(id, "IGNORED", null);
}
}

View File

@ -0,0 +1,32 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.ProductVo;
import com.zioinfo.esn.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper mapper;
public List<ProductVo> list(String tenantCode, Long storeId, String keyword) {
return mapper.findAll(tenantCode, storeId, keyword);
}
public ProductVo get(Long id) { return mapper.findById(id); }
public ProductVo create(ProductVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public ProductVo update(Long id, ProductVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,29 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.StoreGroupVo;
import com.zioinfo.esn.mapper.StoreGroupMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class StoreGroupService {
private final StoreGroupMapper mapper;
public List<StoreGroupVo> list(String tenantCode) { return mapper.findAll(tenantCode); }
public StoreGroupVo get(Long id) { return mapper.findById(id); }
public StoreGroupVo create(StoreGroupVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public StoreGroupVo update(Long id, StoreGroupVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,32 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.StoreVo;
import com.zioinfo.esn.mapper.StoreMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class StoreService {
private final StoreMapper mapper;
public List<StoreVo> list(String tenantCode, Long storeGroupId, String keyword) {
return mapper.findAll(tenantCode, storeGroupId, keyword);
}
public StoreVo get(Long id) { return mapper.findById(id); }
public StoreVo create(StoreVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public StoreVo update(Long id, StoreVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,70 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.TagBindingVo;
import com.zioinfo.esn.domain.UpdateQueueVo;
import com.zioinfo.esn.mapper.TagBindingMapper;
import com.zioinfo.esn.mapper.UpdateQueueMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TagBindingService {
private final TagBindingMapper bindingMapper;
private final UpdateQueueMapper queueMapper;
public List<TagBindingVo> list(String tenantCode, Long storeId,
String bindStatus, String keyword) {
return bindingMapper.findAll(tenantCode, storeId, bindStatus, keyword);
}
public TagBindingVo get(Long id) { return bindingMapper.findById(id); }
public TagBindingVo getByDeviceId(String deviceId) {
return bindingMapper.findByDeviceId(deviceId);
}
@Transactional
public TagBindingVo bind(TagBindingVo vo) {
// 기존 바인딩이 있으면 해제 재바인딩
TagBindingVo existing = bindingMapper.findByDeviceId(vo.getDeviceId());
if (existing != null) {
bindingMapper.unbind(existing.getId());
}
bindingMapper.insert(vo);
return bindingMapper.findById(vo.getId());
}
@Transactional
public void unbind(Long id) {
bindingMapper.unbind(id);
}
/**
* 가격 변경 해당 상품에 바인딩된 모든 태그에 업데이트 생성
* POS 가격변경 메서드 e-paper 태그들에게 전파
*/
@Transactional
public int enqueueByProductCode(String tenantCode, Long storeId,
String productCode, java.math.BigDecimal newPrice,
java.math.BigDecimal newSalePrice, Long posCvtId) {
List<TagBindingVo> bindings = bindingMapper.findByProductCode(tenantCode, storeId, productCode);
for (TagBindingVo b : bindings) {
UpdateQueueVo q = new UpdateQueueVo();
q.setTenantCode(tenantCode);
q.setStoreId(storeId);
q.setBindingId(b.getId());
q.setDeviceId(b.getDeviceId());
q.setProductCode(productCode);
q.setNewPrice(newPrice);
q.setNewSalePrice(newSalePrice);
q.setTemplateCode(b.getTemplateCode());
q.setPriority(5);
q.setPosCvtId(posCvtId);
queueMapper.insert(q);
}
return bindings.size();
}
}

View File

@ -0,0 +1,32 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.TemplateVo;
import com.zioinfo.esn.mapper.TemplateMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TemplateService {
private final TemplateMapper mapper;
public List<TemplateVo> list(String tenantCode, String templateType) {
return mapper.findAll(tenantCode, templateType);
}
public TemplateVo get(Long id) { return mapper.findById(id); }
public TemplateVo create(TemplateVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public TemplateVo update(Long id, TemplateVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,29 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.TenantVo;
import com.zioinfo.esn.mapper.TenantMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TenantService {
private final TenantMapper mapper;
public List<TenantVo> list() { return mapper.findAll(); }
public TenantVo get(Long id) { return mapper.findById(id); }
public TenantVo create(TenantVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public TenantVo update(Long id, TenantVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,60 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.UpdateQueueVo;
import com.zioinfo.esn.mapper.UpdateQueueMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UpdateQueueService {
private final UpdateQueueMapper mapper;
public List<UpdateQueueVo> list(String tenantCode, Long storeId, String status) {
return mapper.findAll(tenantCode, storeId, status);
}
public UpdateQueueVo get(Long id) { return mapper.findById(id); }
/** Gateway 폴링: PENDING 큐 N건 반환 후 SENDING으로 전환 */
@Transactional
public List<UpdateQueueVo> pollPending(String tenantCode, int limit) {
List<UpdateQueueVo> items = mapper.findPending(tenantCode, limit);
for (UpdateQueueVo q : items) {
mapper.updateStatus(q.getId(), "SENDING", null);
}
return items;
}
/** Gateway → 태그 전송 완료 */
@Transactional
public void markSent(Long id) {
mapper.updateStatus(id, "SENT", null);
}
/** 태그 확인 응답 수신 (device_id 기반) */
@Transactional
public void confirm(String deviceId) {
mapper.confirmByDeviceId(deviceId);
}
/** 전송 실패 처리 — max_retry 초과 시 FAILED */
@Transactional
public void markFailed(Long id, String errorMessage) {
UpdateQueueVo q = mapper.findById(id);
if (q == null) return;
mapper.incrementRetry(id);
if (q.getRetryCount() + 1 >= q.getMaxRetry()) {
mapper.updateStatus(id, "FAILED", errorMessage);
} else {
mapper.updateStatus(id, "PENDING", errorMessage);
}
}
@Transactional
public void cancel(Long id) {
mapper.updateStatus(id, "CANCELLED", null);
}
}

View File

@ -0,0 +1,39 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.auth.EsnUser;
import com.zioinfo.esn.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper mapper;
private final PasswordEncoder passwordEncoder;
public List<EsnUser> list(String tenantCode, String role) {
return mapper.findAll(tenantCode, role);
}
public EsnUser get(Long id) { return mapper.findById(id); }
public EsnUser create(EsnUser v, String rawPassword) {
v.setPasswordHash(passwordEncoder.encode(rawPassword));
mapper.insert(v);
return mapper.findById(v.getId());
}
public EsnUser update(Long id, EsnUser v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void changePassword(Long id, String rawPassword) {
mapper.updatePassword(id, passwordEncoder.encode(rawPassword));
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,32 @@
package com.zioinfo.esn.service;
import com.zioinfo.esn.domain.WorkHistoryVo;
import com.zioinfo.esn.mapper.WorkMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class WorkService {
private final WorkMapper mapper;
public List<WorkHistoryVo> list(String tenantCode, Long storeId, String status, String workType) {
return mapper.findAll(tenantCode, storeId, status, workType);
}
public WorkHistoryVo get(Long id) { return mapper.findById(id); }
public WorkHistoryVo create(WorkHistoryVo v) {
mapper.insert(v);
return mapper.findById(v.getId());
}
public WorkHistoryVo update(Long id, WorkHistoryVo v) {
v.setId(id);
mapper.update(v);
return mapper.findById(id);
}
public void delete(Long id) { mapper.delete(id); }
}

View File

@ -0,0 +1,48 @@
server:
port: 8016
spring:
application:
name: zioinfo-esn
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/esn_db}
username: ${DB_USER:esn_user}
password: ${DB_PASS:esn_pass2026}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 3
connection-timeout: 30000
web:
resources:
static-locations: classpath:/static/
mvc:
throw-exception-if-no-handler-found: true
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
type-aliases-package: com.zioinfo.esn.domain
guardia:
ollama-url: ${OLLAMA_URL:http://localhost:11434}
ollama-text-model: ${OLLAMA_TEXT_MODEL:llama3}
itsm-url: ${ITSM_URL:http://localhost:9001}
jwt:
secret: ${JWT_SECRET:ZioInfoEsnJwtSecret2026VeryLongSecretKeyForHS256AlgorithmAtLeast256Bits}
expiration: 86400000
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: never
logging:
level:
com.zioinfo.esn: DEBUG
org.mybatis: WARN

View File

@ -0,0 +1,333 @@
-- ============================================================================
-- zioinfo-esn ESL 통합 플랫폼 — PostgreSQL 스키마 (esn_db)
-- 레거시 Spring Boot 1.5 / Java 8 6개 프로젝트 → 단일 플랫폼
-- 멀티테넌트: LGINNOTEK / LGIT / EMART / ZIOINFO
-- ============================================================================
-- ── 테넌트 ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_tenant (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) UNIQUE NOT NULL,
tenant_name VARCHAR(200) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO esn_tenant (tenant_code, tenant_name, description) VALUES
('LGINNOTEK', 'LG 이노텍', 'LG 이노텍 ESL 관리'),
('LGIT', 'LG IT', 'LG IT 서비스 ESL'),
('EMART', '이마트', '이마트 전자가격표 시스템'),
('ZIOINFO', '지오정보기술', '지오정보기술 자체 ESL')
ON CONFLICT (tenant_code) DO NOTHING;
-- ── 매장 그룹 ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_store_group (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
group_code VARCHAR(50) NOT NULL,
group_name VARCHAR(200) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (tenant_code, group_code)
);
INSERT INTO esn_store_group (tenant_code, group_code, group_name) VALUES
('LGINNOTEK', 'GRP-LGI-01', 'LG이노텍 구미공장'),
('LGIT', 'GRP-LGIT-01','LG IT 본사'),
('EMART', 'GRP-EM-SEOUL', '이마트 서울권'),
('EMART', 'GRP-EM-GYEONG','이마트 경기권'),
('ZIOINFO', 'GRP-ZIO-01', '지오정보기술 본사')
ON CONFLICT (tenant_code, group_code) DO NOTHING;
-- ── 매장 ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_store (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_code VARCHAR(50) NOT NULL,
store_name VARCHAR(200) NOT NULL,
address VARCHAR(500),
phone VARCHAR(50),
store_group_id BIGINT REFERENCES esn_store_group(id),
manager_name VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (tenant_code, store_code)
);
INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id)
SELECT 'LGINNOTEK', 'LGI-001', 'LG이노텍 구미 1라인', '경북 구미시 공단동 1', '김철수', g.id
FROM esn_store_group g WHERE g.group_code = 'GRP-LGI-01' AND g.tenant_code = 'LGINNOTEK'
ON CONFLICT (tenant_code, store_code) DO NOTHING;
INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id)
SELECT 'EMART', 'EM-001', '이마트 강남점', '서울 강남구 삼성동', '박영희', g.id
FROM esn_store_group g WHERE g.group_code = 'GRP-EM-SEOUL' AND g.tenant_code = 'EMART'
ON CONFLICT (tenant_code, store_code) DO NOTHING;
INSERT INTO esn_store (tenant_code, store_code, store_name, address, manager_name, store_group_id)
SELECT 'EMART', 'EM-002', '이마트 수원점', '경기 수원시 팔달구', '이민수', g.id
FROM esn_store_group g WHERE g.group_code = 'GRP-EM-GYEONG' AND g.tenant_code = 'EMART'
ON CONFLICT (tenant_code, store_code) DO NOTHING;
-- ── ESL 템플릿 ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_template (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
template_code VARCHAR(50) NOT NULL,
template_name VARCHAR(200) NOT NULL,
template_type VARCHAR(50) DEFAULT 'PRICE', -- PRICE, INFO, PROMO, CUSTOM
width INTEGER,
height INTEGER,
description TEXT,
layout_json TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (tenant_code, template_code)
);
INSERT INTO esn_template (tenant_code, template_code, template_name, template_type, width, height, description) VALUES
('EMART', 'EM-PRICE-STD', '이마트 표준 가격표', 'PRICE', 152, 76, '표준 2.9인치 ESL 템플릿'),
('EMART', 'EM-PROMO', '이마트 프로모션', 'PROMO', 296, 128, '5인치 프로모션 표시'),
('LGINNOTEK', 'LGI-INFO', 'LG이노텍 부품 정보', 'INFO', 152, 76, '부품 코드·수량 표시')
ON CONFLICT (tenant_code, template_code) DO NOTHING;
-- ── POS 가격 변환 ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_pos_cvt (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
pos_code VARCHAR(100),
product_code VARCHAR(100) NOT NULL,
product_name VARCHAR(300),
price NUMERIC(12,2),
sale_price NUMERIC(12,2),
currency VARCHAR(10) DEFAULT 'KRW',
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, PROCESSED, ERROR, IGNORED
error_message TEXT,
processed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pos_cvt_tenant ON esn_pos_cvt(tenant_code, status);
CREATE INDEX IF NOT EXISTS idx_pos_cvt_store ON esn_pos_cvt(store_id, status);
-- ── 알람 ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_alarm (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
alarm_type VARCHAR(50) NOT NULL, -- DEVICE, NETWORK, BATTERY, FIRMWARE, SYSTEM
alarm_code VARCHAR(100),
severity VARCHAR(20) NOT NULL, -- CRITICAL, HIGH, MEDIUM, LOW
message TEXT,
status VARCHAR(20) DEFAULT 'OPEN', -- OPEN, ACKNOWLEDGED, RESOLVED
resolved_by VARCHAR(100),
resolution TEXT,
resolved_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_alarm_tenant ON esn_alarm(tenant_code, status);
CREATE INDEX IF NOT EXISTS idx_alarm_severity ON esn_alarm(severity, status);
-- 샘플 알람
INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status)
SELECT 'EMART', s.id, 'DEVICE', 'DEV-OFFLINE', 'HIGH', 'ESL 장치 오프라인 감지 — 매장 내 점검 필요', 'OPEN'
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1;
INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status)
SELECT 'EMART', s.id, 'BATTERY', 'BAT-LOW', 'MEDIUM', 'ESL 배터리 잔량 15% 미만 — 배터리 교체 예정', 'OPEN'
FROM esn_store s WHERE s.store_code = 'EM-002' LIMIT 1;
-- ── HCore 장치 (게이트웨이/허브/ESL) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_hcore_device (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
device_type VARCHAR(50) NOT NULL, -- GATEWAY, HUB, ESL_DEVICE
device_id VARCHAR(100) NOT NULL,
ip_address VARCHAR(50),
mac_address VARCHAR(50),
firmware_version VARCHAR(50),
status VARCHAR(20) DEFAULT 'ONLINE', -- ONLINE, OFFLINE, ERROR, UPDATING
battery_level INTEGER,
signal_strength VARCHAR(20),
last_seen_at TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (device_id)
);
CREATE INDEX IF NOT EXISTS idx_hcore_tenant ON esn_hcore_device(tenant_code, status);
CREATE INDEX IF NOT EXISTS idx_hcore_store ON esn_hcore_device(store_id, status);
-- 샘플 HCore 장치
INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status)
SELECT 'EMART', s.id, 'GATEWAY', 'GW-EM-001-001', '192.168.1.1', '2.1.5', 'ONLINE'
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1
ON CONFLICT (device_id) DO NOTHING;
INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status, battery_level)
SELECT 'EMART', s.id, 'ESL_DEVICE', 'ESL-EM-001-0001', NULL, '1.8.2', 'ONLINE', 85
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1
ON CONFLICT (device_id) DO NOTHING;
INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address, firmware_version, status, battery_level)
SELECT 'EMART', s.id, 'ESL_DEVICE', 'ESL-EM-001-0002', NULL, '1.8.2', 'OFFLINE', 8
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1
ON CONFLICT (device_id) DO NOTHING;
-- ── 작업 이력 ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_work_history (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
work_type VARCHAR(50) NOT NULL, -- INSTALL, MAINTENANCE, FIRMWARE_UPDATE, REPLACEMENT, INSPECTION
work_content TEXT,
worker_name VARCHAR(100),
status VARCHAR(20) DEFAULT 'SCHEDULED', -- SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED
remarks TEXT,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_work_tenant ON esn_work_history(tenant_code, status);
-- ── 펌웨어 ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_firmware (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
firmware_version VARCHAR(50) NOT NULL,
device_type VARCHAR(50) NOT NULL,
file_name VARCHAR(300),
file_path VARCHAR(500),
file_size BIGINT,
checksum VARCHAR(200),
description TEXT,
is_latest BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO esn_firmware (tenant_code, firmware_version, device_type, file_name, description, is_latest) VALUES
('EMART', '2.1.5', 'GATEWAY', 'gw_v2.1.5.bin', 'EMART 게이트웨이 최신 펌웨어', true),
('EMART', '1.8.2', 'ESL_DEVICE', 'esl_v1.8.2.bin', 'EMART ESL 디바이스 최신 펌웨어', true)
ON CONFLICT DO NOTHING;
-- ── 사용자 ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_user (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) REFERENCES esn_tenant(tenant_code),
username VARCHAR(200) UNIQUE NOT NULL,
password_hash VARCHAR(500) NOT NULL,
role VARCHAR(50) DEFAULT 'USER', -- ADMIN, MANAGER, USER
email VARCHAR(300),
phone VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
last_login_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- 기본 관리자 (비밀번호: admin123 / BCrypt)
INSERT INTO esn_user (tenant_code, username, password_hash, role)
VALUES (NULL, 'admin', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'ADMIN')
ON CONFLICT (username) DO NOTHING;
INSERT INTO esn_user (tenant_code, username, password_hash, role)
VALUES ('EMART', 'emart_manager', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'MANAGER')
ON CONFLICT (username) DO NOTHING;
INSERT INTO esn_user (tenant_code, username, password_hash, role)
VALUES ('LGINNOTEK', 'lgi_manager', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LnCWh7Vz.Xm', 'MANAGER')
ON CONFLICT (username) DO NOTHING;
-- ── 상품 ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_product (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
product_code VARCHAR(100) NOT NULL,
product_name VARCHAR(300) NOT NULL,
category VARCHAR(100),
price NUMERIC(12,2),
sale_price NUMERIC(12,2),
currency VARCHAR(10) DEFAULT 'KRW',
is_active BOOLEAN DEFAULT TRUE,
updated_at TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_product_store ON esn_product(store_id, product_code);
-- 샘플 상품
INSERT INTO esn_product (tenant_code, store_id, product_code, product_name, category, price, sale_price)
SELECT 'EMART', s.id, 'P-001', '신라면', '라면/면류', 850, 800
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1
ON CONFLICT DO NOTHING;
-- ── 태그 위치/상품 바인딩 ──────────────────────────────────────────────────────
-- e-paper 태그와 선반 위치 + 상품을 연결하는 핵심 테이블
CREATE TABLE IF NOT EXISTS esn_tag_binding (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT NOT NULL REFERENCES esn_store(id),
device_id VARCHAR(100) NOT NULL, -- ESL_DEVICE device_id
product_code VARCHAR(100), -- 현재 표시 중인 상품
location_code VARCHAR(100), -- 선반 위치코드 (예: A01-03, 통로A-1단)
template_code VARCHAR(50), -- 적용 템플릿
display_data TEXT, -- 현재 화면에 표시된 데이터 (JSON)
last_price NUMERIC(12,2), -- 마지막으로 전송된 가격
last_updated_at TIMESTAMP, -- 마지막 화면 갱신 시각
bind_status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE, UNBOUND, ERROR
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (device_id)
);
CREATE INDEX IF NOT EXISTS idx_binding_store ON esn_tag_binding(store_id, bind_status);
CREATE INDEX IF NOT EXISTS idx_binding_product ON esn_tag_binding(tenant_code, product_code);
CREATE INDEX IF NOT EXISTS idx_binding_loc ON esn_tag_binding(store_id, location_code);
-- 샘플 바인딩 (이마트 강남점 ESL-EM-001-0001 ↔ 신라면)
INSERT INTO esn_tag_binding (tenant_code, store_id, device_id, product_code, location_code, template_code, last_price)
SELECT 'EMART', s.id, 'ESL-EM-001-0001', 'P-001', 'A01-03', 'EM-PRICE-STD', 850
FROM esn_store s WHERE s.store_code = 'EM-001' LIMIT 1
ON CONFLICT (device_id) DO NOTHING;
-- ── 가격 업데이트 전파 큐 ────────────────────────────────────────────────────────
-- POS 가격 변경이 e-paper 태그에 도달하는 전 과정을 추적
CREATE TABLE IF NOT EXISTS esn_update_queue (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20) NOT NULL REFERENCES esn_tenant(tenant_code),
store_id BIGINT REFERENCES esn_store(id),
binding_id BIGINT REFERENCES esn_tag_binding(id),
device_id VARCHAR(100) NOT NULL,
product_code VARCHAR(100),
new_price NUMERIC(12,2),
new_sale_price NUMERIC(12,2),
template_code VARCHAR(50),
render_data TEXT, -- 렌더링된 화면 데이터 (JSON)
priority INTEGER DEFAULT 5, -- 1(긴급) ~ 10(일반)
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, SENDING, SENT, CONFIRMED, FAILED, CANCELLED
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
error_message TEXT,
sent_at TIMESTAMP,
confirmed_at TIMESTAMP,
pos_cvt_id BIGINT REFERENCES esn_pos_cvt(id),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_queue_device ON esn_update_queue(device_id, status);
CREATE INDEX IF NOT EXISTS idx_queue_store ON esn_update_queue(store_id, status, created_at);
CREATE INDEX IF NOT EXISTS idx_queue_pend ON esn_update_queue(status, priority, created_at) WHERE status = 'PENDING';
-- ── 감사 로그 ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS esn_audit_log (
id BIGSERIAL PRIMARY KEY,
tenant_code VARCHAR(20),
username VARCHAR(200),
action VARCHAR(200) NOT NULL,
resource VARCHAR(200),
resource_id VARCHAR(100),
detail TEXT,
ip_address VARCHAR(50),
user_agent VARCHAR(500),
success BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_audit_created ON esn_audit_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON esn_audit_log(tenant_code, created_at DESC);

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.AlarmMapper">
<resultMap id="alarmMap" type="com.zioinfo.esn.domain.AlarmVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="alarmType" column="alarm_type"/>
<result property="alarmCode" column="alarm_code"/>
<result property="severity" column="severity"/>
<result property="message" column="message"/>
<result property="status" column="status"/>
<result property="resolvedBy" column="resolved_by"/>
<result property="resolution" column="resolution"/>
<result property="resolvedAt" column="resolved_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="alarmMap">
SELECT a.id, a.tenant_code, a.store_id, s.store_name, a.alarm_type, a.alarm_code,
a.severity, a.message, a.status, a.resolved_by, a.resolution, a.resolved_at, a.created_at
FROM esn_alarm a
LEFT JOIN esn_store s ON a.store_id = s.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND a.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND a.store_id = #{storeId}</if>
<if test="severity != null and severity != ''">AND a.severity = #{severity}</if>
<if test="status != null and status != ''">AND a.status = #{status}</if>
</where>
ORDER BY a.created_at DESC LIMIT 200
</select>
<select id="findById" resultMap="alarmMap">
SELECT a.id, a.tenant_code, a.store_id, s.store_name, a.alarm_type, a.alarm_code,
a.severity, a.message, a.status, a.resolved_by, a.resolution, a.resolved_at, a.created_at
FROM esn_alarm a
LEFT JOIN esn_store s ON a.store_id = s.id
WHERE a.id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status)
VALUES (#{tenantCode}, #{storeId}, #{alarmType}, #{alarmCode}, #{severity}, #{message}, 'OPEN')
</insert>
<update id="update">
UPDATE esn_alarm SET
alarm_type = #{alarmType},
severity = #{severity},
message = #{message},
status = #{status}
WHERE id = #{id}
</update>
<update id="resolve">
UPDATE esn_alarm SET
status = 'RESOLVED',
resolved_by = #{resolvedBy},
resolution = #{resolution},
resolved_at = NOW()
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_alarm WHERE id = #{id}</delete>
<select id="countUnresolved" resultType="long">
SELECT COUNT(*) FROM esn_alarm WHERE status != 'RESOLVED'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countCritical" resultType="long">
SELECT COUNT(*) FROM esn_alarm WHERE severity = 'CRITICAL' AND status != 'RESOLVED'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countAll" resultType="long">
SELECT COUNT(*) FROM esn_alarm
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</where>
</select>
</mapper>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.DashboardMapper">
<select id="getSummary" resultType="com.zioinfo.esn.domain.DashboardVo">
SELECT
(SELECT COUNT(*) FROM esn_store
WHERE 1=1 <if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS total_stores,
(SELECT COUNT(*) FROM esn_store WHERE is_active = true
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS active_stores,
(SELECT COUNT(*) FROM esn_alarm
WHERE 1=1 <if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS total_alarms,
(SELECT COUNT(*) FROM esn_alarm WHERE status != 'RESOLVED'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS unresolved_alarms,
(SELECT COUNT(*) FROM esn_alarm WHERE severity = 'CRITICAL' AND status != 'RESOLVED'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS critical_alarms,
(SELECT COUNT(*) FROM esn_hcore_device
WHERE 1=1 <if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS total_devices,
(SELECT COUNT(*) FROM esn_hcore_device WHERE status = 'ONLINE'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS online_devices,
(SELECT COUNT(*) FROM esn_hcore_device WHERE status = 'OFFLINE'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS offline_devices,
(SELECT COUNT(*) FROM esn_work_history WHERE created_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS total_work_today,
(SELECT COUNT(*) FROM esn_work_history WHERE status = 'COMPLETED' AND completed_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS completed_work_today,
(SELECT COUNT(*) FROM esn_pos_cvt WHERE status = 'PENDING'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS pending_pos_cvt,
(SELECT COUNT(*) FROM esn_pos_cvt WHERE status = 'PROCESSED' AND processed_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
) AS processed_pos_cvt_today
</select>
</mapper>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.FirmwareMapper">
<resultMap id="firmwareMap" type="com.zioinfo.esn.domain.FirmwareVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="firmwareVersion" column="firmware_version"/>
<result property="deviceType" column="device_type"/>
<result property="fileName" column="file_name"/>
<result property="filePath" column="file_path"/>
<result property="fileSize" column="file_size"/>
<result property="checksum" column="checksum"/>
<result property="description" column="description"/>
<result property="latest" column="is_latest"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="firmwareMap">
SELECT id, tenant_code, firmware_version, device_type, file_name, file_path,
file_size, checksum, description, is_latest, created_at
FROM esn_firmware
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
<if test="deviceType != null and deviceType != ''">AND device_type = #{deviceType}</if>
</where>
ORDER BY created_at DESC
</select>
<select id="findById" resultMap="firmwareMap">
SELECT id, tenant_code, firmware_version, device_type, file_name, file_path,
file_size, checksum, description, is_latest, created_at
FROM esn_firmware WHERE id = #{id}
</select>
<select id="findLatest" resultMap="firmwareMap">
SELECT id, tenant_code, firmware_version, device_type, file_name, file_path,
file_size, checksum, description, is_latest, created_at
FROM esn_firmware
WHERE device_type = #{deviceType} AND is_latest = true
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
LIMIT 1
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_firmware (tenant_code, firmware_version, device_type, file_name, file_path,
file_size, checksum, description, is_latest)
VALUES (#{tenantCode}, #{firmwareVersion}, #{deviceType}, #{fileName}, #{filePath},
#{fileSize}, #{checksum}, #{description}, #{latest})
</insert>
<update id="update">
UPDATE esn_firmware SET
firmware_version = #{firmwareVersion},
description = #{description},
is_latest = #{latest}
WHERE id = #{id}
</update>
<update id="clearLatest">
UPDATE esn_firmware SET is_latest = false
WHERE device_type = #{deviceType}
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</update>
<delete id="delete">DELETE FROM esn_firmware WHERE id = #{id}</delete>
</mapper>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.HCoreMapper">
<resultMap id="hcoreMap" type="com.zioinfo.esn.domain.HCoreVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="deviceType" column="device_type"/>
<result property="deviceId" column="device_id"/>
<result property="ipAddress" column="ip_address"/>
<result property="macAddress" column="mac_address"/>
<result property="firmwareVersion" column="firmware_version"/>
<result property="status" column="status"/>
<result property="batteryLevel" column="battery_level"/>
<result property="signalStrength" column="signal_strength"/>
<result property="lastSeenAt" column="last_seen_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="hcoreMap">
SELECT h.id, h.tenant_code, h.store_id, s.store_name, h.device_type, h.device_id,
h.ip_address, h.mac_address, h.firmware_version, h.status, h.battery_level,
h.signal_strength, h.last_seen_at, h.created_at
FROM esn_hcore_device h
LEFT JOIN esn_store s ON h.store_id = s.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND h.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND h.store_id = #{storeId}</if>
<if test="deviceType != null and deviceType != ''">AND h.device_type = #{deviceType}</if>
<if test="status != null and status != ''">AND h.status = #{status}</if>
</where>
ORDER BY h.device_id
</select>
<select id="findById" resultMap="hcoreMap">
SELECT h.id, h.tenant_code, h.store_id, s.store_name, h.device_type, h.device_id,
h.ip_address, h.mac_address, h.firmware_version, h.status, h.battery_level,
h.signal_strength, h.last_seen_at, h.created_at
FROM esn_hcore_device h
LEFT JOIN esn_store s ON h.store_id = s.id
WHERE h.id = #{id}
</select>
<select id="findByDeviceId" resultMap="hcoreMap">
SELECT h.id, h.tenant_code, h.store_id, s.store_name, h.device_type, h.device_id,
h.ip_address, h.mac_address, h.firmware_version, h.status, h.battery_level,
h.signal_strength, h.last_seen_at, h.created_at
FROM esn_hcore_device h
LEFT JOIN esn_store s ON h.store_id = s.id
WHERE h.device_id = #{deviceId}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_hcore_device (tenant_code, store_id, device_type, device_id, ip_address,
mac_address, firmware_version, status)
VALUES (#{tenantCode}, #{storeId}, #{deviceType}, #{deviceId}, #{ipAddress},
#{macAddress}, #{firmwareVersion}, #{status})
</insert>
<update id="update">
UPDATE esn_hcore_device SET
ip_address = #{ipAddress},
mac_address = #{macAddress},
firmware_version = #{firmwareVersion},
status = #{status},
battery_level = #{batteryLevel},
signal_strength = #{signalStrength},
last_seen_at = NOW()
WHERE id = #{id}
</update>
<update id="updateStatus">
UPDATE esn_hcore_device SET status = #{status}, last_seen_at = NOW()
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_hcore_device WHERE id = #{id}</delete>
<select id="countOnline" resultType="long">
SELECT COUNT(*) FROM esn_hcore_device WHERE status = 'ONLINE'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countOffline" resultType="long">
SELECT COUNT(*) FROM esn_hcore_device WHERE status = 'OFFLINE'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countAll" resultType="long">
SELECT COUNT(*) FROM esn_hcore_device
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</where>
</select>
</mapper>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.PosCvtMapper">
<resultMap id="posMap" type="com.zioinfo.esn.domain.PosCvtVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="posCode" column="pos_code"/>
<result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/>
<result property="price" column="price"/>
<result property="salePrice" column="sale_price"/>
<result property="currency" column="currency"/>
<result property="status" column="status"/>
<result property="errorMessage" column="error_message"/>
<result property="processedAt" column="processed_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="posMap">
SELECT p.id, p.tenant_code, p.store_id, s.store_name, p.pos_code,
p.product_code, p.product_name, p.price, p.sale_price, p.currency,
p.status, p.error_message, p.processed_at, p.created_at
FROM esn_pos_cvt p
LEFT JOIN esn_store s ON p.store_id = s.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND p.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND p.store_id = #{storeId}</if>
<if test="status != null and status != ''">AND p.status = #{status}</if>
<if test="keyword != null and keyword != ''">
AND (p.product_code ILIKE '%' || #{keyword} || '%' OR p.product_name ILIKE '%' || #{keyword} || '%')
</if>
</where>
ORDER BY p.created_at DESC LIMIT 500
</select>
<select id="findById" resultMap="posMap">
SELECT p.id, p.tenant_code, p.store_id, s.store_name, p.pos_code,
p.product_code, p.product_name, p.price, p.sale_price, p.currency,
p.status, p.error_message, p.processed_at, p.created_at
FROM esn_pos_cvt p
LEFT JOIN esn_store s ON p.store_id = s.id
WHERE p.id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_pos_cvt (tenant_code, store_id, pos_code, product_code, product_name,
price, sale_price, currency, status)
VALUES (#{tenantCode}, #{storeId}, #{posCode}, #{productCode}, #{productName},
#{price}, #{salePrice}, #{currency}, 'PENDING')
</insert>
<update id="updateStatus">
UPDATE esn_pos_cvt SET
status = #{status},
error_message = #{errorMessage},
processed_at = CASE WHEN #{status} = 'PROCESSED' THEN NOW() ELSE processed_at END
WHERE id = #{id}
</update>
<select id="countPending" resultType="long">
SELECT COUNT(*) FROM esn_pos_cvt WHERE status = 'PENDING'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countProcessedToday" resultType="long">
SELECT COUNT(*) FROM esn_pos_cvt
WHERE status = 'PROCESSED' AND processed_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
</mapper>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.ProductMapper">
<resultMap id="productMap" type="com.zioinfo.esn.domain.ProductVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/>
<result property="category" column="category"/>
<result property="price" column="price"/>
<result property="salePrice" column="sale_price"/>
<result property="currency" column="currency"/>
<result property="active" column="is_active"/>
<result property="updatedAt" column="updated_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="productMap">
SELECT p.id, p.tenant_code, p.store_id, s.store_name, p.product_code, p.product_name,
p.category, p.price, p.sale_price, p.currency, p.is_active, p.updated_at, p.created_at
FROM esn_product p
LEFT JOIN esn_store s ON p.store_id = s.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND p.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND p.store_id = #{storeId}</if>
<if test="keyword != null and keyword != ''">
AND (p.product_code ILIKE '%' || #{keyword} || '%' OR p.product_name ILIKE '%' || #{keyword} || '%')
</if>
</where>
ORDER BY p.product_code LIMIT 500
</select>
<select id="findById" resultMap="productMap">
SELECT p.id, p.tenant_code, p.store_id, s.store_name, p.product_code, p.product_name,
p.category, p.price, p.sale_price, p.currency, p.is_active, p.updated_at, p.created_at
FROM esn_product p
LEFT JOIN esn_store s ON p.store_id = s.id
WHERE p.id = #{id}
</select>
<select id="findByCode" resultMap="productMap">
SELECT p.id, p.tenant_code, p.store_id, s.store_name, p.product_code, p.product_name,
p.category, p.price, p.sale_price, p.currency, p.is_active, p.updated_at, p.created_at
FROM esn_product p
LEFT JOIN esn_store s ON p.store_id = s.id
WHERE p.product_code = #{productCode} AND p.store_id = #{storeId}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_product (tenant_code, store_id, product_code, product_name,
category, price, sale_price, currency, is_active)
VALUES (#{tenantCode}, #{storeId}, #{productCode}, #{productName},
#{category}, #{price}, #{salePrice}, #{currency}, #{active})
</insert>
<update id="update">
UPDATE esn_product SET
product_name = #{productName},
category = #{category},
price = #{price},
sale_price = #{salePrice},
currency = #{currency},
is_active = #{active},
updated_at = NOW()
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_product WHERE id = #{id}</delete>
</mapper>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.StoreGroupMapper">
<resultMap id="groupMap" type="com.zioinfo.esn.domain.StoreGroupVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="groupCode" column="group_code"/>
<result property="groupName" column="group_name"/>
<result property="description" column="description"/>
<result property="active" column="is_active"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="groupMap">
SELECT id, tenant_code, group_code, group_name, description, is_active, created_at
FROM esn_store_group
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</where>
ORDER BY group_code
</select>
<select id="findById" resultMap="groupMap">
SELECT id, tenant_code, group_code, group_name, description, is_active, created_at
FROM esn_store_group WHERE id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_store_group (tenant_code, group_code, group_name, description, is_active)
VALUES (#{tenantCode}, #{groupCode}, #{groupName}, #{description}, #{active})
</insert>
<update id="update">
UPDATE esn_store_group SET
group_name = #{groupName},
description = #{description},
is_active = #{active}
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_store_group WHERE id = #{id}</delete>
</mapper>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.StoreMapper">
<resultMap id="storeMap" type="com.zioinfo.esn.domain.StoreVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeCode" column="store_code"/>
<result property="storeName" column="store_name"/>
<result property="address" column="address"/>
<result property="phone" column="phone"/>
<result property="storeGroupId" column="store_group_id"/>
<result property="storeGroupName" column="group_name"/>
<result property="managerName" column="manager_name"/>
<result property="active" column="is_active"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="storeMap">
SELECT s.id, s.tenant_code, s.store_code, s.store_name, s.address, s.phone,
s.store_group_id, g.group_name, s.manager_name, s.is_active, s.created_at
FROM esn_store s
LEFT JOIN esn_store_group g ON s.store_group_id = g.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND s.tenant_code = #{tenantCode}</if>
<if test="storeGroupId != null">AND s.store_group_id = #{storeGroupId}</if>
<if test="keyword != null and keyword != ''">
AND (s.store_name ILIKE '%' || #{keyword} || '%' OR s.store_code ILIKE '%' || #{keyword} || '%')
</if>
</where>
ORDER BY s.store_code
</select>
<select id="findById" resultMap="storeMap">
SELECT s.id, s.tenant_code, s.store_code, s.store_name, s.address, s.phone,
s.store_group_id, g.group_name, s.manager_name, s.is_active, s.created_at
FROM esn_store s
LEFT JOIN esn_store_group g ON s.store_group_id = g.id
WHERE s.id = #{id}
</select>
<select id="findByCode" resultMap="storeMap">
SELECT s.id, s.tenant_code, s.store_code, s.store_name, s.address, s.phone,
s.store_group_id, g.group_name, s.manager_name, s.is_active, s.created_at
FROM esn_store s
LEFT JOIN esn_store_group g ON s.store_group_id = g.id
WHERE s.store_code = #{storeCode} AND s.tenant_code = #{tenantCode}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_store (tenant_code, store_code, store_name, address, phone,
store_group_id, manager_name, is_active)
VALUES (#{tenantCode}, #{storeCode}, #{storeName}, #{address}, #{phone},
#{storeGroupId}, #{managerName}, #{active})
</insert>
<update id="update">
UPDATE esn_store SET
store_name = #{storeName},
address = #{address},
phone = #{phone},
store_group_id = #{storeGroupId},
manager_name = #{managerName},
is_active = #{active}
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_store WHERE id = #{id}</delete>
<select id="countActive" resultType="long">
SELECT COUNT(*) FROM esn_store
WHERE is_active = true
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countAll" resultType="long">
SELECT COUNT(*) FROM esn_store
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</where>
</select>
</mapper>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.TagBindingMapper">
<resultMap id="bindingMap" type="com.zioinfo.esn.domain.TagBindingVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="deviceId" column="device_id"/>
<result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/>
<result property="locationCode" column="location_code"/>
<result property="templateCode" column="template_code"/>
<result property="displayData" column="display_data"/>
<result property="lastPrice" column="last_price"/>
<result property="lastUpdatedAt" column="last_updated_at"/>
<result property="bindStatus" column="bind_status"/>
<result property="deviceStatus" column="device_status"/>
<result property="batteryLevel" column="battery_level"/>
<result property="signalStrength" column="signal_strength"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="bindingMap">
SELECT b.id, b.tenant_code, b.store_id, s.store_name,
b.device_id, b.product_code, p.product_name,
b.location_code, b.template_code, b.display_data,
b.last_price, b.last_updated_at, b.bind_status,
h.status AS device_status, h.battery_level, h.signal_strength,
b.created_at
FROM esn_tag_binding b
LEFT JOIN esn_store s ON b.store_id = s.id
LEFT JOIN esn_product p ON b.store_id = p.store_id AND b.product_code = p.product_code
LEFT JOIN esn_hcore_device h ON b.device_id = h.device_id
<where>
<if test="tenantCode != null and tenantCode != ''">AND b.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND b.store_id = #{storeId}</if>
<if test="bindStatus != null and bindStatus != ''">AND b.bind_status = #{bindStatus}</if>
<if test="keyword != null and keyword != ''">
AND (b.device_id ILIKE '%'||#{keyword}||'%'
OR b.product_code ILIKE '%'||#{keyword}||'%'
OR b.location_code ILIKE '%'||#{keyword}||'%'
OR p.product_name ILIKE '%'||#{keyword}||'%')
</if>
</where>
ORDER BY b.store_id, b.location_code
</select>
<select id="findById" resultMap="bindingMap">
SELECT b.id, b.tenant_code, b.store_id, s.store_name,
b.device_id, b.product_code, p.product_name,
b.location_code, b.template_code, b.display_data,
b.last_price, b.last_updated_at, b.bind_status,
h.status AS device_status, h.battery_level, h.signal_strength,
b.created_at
FROM esn_tag_binding b
LEFT JOIN esn_store s ON b.store_id = s.id
LEFT JOIN esn_product p ON b.store_id = p.store_id AND b.product_code = p.product_code
LEFT JOIN esn_hcore_device h ON b.device_id = h.device_id
WHERE b.id = #{id}
</select>
<select id="findByDeviceId" resultMap="bindingMap">
SELECT b.id, b.tenant_code, b.store_id, s.store_name,
b.device_id, b.product_code, p.product_name,
b.location_code, b.template_code, b.display_data,
b.last_price, b.last_updated_at, b.bind_status,
h.status AS device_status, h.battery_level, h.signal_strength,
b.created_at
FROM esn_tag_binding b
LEFT JOIN esn_store s ON b.store_id = s.id
LEFT JOIN esn_product p ON b.store_id = p.store_id AND b.product_code = p.product_code
LEFT JOIN esn_hcore_device h ON b.device_id = h.device_id
WHERE b.device_id = #{deviceId}
</select>
<select id="findByProductCode" resultMap="bindingMap">
SELECT b.id, b.tenant_code, b.store_id, s.store_name,
b.device_id, b.product_code, p.product_name,
b.location_code, b.template_code, b.display_data,
b.last_price, b.last_updated_at, b.bind_status,
h.status AS device_status, h.battery_level, h.signal_strength,
b.created_at
FROM esn_tag_binding b
LEFT JOIN esn_store s ON b.store_id = s.id
LEFT JOIN esn_product p ON b.store_id = p.store_id AND b.product_code = p.product_code
LEFT JOIN esn_hcore_device h ON b.device_id = h.device_id
WHERE b.tenant_code = #{tenantCode}
AND b.store_id = #{storeId}
AND b.product_code = #{productCode}
AND b.bind_status = 'ACTIVE'
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_tag_binding
(tenant_code, store_id, device_id, product_code, location_code,
template_code, last_price, bind_status)
VALUES
(#{tenantCode}, #{storeId}, #{deviceId}, #{productCode}, #{locationCode},
#{templateCode}, #{lastPrice}, 'ACTIVE')
</insert>
<update id="update">
UPDATE esn_tag_binding SET
product_code = #{productCode},
location_code = #{locationCode},
template_code = #{templateCode},
display_data = #{displayData},
last_price = #{lastPrice},
last_updated_at = NOW(),
bind_status = #{bindStatus}
WHERE id = #{id}
</update>
<update id="unbind">
UPDATE esn_tag_binding SET
bind_status = 'UNBOUND',
product_code = NULL,
last_updated_at = NOW()
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_tag_binding WHERE id = #{id}</delete>
<select id="countAll" resultType="long">
SELECT COUNT(*) FROM esn_tag_binding
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</where>
</select>
<select id="countActive" resultType="long">
SELECT COUNT(*) FROM esn_tag_binding WHERE bind_status = 'ACTIVE'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
</mapper>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.TemplateMapper">
<resultMap id="templateMap" type="com.zioinfo.esn.domain.TemplateVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="templateCode" column="template_code"/>
<result property="templateName" column="template_name"/>
<result property="templateType" column="template_type"/>
<result property="width" column="width"/>
<result property="height" column="height"/>
<result property="description" column="description"/>
<result property="layoutJson" column="layout_json"/>
<result property="active" column="is_active"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="templateMap">
SELECT id, tenant_code, template_code, template_name, template_type,
width, height, description, layout_json, is_active, created_at
FROM esn_template
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
<if test="templateType != null and templateType != ''">AND template_type = #{templateType}</if>
</where>
ORDER BY template_code
</select>
<select id="findById" resultMap="templateMap">
SELECT id, tenant_code, template_code, template_name, template_type,
width, height, description, layout_json, is_active, created_at
FROM esn_template WHERE id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_template (tenant_code, template_code, template_name, template_type,
width, height, description, layout_json, is_active)
VALUES (#{tenantCode}, #{templateCode}, #{templateName}, #{templateType},
#{width}, #{height}, #{description}, #{layoutJson}, #{active})
</insert>
<update id="update">
UPDATE esn_template SET
template_name = #{templateName},
template_type = #{templateType},
width = #{width},
height = #{height},
description = #{description},
layout_json = #{layoutJson},
is_active = #{active}
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_template WHERE id = #{id}</delete>
</mapper>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.TenantMapper">
<resultMap id="tenantMap" type="com.zioinfo.esn.domain.TenantVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="tenantName" column="tenant_name"/>
<result property="description" column="description"/>
<result property="active" column="is_active"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="tenantMap">
SELECT id, tenant_code, tenant_name, description, is_active, created_at
FROM esn_tenant ORDER BY tenant_code
</select>
<select id="findById" resultMap="tenantMap">
SELECT id, tenant_code, tenant_name, description, is_active, created_at
FROM esn_tenant WHERE id = #{id}
</select>
<select id="findByCode" resultMap="tenantMap">
SELECT id, tenant_code, tenant_name, description, is_active, created_at
FROM esn_tenant WHERE tenant_code = #{tenantCode}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_tenant (tenant_code, tenant_name, description, is_active)
VALUES (#{tenantCode}, #{tenantName}, #{description}, #{active})
</insert>
<update id="update">
UPDATE esn_tenant SET
tenant_name = #{tenantName},
description = #{description},
is_active = #{active}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM esn_tenant WHERE id = #{id}
</delete>
</mapper>

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.UpdateQueueMapper">
<resultMap id="queueMap" type="com.zioinfo.esn.domain.UpdateQueueVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="bindingId" column="binding_id"/>
<result property="deviceId" column="device_id"/>
<result property="locationCode" column="location_code"/>
<result property="productCode" column="product_code"/>
<result property="productName" column="product_name"/>
<result property="newPrice" column="new_price"/>
<result property="newSalePrice" column="new_sale_price"/>
<result property="templateCode" column="template_code"/>
<result property="renderData" column="render_data"/>
<result property="priority" column="priority"/>
<result property="status" column="status"/>
<result property="retryCount" column="retry_count"/>
<result property="maxRetry" column="max_retry"/>
<result property="errorMessage" column="error_message"/>
<result property="posCvtId" column="pos_cvt_id"/>
<result property="sentAt" column="sent_at"/>
<result property="confirmedAt" column="confirmed_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="queueMap">
SELECT q.id, q.tenant_code, q.store_id, s.store_name,
q.binding_id, q.device_id, b.location_code,
q.product_code, p.product_name,
q.new_price, q.new_sale_price, q.template_code, q.render_data,
q.priority, q.status, q.retry_count, q.max_retry, q.error_message,
q.pos_cvt_id, q.sent_at, q.confirmed_at, q.created_at
FROM esn_update_queue q
LEFT JOIN esn_store s ON q.store_id = s.id
LEFT JOIN esn_tag_binding b ON q.binding_id = b.id
LEFT JOIN esn_product p ON q.store_id = p.store_id AND q.product_code = p.product_code
<where>
<if test="tenantCode != null and tenantCode != ''">AND q.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND q.store_id = #{storeId}</if>
<if test="status != null and status != ''">AND q.status = #{status}</if>
</where>
ORDER BY q.priority ASC, q.created_at ASC
LIMIT 200
</select>
<!-- Gateway가 전송할 PENDING 목록 (우선순위·시간 순) -->
<select id="findPending" resultMap="queueMap">
SELECT q.id, q.tenant_code, q.store_id, s.store_name,
q.binding_id, q.device_id, b.location_code,
q.product_code, p.product_name,
q.new_price, q.new_sale_price, q.template_code, q.render_data,
q.priority, q.status, q.retry_count, q.max_retry, q.error_message,
q.pos_cvt_id, q.sent_at, q.confirmed_at, q.created_at
FROM esn_update_queue q
LEFT JOIN esn_store s ON q.store_id = s.id
LEFT JOIN esn_tag_binding b ON q.binding_id = b.id
LEFT JOIN esn_product p ON q.store_id = p.store_id AND q.product_code = p.product_code
WHERE q.status = 'PENDING'
AND q.retry_count &lt; q.max_retry
<if test="tenantCode != null and tenantCode != ''">AND q.tenant_code = #{tenantCode}</if>
ORDER BY q.priority ASC, q.created_at ASC
LIMIT #{limit}
</select>
<select id="findById" resultMap="queueMap">
SELECT q.id, q.tenant_code, q.store_id, s.store_name,
q.binding_id, q.device_id, b.location_code,
q.product_code, p.product_name,
q.new_price, q.new_sale_price, q.template_code, q.render_data,
q.priority, q.status, q.retry_count, q.max_retry, q.error_message,
q.pos_cvt_id, q.sent_at, q.confirmed_at, q.created_at
FROM esn_update_queue q
LEFT JOIN esn_store s ON q.store_id = s.id
LEFT JOIN esn_tag_binding b ON q.binding_id = b.id
LEFT JOIN esn_product p ON q.store_id = p.store_id AND q.product_code = p.product_code
WHERE q.id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_update_queue
(tenant_code, store_id, binding_id, device_id, product_code,
new_price, new_sale_price, template_code, priority, status, pos_cvt_id)
VALUES
(#{tenantCode}, #{storeId}, #{bindingId}, #{deviceId}, #{productCode},
#{newPrice}, #{newSalePrice}, #{templateCode}, #{priority}, 'PENDING', #{posCvtId})
</insert>
<update id="updateStatus">
UPDATE esn_update_queue SET
status = #{status},
error_message = #{errorMessage},
sent_at = CASE WHEN #{status} = 'SENT' THEN NOW() ELSE sent_at END,
confirmed_at = CASE WHEN #{status} = 'CONFIRMED' THEN NOW() ELSE confirmed_at END
WHERE id = #{id}
</update>
<!-- 태그가 확인 응답 시 — device_id로 SENT → CONFIRMED -->
<update id="confirmByDeviceId">
UPDATE esn_update_queue SET
status = 'CONFIRMED',
confirmed_at = NOW()
WHERE device_id = #{deviceId}
AND status IN ('SENDING', 'SENT')
</update>
<update id="incrementRetry">
UPDATE esn_update_queue SET retry_count = retry_count + 1
WHERE id = #{id}
</update>
<select id="countPending" resultType="long">
SELECT COUNT(*) FROM esn_update_queue WHERE status = 'PENDING'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countFailed" resultType="long">
SELECT COUNT(*) FROM esn_update_queue WHERE status = 'FAILED'
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countByStatus" resultType="long">
SELECT COUNT(*) FROM esn_update_queue WHERE status = #{status}
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
</mapper>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.auth.mapper.UserAuthMapper">
<resultMap id="userMap" type="com.zioinfo.esn.auth.EsnUser">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="username" column="username"/>
<result property="passwordHash" column="password_hash"/>
<result property="role" column="role"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="active" column="is_active"/>
<result property="lastLoginAt" column="last_login_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findByUsername" resultMap="userMap">
SELECT id, tenant_code, username, password_hash, role, email, phone,
is_active, last_login_at, created_at
FROM esn_user
WHERE username = #{username}
</select>
<update id="updateLastLogin">
UPDATE esn_user SET last_login_at = NOW() WHERE username = #{username}
</update>
</mapper>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.UserMapper">
<resultMap id="userMap" type="com.zioinfo.esn.auth.EsnUser">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="username" column="username"/>
<result property="role" column="role"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="active" column="is_active"/>
<result property="lastLoginAt" column="last_login_at"/>
<result property="createdAt" column="created_at"/>
<!-- password_hash 는 resultMap에서 제외 — API 응답 미노출 -->
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT id, tenant_code, username, role, email, phone, is_active, last_login_at, created_at
FROM esn_user
<where>
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
<if test="role != null and role != ''">AND role = #{role}</if>
</where>
ORDER BY username
</select>
<select id="findById" resultMap="userMap">
SELECT id, tenant_code, username, role, email, phone, is_active, last_login_at, created_at
FROM esn_user WHERE id = #{id}
</select>
<select id="findByUsername" resultType="com.zioinfo.esn.auth.EsnUser">
SELECT id, tenant_code, username, password_hash, role, email, phone,
is_active, last_login_at, created_at
FROM esn_user WHERE username = #{username}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_user (tenant_code, username, password_hash, role, email, phone, is_active)
VALUES (#{tenantCode}, #{username}, #{passwordHash}, #{role}, #{email}, #{phone}, #{active})
</insert>
<update id="update">
UPDATE esn_user SET
tenant_code = #{tenantCode},
role = #{role},
email = #{email},
phone = #{phone},
is_active = #{active}
WHERE id = #{id}
</update>
<update id="updatePassword">
UPDATE esn_user SET password_hash = #{passwordHash} WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_user WHERE id = #{id}</delete>
</mapper>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zioinfo.esn.mapper.WorkMapper">
<resultMap id="workMap" type="com.zioinfo.esn.domain.WorkHistoryVo">
<id property="id" column="id"/>
<result property="tenantCode" column="tenant_code"/>
<result property="storeId" column="store_id"/>
<result property="storeName" column="store_name"/>
<result property="workType" column="work_type"/>
<result property="workContent" column="work_content"/>
<result property="workerName" column="worker_name"/>
<result property="status" column="status"/>
<result property="remarks" column="remarks"/>
<result property="startedAt" column="started_at"/>
<result property="completedAt" column="completed_at"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findAll" resultMap="workMap">
SELECT w.id, w.tenant_code, w.store_id, s.store_name, w.work_type, w.work_content,
w.worker_name, w.status, w.remarks, w.started_at, w.completed_at, w.created_at
FROM esn_work_history w
LEFT JOIN esn_store s ON w.store_id = s.id
<where>
<if test="tenantCode != null and tenantCode != ''">AND w.tenant_code = #{tenantCode}</if>
<if test="storeId != null">AND w.store_id = #{storeId}</if>
<if test="status != null and status != ''">AND w.status = #{status}</if>
<if test="workType != null and workType != ''">AND w.work_type = #{workType}</if>
</where>
ORDER BY w.created_at DESC LIMIT 200
</select>
<select id="findById" resultMap="workMap">
SELECT w.id, w.tenant_code, w.store_id, s.store_name, w.work_type, w.work_content,
w.worker_name, w.status, w.remarks, w.started_at, w.completed_at, w.created_at
FROM esn_work_history w
LEFT JOIN esn_store s ON w.store_id = s.id
WHERE w.id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO esn_work_history (tenant_code, store_id, work_type, work_content,
worker_name, status, remarks, started_at)
VALUES (#{tenantCode}, #{storeId}, #{workType}, #{workContent},
#{workerName}, #{status}, #{remarks}, #{startedAt})
</insert>
<update id="update">
UPDATE esn_work_history SET
work_type = #{workType},
work_content = #{workContent},
worker_name = #{workerName},
status = #{status},
remarks = #{remarks},
started_at = #{startedAt},
completed_at = #{completedAt}
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM esn_work_history WHERE id = #{id}</delete>
<select id="countToday" resultType="long">
SELECT COUNT(*) FROM esn_work_history WHERE created_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
<select id="countCompletedToday" resultType="long">
SELECT COUNT(*) FROM esn_work_history
WHERE status = 'COMPLETED' AND completed_at::date = CURRENT_DATE
<if test="tenantCode != null and tenantCode != ''">AND tenant_code = #{tenantCode}</if>
</select>
</mapper>

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>zioinfo-esn — ESL 통합 관리 플랫폼</title>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3416
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "zioinfo-esn-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite --port 3014",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0",
"axios": "^1.7.0",
"@tanstack/react-query": "^5.0.0",
"recharts": "^2.12.0",
"lucide-react": "^0.400.0",
"date-fns": "^3.6.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5.5.0",
"vite": "^5.3.0",
"tailwindcss": "^3.4.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
frontend/public/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

49
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,49 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Layout from './components/Layout'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import StoreList from './pages/StoreList'
import TemplateList from './pages/TemplateList'
import PosCvtList from './pages/PosCvtList'
import AlarmList from './pages/AlarmList'
import HCoreStatus from './pages/HCoreStatus'
import WorkHistory from './pages/WorkHistory'
import FirmwareList from './pages/FirmwareList'
import UserList from './pages/UserList'
import ProductList from './pages/ProductList'
import TenantAdmin from './pages/TenantAdmin'
import AiAnalysis from './pages/AiAnalysis'
import TagBindingList from './pages/TagBindingList'
import UpdateQueueList from './pages/UpdateQueueList'
const qc = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={qc}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<Layout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/stores" element={<StoreList />} />
<Route path="/templates" element={<TemplateList />} />
<Route path="/pos-cvt" element={<PosCvtList />} />
<Route path="/tag-binding" element={<TagBindingList />} />
<Route path="/update-queue" element={<UpdateQueueList />} />
<Route path="/alarms" element={<AlarmList />} />
<Route path="/hcore" element={<HCoreStatus />} />
<Route path="/works" element={<WorkHistory />} />
<Route path="/firmware" element={<FirmwareList />} />
<Route path="/products" element={<ProductList />} />
<Route path="/users" element={<UserList />} />
<Route path="/tenants" element={<TenantAdmin />} />
<Route path="/ai" element={<AiAnalysis />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
)
}

105
frontend/src/api/client.ts Normal file
View File

@ -0,0 +1,105 @@
import axios from 'axios'
const api = axios.create({ baseURL: '' })
api.interceptors.request.use(config => {
const token = localStorage.getItem('esn_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
api.interceptors.response.use(
res => res,
err => {
if (err.response?.status === 401) {
localStorage.removeItem('esn_token')
if (location.pathname !== '/login') location.href = '/login'
}
return Promise.reject(err)
}
)
export default api
const unwrap = (p: Promise<any>) => p.then(r => r.data?.data)
// ── Auth ──────────────────────────────────────────────────────────────────
export const login = (username: string, password: string) =>
api.post('/api/auth/login', { username, password })
export const getMe = () => api.get('/api/auth/me')
export const logout = () => api.post('/api/auth/logout')
// ── Dashboard ────────────────────────────────────────────────────────────
export const getDashboard = (tenantCode?: string) =>
unwrap(api.get(`/api/dashboard${tenantCode ? `?tenantCode=${tenantCode}` : ''}`))
// ── Tenants ──────────────────────────────────────────────────────────────
export const getTenants = () => unwrap(api.get('/api/tenants'))
export const createTenant = (d: object) => unwrap(api.post('/api/tenants', d))
export const updateTenant = (id: number, d: object) => unwrap(api.put(`/api/tenants/${id}`, d))
export const deleteTenant = (id: number) => api.delete(`/api/tenants/${id}`)
// ── Stores ───────────────────────────────────────────────────────────────
export const getStores = (params?: object) => unwrap(api.get('/api/stores', { params }))
export const getStore = (id: number) => unwrap(api.get(`/api/stores/${id}`))
export const createStore = (d: object) => unwrap(api.post('/api/stores', d))
export const updateStore = (id: number, d: object) => unwrap(api.put(`/api/stores/${id}`, d))
export const deleteStore = (id: number) => api.delete(`/api/stores/${id}`)
// ── Store Groups ─────────────────────────────────────────────────────────
export const getStoreGroups = (tenantCode?: string) =>
unwrap(api.get('/api/store-groups', { params: { tenantCode } }))
export const createStoreGroup = (d: object) => unwrap(api.post('/api/store-groups', d))
export const updateStoreGroup = (id: number, d: object) => unwrap(api.put(`/api/store-groups/${id}`, d))
export const deleteStoreGroup = (id: number) => api.delete(`/api/store-groups/${id}`)
// ── Templates ────────────────────────────────────────────────────────────
export const getTemplates = (params?: object) => unwrap(api.get('/api/templates', { params }))
export const createTemplate = (d: object) => unwrap(api.post('/api/templates', d))
export const updateTemplate = (id: number, d: object) => unwrap(api.put(`/api/templates/${id}`, d))
export const deleteTemplate = (id: number) => api.delete(`/api/templates/${id}`)
// ── POS CVT ──────────────────────────────────────────────────────────────
export const getPosCvt = (params?: object) => unwrap(api.get('/api/pos-cvt', { params }))
export const processPosCvt = (id: number) => unwrap(api.put(`/api/pos-cvt/${id}/process`))
export const ignorePosCvt = (id: number) => unwrap(api.put(`/api/pos-cvt/${id}/ignore`))
// ── Alarms ───────────────────────────────────────────────────────────────
export const getAlarms = (params?: object) => unwrap(api.get('/api/alarms', { params }))
export const createAlarm = (d: object) => unwrap(api.post('/api/alarms', d))
export const resolveAlarm = (id: number, resolution: string) =>
unwrap(api.put(`/api/alarms/${id}/resolve`, { resolution }))
export const deleteAlarm = (id: number) => api.delete(`/api/alarms/${id}`)
// ── HCore ────────────────────────────────────────────────────────────────
export const getHCore = (params?: object) => unwrap(api.get('/api/hcore', { params }))
export const updateHCoreStatus = (id: number, status: string) =>
unwrap(api.put(`/api/hcore/${id}/status`, { status }))
// ── Works ────────────────────────────────────────────────────────────────
export const getWorks = (params?: object) => unwrap(api.get('/api/works', { params }))
export const createWork = (d: object) => unwrap(api.post('/api/works', d))
export const updateWork = (id: number, d: object) => unwrap(api.put(`/api/works/${id}`, d))
export const deleteWork = (id: number) => api.delete(`/api/works/${id}`)
// ── Firmware ─────────────────────────────────────────────────────────────
export const getFirmware = (params?: object) => unwrap(api.get('/api/firmware', { params }))
export const createFirmware = (d: object) => unwrap(api.post('/api/firmware', d))
export const deleteFirmware = (id: number) => api.delete(`/api/firmware/${id}`)
// ── Users ────────────────────────────────────────────────────────────────
export const getUsers = (params?: object) => unwrap(api.get('/api/users', { params }))
export const createUser = (d: object) => unwrap(api.post('/api/users', d))
export const updateUser = (id: number, d: object) => unwrap(api.put(`/api/users/${id}`, d))
export const deleteUser = (id: number) => api.delete(`/api/users/${id}`)
// ── Products ─────────────────────────────────────────────────────────────
export const getProducts = (params?: object) => unwrap(api.get('/api/products', { params }))
export const createProduct = (d: object) => unwrap(api.post('/api/products', d))
export const updateProduct = (id: number, d: object) => unwrap(api.put(`/api/products/${id}`, d))
export const deleteProduct = (id: number) => api.delete(`/api/products/${id}`)
// ── AI ───────────────────────────────────────────────────────────────────
export const analyzeAlarm = (d: object) => unwrap(api.post('/api/ai/analyze-alarm', d))
export const classifyPos = (d: object) => unwrap(api.post('/api/ai/classify-pos', d))
export const aiChat = (message: string) => unwrap(api.post('/api/ai/chat', { message }))

View File

@ -0,0 +1,16 @@
interface AlarmBadgeProps {
severity: string
}
export default function AlarmBadge({ severity }: AlarmBadgeProps) {
const map: Record<string, string> = {
CRITICAL: 'bg-red-500/20 text-red-400 border border-red-500/40',
HIGH: 'bg-orange-500/20 text-orange-400 border border-orange-500/40',
MEDIUM: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/40',
LOW: 'bg-gray-500/20 text-gray-400 border border-gray-500/40',
}
const cls = map[severity] || map.LOW
return (
<span className={`px-2 py-0.5 rounded text-xs font-medium ${cls}`}>{severity}</span>
)
}

Some files were not shown because too many files have changed in this diff Show More