feat(esn): zioinfo ESN ESL 통합 플랫폼 초기 구현
Spring Boot 3.5 / Java 17 + React 19 + PostgreSQL 단일 JAR. 멀티테넌트(LGINNOTEK/LGIT/EMART/ZIOINFO), HCore 게이트웨이 관제, POS 연동 가격 자동 업데이트, Ollama 온프레미스 AI 알람 분석·POS 분류. 레거시 ESN 6개 프로젝트(Spring Boot 1.5/Java 8) 현대화 통합. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
969fcd7284
64
Jenkinsfile
vendored
Normal file
64
Jenkinsfile
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
APP_NAME = 'zioinfo-esn'
|
||||
DEPLOY_DIR = '/opt/zioinfo-esn/src'
|
||||
SERVICE = 'zioinfo-esn'
|
||||
JAR_NAME = 'esn.jar'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Frontend') {
|
||||
steps {
|
||||
dir('frontend') {
|
||||
sh 'npm ci'
|
||||
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}/backend/target
|
||||
sudo cp backend/target/${JAR_NAME} ${DEPLOY_DIR}/backend/target/${JAR_NAME}
|
||||
sudo systemctl start ${SERVICE}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
stage('Health Check') {
|
||||
steps {
|
||||
sh """
|
||||
sleep 15
|
||||
curl -sf http://localhost:8015/actuator/health | grep -q UP || exit 1
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "zioinfo-esn 배포 성공"
|
||||
}
|
||||
failure {
|
||||
echo "zioinfo-esn 배포 실패 — 롤백 필요"
|
||||
}
|
||||
}
|
||||
}
|
||||
77
backend/pom.xml
Normal file
77
backend/pom.xml
Normal 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</finalName>
|
||||
</build>
|
||||
</project>
|
||||
30
backend/src/main/java/com/zioinfo/esn/EsnApplication.java
Normal file
30
backend/src/main/java/com/zioinfo/esn/EsnApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
37
backend/src/main/java/com/zioinfo/esn/auth/AuthService.java
Normal file
37
backend/src/main/java/com/zioinfo/esn/auth/AuthService.java
Normal 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) : ""
|
||||
);
|
||||
}
|
||||
}
|
||||
21
backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java
Normal file
21
backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java
Normal 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;
|
||||
}
|
||||
40
backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java
Normal file
40
backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java
Normal file
56
backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java
Normal 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); }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
68
backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java
Normal file
68
backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 분석 결과: 현재 시스템 상태를 검토하고 운영 매뉴얼을 참조하세요.";
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
23
backend/src/main/java/com/zioinfo/esn/config/WebConfig.java
Normal file
23
backend/src/main/java/com/zioinfo/esn/config/WebConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<Void> process(@PathVariable Long id) {
|
||||
service.process(id);
|
||||
return ApiResponse.ok("처리 완료", null);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
21
backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java
Normal file
21
backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
19
backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java
Normal file
19
backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java
Normal 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;
|
||||
}
|
||||
22
backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java
Normal file
22
backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java
Normal 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;
|
||||
}
|
||||
23
backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java
Normal file
23
backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java
Normal 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;
|
||||
}
|
||||
22
backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java
Normal file
22
backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
19
backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java
Normal file
19
backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java
Normal 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;
|
||||
}
|
||||
19
backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java
Normal file
19
backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java
Normal 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;
|
||||
}
|
||||
14
backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java
Normal file
14
backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
18
backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java
Normal file
18
backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java
Normal 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);
|
||||
}
|
||||
20
backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java
Normal file
20
backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java
Normal 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);
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.zioinfo.esn.service;
|
||||
|
||||
import com.zioinfo.esn.domain.PosCvtVo;
|
||||
import com.zioinfo.esn.mapper.PosCvtMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PosCvtService {
|
||||
private final PosCvtMapper mapper;
|
||||
|
||||
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); }
|
||||
|
||||
public void process(Long id) {
|
||||
mapper.updateStatus(id, "PROCESSED", null);
|
||||
}
|
||||
|
||||
public void markError(Long id, String errorMessage) {
|
||||
mapper.updateStatus(id, "ERROR", errorMessage);
|
||||
}
|
||||
|
||||
public void ignore(Long id) {
|
||||
mapper.updateStatus(id, "IGNORED", null);
|
||||
}
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
48
backend/src/main/resources/application.yml
Normal file
48
backend/src/main/resources/application.yml
Normal file
@ -0,0 +1,48 @@
|
||||
server:
|
||||
port: 8015
|
||||
|
||||
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
|
||||
262
backend/src/main/resources/db/schema.sql
Normal file
262
backend/src/main/resources/db/schema.sql
Normal file
@ -0,0 +1,262 @@
|
||||
-- ============================================================================
|
||||
-- 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;
|
||||
86
backend/src/main/resources/mapper/AlarmMapper.xml
Normal file
86
backend/src/main/resources/mapper/AlarmMapper.xml
Normal 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>
|
||||
46
backend/src/main/resources/mapper/DashboardMapper.xml
Normal file
46
backend/src/main/resources/mapper/DashboardMapper.xml
Normal 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>
|
||||
69
backend/src/main/resources/mapper/FirmwareMapper.xml
Normal file
69
backend/src/main/resources/mapper/FirmwareMapper.xml
Normal 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>
|
||||
99
backend/src/main/resources/mapper/HCoreMapper.xml
Normal file
99
backend/src/main/resources/mapper/HCoreMapper.xml
Normal 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>
|
||||
75
backend/src/main/resources/mapper/PosCvtMapper.xml
Normal file
75
backend/src/main/resources/mapper/PosCvtMapper.xml
Normal 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>
|
||||
74
backend/src/main/resources/mapper/ProductMapper.xml
Normal file
74
backend/src/main/resources/mapper/ProductMapper.xml
Normal 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>
|
||||
45
backend/src/main/resources/mapper/StoreGroupMapper.xml
Normal file
45
backend/src/main/resources/mapper/StoreGroupMapper.xml
Normal 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>
|
||||
84
backend/src/main/resources/mapper/StoreMapper.xml
Normal file
84
backend/src/main/resources/mapper/StoreMapper.xml
Normal 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>
|
||||
58
backend/src/main/resources/mapper/TemplateMapper.xml
Normal file
58
backend/src/main/resources/mapper/TemplateMapper.xml
Normal 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>
|
||||
47
backend/src/main/resources/mapper/TenantMapper.xml
Normal file
47
backend/src/main/resources/mapper/TenantMapper.xml
Normal 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>
|
||||
30
backend/src/main/resources/mapper/UserAuthMapper.xml
Normal file
30
backend/src/main/resources/mapper/UserAuthMapper.xml
Normal 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>
|
||||
61
backend/src/main/resources/mapper/UserMapper.xml
Normal file
61
backend/src/main/resources/mapper/UserMapper.xml
Normal 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>
|
||||
75
backend/src/main/resources/mapper/WorkMapper.xml
Normal file
75
backend/src/main/resources/mapper/WorkMapper.xml
Normal 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
13
frontend/index.html
Normal 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>
|
||||
30
frontend/package.json
Normal file
30
frontend/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "zioinfo-esn-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --port 3014",
|
||||
"build": "tsc && 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"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
45
frontend/src/App.tsx
Normal file
45
frontend/src/App.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
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'
|
||||
|
||||
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="/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
105
frontend/src/api/client.ts
Normal 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 }))
|
||||
16
frontend/src/components/AlarmBadge.tsx
Normal file
16
frontend/src/components/AlarmBadge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/Header.tsx
Normal file
34
frontend/src/components/Header.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { LogOut, User } from 'lucide-react'
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate()
|
||||
const user = localStorage.getItem('esn_user') || 'admin'
|
||||
const role = localStorage.getItem('esn_role') || 'ADMIN'
|
||||
const tenant = localStorage.getItem('esn_tenant') || 'ALL'
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('esn_token')
|
||||
localStorage.removeItem('esn_role')
|
||||
localStorage.removeItem('esn_user')
|
||||
localStorage.removeItem('esn_tenant')
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="h-14 bg-panel border-b border-edge flex items-center px-6 gap-4 flex-shrink-0">
|
||||
<div className="flex-1" />
|
||||
<span className="text-xs text-gray-400">테넌트: <span className="text-brand">{tenant}</span></span>
|
||||
<span className="text-xs text-gray-400">|</span>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-300">
|
||||
<User size={14} />
|
||||
<span>{user}</span>
|
||||
<span className="text-xs text-brand bg-brand/10 px-2 py-0.5 rounded">{role}</span>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="flex items-center gap-1.5 text-gray-400 hover:text-white text-sm">
|
||||
<LogOut size={14} />
|
||||
로그아웃
|
||||
</button>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
35
frontend/src/components/Layout.tsx
Normal file
35
frontend/src/components/Layout.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Outlet, Navigate } from 'react-router-dom'
|
||||
import Sidebar from './Sidebar'
|
||||
import Header from './Header'
|
||||
import { getMe } from '../api/client'
|
||||
|
||||
export default function Layout() {
|
||||
const token = localStorage.getItem('esn_token')
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
getMe()
|
||||
.then(r => {
|
||||
const me = r.data?.data || {}
|
||||
if (me.role) localStorage.setItem('esn_role', me.role)
|
||||
if (me.username) localStorage.setItem('esn_user', me.username)
|
||||
if (me.tenant) localStorage.setItem('esn_tenant', me.tenant)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [token])
|
||||
|
||||
if (!token) return <Navigate to="/login" replace />
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-ink">
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Header />
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
frontend/src/components/Sidebar.tsx
Normal file
52
frontend/src/components/Sidebar.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard, Store, FileText, RefreshCw,
|
||||
Bell, Cpu, ClipboardList, Zap, Users, Package, Building2, Brain
|
||||
} from 'lucide-react'
|
||||
|
||||
const nav = [
|
||||
{ to: '/dashboard', icon: LayoutDashboard, label: '대시보드' },
|
||||
{ to: '/stores', icon: Store, label: '매장 관리' },
|
||||
{ to: '/templates', icon: FileText, label: 'ESL 템플릿' },
|
||||
{ to: '/pos-cvt', icon: RefreshCw, label: 'POS 변환' },
|
||||
{ to: '/alarms', icon: Bell, label: '알람 관리' },
|
||||
{ to: '/hcore', icon: Cpu, label: 'HCore 장치' },
|
||||
{ to: '/works', icon: ClipboardList, label: '작업 이력' },
|
||||
{ to: '/firmware', icon: Zap, label: '펌웨어' },
|
||||
{ to: '/products', icon: Package, label: '상품/가격' },
|
||||
{ to: '/users', icon: Users, label: '사용자' },
|
||||
{ to: '/tenants', icon: Building2, label: '테넌트 관리' },
|
||||
{ to: '/ai', icon: Brain, label: 'AI 분석' },
|
||||
]
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="w-56 bg-panel flex flex-col border-r border-edge flex-shrink-0">
|
||||
<div className="h-14 flex items-center px-4 border-b border-edge">
|
||||
<span className="text-brand font-bold text-sm tracking-wide">ESN</span>
|
||||
<span className="ml-2 text-xs text-gray-400">ESL 통합 관리</span>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto py-3">
|
||||
{nav.map(({ to, icon: Icon, label }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
isActive
|
||||
? 'bg-brand/15 text-brand border-r-2 border-brand'
|
||||
: 'text-gray-400 hover:text-white hover:bg-edge/50'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon size={16} />
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-edge text-xs text-gray-500">
|
||||
zioinfo-esn v1.0
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/StatCard.tsx
Normal file
34
frontend/src/components/StatCard.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
|
||||
interface StatCardProps {
|
||||
title: string
|
||||
value: number | string
|
||||
icon: LucideIcon
|
||||
color?: string
|
||||
sub?: string
|
||||
}
|
||||
|
||||
export default function StatCard({ title, value, icon: Icon, color = 'brand', sub }: StatCardProps) {
|
||||
const colorMap: Record<string, string> = {
|
||||
brand: 'text-brand bg-brand/10',
|
||||
green: 'text-green-400 bg-green-400/10',
|
||||
red: 'text-red-400 bg-red-400/10',
|
||||
orange: 'text-orange-400 bg-orange-400/10',
|
||||
yellow: 'text-yellow-400 bg-yellow-400/10',
|
||||
accent: 'text-accent bg-accent/10',
|
||||
}
|
||||
const cls = colorMap[color] || colorMap.brand
|
||||
|
||||
return (
|
||||
<div className="bg-card border border-edge rounded-lg p-4 flex items-center gap-4">
|
||||
<div className={`p-3 rounded-lg ${cls}`}>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">{title}</p>
|
||||
<p className={`text-2xl font-bold ${cls.split(' ')[0]}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-gray-500">{sub}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
frontend/src/index.css
Normal file
13
frontend/src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-ink text-white;
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: #131927; }
|
||||
::-webkit-scrollbar-thumb { background: #26304a; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #00a0c8; }
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
85
frontend/src/pages/AiAnalysis.tsx
Normal file
85
frontend/src/pages/AiAnalysis.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { analyzeAlarm, classifyPos } from '../api/client'
|
||||
import { Brain, Zap, AlertTriangle, Tag } from 'lucide-react'
|
||||
|
||||
export default function AiAnalysis() {
|
||||
const [alarmText, setAlarmText] = useState('')
|
||||
const [alarmResult, setAlarmResult] = useState<string>('')
|
||||
const [posText, setPosText] = useState('')
|
||||
const [posResult, setPosResult] = useState<string>('')
|
||||
|
||||
const alarmMut = useMutation({
|
||||
mutationFn: analyzeAlarm,
|
||||
onSuccess: (data: any) => setAlarmResult(data.analysis || JSON.stringify(data)),
|
||||
onError: () => setAlarmResult('Ollama 응답 실패 — 서비스를 확인해주세요.'),
|
||||
})
|
||||
|
||||
const posMut = useMutation({
|
||||
mutationFn: classifyPos,
|
||||
onSuccess: (data: any) => setPosResult(data.category || JSON.stringify(data)),
|
||||
onError: () => setPosResult('Ollama 응답 실패 — 서비스를 확인해주세요.'),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<Brain size={20} className="text-brand" /> AI 분석
|
||||
</h1>
|
||||
|
||||
{/* Alarm Analysis */}
|
||||
<div className="bg-card border border-edge rounded-lg p-5">
|
||||
<h2 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
|
||||
<AlertTriangle size={16} className="text-yellow-400" /> 알람 원인 분석
|
||||
</h2>
|
||||
<textarea
|
||||
value={alarmText}
|
||||
onChange={e => setAlarmText(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="알람 메시지를 입력하세요 예: ESL-001 배터리 부족 경고 — Store: LG이노텍 구미, Device: ESL-A123"
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-gray-300 placeholder-gray-600 resize-none mb-3"
|
||||
/>
|
||||
<button
|
||||
onClick={() => alarmMut.mutate({ message: alarmText })}
|
||||
disabled={!alarmText.trim() || alarmMut.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-brand/20 text-brand border border-brand/40 rounded hover:bg-brand/30 disabled:opacity-40 text-sm"
|
||||
>
|
||||
<Zap size={14} />
|
||||
{alarmMut.isPending ? '분석 중...' : 'Ollama 분석'}
|
||||
</button>
|
||||
{alarmResult && (
|
||||
<div className="mt-4 bg-panel border border-edge rounded p-4 text-sm text-gray-300 whitespace-pre-wrap">
|
||||
{alarmResult}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* POS Classification */}
|
||||
<div className="bg-card border border-edge rounded-lg p-5">
|
||||
<h2 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
|
||||
<Tag size={16} className="text-accent" /> POS 데이터 자동 분류
|
||||
</h2>
|
||||
<textarea
|
||||
value={posText}
|
||||
onChange={e => setPosText(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="POS 데이터를 입력하세요 예: 상품코드=P001, 상품명=과자, 가격=1500, 판매수량=100, 점포=이마트 광명점"
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-gray-300 placeholder-gray-600 resize-none mb-3"
|
||||
/>
|
||||
<button
|
||||
onClick={() => posMut.mutate({ data: posText })}
|
||||
disabled={!posText.trim() || posMut.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-accent/20 text-accent border border-accent/40 rounded hover:bg-accent/30 disabled:opacity-40 text-sm"
|
||||
>
|
||||
<Zap size={14} />
|
||||
{posMut.isPending ? '분류 중...' : 'Ollama 분류'}
|
||||
</button>
|
||||
{posResult && (
|
||||
<div className="mt-4 bg-panel border border-edge rounded p-4 text-sm text-gray-300 whitespace-pre-wrap">
|
||||
{posResult}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
frontend/src/pages/AlarmList.tsx
Normal file
105
frontend/src/pages/AlarmList.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { CheckCircle, Trash2 } from 'lucide-react'
|
||||
import AlarmBadge from '../components/AlarmBadge'
|
||||
import { getAlarms, resolveAlarm, deleteAlarm } from '../api/client'
|
||||
|
||||
export default function AlarmList() {
|
||||
const qc = useQueryClient()
|
||||
const [severity, setSeverity] = useState('')
|
||||
const [status, setStatus] = useState('OPEN')
|
||||
|
||||
const { data: alarms = [], isLoading } = useQuery({
|
||||
queryKey: ['alarms', severity, status],
|
||||
queryFn: () => getAlarms({ severity: severity || undefined, status: status || undefined }),
|
||||
})
|
||||
|
||||
const resolveMut = useMutation({
|
||||
mutationFn: ({ id, resolution }: { id: number, resolution: string }) =>
|
||||
resolveAlarm(id, resolution),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['alarms'] }),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteAlarm,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['alarms'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">알람 관리</h1>
|
||||
<div className="flex gap-2">
|
||||
<select value={severity} onChange={e => setSeverity(e.target.value)}
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300">
|
||||
<option value="">전체 심각도</option>
|
||||
{['CRITICAL','HIGH','MEDIUM','LOW'].map(s =>
|
||||
<option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<select value={status} onChange={e => setStatus(e.target.value)}
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300">
|
||||
<option value="">전체 상태</option>
|
||||
{['OPEN','ACKNOWLEDGED','RESOLVED'].map(s =>
|
||||
<option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['매장','유형','심각도','메시지','상태','발생일시','액션'].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(alarms as any[]).map((a: any) => (
|
||||
<tr key={a.id} className="border-b border-edge hover:bg-edge/30 transition-colors">
|
||||
<td className="px-4 py-3 text-gray-300">{a.storeName || '-'}</td>
|
||||
<td className="px-4 py-3 text-gray-300">{a.alarmType}</td>
|
||||
<td className="px-4 py-3"><AlarmBadge severity={a.severity} /></td>
|
||||
<td className="px-4 py-3 text-gray-400 max-w-xs truncate">{a.message}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
a.status === 'RESOLVED' ? 'bg-green-500/20 text-green-400' :
|
||||
a.status === 'OPEN' ? 'bg-red-500/20 text-red-400' :
|
||||
'bg-yellow-500/20 text-yellow-400'
|
||||
}`}>{a.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-500 text-xs">
|
||||
{a.createdAt ? new Date(a.createdAt).toLocaleString('ko-KR') : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2">
|
||||
{a.status !== 'RESOLVED' && (
|
||||
<button
|
||||
onClick={() => resolveMut.mutate({ id: a.id, resolution: '운영자 확인 후 해결' })}
|
||||
className="text-green-400 hover:text-green-300"
|
||||
title="해결 처리"
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { if (confirm('삭제?')) deleteMut.mutate(a.id) }}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(alarms as any[]).length === 0 && (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">알람이 없습니다</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
frontend/src/pages/Dashboard.tsx
Normal file
101
frontend/src/pages/Dashboard.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
||||
import { Store, Bell, Cpu, ClipboardList, RefreshCw, AlertTriangle } from 'lucide-react'
|
||||
import StatCard from '../components/StatCard'
|
||||
import { getDashboard } from '../api/client'
|
||||
|
||||
const COLORS = ['#00a0c8','#3ddc97','#f59e0b','#ef4444']
|
||||
|
||||
export default function Dashboard() {
|
||||
const tenant = localStorage.getItem('esn_tenant') || undefined
|
||||
const { data: d, isLoading } = useQuery({
|
||||
queryKey: ['dashboard', tenant],
|
||||
queryFn: () => getDashboard(tenant),
|
||||
refetchInterval: 30000,
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
if (!d) return <div className="text-red-400 p-8">데이터를 가져올 수 없습니다.</div>
|
||||
|
||||
const deviceData = [
|
||||
{ name: 'ONLINE', value: d.onlineDevices || 0 },
|
||||
{ name: 'OFFLINE', value: d.offlineDevices || 0 },
|
||||
]
|
||||
|
||||
const workData = [
|
||||
{ name: '오늘 작업', value: d.totalWorkToday || 0 },
|
||||
{ name: '완료', value: d.completedWorkToday || 0 },
|
||||
{ name: 'POS 대기', value: d.pendingPosCvt || 0 },
|
||||
{ name: 'POS 처리', value: d.processedPosCvtToday || 0 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-xl font-semibold text-white">대시보드</h1>
|
||||
|
||||
{/* 주요 지표 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<StatCard title="전체 매장" value={d.totalStores || 0} icon={Store} color="brand" />
|
||||
<StatCard title="운영 매장" value={d.activeStores || 0} icon={Store} color="green" />
|
||||
<StatCard title="미해결 알람" value={d.unresolvedAlarms || 0} icon={Bell} color="red" />
|
||||
<StatCard title="긴급 알람" value={d.criticalAlarms || 0} icon={AlertTriangle} color="red" />
|
||||
<StatCard title="온라인 장치" value={d.onlineDevices || 0} icon={Cpu} color="green" />
|
||||
<StatCard title="POS 대기" value={d.pendingPosCvt || 0} icon={RefreshCw} color="yellow" />
|
||||
</div>
|
||||
|
||||
{/* 차트 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<h2 className="text-sm font-medium text-gray-300 mb-4">장치 상태 현황</h2>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<PieChart>
|
||||
<Pie data={deviceData} cx="50%" cy="50%" outerRadius={70}
|
||||
dataKey="value" label={({ name, value }) => `${name}: ${value}`}>
|
||||
{deviceData.map((_, i) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ background: '#1a2234', border: '1px solid #26304a', color: '#fff' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<h2 className="text-sm font-medium text-gray-300 mb-4">오늘의 작업/POS 현황</h2>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={workData}>
|
||||
<XAxis dataKey="name" tick={{ fill: '#9ca3af', fontSize: 11 }} />
|
||||
<YAxis tick={{ fill: '#9ca3af', fontSize: 11 }} />
|
||||
<Tooltip contentStyle={{ background: '#1a2234', border: '1px solid #26304a', color: '#fff' }} />
|
||||
<Bar dataKey="value" fill="#00a0c8" radius={[3,3,0,0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<p className="text-xs text-gray-400">오늘 작업</p>
|
||||
<p className="text-2xl font-bold text-brand">{d.totalWorkToday || 0}</p>
|
||||
<p className="text-xs text-gray-500">완료: {d.completedWorkToday || 0}</p>
|
||||
</div>
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<p className="text-xs text-gray-400">전체 알람</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{d.totalAlarms || 0}</p>
|
||||
<p className="text-xs text-gray-500">미해결: {d.unresolvedAlarms || 0}</p>
|
||||
</div>
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<p className="text-xs text-gray-400">전체 장치</p>
|
||||
<p className="text-2xl font-bold text-accent">{d.totalDevices || 0}</p>
|
||||
<p className="text-xs text-gray-500">오프라인: {d.offlineDevices || 0}</p>
|
||||
</div>
|
||||
<div className="bg-card border border-edge rounded-lg p-4">
|
||||
<p className="text-xs text-gray-400">POS 처리(오늘)</p>
|
||||
<p className="text-2xl font-bold text-green-400">{d.processedPosCvtToday || 0}</p>
|
||||
<p className="text-xs text-gray-500">대기: {d.pendingPosCvt || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
frontend/src/pages/FirmwareList.tsx
Normal file
64
frontend/src/pages/FirmwareList.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Trash2, Star } from 'lucide-react'
|
||||
import { getFirmware, deleteFirmware } from '../api/client'
|
||||
|
||||
export default function FirmwareList() {
|
||||
const qc = useQueryClient()
|
||||
const { data: fws = [], isLoading } = useQuery({
|
||||
queryKey: ['firmware'],
|
||||
queryFn: () => getFirmware(),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteFirmware,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['firmware'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-semibold text-white">펌웨어 관리</h1>
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['버전','테넌트','장치유형','파일명','크기','체크섬','최신','등록일','액션'].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(fws as any[]).map((f: any) => (
|
||||
<tr key={f.id} className="border-b border-edge hover:bg-edge/30">
|
||||
<td className="px-4 py-3 text-brand font-mono text-xs">{f.firmwareVersion}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">{f.tenantCode}</td>
|
||||
<td className="px-4 py-3 text-gray-300 text-xs">{f.deviceType}</td>
|
||||
<td className="px-4 py-3 text-gray-300 text-xs">{f.fileName}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">
|
||||
{f.fileSize ? `${(f.fileSize / 1024).toFixed(1)}KB` : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-500 text-xs font-mono truncate max-w-[80px]">
|
||||
{f.checksum ? f.checksum.substring(0, 8) + '...' : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{f.latest && <Star size={14} className="text-yellow-400" fill="currentColor" />}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-500 text-xs">
|
||||
{f.createdAt ? new Date(f.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(f.id) }}
|
||||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(fws as any[]).length === 0 && (
|
||||
<tr><td colSpan={9} className="px-4 py-8 text-center text-gray-500">펌웨어가 없습니다</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
frontend/src/pages/HCoreStatus.tsx
Normal file
94
frontend/src/pages/HCoreStatus.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Cpu, Wifi, WifiOff, Battery, AlertTriangle } from 'lucide-react'
|
||||
import { getHCore, updateHCoreStatus } from '../api/client'
|
||||
|
||||
const statusIcon: Record<string, React.ReactNode> = {
|
||||
ONLINE: <Wifi size={14} className="text-green-400" />,
|
||||
OFFLINE: <WifiOff size={14} className="text-red-400" />,
|
||||
ERROR: <AlertTriangle size={14} className="text-orange-400" />,
|
||||
UPDATING: <Cpu size={14} className="text-yellow-400" />,
|
||||
}
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
ONLINE: 'border-green-500/30 bg-green-500/5',
|
||||
OFFLINE: 'border-red-500/30 bg-red-500/5',
|
||||
ERROR: 'border-orange-500/30 bg-orange-500/5',
|
||||
UPDATING: 'border-yellow-500/30 bg-yellow-500/5',
|
||||
}
|
||||
|
||||
export default function HCoreStatus() {
|
||||
const qc = useQueryClient()
|
||||
const [deviceType, setDeviceType] = React.useState('')
|
||||
const [status, setStatus] = React.useState('')
|
||||
|
||||
const { data: devices = [], isLoading } = useQuery({
|
||||
queryKey: ['hcore', deviceType, status],
|
||||
queryFn: () => getHCore({ deviceType: deviceType || undefined, status: status || undefined }),
|
||||
refetchInterval: 15000,
|
||||
})
|
||||
|
||||
const statusMut = useMutation({
|
||||
mutationFn: ({ id, status }: { id: number; status: string }) => updateHCoreStatus(id, status),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['hcore'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">HCore 장치 현황</h1>
|
||||
<div className="flex gap-2">
|
||||
<select value={deviceType} onChange={e => setDeviceType(e.target.value)}
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300">
|
||||
<option value="">전체 유형</option>
|
||||
{['GATEWAY','HUB','ESL_DEVICE'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
<select value={status} onChange={e => setStatus(e.target.value)}
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300">
|
||||
<option value="">전체 상태</option>
|
||||
{['ONLINE','OFFLINE','ERROR','UPDATING'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{(devices as any[]).map((d: any) => (
|
||||
<div key={d.id} className={`border rounded-lg p-4 ${statusColor[d.status] || 'border-edge bg-card'}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{statusIcon[d.status]}
|
||||
<span className="text-xs font-mono text-gray-300">{d.deviceId}</span>
|
||||
</div>
|
||||
<span className="text-xs text-brand">{d.deviceType}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">{d.storeName || '미배정'}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">FW: {d.firmwareVersion || '-'}</p>
|
||||
{d.ipAddress && <p className="text-xs text-gray-600 font-mono">{d.ipAddress}</p>}
|
||||
{d.batteryLevel != null && (
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
<Battery size={12} className={d.batteryLevel < 20 ? 'text-red-400' : 'text-gray-400'} />
|
||||
<span className={`text-xs ${d.batteryLevel < 20 ? 'text-red-400' : 'text-gray-400'}`}>
|
||||
{d.batteryLevel}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{d.status === 'OFFLINE' && (
|
||||
<button
|
||||
onClick={() => statusMut.mutate({ id: d.id, status: 'ONLINE' })}
|
||||
className="mt-2 text-xs text-brand hover:underline"
|
||||
>
|
||||
온라인으로 변경
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(devices as any[]).length === 0 && (
|
||||
<div className="col-span-4 text-center text-gray-500 py-8">장치가 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
import React from 'react'
|
||||
68
frontend/src/pages/Login.tsx
Normal file
68
frontend/src/pages/Login.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { login } from '../api/client'
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await login(username, password)
|
||||
const token = res.data?.data?.token
|
||||
if (!token) throw new Error('토큰 없음')
|
||||
localStorage.setItem('esn_token', token)
|
||||
navigate('/dashboard')
|
||||
} catch {
|
||||
setError('로그인 실패: 아이디/비밀번호를 확인하세요.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-ink flex items-center justify-center">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-brand">zioinfo-esn</h1>
|
||||
<p className="text-sm text-gray-400 mt-1">ESL 통합 관리 플랫폼</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="bg-card border border-edge rounded-lg p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">아이디</label>
|
||||
<input
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
placeholder="admin"
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-brand focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">비밀번호</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-brand focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-xs">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-brand hover:bg-brand2 text-white py-2 rounded text-sm font-medium transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? '로그인 중...' : '로그인'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
frontend/src/pages/PosCvtList.tsx
Normal file
94
frontend/src/pages/PosCvtList.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-react'
|
||||
import { getPosCvt, processPosCvt, ignorePosCvt } from '../api/client'
|
||||
|
||||
export default function PosCvtList() {
|
||||
const qc = useQueryClient()
|
||||
const [status, setStatus] = useState('PENDING')
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
const { data: items = [], isLoading } = useQuery({
|
||||
queryKey: ['pos-cvt', status, keyword],
|
||||
queryFn: () => getPosCvt({ status: status || undefined, keyword: keyword || undefined }),
|
||||
})
|
||||
|
||||
const processMut = useMutation({
|
||||
mutationFn: processPosCvt,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pos-cvt'] }),
|
||||
})
|
||||
const ignoreMut = useMutation({
|
||||
mutationFn: ignorePosCvt,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pos-cvt'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">POS 가격 변환</h1>
|
||||
<div className="flex gap-2">
|
||||
<input value={keyword} onChange={e => setKeyword(e.target.value)}
|
||||
placeholder="상품코드·상품명 검색"
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300 w-48" />
|
||||
<select value={status} onChange={e => setStatus(e.target.value)}
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300">
|
||||
{['','PENDING','PROCESSED','ERROR','IGNORED'].map(s =>
|
||||
<option key={s} value={s}>{s || '전체'}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['테넌트','매장','상품코드','상품명','정가','판매가','상태','수신일시','액션'].map(h => (
|
||||
<th key={h} className="px-3 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(items as any[]).map((p: any) => (
|
||||
<tr key={p.id} className="border-b border-edge hover:bg-edge/30">
|
||||
<td className="px-3 py-3 text-brand text-xs">{p.tenantCode}</td>
|
||||
<td className="px-3 py-3 text-gray-300 text-xs">{p.storeName || '-'}</td>
|
||||
<td className="px-3 py-3 text-gray-300 font-mono text-xs">{p.productCode}</td>
|
||||
<td className="px-3 py-3 text-white text-xs">{p.productName}</td>
|
||||
<td className="px-3 py-3 text-gray-300 text-xs">{p.price?.toLocaleString()}</td>
|
||||
<td className="px-3 py-3 text-accent text-xs">{p.salePrice?.toLocaleString()}</td>
|
||||
<td className="px-3 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
p.status === 'PROCESSED' ? 'bg-green-500/20 text-green-400' :
|
||||
p.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
p.status === 'ERROR' ? 'bg-red-500/20 text-red-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>{p.status}</span>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-gray-500 text-xs">
|
||||
{p.createdAt ? new Date(p.createdAt).toLocaleString('ko-KR') : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3">
|
||||
{p.status === 'PENDING' && (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => processMut.mutate(p.id)} title="처리" className="text-green-400 hover:text-green-300">
|
||||
<CheckCircle size={14} />
|
||||
</button>
|
||||
<button onClick={() => ignoreMut.mutate(p.id)} title="무시" className="text-gray-400 hover:text-gray-300">
|
||||
<MinusCircle size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(items as any[]).length === 0 && (
|
||||
<tr><td colSpan={9} className="px-4 py-8 text-center text-gray-500">데이터가 없습니다</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
69
frontend/src/pages/ProductList.tsx
Normal file
69
frontend/src/pages/ProductList.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { getProducts, deleteProduct } from '../api/client'
|
||||
|
||||
export default function ProductList() {
|
||||
const qc = useQueryClient()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
const { data: products = [], isLoading } = useQuery({
|
||||
queryKey: ['products', keyword],
|
||||
queryFn: () => getProducts({ keyword: keyword || undefined }),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteProduct,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['products'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">상품/가격 관리</h1>
|
||||
<input value={keyword} onChange={e => setKeyword(e.target.value)}
|
||||
placeholder="상품명·코드 검색"
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300 w-48" />
|
||||
</div>
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['테넌트','매장','상품코드','상품명','카테고리','정가','판매가','화폐','상태','액션'].map(h => (
|
||||
<th key={h} className="px-3 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(products as any[]).map((p: any) => (
|
||||
<tr key={p.id} className="border-b border-edge hover:bg-edge/30">
|
||||
<td className="px-3 py-3 text-brand text-xs">{p.tenantCode}</td>
|
||||
<td className="px-3 py-3 text-gray-400 text-xs">{p.storeName || '-'}</td>
|
||||
<td className="px-3 py-3 text-gray-300 font-mono text-xs">{p.productCode}</td>
|
||||
<td className="px-3 py-3 text-white">{p.productName}</td>
|
||||
<td className="px-3 py-3 text-gray-400 text-xs">{p.category}</td>
|
||||
<td className="px-3 py-3 text-gray-300 text-xs">{p.price?.toLocaleString()}</td>
|
||||
<td className="px-3 py-3 text-accent text-xs font-medium">{p.salePrice?.toLocaleString()}</td>
|
||||
<td className="px-3 py-3 text-gray-500 text-xs">{p.currency}</td>
|
||||
<td className="px-3 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${p.active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}`}>
|
||||
{p.active ? '활성' : '비활성'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-3">
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(p.id) }}
|
||||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(products as any[]).length === 0 && (
|
||||
<tr><td colSpan={10} className="px-4 py-8 text-center text-gray-500">상품이 없습니다</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
138
frontend/src/pages/StoreList.tsx
Normal file
138
frontend/src/pages/StoreList.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Edit, Trash2, X } from 'lucide-react'
|
||||
import { getStores, createStore, updateStore, deleteStore } from '../api/client'
|
||||
|
||||
export default function StoreList() {
|
||||
const qc = useQueryClient()
|
||||
const [modal, setModal] = useState<any>(null)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
const { data: stores = [], isLoading } = useQuery({
|
||||
queryKey: ['stores', keyword],
|
||||
queryFn: () => getStores({ keyword: keyword || undefined }),
|
||||
})
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: (d: any) => d.id ? updateStore(d.id, d) : createStore(d),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['stores'] }); setModal(null) },
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteStore,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['stores'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">매장 관리</h1>
|
||||
<div className="flex gap-2">
|
||||
<input value={keyword} onChange={e => setKeyword(e.target.value)}
|
||||
placeholder="매장명·코드 검색"
|
||||
className="bg-panel border border-edge rounded px-3 py-1.5 text-sm text-gray-300 w-48" />
|
||||
<button onClick={() => setModal({})}
|
||||
className="flex items-center gap-1.5 bg-brand hover:bg-brand2 text-white px-3 py-1.5 rounded text-sm">
|
||||
<Plus size={14} /> 매장 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['테넌트','코드','매장명','주소','담당자','상태','액션'].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(stores as any[]).map((s: any) => (
|
||||
<tr key={s.id} className="border-b border-edge hover:bg-edge/30">
|
||||
<td className="px-4 py-3 text-brand text-xs">{s.tenantCode}</td>
|
||||
<td className="px-4 py-3 text-gray-300 font-mono text-xs">{s.storeCode}</td>
|
||||
<td className="px-4 py-3 text-white">{s.storeName}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs truncate max-w-xs">{s.address}</td>
|
||||
<td className="px-4 py-3 text-gray-400">{s.managerName}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${s.active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}`}>
|
||||
{s.active ? '운영' : '중지'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setModal(s)} className="text-brand hover:text-blue-300"><Edit size={14} /></button>
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(s.id) }} className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(stores as any[]).length === 0 && (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">매장이 없습니다</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{modal && (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div className="bg-card border border-edge rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between mb-4">
|
||||
<h2 className="font-semibold">{modal.id ? '매장 수정' : '매장 추가'}</h2>
|
||||
<button onClick={() => setModal(null)}><X size={16} /></button>
|
||||
</div>
|
||||
<StoreForm initial={modal} onSave={(d: any) => saveMut.mutate(d)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StoreForm({ initial, onSave }: { initial: any; onSave: (d: any) => void }) {
|
||||
const [f, setF] = useState({
|
||||
tenantCode: initial.tenantCode || '',
|
||||
storeCode: initial.storeCode || '',
|
||||
storeName: initial.storeName || '',
|
||||
address: initial.address || '',
|
||||
phone: initial.phone || '',
|
||||
managerName: initial.managerName || '',
|
||||
active: initial.active !== false,
|
||||
id: initial.id,
|
||||
})
|
||||
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
|
||||
setF(p => ({ ...p, [k]: e.target.value }))
|
||||
|
||||
return (
|
||||
<form onSubmit={e => { e.preventDefault(); onSave(f) }} className="space-y-3">
|
||||
{[
|
||||
{ label: '테넌트', key: 'tenantCode', type: 'select',
|
||||
opts: ['LGINNOTEK','LGIT','EMART','ZIOINFO'] },
|
||||
{ label: '매장코드', key: 'storeCode' },
|
||||
{ label: '매장명', key: 'storeName' },
|
||||
{ label: '주소', key: 'address' },
|
||||
{ label: '전화', key: 'phone' },
|
||||
{ label: '담당자', key: 'managerName' },
|
||||
].map(({ label, key, type, opts }) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs text-gray-400 mb-1">{label}</label>
|
||||
{type === 'select' ? (
|
||||
<select value={(f as any)[key]} onChange={set(key)}
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
|
||||
{(opts || []).map(o => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input value={(f as any)[key]} onChange={set(key)}
|
||||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button type="submit" className="w-full bg-brand hover:bg-brand2 text-white py-2 rounded text-sm">
|
||||
저장
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
51
frontend/src/pages/TemplateList.tsx
Normal file
51
frontend/src/pages/TemplateList.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { getTemplates, createTemplate, deleteTemplate } from '../api/client'
|
||||
|
||||
export default function TemplateList() {
|
||||
const qc = useQueryClient()
|
||||
const { data: templates = [], isLoading } = useQuery({
|
||||
queryKey: ['templates'],
|
||||
queryFn: () => getTemplates(),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteTemplate,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['templates'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">ESL 템플릿</h1>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{(templates as any[]).map((t: any) => (
|
||||
<div key={t.id} className="bg-card border border-edge rounded-lg p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{t.templateName}</p>
|
||||
<p className="text-xs text-brand mt-0.5">{t.tenantCode} · {t.templateType}</p>
|
||||
</div>
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(t.id) }}
|
||||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</div>
|
||||
<div className="bg-panel border border-edge rounded p-3 flex items-center justify-center"
|
||||
style={{ height: Math.min((t.height || 76) / 2 + 40, 120) }}>
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-gray-400">{t.width}×{t.height}px</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{t.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">코드: {t.templateCode}</p>
|
||||
</div>
|
||||
))}
|
||||
{(templates as any[]).length === 0 && (
|
||||
<div className="col-span-3 text-center text-gray-500 py-8">템플릿이 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
frontend/src/pages/TenantAdmin.tsx
Normal file
52
frontend/src/pages/TenantAdmin.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { getTenants, deleteTenant } from '../api/client'
|
||||
import { Building2, Trash2 } from 'lucide-react'
|
||||
|
||||
export default function TenantAdmin() {
|
||||
const qc = useQueryClient()
|
||||
const { data: tenants = [], isLoading } = useQuery({
|
||||
queryKey: ['tenants'],
|
||||
queryFn: () => getTenants(),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteTenant,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenants'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
const role = localStorage.getItem('esn_role')
|
||||
if (role !== 'ADMIN') return (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-400">관리자 전용 메뉴입니다.</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-semibold text-white">테넌트 관리</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{(tenants as any[]).map((t: any) => (
|
||||
<div key={t.id} className="bg-card border border-edge rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 size={18} className="text-brand" />
|
||||
<span className="font-bold text-brand">{t.tenantCode}</span>
|
||||
</div>
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(t.id) }}
|
||||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</div>
|
||||
<p className="text-sm text-white">{t.tenantName}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{t.description}</p>
|
||||
<div className="mt-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${t.active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}`}>
|
||||
{t.active ? '운영중' : '비활성'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
frontend/src/pages/UserList.tsx
Normal file
64
frontend/src/pages/UserList.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { getUsers, deleteUser } from '../api/client'
|
||||
|
||||
export default function UserList() {
|
||||
const qc = useQueryClient()
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => getUsers(),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteUser,
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-semibold text-white">사용자 관리</h1>
|
||||
<div className="bg-card border border-edge rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-panel border-b border-edge">
|
||||
<tr>
|
||||
{['아이디','테넌트','역할','이메일','전화','상태','마지막 로그인','액션'].map(h => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs text-gray-400 font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(users as any[]).map((u: any) => (
|
||||
<tr key={u.id} className="border-b border-edge hover:bg-edge/30">
|
||||
<td className="px-4 py-3 text-white font-medium">{u.username}</td>
|
||||
<td className="px-4 py-3 text-brand text-xs">{u.tenantCode || 'SUPER'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
u.role === 'ADMIN' ? 'bg-red-500/20 text-red-400' :
|
||||
u.role === 'MANAGER' ? 'bg-brand/20 text-brand' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>{u.role}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">{u.email || '-'}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">{u.phone || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${u.active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}`}>
|
||||
{u.active ? '활성' : '비활성'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-500 text-xs">
|
||||
{u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString('ko-KR') : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(u.id) }}
|
||||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user