commit 969fcd7284ad0dee8bed5e43fa49f963ec793e47 Author: DESKTOP-TKLFCPR\ython Date: Tue Jun 16 01:07:05 2026 +0900 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 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..cc99583 --- /dev/null +++ b/Jenkinsfile @@ -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 배포 실패 — 롤백 필요" + } + } +} diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..617481f --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.zioinfo + zioinfo-esn + 1.0.0 + zioinfo-esn + ESL(전자 가격표) 통합 관리 플랫폼 — 레거시 Spring Boot 1.5 현대화 (Spring Boot 3.5 + Java 17 + React 19) + + + 17 + 0.12.6 + 3.0.3 + 42.7.3 + + + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-validation + org.springframework.bootspring-boot-starter-actuator + org.springframework.bootspring-boot-starter-aop + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.version} + + + + + org.postgresql + postgresql + ${postgresql.version} + + + + io.jsonwebtokenjjwt-api${jjwt.version} + io.jsonwebtokenjjwt-impl${jjwt.version}runtime + io.jsonwebtokenjjwt-jackson${jjwt.version}runtime + + + org.projectlomboklomboktrue + + + org.springframework.bootspring-boot-starter-testtest + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + esn + + diff --git a/backend/src/main/java/com/zioinfo/esn/EsnApplication.java b/backend/src/main/java/com/zioinfo/esn/EsnApplication.java new file mode 100644 index 0000000..4855edf --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/EsnApplication.java @@ -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 통합 관리 플랫폼. + * + *

레거시 Spring Boot 1.5 / Java 8 ESL 시스템 6개를 Spring Boot 3.5 / Java 17 단일 플랫폼으로 재구현. + * 멀티테넌트(LGINNOTEK·LGIT·EMART·ZIOINFO), 매장/HCore 장치 관리, POS 가격 연동, + * 알람 모니터링, 펌웨어 관리, Ollama 온프레미스 AI 이상 감지. + * + *

보안 불변 규칙: 외부 AI API 절대 금지 (Ollama localhost:11434만 허용), + * 자격증명은 AES-256-GCM 암호화 저장, API 응답에서 password_hash 완전 제외. + * + *

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); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java new file mode 100644 index 0000000..957e896 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java @@ -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> 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> me(@RequestHeader("Authorization") String header) { + String token = header.replace("Bearer ", ""); + return ApiResponse.ok(authService.me(token)); + } + + @PostMapping("/logout") + public ApiResponse logout() { + // JWT stateless — 클라이언트에서 토큰 삭제 + return ApiResponse.ok("로그아웃 성공", null); + } + + record LoginRequest(String username, String password) {} +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java new file mode 100644 index 0000000..58a69bb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/AuthService.java @@ -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 me(String token) { + return Map.of( + "username", jwtUtil.getUsername(token), + "role", jwtUtil.getRole(token), + "tenant", jwtUtil.getTenant(token) != null ? jwtUtil.getTenant(token) : "" + ); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java b/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java new file mode 100644 index 0000000..dc6b130 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/EsnUser.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java new file mode 100644 index 0000000..d8c39ee --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtFilter.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java new file mode 100644 index 0000000..06d23f0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/JwtUtil.java @@ -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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java b/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java new file mode 100644 index 0000000..f834e7d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/auth/mapper/UserAuthMapper.java @@ -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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java b/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java new file mode 100644 index 0000000..c4a45d2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/common/ApiResponse.java @@ -0,0 +1,27 @@ +package com.zioinfo.esn.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ApiResponse { + private boolean success; + private String message; + private T data; + private LocalDateTime timestamp; + + public static ApiResponse ok(T data) { + return new ApiResponse<>(true, "OK", data, LocalDateTime.now()); + } + + public static ApiResponse ok(String message, T data) { + return new ApiResponse<>(true, message, data, LocalDateTime.now()); + } + + public static ApiResponse fail(String message) { + return new ApiResponse<>(false, message, null, LocalDateTime.now()); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java b/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java new file mode 100644 index 0000000..615ba21 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/CryptoUtil.java @@ -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); + } + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java new file mode 100644 index 0000000..a907d38 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/OllamaClient.java @@ -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 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 분석 결과: 현재 시스템 상태를 검토하고 운영 매뉴얼을 참조하세요."; + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java new file mode 100644 index 0000000..df39263 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/SecurityConfig.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java b/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java new file mode 100644 index 0000000..0a843e5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/config/WebConfig.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/AiController.java b/backend/src/main/java/com/zioinfo/esn/controller/AiController.java new file mode 100644 index 0000000..ba727e9 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/AiController.java @@ -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> analyzeAlarm(@RequestBody Map 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> classifyPos(@RequestBody Map 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> chat(@RequestBody Map body) { + String message = body.getOrDefault("message", ""); + String result = ollama.chat(message, "ESL 전자 가격표 통합 관리 플랫폼 운영 전문가입니다."); + return ApiResponse.ok(Map.of("response", result)); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java b/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java new file mode 100644 index 0000000..390ecfa --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/AlarmController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody AlarmVo v) { + return ApiResponse.ok("알람 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody AlarmVo v) { + return ApiResponse.ok("알람 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/resolve") + public ApiResponse resolve(@PathVariable Long id, + @RequestBody Map 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 delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("알람 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java b/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java new file mode 100644 index 0000000..f611990 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/DashboardController.java @@ -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 summary(@RequestParam(required = false) String tenantCode) { + return ApiResponse.ok(mapper.getSummary(tenantCode)); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java new file mode 100644 index 0000000..f5a0944 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/FirmwareController.java @@ -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( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String deviceType) { + return ApiResponse.ok(service.list(tenantCode, deviceType)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody FirmwareVo v) { + return ApiResponse.ok("펌웨어 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody FirmwareVo v) { + return ApiResponse.ok("펌웨어 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("펌웨어 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java b/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java new file mode 100644 index 0000000..fb848f2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/HCoreController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody HCoreVo v) { + return ApiResponse.ok("장치 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody HCoreVo v) { + return ApiResponse.ok("장치 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/status") + public ApiResponse updateStatus(@PathVariable Long id, @RequestBody Map body) { + service.updateStatus(id, body.getOrDefault("status", "OFFLINE")); + return ApiResponse.ok("상태 업데이트 완료", null); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("장치 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java new file mode 100644 index 0000000..423e01d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/PosCvtController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PutMapping("/{id}/process") + public ApiResponse process(@PathVariable Long id) { + service.process(id); + return ApiResponse.ok("처리 완료", null); + } + + @PutMapping("/{id}/ignore") + public ApiResponse ignore(@PathVariable Long id) { + service.ignore(id); + return ApiResponse.ok("무시 처리 완료", null); + } + + @PutMapping("/{id}/error") + public ApiResponse error(@PathVariable Long id, @RequestBody Map body) { + service.markError(id, body.getOrDefault("message", "처리 오류")); + return ApiResponse.ok("오류 처리 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java b/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java new file mode 100644 index 0000000..cf1f2c3 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/ProductController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody ProductVo v) { + return ApiResponse.ok("상품 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody ProductVo v) { + return ApiResponse.ok("상품 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("상품 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java b/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java new file mode 100644 index 0000000..f0e549d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/StoreController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody StoreVo v) { + return ApiResponse.ok("매장 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody StoreVo v) { + return ApiResponse.ok("매장 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("매장 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java b/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java new file mode 100644 index 0000000..31a7ff0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/StoreGroupController.java @@ -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(@RequestParam(required = false) String tenantCode) { + return ApiResponse.ok(service.list(tenantCode)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody StoreGroupVo v) { + return ApiResponse.ok("매장그룹 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody StoreGroupVo v) { + return ApiResponse.ok("매장그룹 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("매장그룹 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java b/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java new file mode 100644 index 0000000..98b85cb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/TemplateController.java @@ -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( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String templateType) { + return ApiResponse.ok(service.list(tenantCode, templateType)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody TemplateVo v) { + return ApiResponse.ok("템플릿 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody TemplateVo v) { + return ApiResponse.ok("템플릿 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("템플릿 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java new file mode 100644 index 0000000..7e5007f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/TenantController.java @@ -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() { + return ApiResponse.ok(service.list()); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody TenantVo v) { + return ApiResponse.ok("테넌트 생성 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody TenantVo v) { + return ApiResponse.ok("테넌트 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("테넌트 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/UserController.java b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java new file mode 100644 index 0000000..fd67054 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/UserController.java @@ -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( + @RequestParam(required = false) String tenantCode, + @RequestParam(required = false) String role) { + return ApiResponse.ok(service.list(tenantCode, role)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody Map 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 update(@PathVariable Long id, @RequestBody EsnUser v) { + return ApiResponse.ok("사용자 수정 완료", service.update(id, v)); + } + + @PutMapping("/{id}/password") + public ApiResponse changePassword(@PathVariable Long id, @RequestBody Map body) { + service.changePassword(id, body.getOrDefault("password", "changeme123")); + return ApiResponse.ok("비밀번호 변경 완료", null); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("사용자 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java b/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java new file mode 100644 index 0000000..355294f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/controller/WorkController.java @@ -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( + @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 get(@PathVariable Long id) { + return ApiResponse.ok(service.get(id)); + } + + @PostMapping + public ApiResponse create(@RequestBody WorkHistoryVo v) { + return ApiResponse.ok("작업 등록 완료", service.create(v)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @RequestBody WorkHistoryVo v) { + return ApiResponse.ok("작업 수정 완료", service.update(id, v)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable Long id) { + service.delete(id); + return ApiResponse.ok("작업 삭제 완료", null); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java b/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java new file mode 100644 index 0000000..43f9734 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/AlarmVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java b/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java new file mode 100644 index 0000000..7916ec5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/DashboardVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java b/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java new file mode 100644 index 0000000..98d2fea --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/FirmwareVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java b/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java new file mode 100644 index 0000000..742aa69 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/HCoreVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java b/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java new file mode 100644 index 0000000..8622086 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/PosCvtVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java b/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java new file mode 100644 index 0000000..677e224 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/ProductVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java b/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java new file mode 100644 index 0000000..93485d6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/StoreGroupVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java b/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java new file mode 100644 index 0000000..b2ace76 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/StoreVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java b/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java new file mode 100644 index 0000000..c2b6a82 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/TemplateVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java b/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java new file mode 100644 index 0000000..3109d44 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/TenantVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java b/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java new file mode 100644 index 0000000..dc72b63 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/domain/WorkHistoryVo.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java new file mode 100644 index 0000000..6b5d75c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/AlarmMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java new file mode 100644 index 0000000..e3c6101 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/DashboardMapper.java @@ -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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java new file mode 100644 index 0000000..08c5602 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/FirmwareMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java new file mode 100644 index 0000000..be23434 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/HCoreMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java new file mode 100644 index 0000000..2ee2282 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/PosCvtMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java new file mode 100644 index 0000000..00a673a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/ProductMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java new file mode 100644 index 0000000..f8bba82 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/StoreGroupMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java new file mode 100644 index 0000000..e15e286 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/StoreMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java new file mode 100644 index 0000000..7b80214 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/TemplateMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java new file mode 100644 index 0000000..950fce2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/TenantMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java new file mode 100644 index 0000000..2cfb891 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/UserMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java b/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java new file mode 100644 index 0000000..f8bc439 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/mapper/WorkMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java b/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java new file mode 100644 index 0000000..67b0546 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/AlarmService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java b/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java new file mode 100644 index 0000000..c0f5411 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/FirmwareService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java b/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java new file mode 100644 index 0000000..922855d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/HCoreService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java b/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java new file mode 100644 index 0000000..f72271b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/PosCvtService.java @@ -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 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); + } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/ProductService.java b/backend/src/main/java/com/zioinfo/esn/service/ProductService.java new file mode 100644 index 0000000..be7dfa3 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/ProductService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java b/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java new file mode 100644 index 0000000..fda090f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/StoreGroupService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/StoreService.java b/backend/src/main/java/com/zioinfo/esn/service/StoreService.java new file mode 100644 index 0000000..587f28c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/StoreService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java b/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java new file mode 100644 index 0000000..d766789 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/TemplateService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/TenantService.java b/backend/src/main/java/com/zioinfo/esn/service/TenantService.java new file mode 100644 index 0000000..d31f872 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/TenantService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/UserService.java b/backend/src/main/java/com/zioinfo/esn/service/UserService.java new file mode 100644 index 0000000..f930f76 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/UserService.java @@ -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 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); } +} diff --git a/backend/src/main/java/com/zioinfo/esn/service/WorkService.java b/backend/src/main/java/com/zioinfo/esn/service/WorkService.java new file mode 100644 index 0000000..02bf2ad --- /dev/null +++ b/backend/src/main/java/com/zioinfo/esn/service/WorkService.java @@ -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 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); } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..899c5ad --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/backend/src/main/resources/db/schema.sql b/backend/src/main/resources/db/schema.sql new file mode 100644 index 0000000..34ee631 --- /dev/null +++ b/backend/src/main/resources/db/schema.sql @@ -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; diff --git a/backend/src/main/resources/mapper/AlarmMapper.xml b/backend/src/main/resources/mapper/AlarmMapper.xml new file mode 100644 index 0000000..b381f83 --- /dev/null +++ b/backend/src/main/resources/mapper/AlarmMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_alarm (tenant_code, store_id, alarm_type, alarm_code, severity, message, status) + VALUES (#{tenantCode}, #{storeId}, #{alarmType}, #{alarmCode}, #{severity}, #{message}, 'OPEN') + + + + UPDATE esn_alarm SET + alarm_type = #{alarmType}, + severity = #{severity}, + message = #{message}, + status = #{status} + WHERE id = #{id} + + + + UPDATE esn_alarm SET + status = 'RESOLVED', + resolved_by = #{resolvedBy}, + resolution = #{resolution}, + resolved_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_alarm WHERE id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/DashboardMapper.xml b/backend/src/main/resources/mapper/DashboardMapper.xml new file mode 100644 index 0000000..91fbcec --- /dev/null +++ b/backend/src/main/resources/mapper/DashboardMapper.xml @@ -0,0 +1,46 @@ + + + + + + + diff --git a/backend/src/main/resources/mapper/FirmwareMapper.xml b/backend/src/main/resources/mapper/FirmwareMapper.xml new file mode 100644 index 0000000..39759d4 --- /dev/null +++ b/backend/src/main/resources/mapper/FirmwareMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + UPDATE esn_firmware SET + firmware_version = #{firmwareVersion}, + description = #{description}, + is_latest = #{latest} + WHERE id = #{id} + + + + UPDATE esn_firmware SET is_latest = false + WHERE device_type = #{deviceType} + AND tenant_code = #{tenantCode} + + + DELETE FROM esn_firmware WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/HCoreMapper.xml b/backend/src/main/resources/mapper/HCoreMapper.xml new file mode 100644 index 0000000..ad2ccb9 --- /dev/null +++ b/backend/src/main/resources/mapper/HCoreMapper.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + 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 esn_hcore_device SET status = #{status}, last_seen_at = NOW() + WHERE id = #{id} + + + DELETE FROM esn_hcore_device WHERE id = #{id} + + + + + + + + diff --git a/backend/src/main/resources/mapper/PosCvtMapper.xml b/backend/src/main/resources/mapper/PosCvtMapper.xml new file mode 100644 index 0000000..2aa73e5 --- /dev/null +++ b/backend/src/main/resources/mapper/PosCvtMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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') + + + + 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} + + + + + + + diff --git a/backend/src/main/resources/mapper/ProductMapper.xml b/backend/src/main/resources/mapper/ProductMapper.xml new file mode 100644 index 0000000..4e3bd74 --- /dev/null +++ b/backend/src/main/resources/mapper/ProductMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + 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} + + + DELETE FROM esn_product WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/StoreGroupMapper.xml b/backend/src/main/resources/mapper/StoreGroupMapper.xml new file mode 100644 index 0000000..9fa9e51 --- /dev/null +++ b/backend/src/main/resources/mapper/StoreGroupMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_store_group (tenant_code, group_code, group_name, description, is_active) + VALUES (#{tenantCode}, #{groupCode}, #{groupName}, #{description}, #{active}) + + + + UPDATE esn_store_group SET + group_name = #{groupName}, + description = #{description}, + is_active = #{active} + WHERE id = #{id} + + + DELETE FROM esn_store_group WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/StoreMapper.xml b/backend/src/main/resources/mapper/StoreMapper.xml new file mode 100644 index 0000000..56251b2 --- /dev/null +++ b/backend/src/main/resources/mapper/StoreMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + UPDATE esn_store SET + store_name = #{storeName}, + address = #{address}, + phone = #{phone}, + store_group_id = #{storeGroupId}, + manager_name = #{managerName}, + is_active = #{active} + WHERE id = #{id} + + + DELETE FROM esn_store WHERE id = #{id} + + + + + + diff --git a/backend/src/main/resources/mapper/TemplateMapper.xml b/backend/src/main/resources/mapper/TemplateMapper.xml new file mode 100644 index 0000000..66d3a9b --- /dev/null +++ b/backend/src/main/resources/mapper/TemplateMapper.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + 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} + + + DELETE FROM esn_template WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/TenantMapper.xml b/backend/src/main/resources/mapper/TenantMapper.xml new file mode 100644 index 0000000..297d051 --- /dev/null +++ b/backend/src/main/resources/mapper/TenantMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_tenant (tenant_code, tenant_name, description, is_active) + VALUES (#{tenantCode}, #{tenantName}, #{description}, #{active}) + + + + UPDATE esn_tenant SET + tenant_name = #{tenantName}, + description = #{description}, + is_active = #{active} + WHERE id = #{id} + + + + DELETE FROM esn_tenant WHERE id = #{id} + + + diff --git a/backend/src/main/resources/mapper/UserAuthMapper.xml b/backend/src/main/resources/mapper/UserAuthMapper.xml new file mode 100644 index 0000000..f492bc2 --- /dev/null +++ b/backend/src/main/resources/mapper/UserAuthMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + UPDATE esn_user SET last_login_at = NOW() WHERE username = #{username} + + + diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..3e0353b --- /dev/null +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO esn_user (tenant_code, username, password_hash, role, email, phone, is_active) + VALUES (#{tenantCode}, #{username}, #{passwordHash}, #{role}, #{email}, #{phone}, #{active}) + + + + UPDATE esn_user SET + tenant_code = #{tenantCode}, + role = #{role}, + email = #{email}, + phone = #{phone}, + is_active = #{active} + WHERE id = #{id} + + + + UPDATE esn_user SET password_hash = #{passwordHash} WHERE id = #{id} + + + DELETE FROM esn_user WHERE id = #{id} + + diff --git a/backend/src/main/resources/mapper/WorkMapper.xml b/backend/src/main/resources/mapper/WorkMapper.xml new file mode 100644 index 0000000..efc4a66 --- /dev/null +++ b/backend/src/main/resources/mapper/WorkMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + + 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} + + + DELETE FROM esn_work_history WHERE id = #{id} + + + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c2d491d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + zioinfo-esn — ESL 통합 관리 플랫폼 + + + +

+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ea44ef7 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..bed5840 --- /dev/null +++ b/frontend/src/App.tsx @@ -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 ( + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + ) +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..90f339f --- /dev/null +++ b/frontend/src/api/client.ts @@ -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) => 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 })) diff --git a/frontend/src/components/AlarmBadge.tsx b/frontend/src/components/AlarmBadge.tsx new file mode 100644 index 0000000..7309c5a --- /dev/null +++ b/frontend/src/components/AlarmBadge.tsx @@ -0,0 +1,16 @@ +interface AlarmBadgeProps { + severity: string +} + +export default function AlarmBadge({ severity }: AlarmBadgeProps) { + const map: Record = { + 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 ( + {severity} + ) +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..d07103d --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -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 ( +
+
+ 테넌트: {tenant} + | +
+ + {user} + {role} +
+ +
+ ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..ffddea7 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -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 + + return ( +
+ +
+
+
+ +
+
+
+ ) +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..7758bb5 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -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 ( + + ) +} diff --git a/frontend/src/components/StatCard.tsx b/frontend/src/components/StatCard.tsx new file mode 100644 index 0000000..7578432 --- /dev/null +++ b/frontend/src/components/StatCard.tsx @@ -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 = { + 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 ( +
+
+ +
+
+

{title}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..ee31189 --- /dev/null +++ b/frontend/src/index.css @@ -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; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..520b520 --- /dev/null +++ b/frontend/src/main.tsx @@ -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( + + + +) diff --git a/frontend/src/pages/AiAnalysis.tsx b/frontend/src/pages/AiAnalysis.tsx new file mode 100644 index 0000000..e1bfce2 --- /dev/null +++ b/frontend/src/pages/AiAnalysis.tsx @@ -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('') + const [posText, setPosText] = useState('') + const [posResult, setPosResult] = useState('') + + 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 ( +
+

+ AI 분석 +

+ + {/* Alarm Analysis */} +
+

+ 알람 원인 분석 +

+