feat: GUARDiA FA v1.0 - e-Paper Factory Automation Platform
This commit is contained in:
commit
24f50144b7
53
Jenkinsfile
vendored
Normal file
53
Jenkinsfile
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
environment {
|
||||||
|
APP_NAME = 'guardia-fa'
|
||||||
|
DEPLOY_DIR = '/opt/guardia-fa'
|
||||||
|
JAR_NAME = 'guardia-fa-1.0.0.jar'
|
||||||
|
APP_PORT = '8017'
|
||||||
|
GIT_REPO = 'http://127.0.0.1:9003/zio/guardia-fa.git'
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
echo "GUARDiA FA 빌드 시작 - Branch: ${env.BRANCH_NAME}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Frontend Build') {
|
||||||
|
steps {
|
||||||
|
dir('workspace/guardia-fa/frontend') {
|
||||||
|
sh 'npm install'
|
||||||
|
sh 'npm run build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Backend Build') {
|
||||||
|
steps {
|
||||||
|
dir('workspace/guardia-fa/backend') {
|
||||||
|
sh 'mvn clean package -DskipTests'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Deploy') {
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
systemctl stop guardia-fa || true
|
||||||
|
sleep 2
|
||||||
|
mkdir -p ${DEPLOY_DIR}
|
||||||
|
cp workspace/guardia-fa/backend/target/${JAR_NAME} ${DEPLOY_DIR}/${JAR_NAME}
|
||||||
|
systemctl start guardia-fa
|
||||||
|
sleep 5
|
||||||
|
curl -f http://localhost:${APP_PORT}/api/fa/dashboard/overview || exit 1
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "GUARDiA FA 8017 배포 성공"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "GUARDiA FA 배포 실패 - 롤백 필요"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/pom.xml
Normal file
38
backend/pom.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
</parent>
|
||||||
|
<groupId>com.zioinfo</groupId>
|
||||||
|
<artifactId>guardia-fa</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
|
||||||
|
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency>
|
||||||
|
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency>
|
||||||
|
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.6</version></dependency>
|
||||||
|
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.6</version><scope>runtime</scope></dependency>
|
||||||
|
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.6</version><scope>runtime</scope></dependency>
|
||||||
|
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
|
||||||
|
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
14
backend/src/main/java/com/zioinfo/fa/FaApplication.java
Normal file
14
backend/src/main/java/com/zioinfo/fa/FaApplication.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.zioinfo.fa;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@MapperScan(annotationClass = Mapper.class)
|
||||||
|
public class FaApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(FaApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java
Normal file
39
backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package com.zioinfo.fa.auth;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class JwtFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
public JwtFilter(JwtUtil jwtUtil) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String header = request.getHeader("Authorization");
|
||||||
|
if (header != null && header.startsWith("Bearer ")) {
|
||||||
|
String token = header.substring(7);
|
||||||
|
if (jwtUtil.isValid(token)) {
|
||||||
|
String username = jwtUtil.extractUsername(token);
|
||||||
|
String role = jwtUtil.extractRole(token);
|
||||||
|
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role)));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java
Normal file
59
backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package com.zioinfo.fa.auth;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtUtil {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private long expiration;
|
||||||
|
|
||||||
|
private SecretKey getKey() {
|
||||||
|
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String username, String role) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.claim("role", role)
|
||||||
|
.issuedAt(new Date())
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + expiration))
|
||||||
|
.signWith(getKey())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String extractUsername(String token) {
|
||||||
|
return getClaims(token).getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String extractRole(String token) {
|
||||||
|
return getClaims(token).get("role", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid(String token) {
|
||||||
|
try {
|
||||||
|
getClaims(token);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims getClaims(String token) {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(getKey())
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.zioinfo.fa.config;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.auth.JwtFilter;
|
||||||
|
import com.zioinfo.fa.auth.JwtUtil;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
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;
|
||||||
|
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||||
|
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
public SecurityConfig(JwtUtil jwtUtil) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers(
|
||||||
|
"/api/fa/auth/**",
|
||||||
|
"/api/fa/qr/scan",
|
||||||
|
"/static/**",
|
||||||
|
"/assets/**",
|
||||||
|
"/",
|
||||||
|
"/index.html",
|
||||||
|
"/favicon.ico"
|
||||||
|
).permitAll()
|
||||||
|
.requestMatchers("/api/fa/**").authenticated()
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/src/main/java/com/zioinfo/fa/config/WebConfig.java
Normal file
38
backend/src/main/java/com/zioinfo/fa/config/WebConfig.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.zioinfo.fa.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.resourceChain(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new SpaFallbackInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SpaFallbackInterceptor implements HandlerInterceptor {
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
if (!path.startsWith("/api/") && !path.startsWith("/static/") && !path.startsWith("/assets/")
|
||||||
|
&& !path.contains(".")) {
|
||||||
|
request.getRequestDispatcher("/index.html").forward(request, response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.AndonEvent;
|
||||||
|
import com.zioinfo.fa.service.AndonService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/andon")
|
||||||
|
public class AndonController {
|
||||||
|
private final AndonService service;
|
||||||
|
|
||||||
|
public AndonController(AndonService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/events")
|
||||||
|
public ResponseEntity<List<AndonEvent>> findAll(
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String severity) {
|
||||||
|
return ResponseEntity.ok(service.findAll(status, severity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/call")
|
||||||
|
public ResponseEntity<AndonEvent> call(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.call(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/respond")
|
||||||
|
public ResponseEntity<AndonEvent> respond(
|
||||||
|
@PathVariable Long id, @RequestBody Map<String, String> req) {
|
||||||
|
return ResponseEntity.ok(service.respond(id, req.get("responseBy")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/resolve")
|
||||||
|
public ResponseEntity<AndonEvent> resolve(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.resolve(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public ResponseEntity<Map<String, Object>> getStats() {
|
||||||
|
return ResponseEntity.ok(service.getStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/board")
|
||||||
|
public ResponseEntity<List<AndonEvent>> getBoard() {
|
||||||
|
return ResponseEntity.ok(service.getBoard());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.service.AuthService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/auth")
|
||||||
|
public class AuthController {
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
public AuthController(AuthService authService) {
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> req) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> result = authService.login(req.get("username"), req.get("password"));
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(401).body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperDisplay;
|
||||||
|
import com.zioinfo.fa.service.EpaperDisplayService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/epaper")
|
||||||
|
public class EpaperDisplayController {
|
||||||
|
private final EpaperDisplayService service;
|
||||||
|
|
||||||
|
public EpaperDisplayController(EpaperDisplayService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<EpaperDisplay>> findAll(
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String tenantCode) {
|
||||||
|
return ResponseEntity.ok(service.findAll(status, tenantCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<EpaperDisplay> create(@RequestBody EpaperDisplay d) {
|
||||||
|
return ResponseEntity.ok(service.create(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<EpaperDisplay> findById(@PathVariable Long id) {
|
||||||
|
EpaperDisplay d = service.findById(id);
|
||||||
|
return d != null ? ResponseEntity.ok(d) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<EpaperDisplay> update(@PathVariable Long id, @RequestBody EpaperDisplay d) {
|
||||||
|
return ResponseEntity.ok(service.update(id, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/push")
|
||||||
|
public ResponseEntity<Map<String, Object>> push(@PathVariable Long id, @RequestBody Map<String, String> req) {
|
||||||
|
return ResponseEntity.ok(service.pushContent(id, req.get("content"), req.get("templateId")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/refresh")
|
||||||
|
public ResponseEntity<Map<String, Object>> refresh(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.refresh(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/offline")
|
||||||
|
public ResponseEntity<List<EpaperDisplay>> offline() {
|
||||||
|
return ResponseEntity.ok(service.findOffline());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/battery-low")
|
||||||
|
public ResponseEntity<List<EpaperDisplay>> batteryLow(
|
||||||
|
@RequestParam(required = false, defaultValue = "20") Double threshold) {
|
||||||
|
return ResponseEntity.ok(service.findBatteryLow(threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batch-push")
|
||||||
|
public ResponseEntity<Map<String, Object>> batchPush(@RequestBody Map<String, Object> req) {
|
||||||
|
List<Long> ids = (List<Long>) req.get("ids");
|
||||||
|
return ResponseEntity.ok(service.batchPush(ids, (String) req.get("content"), (String) req.get("templateId")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
public ResponseEntity<Map<String, Object>> dashboard() {
|
||||||
|
return ResponseEntity.ok(service.getDashboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperTemplate;
|
||||||
|
import com.zioinfo.fa.service.EpaperTemplateService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/templates")
|
||||||
|
public class EpaperTemplateController {
|
||||||
|
private final EpaperTemplateService service;
|
||||||
|
|
||||||
|
public EpaperTemplateController(EpaperTemplateService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<EpaperTemplate>> findAll() {
|
||||||
|
return ResponseEntity.ok(service.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<EpaperTemplate> create(@RequestBody EpaperTemplate t) {
|
||||||
|
return ResponseEntity.ok(service.create(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<EpaperTemplate> findById(@PathVariable Long id) {
|
||||||
|
EpaperTemplate t = service.findById(id);
|
||||||
|
return t != null ? ResponseEntity.ok(t) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<EpaperTemplate> update(@PathVariable Long id, @RequestBody EpaperTemplate t) {
|
||||||
|
return ResponseEntity.ok(service.update(id, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/preview")
|
||||||
|
public ResponseEntity<EpaperTemplate> preview(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.preview(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/type/{type}")
|
||||||
|
public ResponseEntity<List<EpaperTemplate>> findByType(@PathVariable String type) {
|
||||||
|
return ResponseEntity.ok(service.findByType(type));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Equipment;
|
||||||
|
import com.zioinfo.fa.service.EquipmentService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/equipment")
|
||||||
|
public class EquipmentController {
|
||||||
|
private final EquipmentService service;
|
||||||
|
|
||||||
|
public EquipmentController(EquipmentService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Equipment>> findAll(
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String workstationCode) {
|
||||||
|
return ResponseEntity.ok(service.findAll(status, workstationCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Equipment> create(@RequestBody Equipment e) {
|
||||||
|
return ResponseEntity.ok(service.create(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Equipment> findById(@PathVariable Long id) {
|
||||||
|
Equipment e = service.findById(id);
|
||||||
|
return e != null ? ResponseEntity.ok(e) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Equipment> update(@PathVariable Long id, @RequestBody Equipment e) {
|
||||||
|
return ResponseEntity.ok(service.update(id, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/oee")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOee(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.getOee(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/maintenance")
|
||||||
|
public ResponseEntity<Map<String, Object>> getMaintenance(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.getMaintenance(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/maintenance")
|
||||||
|
public ResponseEntity<Equipment> recordMaintenance(
|
||||||
|
@PathVariable Long id, @RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.recordMaintenance(id, req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ai-predict")
|
||||||
|
public ResponseEntity<Map<String, Object>> aiPredict(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.aiPredict(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/breakdown-risk")
|
||||||
|
public ResponseEntity<List<Equipment>> getBreakdownRisk() {
|
||||||
|
return ResponseEntity.ok(service.getBreakdownRisk());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.service.DashboardService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/dashboard")
|
||||||
|
public class FaDashboardController {
|
||||||
|
private final DashboardService service;
|
||||||
|
|
||||||
|
public FaDashboardController(DashboardService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/overview")
|
||||||
|
public ResponseEntity<Map<String, Object>> overview() {
|
||||||
|
return ResponseEntity.ok(service.getOverview());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/oee-summary")
|
||||||
|
public ResponseEntity<Map<String, Object>> oeeSummary() {
|
||||||
|
return ResponseEntity.ok(service.getOeeSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/production-status")
|
||||||
|
public ResponseEntity<Map<String, Object>> productionStatus() {
|
||||||
|
return ResponseEntity.ok(service.getProductionStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/quality-summary")
|
||||||
|
public ResponseEntity<Map<String, Object>> qualitySummary() {
|
||||||
|
return ResponseEntity.ok(service.getQualitySummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/andon-summary")
|
||||||
|
public ResponseEntity<Map<String, Object>> andonSummary() {
|
||||||
|
return ResponseEntity.ok(service.getAndonSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/epaper-status")
|
||||||
|
public ResponseEntity<Map<String, Object>> epaperStatus() {
|
||||||
|
return ResponseEntity.ok(service.getEpaperStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.FactoryInventory;
|
||||||
|
import com.zioinfo.fa.service.InventoryService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/inventory")
|
||||||
|
public class InventoryController {
|
||||||
|
private final InventoryService service;
|
||||||
|
|
||||||
|
public InventoryController(InventoryService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<FactoryInventory>> findAll(
|
||||||
|
@RequestParam(required = false) String locationCode) {
|
||||||
|
return ResponseEntity.ok(service.findAll(locationCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/movement")
|
||||||
|
public ResponseEntity<Map<String, Object>> movement(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.movement(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/low-stock")
|
||||||
|
public ResponseEntity<List<FactoryInventory>> getLowStock() {
|
||||||
|
return ResponseEntity.ok(service.getLowStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ai-optimize")
|
||||||
|
public ResponseEntity<Map<String, Object>> aiOptimize(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.aiOptimize(req));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.ProductionOrder;
|
||||||
|
import com.zioinfo.fa.service.ProductionOrderService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/orders")
|
||||||
|
public class ProductionOrderController {
|
||||||
|
private final ProductionOrderService service;
|
||||||
|
|
||||||
|
public ProductionOrderController(ProductionOrderService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<ProductionOrder>> findAll(
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String lineCode) {
|
||||||
|
return ResponseEntity.ok(service.findAll(status, lineCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<ProductionOrder> create(@RequestBody ProductionOrder o) {
|
||||||
|
return ResponseEntity.ok(service.create(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<ProductionOrder> findById(@PathVariable Long id) {
|
||||||
|
ProductionOrder o = service.findById(id);
|
||||||
|
return o != null ? ResponseEntity.ok(o) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<ProductionOrder> update(@PathVariable Long id, @RequestBody ProductionOrder o) {
|
||||||
|
return ResponseEntity.ok(service.update(id, o));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/release")
|
||||||
|
public ResponseEntity<ProductionOrder> release(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.release(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/complete")
|
||||||
|
public ResponseEntity<ProductionOrder> complete(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.complete(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/progress")
|
||||||
|
public ResponseEntity<Map<String, Object>> getProgress(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.getProgress(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/qr-codes")
|
||||||
|
public ResponseEntity<Map<String, Object>> getQrCodes(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(Map.of("orderId", id, "qrCodes", List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/today")
|
||||||
|
public ResponseEntity<List<ProductionOrder>> getToday() {
|
||||||
|
return ResponseEntity.ok(service.findToday());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
public ResponseEntity<Map<String, Object>> getDashboard() {
|
||||||
|
return ResponseEntity.ok(service.getDashboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QrProcessCode;
|
||||||
|
import com.zioinfo.fa.domain.QrScanEvent;
|
||||||
|
import com.zioinfo.fa.service.BomRouteService;
|
||||||
|
import com.zioinfo.fa.service.QrService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/qr")
|
||||||
|
public class QrController {
|
||||||
|
private final QrService qrService;
|
||||||
|
private final BomRouteService bomRouteService;
|
||||||
|
|
||||||
|
public QrController(QrService qrService, BomRouteService bomRouteService) {
|
||||||
|
this.qrService = qrService;
|
||||||
|
this.bomRouteService = bomRouteService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/generate")
|
||||||
|
public ResponseEntity<QrProcessCode> generate(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(qrService.generate(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{code}")
|
||||||
|
public ResponseEntity<QrProcessCode> findByCode(@PathVariable String code) {
|
||||||
|
QrProcessCode q = qrService.findByCode(code);
|
||||||
|
return q != null ? ResponseEntity.ok(q) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/scan")
|
||||||
|
public ResponseEntity<Map<String, Object>> scan(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(qrService.scan(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{code}/history")
|
||||||
|
public ResponseEntity<List<QrScanEvent>> getHistory(@PathVariable String code) {
|
||||||
|
return ResponseEntity.ok(qrService.getHistory(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{code}/route")
|
||||||
|
public ResponseEntity<?> getRoute(@PathVariable String code) {
|
||||||
|
return ResponseEntity.ok(qrService.getRoute(code, bomRouteService.getRouteMapper()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/lot/{lot}")
|
||||||
|
public ResponseEntity<List<QrProcessCode>> findByLot(@PathVariable String lot) {
|
||||||
|
return ResponseEntity.ok(qrService.findByLot(lot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/workstation/{ws}/recent")
|
||||||
|
public ResponseEntity<List<QrScanEvent>> recentByWorkstation(
|
||||||
|
@PathVariable String ws,
|
||||||
|
@RequestParam(defaultValue = "20") int limit) {
|
||||||
|
return ResponseEntity.ok(qrService.getRecentByWorkstation(ws, limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batch-generate")
|
||||||
|
public ResponseEntity<List<QrProcessCode>> batchGenerate(@RequestBody List<Map<String, Object>> requests) {
|
||||||
|
return ResponseEntity.ok(qrService.batchGenerate(requests));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QualityInspection;
|
||||||
|
import com.zioinfo.fa.service.QualityService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/quality")
|
||||||
|
public class QualityController {
|
||||||
|
private final QualityService service;
|
||||||
|
|
||||||
|
public QualityController(QualityService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/inspections")
|
||||||
|
public ResponseEntity<List<QualityInspection>> findAll(
|
||||||
|
@RequestParam(required = false) String inspectionType,
|
||||||
|
@RequestParam(required = false) String result) {
|
||||||
|
return ResponseEntity.ok(service.findAll(inspectionType, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/inspections")
|
||||||
|
public ResponseEntity<QualityInspection> create(@RequestBody QualityInspection q) {
|
||||||
|
return ResponseEntity.ok(service.create(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<QualityInspection> findById(@PathVariable Long id) {
|
||||||
|
QualityInspection q = service.findById(id);
|
||||||
|
return q != null ? ResponseEntity.ok(q) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/result")
|
||||||
|
public ResponseEntity<QualityInspection> setResult(
|
||||||
|
@PathVariable Long id, @RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.setResult(id, req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/defects/pareto")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getDefectPareto() {
|
||||||
|
return ResponseEntity.ok(service.getDefectPareto());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/spc")
|
||||||
|
public ResponseEntity<Map<String, Object>> getSpc() {
|
||||||
|
return ResponseEntity.ok(Map.of("cp", 1.33, "cpk", 1.25, "status", "STABLE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ai-analyze")
|
||||||
|
public ResponseEntity<Map<String, Object>> aiAnalyze(@RequestBody Map<String, Object> req) {
|
||||||
|
return ResponseEntity.ok(service.aiAnalyze(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
public ResponseEntity<Map<String, Object>> getDashboard() {
|
||||||
|
return ResponseEntity.ok(service.getDashboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package com.zioinfo.fa.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Workstation;
|
||||||
|
import com.zioinfo.fa.service.WorkstationService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/fa/workstations")
|
||||||
|
public class WorkstationController {
|
||||||
|
private final WorkstationService service;
|
||||||
|
|
||||||
|
public WorkstationController(WorkstationService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Workstation>> findAll(
|
||||||
|
@RequestParam(required = false) String lineCode,
|
||||||
|
@RequestParam(required = false) String status) {
|
||||||
|
return ResponseEntity.ok(service.findAll(lineCode, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Workstation> create(@RequestBody Workstation w) {
|
||||||
|
return ResponseEntity.ok(service.create(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Workstation> findById(@PathVariable Long id) {
|
||||||
|
Workstation w = service.findById(id);
|
||||||
|
return w != null ? ResponseEntity.ok(w) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Workstation> update(@PathVariable Long id, @RequestBody Workstation w) {
|
||||||
|
return ResponseEntity.ok(service.update(id, w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/status")
|
||||||
|
public ResponseEntity<Workstation> getStatus(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.findById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/andon")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateAndon(
|
||||||
|
@PathVariable Long id, @RequestBody Map<String, String> req) {
|
||||||
|
Workstation w = service.findById(id);
|
||||||
|
if (w == null) return ResponseEntity.notFound().build();
|
||||||
|
return ResponseEntity.ok(service.updateAndon(w.getWorkstationCode(), req.get("andonStatus")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/order")
|
||||||
|
public ResponseEntity<Workstation> getCurrentOrder(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(service.findById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/epaper-push")
|
||||||
|
public ResponseEntity<Map<String, Object>> epaperPush(
|
||||||
|
@PathVariable Long id, @RequestBody Map<String, String> req) {
|
||||||
|
return ResponseEntity.ok(Map.of("pushed", true, "workstationId", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/floor-map")
|
||||||
|
public ResponseEntity<List<Workstation>> getFloorMap() {
|
||||||
|
return ResponseEntity.ok(service.getFloorMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/andon-board")
|
||||||
|
public ResponseEntity<List<Workstation>> getAndonBoard() {
|
||||||
|
return ResponseEntity.ok(service.findAll(null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java
Normal file
22
backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AndonEvent {
|
||||||
|
private Long id;
|
||||||
|
private String workstationCode;
|
||||||
|
private String andonType;
|
||||||
|
private String severity;
|
||||||
|
private String description;
|
||||||
|
private String calledBy;
|
||||||
|
private String responseBy;
|
||||||
|
private LocalDateTime calledAt;
|
||||||
|
private LocalDateTime respondedAt;
|
||||||
|
private LocalDateTime resolvedAt;
|
||||||
|
private Integer responseTimeSeconds;
|
||||||
|
private Integer resolutionTimeSeconds;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
18
backend/src/main/java/com/zioinfo/fa/domain/Bom.java
Normal file
18
backend/src/main/java/com/zioinfo/fa/domain/Bom.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Bom {
|
||||||
|
private Long id;
|
||||||
|
private String productCode;
|
||||||
|
private String componentCode;
|
||||||
|
private String componentName;
|
||||||
|
private Double requiredQty;
|
||||||
|
private String unit;
|
||||||
|
private Integer level;
|
||||||
|
private String processCode;
|
||||||
|
private String substituteCode;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class EpaperDisplay {
|
||||||
|
private Long id;
|
||||||
|
private String displayId;
|
||||||
|
private String workstationId;
|
||||||
|
private String displaySize;
|
||||||
|
private String displayType;
|
||||||
|
private Integer resolutionW;
|
||||||
|
private Integer resolutionH;
|
||||||
|
private String colorMode;
|
||||||
|
private String refreshMode;
|
||||||
|
private String currentTemplate;
|
||||||
|
private String displayContent;
|
||||||
|
private Double batteryLevel;
|
||||||
|
private Integer signalStrength;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime lastUpdated;
|
||||||
|
private LocalDateTime lastSeen;
|
||||||
|
private String gatewayId;
|
||||||
|
private String protocol;
|
||||||
|
private String tenantCode;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class EpaperTemplate {
|
||||||
|
private Long id;
|
||||||
|
private String templateCode;
|
||||||
|
private String templateName;
|
||||||
|
private String templateType;
|
||||||
|
private String displaySize;
|
||||||
|
private String layoutJson;
|
||||||
|
private String previewBase64;
|
||||||
|
private Boolean active;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
24
backend/src/main/java/com/zioinfo/fa/domain/Equipment.java
Normal file
24
backend/src/main/java/com/zioinfo/fa/domain/Equipment.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Equipment {
|
||||||
|
private Long id;
|
||||||
|
private String equipmentCode;
|
||||||
|
private String equipmentName;
|
||||||
|
private String equipmentType;
|
||||||
|
private String workstationCode;
|
||||||
|
private String manufacturer;
|
||||||
|
private String modelNumber;
|
||||||
|
private LocalDate installDate;
|
||||||
|
private LocalDate warrantyExpiry;
|
||||||
|
private String status;
|
||||||
|
private Double oee;
|
||||||
|
private LocalDateTime lastMaintenanceAt;
|
||||||
|
private LocalDateTime nextMaintenanceAt;
|
||||||
|
private String epaperDisplayId;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
16
backend/src/main/java/com/zioinfo/fa/domain/FaUser.java
Normal file
16
backend/src/main/java/com/zioinfo/fa/domain/FaUser.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class FaUser {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String passwordHash;
|
||||||
|
private String role;
|
||||||
|
private String workstationCode;
|
||||||
|
private Boolean active;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class FactoryInventory {
|
||||||
|
private Long id;
|
||||||
|
private String itemCode;
|
||||||
|
private String itemName;
|
||||||
|
private String locationCode;
|
||||||
|
private Double quantity;
|
||||||
|
private Double safetyStock;
|
||||||
|
private Double reorderPoint;
|
||||||
|
private String unit;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime lastUpdated;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ProcessRoute {
|
||||||
|
private Long id;
|
||||||
|
private String routeCode;
|
||||||
|
private String productCode;
|
||||||
|
private Integer sequence;
|
||||||
|
private String processCode;
|
||||||
|
private String processName;
|
||||||
|
private String workstationCode;
|
||||||
|
private Integer standardTime;
|
||||||
|
private String checkPoints;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ProductionOrder {
|
||||||
|
private Long id;
|
||||||
|
private String orderNumber;
|
||||||
|
private String productCode;
|
||||||
|
private String productName;
|
||||||
|
private Integer plannedQty;
|
||||||
|
private Integer completedQty;
|
||||||
|
private Integer defectQty;
|
||||||
|
private String status;
|
||||||
|
private Integer priority;
|
||||||
|
private LocalDateTime plannedStart;
|
||||||
|
private LocalDateTime plannedEnd;
|
||||||
|
private LocalDateTime actualStart;
|
||||||
|
private LocalDateTime actualEnd;
|
||||||
|
private String lineCode;
|
||||||
|
private String batchNumber;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QrProcessCode {
|
||||||
|
private Long id;
|
||||||
|
private String qrCode;
|
||||||
|
private String codeType;
|
||||||
|
private String productCode;
|
||||||
|
private String lotNumber;
|
||||||
|
private Integer quantity;
|
||||||
|
private String currentProcess;
|
||||||
|
private String currentWorkstation;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime lastScannedAt;
|
||||||
|
}
|
||||||
17
backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java
Normal file
17
backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QrScanEvent {
|
||||||
|
private Long id;
|
||||||
|
private String qrCode;
|
||||||
|
private String workstationId;
|
||||||
|
private String operatorId;
|
||||||
|
private String scanType;
|
||||||
|
private String result;
|
||||||
|
private String defectCode;
|
||||||
|
private LocalDateTime scannedAt;
|
||||||
|
private Integer processingTime;
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QualityInspection {
|
||||||
|
private Long id;
|
||||||
|
private String inspectionNumber;
|
||||||
|
private Long orderId;
|
||||||
|
private String lotNumber;
|
||||||
|
private String inspectionType;
|
||||||
|
private String productCode;
|
||||||
|
private Integer sampleSize;
|
||||||
|
private Integer defectCount;
|
||||||
|
private String result;
|
||||||
|
private String defectDetails;
|
||||||
|
private String inspectorId;
|
||||||
|
private LocalDateTime inspectedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
23
backend/src/main/java/com/zioinfo/fa/domain/Workstation.java
Normal file
23
backend/src/main/java/com/zioinfo/fa/domain/Workstation.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.fa.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Workstation {
|
||||||
|
private Long id;
|
||||||
|
private String workstationCode;
|
||||||
|
private String workstationName;
|
||||||
|
private String lineCode;
|
||||||
|
private String processCode;
|
||||||
|
private Integer workerCount;
|
||||||
|
private String epaperDisplayId;
|
||||||
|
private String andonStatus;
|
||||||
|
private Double oee;
|
||||||
|
private Integer targetCount;
|
||||||
|
private Integer actualCount;
|
||||||
|
private String currentOrderId;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.AndonEvent;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AndonEventMapper {
|
||||||
|
List<AndonEvent> findAll(@Param("status") String status, @Param("severity") String severity);
|
||||||
|
AndonEvent findById(Long id);
|
||||||
|
List<AndonEvent> findByWorkstation(String workstationCode);
|
||||||
|
int insert(AndonEvent e);
|
||||||
|
int update(AndonEvent e);
|
||||||
|
int respond(@Param("id") Long id, @Param("responseBy") String responseBy);
|
||||||
|
int resolve(@Param("id") Long id);
|
||||||
|
Map<String, Object> getStats();
|
||||||
|
List<AndonEvent> findActive();
|
||||||
|
}
|
||||||
17
backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java
Normal file
17
backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Bom;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BomMapper {
|
||||||
|
List<Bom> findByProductCode(String productCode);
|
||||||
|
List<Bom> findByProcessCode(String processCode);
|
||||||
|
Bom findById(Long id);
|
||||||
|
int insert(Bom b);
|
||||||
|
int update(Bom b);
|
||||||
|
int delete(Long id);
|
||||||
|
int deleteByProductCode(String productCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperDisplay;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EpaperDisplayMapper {
|
||||||
|
List<EpaperDisplay> findAll(@Param("status") String status, @Param("tenantCode") String tenantCode);
|
||||||
|
EpaperDisplay findById(Long id);
|
||||||
|
EpaperDisplay findByDisplayId(String displayId);
|
||||||
|
int insert(EpaperDisplay d);
|
||||||
|
int update(EpaperDisplay d);
|
||||||
|
int delete(Long id);
|
||||||
|
List<EpaperDisplay> findByBatteryLow(@Param("threshold") Double threshold);
|
||||||
|
List<EpaperDisplay> findOffline();
|
||||||
|
int updateStatus(@Param("displayId") String displayId, @Param("status") String status, @Param("lastSeen") LocalDateTime lastSeen);
|
||||||
|
int updateContent(@Param("id") Long id, @Param("content") String content, @Param("templateId") String templateId);
|
||||||
|
long countTotal();
|
||||||
|
long countOnline();
|
||||||
|
long countBatteryLow();
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperTemplate;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EpaperTemplateMapper {
|
||||||
|
List<EpaperTemplate> findAll();
|
||||||
|
List<EpaperTemplate> findByType(String templateType);
|
||||||
|
List<EpaperTemplate> findByDisplaySize(String displaySize);
|
||||||
|
EpaperTemplate findById(Long id);
|
||||||
|
EpaperTemplate findByCode(String templateCode);
|
||||||
|
int insert(EpaperTemplate t);
|
||||||
|
int update(EpaperTemplate t);
|
||||||
|
int delete(Long id);
|
||||||
|
int toggleActive(Long id, Boolean active);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Equipment;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EquipmentMapper {
|
||||||
|
List<Equipment> findAll(@Param("status") String status, @Param("workstationCode") String workstationCode);
|
||||||
|
Equipment findById(Long id);
|
||||||
|
Equipment findByCode(String equipmentCode);
|
||||||
|
int insert(Equipment e);
|
||||||
|
int update(Equipment e);
|
||||||
|
int delete(Long id);
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") String status);
|
||||||
|
int updateOee(@Param("id") Long id, @Param("oee") Double oee);
|
||||||
|
List<Equipment> findBreakdownRisk();
|
||||||
|
List<Equipment> findMaintenanceDue();
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.FaUser;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface FaUserMapper {
|
||||||
|
List<FaUser> findAll();
|
||||||
|
FaUser findById(Long id);
|
||||||
|
FaUser findByUsername(String username);
|
||||||
|
int insert(FaUser u);
|
||||||
|
int update(FaUser u);
|
||||||
|
int delete(Long id);
|
||||||
|
int toggleActive(Long id, Boolean active);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.FactoryInventory;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface FactoryInventoryMapper {
|
||||||
|
List<FactoryInventory> findAll(String locationCode);
|
||||||
|
FactoryInventory findById(Long id);
|
||||||
|
FactoryInventory findByItemCode(String itemCode);
|
||||||
|
List<FactoryInventory> findLowStock();
|
||||||
|
int insert(FactoryInventory i);
|
||||||
|
int update(FactoryInventory i);
|
||||||
|
int updateQuantity(String itemCode, Double delta);
|
||||||
|
int delete(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.ProcessRoute;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ProcessRouteMapper {
|
||||||
|
List<ProcessRoute> findByProductCode(String productCode);
|
||||||
|
List<ProcessRoute> findByRouteCode(String routeCode);
|
||||||
|
ProcessRoute findById(Long id);
|
||||||
|
int insert(ProcessRoute r);
|
||||||
|
int update(ProcessRoute r);
|
||||||
|
int delete(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.ProductionOrder;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ProductionOrderMapper {
|
||||||
|
List<ProductionOrder> findAll(@Param("status") String status, @Param("lineCode") String lineCode);
|
||||||
|
List<ProductionOrder> findToday();
|
||||||
|
ProductionOrder findById(Long id);
|
||||||
|
ProductionOrder findByOrderNumber(String orderNumber);
|
||||||
|
int insert(ProductionOrder o);
|
||||||
|
int update(ProductionOrder o);
|
||||||
|
int delete(Long id);
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") String status);
|
||||||
|
int updateProgress(@Param("id") Long id, @Param("completedQty") int completedQty, @Param("defectQty") int defectQty);
|
||||||
|
List<ProductionOrder> findDashboardSummary();
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QrProcessCode;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface QrProcessCodeMapper {
|
||||||
|
List<QrProcessCode> findAll(@Param("status") String status, @Param("codeType") String codeType);
|
||||||
|
QrProcessCode findByQrCode(String qrCode);
|
||||||
|
QrProcessCode findById(Long id);
|
||||||
|
List<QrProcessCode> findByLotNumber(String lotNumber);
|
||||||
|
List<QrProcessCode> findByWorkstation(String workstationId);
|
||||||
|
int insert(QrProcessCode q);
|
||||||
|
int update(QrProcessCode q);
|
||||||
|
int updateStatus(@Param("qrCode") String qrCode, @Param("status") String status, @Param("workstation") String workstation, @Param("process") String process);
|
||||||
|
int delete(Long id);
|
||||||
|
List<QrProcessCode> batchInsert(List<QrProcessCode> list);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QrScanEvent;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface QrScanEventMapper {
|
||||||
|
List<QrScanEvent> findByQrCode(String qrCode);
|
||||||
|
List<QrScanEvent> findByWorkstation(@Param("workstationId") String workstationId, @Param("limit") int limit);
|
||||||
|
QrScanEvent findById(Long id);
|
||||||
|
int insert(QrScanEvent e);
|
||||||
|
List<QrScanEvent> findRecentByWorkstation(@Param("workstationId") String workstationId, @Param("limit") int limit);
|
||||||
|
long countByResult(@Param("result") String result, @Param("hours") int hours);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QualityInspection;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface QualityInspectionMapper {
|
||||||
|
List<QualityInspection> findAll(@Param("inspectionType") String inspectionType, @Param("result") String result);
|
||||||
|
QualityInspection findById(Long id);
|
||||||
|
QualityInspection findByNumber(String inspectionNumber);
|
||||||
|
List<QualityInspection> findByOrderId(Long orderId);
|
||||||
|
int insert(QualityInspection q);
|
||||||
|
int update(QualityInspection q);
|
||||||
|
int delete(Long id);
|
||||||
|
List<Map<String, Object>> getDefectPareto();
|
||||||
|
Map<String, Object> getDashboardSummary();
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.zioinfo.fa.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Workstation;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface WorkstationMapper {
|
||||||
|
List<Workstation> findAll(@Param("lineCode") String lineCode, @Param("status") String status);
|
||||||
|
Workstation findById(Long id);
|
||||||
|
Workstation findByCode(String workstationCode);
|
||||||
|
int insert(Workstation w);
|
||||||
|
int update(Workstation w);
|
||||||
|
int delete(Long id);
|
||||||
|
int updateAndon(@Param("code") String code, @Param("andonStatus") String andonStatus);
|
||||||
|
int updateStatus(@Param("code") String code, @Param("status") String status);
|
||||||
|
int updateCount(@Param("code") String code, @Param("actualCount") int actualCount);
|
||||||
|
List<Workstation> findFloorMap();
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.AndonEvent;
|
||||||
|
import com.zioinfo.fa.mapper.AndonEventMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AndonService {
|
||||||
|
private final AndonEventMapper mapper;
|
||||||
|
|
||||||
|
public AndonService(AndonEventMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AndonEvent> findAll(String status, String severity) {
|
||||||
|
return mapper.findAll(status, severity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndonEvent findById(Long id) { return mapper.findById(id); }
|
||||||
|
|
||||||
|
public AndonEvent call(Map<String, Object> req) {
|
||||||
|
AndonEvent e = new AndonEvent();
|
||||||
|
e.setWorkstationCode((String) req.get("workstationCode"));
|
||||||
|
e.setAndonType((String) req.get("andonType"));
|
||||||
|
e.setSeverity((String) req.getOrDefault("severity", "WARNING"));
|
||||||
|
e.setDescription((String) req.get("description"));
|
||||||
|
e.setCalledBy((String) req.get("calledBy"));
|
||||||
|
e.setCalledAt(LocalDateTime.now());
|
||||||
|
e.setStatus("OPEN");
|
||||||
|
mapper.insert(e);
|
||||||
|
return mapper.findById(e.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndonEvent respond(Long id, String responseBy) {
|
||||||
|
mapper.respond(id, responseBy);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndonEvent resolve(Long id) {
|
||||||
|
mapper.resolve(id);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getStats() {
|
||||||
|
return mapper.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AndonEvent> getBoard() {
|
||||||
|
return mapper.findActive();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.auth.JwtUtil;
|
||||||
|
import com.zioinfo.fa.domain.FaUser;
|
||||||
|
import com.zioinfo.fa.mapper.FaUserMapper;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AuthService {
|
||||||
|
private final FaUserMapper userMapper;
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public AuthService(FaUserMapper userMapper, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
|
||||||
|
this.userMapper = userMapper;
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> login(String username, String password) {
|
||||||
|
FaUser user = userMapper.findByUsername(username);
|
||||||
|
if (user == null || !Boolean.TRUE.equals(user.getActive())) {
|
||||||
|
throw new RuntimeException("사용자를 찾을 수 없거나 비활성 상태입니다.");
|
||||||
|
}
|
||||||
|
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
||||||
|
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
|
||||||
|
}
|
||||||
|
String token = jwtUtil.generateToken(username, user.getRole());
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("token", token);
|
||||||
|
result.put("username", username);
|
||||||
|
result.put("role", user.getRole());
|
||||||
|
result.put("workstationCode", user.getWorkstationCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Bom;
|
||||||
|
import com.zioinfo.fa.domain.ProcessRoute;
|
||||||
|
import com.zioinfo.fa.mapper.BomMapper;
|
||||||
|
import com.zioinfo.fa.mapper.ProcessRouteMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BomRouteService {
|
||||||
|
private final BomMapper bomMapper;
|
||||||
|
private final ProcessRouteMapper routeMapper;
|
||||||
|
|
||||||
|
public BomRouteService(BomMapper bomMapper, ProcessRouteMapper routeMapper) {
|
||||||
|
this.bomMapper = bomMapper;
|
||||||
|
this.routeMapper = routeMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Bom> getBomByProduct(String productCode) { return bomMapper.findByProductCode(productCode); }
|
||||||
|
public Bom getBomById(Long id) { return bomMapper.findById(id); }
|
||||||
|
public Bom createBom(Bom b) { bomMapper.insert(b); return bomMapper.findById(b.getId()); }
|
||||||
|
public Bom updateBom(Long id, Bom b) { b.setId(id); bomMapper.update(b); return bomMapper.findById(id); }
|
||||||
|
public void deleteBom(Long id) { bomMapper.delete(id); }
|
||||||
|
|
||||||
|
public List<ProcessRoute> getRouteByProduct(String productCode) { return routeMapper.findByProductCode(productCode); }
|
||||||
|
public ProcessRoute getRouteById(Long id) { return routeMapper.findById(id); }
|
||||||
|
public ProcessRoute createRoute(ProcessRoute r) { routeMapper.insert(r); return routeMapper.findById(r.getId()); }
|
||||||
|
public ProcessRoute updateRoute(Long id, ProcessRoute r) { r.setId(id); routeMapper.update(r); return routeMapper.findById(id); }
|
||||||
|
public void deleteRoute(Long id) { routeMapper.delete(id); }
|
||||||
|
public ProcessRouteMapper getRouteMapper() { return routeMapper; }
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.mapper.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DashboardService {
|
||||||
|
private final WorkstationMapper workstationMapper;
|
||||||
|
private final ProductionOrderMapper orderMapper;
|
||||||
|
private final AndonEventMapper andonMapper;
|
||||||
|
private final EquipmentMapper equipmentMapper;
|
||||||
|
private final EpaperDisplayMapper epaperMapper;
|
||||||
|
private final QualityInspectionMapper qualityMapper;
|
||||||
|
|
||||||
|
public DashboardService(WorkstationMapper workstationMapper, ProductionOrderMapper orderMapper,
|
||||||
|
AndonEventMapper andonMapper, EquipmentMapper equipmentMapper,
|
||||||
|
EpaperDisplayMapper epaperMapper, QualityInspectionMapper qualityMapper) {
|
||||||
|
this.workstationMapper = workstationMapper;
|
||||||
|
this.orderMapper = orderMapper;
|
||||||
|
this.andonMapper = andonMapper;
|
||||||
|
this.equipmentMapper = equipmentMapper;
|
||||||
|
this.epaperMapper = epaperMapper;
|
||||||
|
this.qualityMapper = qualityMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getOverview() {
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("workstations", workstationMapper.findAll(null, null).size());
|
||||||
|
d.put("todayOrders", orderMapper.findToday().size());
|
||||||
|
d.put("activeAndon", andonMapper.findActive().size());
|
||||||
|
d.put("epaperOnline", epaperMapper.countOnline());
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getOeeSummary() {
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("avgOee", 0.87);
|
||||||
|
d.put("availability", 0.92);
|
||||||
|
d.put("performance", 0.88);
|
||||||
|
d.put("quality", 0.97);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getProductionStatus() {
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("today", orderMapper.findToday());
|
||||||
|
d.put("workstations", workstationMapper.findAll(null, "RUNNING"));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getQualitySummary() {
|
||||||
|
Map<String, Object> summary = qualityMapper.getDashboardSummary();
|
||||||
|
return summary != null ? summary : new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getAndonSummary() {
|
||||||
|
return andonMapper.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getEpaperStatus() {
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("total", epaperMapper.countTotal());
|
||||||
|
d.put("online", epaperMapper.countOnline());
|
||||||
|
d.put("batteryLow", epaperMapper.findByBatteryLow(20.0).size());
|
||||||
|
d.put("offline", epaperMapper.findOffline().size());
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperDisplay;
|
||||||
|
import com.zioinfo.fa.mapper.EpaperDisplayMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EpaperDisplayService {
|
||||||
|
private final EpaperDisplayMapper mapper;
|
||||||
|
|
||||||
|
public EpaperDisplayService(EpaperDisplayMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EpaperDisplay> findAll(String status, String tenantCode) {
|
||||||
|
return mapper.findAll(status, tenantCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpaperDisplay findById(Long id) {
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpaperDisplay findByDisplayId(String displayId) {
|
||||||
|
return mapper.findByDisplayId(displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpaperDisplay create(EpaperDisplay d) {
|
||||||
|
mapper.insert(d);
|
||||||
|
return mapper.findById(d.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpaperDisplay update(Long id, EpaperDisplay d) {
|
||||||
|
d.setId(id);
|
||||||
|
mapper.update(d);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) {
|
||||||
|
mapper.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> pushContent(Long id, String content, String templateId) {
|
||||||
|
mapper.updateContent(id, content, templateId);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("displayId", id);
|
||||||
|
result.put("pushedAt", LocalDateTime.now());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> refresh(Long id) {
|
||||||
|
EpaperDisplay d = mapper.findById(id);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("displayId", id);
|
||||||
|
result.put("refreshedAt", LocalDateTime.now());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EpaperDisplay> findOffline() {
|
||||||
|
return mapper.findOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EpaperDisplay> findBatteryLow(Double threshold) {
|
||||||
|
return mapper.findByBatteryLow(threshold == null ? 20.0 : threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> batchPush(List<Long> ids, String content, String templateId) {
|
||||||
|
ids.forEach(id -> mapper.updateContent(id, content, templateId));
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("pushedCount", ids.size());
|
||||||
|
result.put("pushedAt", LocalDateTime.now());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getDashboard() {
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("total", mapper.countTotal());
|
||||||
|
d.put("online", mapper.countOnline());
|
||||||
|
d.put("offline", mapper.findOffline().size());
|
||||||
|
d.put("batteryLow", mapper.findByBatteryLow(20.0).size());
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.EpaperTemplate;
|
||||||
|
import com.zioinfo.fa.mapper.EpaperTemplateMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EpaperTemplateService {
|
||||||
|
private final EpaperTemplateMapper mapper;
|
||||||
|
|
||||||
|
public EpaperTemplateService(EpaperTemplateMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EpaperTemplate> findAll() { return mapper.findAll(); }
|
||||||
|
public List<EpaperTemplate> findByType(String type) { return mapper.findByType(type); }
|
||||||
|
public EpaperTemplate findById(Long id) { return mapper.findById(id); }
|
||||||
|
|
||||||
|
public EpaperTemplate create(EpaperTemplate t) {
|
||||||
|
mapper.insert(t);
|
||||||
|
return mapper.findById(t.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpaperTemplate update(Long id, EpaperTemplate t) {
|
||||||
|
t.setId(id);
|
||||||
|
mapper.update(t);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { mapper.delete(id); }
|
||||||
|
|
||||||
|
public EpaperTemplate preview(Long id) {
|
||||||
|
// Returns template with preview data
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Equipment;
|
||||||
|
import com.zioinfo.fa.mapper.EquipmentMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EquipmentService {
|
||||||
|
private final EquipmentMapper mapper;
|
||||||
|
|
||||||
|
@Value("${ollama.base-url:http://localhost:11434}")
|
||||||
|
private String ollamaUrl;
|
||||||
|
|
||||||
|
public EquipmentService(EquipmentMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Equipment> findAll(String status, String workstationCode) {
|
||||||
|
return mapper.findAll(status, workstationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Equipment findById(Long id) { return mapper.findById(id); }
|
||||||
|
|
||||||
|
public Equipment create(Equipment e) {
|
||||||
|
mapper.insert(e);
|
||||||
|
return mapper.findById(e.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Equipment update(Long id, Equipment e) {
|
||||||
|
e.setId(id);
|
||||||
|
mapper.update(e);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { mapper.delete(id); }
|
||||||
|
|
||||||
|
public Map<String, Object> getOee(Long id) {
|
||||||
|
Equipment e = mapper.findById(id);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("equipment", e);
|
||||||
|
result.put("oee", e != null ? e.getOee() : 0);
|
||||||
|
result.put("availability", 0.92);
|
||||||
|
result.put("performance", 0.88);
|
||||||
|
result.put("quality", 0.97);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getMaintenance(Long id) {
|
||||||
|
Equipment e = mapper.findById(id);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("equipment", e);
|
||||||
|
result.put("lastMaintenance", e != null ? e.getLastMaintenanceAt() : null);
|
||||||
|
result.put("nextMaintenance", e != null ? e.getNextMaintenanceAt() : null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Equipment recordMaintenance(Long id, Map<String, Object> req) {
|
||||||
|
mapper.updateStatus(id, "RUNNING");
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> aiPredict(Map<String, Object> req) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
try {
|
||||||
|
RestTemplate rt = new RestTemplate();
|
||||||
|
String prompt = "Equipment: " + req.get("equipmentCode") +
|
||||||
|
". OEE trend: " + req.get("oeeTrend") +
|
||||||
|
". Predict maintenance need and breakdown risk in Korean.";
|
||||||
|
Map<String, Object> body = Map.of("model", "llama3", "prompt", prompt, "stream", false);
|
||||||
|
ResponseEntity<Map> resp = rt.postForEntity(ollamaUrl + "/api/generate", body, Map.class);
|
||||||
|
result.put("prediction", resp.getBody() != null ? resp.getBody().get("response") : "예측 불가");
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("prediction", "AI 예측 일시 중단. 설비 이력을 수동 확인하세요.");
|
||||||
|
}
|
||||||
|
result.put("success", true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Equipment> getBreakdownRisk() {
|
||||||
|
return mapper.findBreakdownRisk();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.FactoryInventory;
|
||||||
|
import com.zioinfo.fa.mapper.FactoryInventoryMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InventoryService {
|
||||||
|
private final FactoryInventoryMapper mapper;
|
||||||
|
|
||||||
|
@Value("${ollama.base-url:http://localhost:11434}")
|
||||||
|
private String ollamaUrl;
|
||||||
|
|
||||||
|
public InventoryService(FactoryInventoryMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FactoryInventory> findAll(String locationCode) {
|
||||||
|
return mapper.findAll(locationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> movement(Map<String, Object> req) {
|
||||||
|
String itemCode = (String) req.get("itemCode");
|
||||||
|
Double delta = Double.valueOf(req.get("delta").toString());
|
||||||
|
mapper.updateQuantity(itemCode, delta);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("itemCode", itemCode);
|
||||||
|
result.put("delta", delta);
|
||||||
|
result.put("success", true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FactoryInventory> getLowStock() {
|
||||||
|
return mapper.findLowStock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> aiOptimize(Map<String, Object> req) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
try {
|
||||||
|
RestTemplate rt = new RestTemplate();
|
||||||
|
String prompt = "Optimize inventory for factory. Current low stock items: " +
|
||||||
|
req.get("lowStockItems") + ". Suggest reorder quantities in Korean.";
|
||||||
|
Map<String, Object> body = Map.of("model", "llama3", "prompt", prompt, "stream", false);
|
||||||
|
ResponseEntity<Map> resp = rt.postForEntity(ollamaUrl + "/api/generate", body, Map.class);
|
||||||
|
result.put("optimization", resp.getBody() != null ? resp.getBody().get("response") : "최적화 불가");
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("optimization", "AI 최적화 일시 중단. 안전재고 기준으로 발주하세요.");
|
||||||
|
}
|
||||||
|
result.put("success", true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.ProductionOrder;
|
||||||
|
import com.zioinfo.fa.mapper.ProductionOrderMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ProductionOrderService {
|
||||||
|
private final ProductionOrderMapper mapper;
|
||||||
|
|
||||||
|
public ProductionOrderService(ProductionOrderMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ProductionOrder> findAll(String status, String lineCode) {
|
||||||
|
return mapper.findAll(status, lineCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ProductionOrder> findToday() { return mapper.findToday(); }
|
||||||
|
public ProductionOrder findById(Long id) { return mapper.findById(id); }
|
||||||
|
|
||||||
|
public ProductionOrder create(ProductionOrder o) {
|
||||||
|
mapper.insert(o);
|
||||||
|
return mapper.findById(o.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProductionOrder update(Long id, ProductionOrder o) {
|
||||||
|
o.setId(id);
|
||||||
|
mapper.update(o);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { mapper.delete(id); }
|
||||||
|
|
||||||
|
public ProductionOrder release(Long id) {
|
||||||
|
mapper.updateStatus(id, "RELEASED");
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProductionOrder complete(Long id) {
|
||||||
|
mapper.updateStatus(id, "COMPLETED");
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getProgress(Long id) {
|
||||||
|
ProductionOrder o = mapper.findById(id);
|
||||||
|
Map<String, Object> p = new HashMap<>();
|
||||||
|
p.put("order", o);
|
||||||
|
if (o != null && o.getPlannedQty() != null && o.getPlannedQty() > 0) {
|
||||||
|
int completed = o.getCompletedQty() != null ? o.getCompletedQty() : 0;
|
||||||
|
p.put("progressPct", (completed * 100.0) / o.getPlannedQty());
|
||||||
|
} else {
|
||||||
|
p.put("progressPct", 0);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getDashboard() {
|
||||||
|
List<ProductionOrder> today = mapper.findToday();
|
||||||
|
int total = today.size();
|
||||||
|
long completed = today.stream().filter(o -> "COMPLETED".equals(o.getStatus())).count();
|
||||||
|
long inProgress = today.stream().filter(o -> "IN_PROGRESS".equals(o.getStatus())).count();
|
||||||
|
Map<String, Object> d = new HashMap<>();
|
||||||
|
d.put("todayTotal", total);
|
||||||
|
d.put("completed", completed);
|
||||||
|
d.put("inProgress", inProgress);
|
||||||
|
d.put("completionRate", total > 0 ? (completed * 100.0 / total) : 0);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
backend/src/main/java/com/zioinfo/fa/service/QrService.java
Normal file
88
backend/src/main/java/com/zioinfo/fa/service/QrService.java
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.ProcessRoute;
|
||||||
|
import com.zioinfo.fa.domain.QrProcessCode;
|
||||||
|
import com.zioinfo.fa.domain.QrScanEvent;
|
||||||
|
import com.zioinfo.fa.mapper.ProcessRouteMapper;
|
||||||
|
import com.zioinfo.fa.mapper.QrProcessCodeMapper;
|
||||||
|
import com.zioinfo.fa.mapper.QrScanEventMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QrService {
|
||||||
|
private final QrProcessCodeMapper codeMapper;
|
||||||
|
private final QrScanEventMapper scanMapper;
|
||||||
|
|
||||||
|
public QrService(QrProcessCodeMapper codeMapper, QrScanEventMapper scanMapper) {
|
||||||
|
this.codeMapper = codeMapper;
|
||||||
|
this.scanMapper = scanMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QrProcessCode generate(Map<String, Object> req) {
|
||||||
|
QrProcessCode q = new QrProcessCode();
|
||||||
|
q.setQrCode(UUID.randomUUID().toString());
|
||||||
|
q.setCodeType((String) req.get("codeType"));
|
||||||
|
q.setProductCode((String) req.get("productCode"));
|
||||||
|
q.setLotNumber((String) req.get("lotNumber"));
|
||||||
|
q.setQuantity((Integer) req.getOrDefault("quantity", 1));
|
||||||
|
q.setCurrentProcess((String) req.get("currentProcess"));
|
||||||
|
q.setCurrentWorkstation((String) req.get("currentWorkstation"));
|
||||||
|
q.setStatus("IN_PROCESS");
|
||||||
|
codeMapper.insert(q);
|
||||||
|
return codeMapper.findByQrCode(q.getQrCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public QrProcessCode findByCode(String code) {
|
||||||
|
return codeMapper.findByQrCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> scan(Map<String, Object> req) {
|
||||||
|
String qrCode = (String) req.get("qrCode");
|
||||||
|
QrScanEvent event = new QrScanEvent();
|
||||||
|
event.setQrCode(qrCode);
|
||||||
|
event.setWorkstationId((String) req.get("workstationId"));
|
||||||
|
event.setOperatorId((String) req.get("operatorId"));
|
||||||
|
event.setScanType((String) req.getOrDefault("scanType", "SCAN_IN"));
|
||||||
|
event.setResult((String) req.getOrDefault("result", "OK"));
|
||||||
|
event.setScannedAt(LocalDateTime.now());
|
||||||
|
scanMapper.insert(event);
|
||||||
|
codeMapper.updateStatus(qrCode, "IN_PROCESS", (String) req.get("workstationId"), (String) req.get("processCode"));
|
||||||
|
|
||||||
|
QrProcessCode code = codeMapper.findByQrCode(qrCode);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("event", event);
|
||||||
|
result.put("processCode", code);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QrScanEvent> getHistory(String code) {
|
||||||
|
return scanMapper.findByQrCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ProcessRoute> getRoute(String code, com.zioinfo.fa.mapper.ProcessRouteMapper routeMapper) {
|
||||||
|
QrProcessCode q = codeMapper.findByQrCode(code);
|
||||||
|
if (q == null) return new ArrayList<>();
|
||||||
|
return routeMapper.findByProductCode(q.getProductCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QrProcessCode> findByLot(String lot) {
|
||||||
|
return codeMapper.findByLotNumber(lot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QrScanEvent> getRecentByWorkstation(String ws, int limit) {
|
||||||
|
return scanMapper.findRecentByWorkstation(ws, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QrProcessCode> batchGenerate(List<Map<String, Object>> requests) {
|
||||||
|
List<QrProcessCode> result = new ArrayList<>();
|
||||||
|
requests.forEach(req -> result.add(generate(req)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.QualityInspection;
|
||||||
|
import com.zioinfo.fa.mapper.QualityInspectionMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QualityService {
|
||||||
|
private final QualityInspectionMapper mapper;
|
||||||
|
|
||||||
|
@Value("${ollama.base-url:http://localhost:11434}")
|
||||||
|
private String ollamaUrl;
|
||||||
|
|
||||||
|
public QualityService(QualityInspectionMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QualityInspection> findAll(String type, String result) {
|
||||||
|
return mapper.findAll(type, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QualityInspection findById(Long id) { return mapper.findById(id); }
|
||||||
|
|
||||||
|
public QualityInspection create(QualityInspection q) {
|
||||||
|
mapper.insert(q);
|
||||||
|
return mapper.findById(q.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public QualityInspection setResult(Long id, Map<String, Object> req) {
|
||||||
|
QualityInspection q = mapper.findById(id);
|
||||||
|
if (q != null) {
|
||||||
|
q.setResult((String) req.get("result"));
|
||||||
|
q.setDefectCount((Integer) req.getOrDefault("defectCount", 0));
|
||||||
|
q.setDefectDetails((String) req.get("defectDetails"));
|
||||||
|
mapper.update(q);
|
||||||
|
}
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getDefectPareto() {
|
||||||
|
return mapper.getDefectPareto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getDashboard() {
|
||||||
|
return mapper.getDashboardSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> aiAnalyze(Map<String, Object> req) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
try {
|
||||||
|
RestTemplate rt = new RestTemplate();
|
||||||
|
String prompt = "Analyze quality defect: " + req.get("defectDescription") +
|
||||||
|
". Product: " + req.get("productCode") +
|
||||||
|
". Suggest root cause and corrective action in Korean.";
|
||||||
|
Map<String, Object> body = Map.of("model", "llama3", "prompt", prompt, "stream", false);
|
||||||
|
ResponseEntity<Map> resp = rt.postForEntity(ollamaUrl + "/api/generate", body, Map.class);
|
||||||
|
result.put("analysis", resp.getBody() != null ? resp.getBody().get("response") : "분석 불가");
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("analysis", "AI 분석 일시 중단. 수동 검토가 필요합니다.");
|
||||||
|
}
|
||||||
|
result.put("success", true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package com.zioinfo.fa.service;
|
||||||
|
|
||||||
|
import com.zioinfo.fa.domain.Workstation;
|
||||||
|
import com.zioinfo.fa.mapper.WorkstationMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class WorkstationService {
|
||||||
|
private final WorkstationMapper mapper;
|
||||||
|
|
||||||
|
public WorkstationService(WorkstationMapper mapper) {
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Workstation> findAll(String lineCode, String status) {
|
||||||
|
return mapper.findAll(lineCode, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Workstation findById(Long id) { return mapper.findById(id); }
|
||||||
|
public Workstation findByCode(String code) { return mapper.findByCode(code); }
|
||||||
|
|
||||||
|
public Workstation create(Workstation w) {
|
||||||
|
mapper.insert(w);
|
||||||
|
return mapper.findById(w.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Workstation update(Long id, Workstation w) {
|
||||||
|
w.setId(id);
|
||||||
|
mapper.update(w);
|
||||||
|
return mapper.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { mapper.delete(id); }
|
||||||
|
|
||||||
|
public Map<String, Object> updateAndon(String code, String andonStatus) {
|
||||||
|
mapper.updateAndon(code, andonStatus);
|
||||||
|
return Map.of("code", code, "andonStatus", andonStatus, "updated", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Workstation> getFloorMap() {
|
||||||
|
return mapper.findFloorMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/src/main/resources/application.yml
Normal file
26
backend/src/main/resources/application.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
server:
|
||||||
|
port: 8017
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/fa_db
|
||||||
|
username: fa_user
|
||||||
|
password: fa_pass2026
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 3
|
||||||
|
mvc:
|
||||||
|
static-path-pattern: /static/**
|
||||||
|
web:
|
||||||
|
resources:
|
||||||
|
static-locations: classpath:/static/
|
||||||
|
mybatis:
|
||||||
|
mapper-locations: classpath:mapper/*.xml
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
jwt:
|
||||||
|
secret: FaJwtSecret2026ZioInfo!@#$%^&*()
|
||||||
|
expiration: 86400000
|
||||||
|
ollama:
|
||||||
|
base-url: http://localhost:11434
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.zioinfo.fa: DEBUG
|
||||||
258
backend/src/main/resources/db/schema.sql
Normal file
258
backend/src/main/resources/db/schema.sql
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
-- GUARDiA FA Database Schema
|
||||||
|
-- DB: fa_db / User: fa_user / Password: fa_pass2026
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_epaper_displays (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
display_id VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
workstation_id VARCHAR(50),
|
||||||
|
display_size VARCHAR(20),
|
||||||
|
display_type VARCHAR(10),
|
||||||
|
resolution_w INT,
|
||||||
|
resolution_h INT,
|
||||||
|
color_mode VARCHAR(20),
|
||||||
|
refresh_mode VARCHAR(10),
|
||||||
|
current_template VARCHAR(50),
|
||||||
|
display_content JSONB,
|
||||||
|
battery_level DECIMAL(5,2),
|
||||||
|
signal_strength INT,
|
||||||
|
status VARCHAR(20) DEFAULT 'OFFLINE',
|
||||||
|
last_updated TIMESTAMP,
|
||||||
|
last_seen TIMESTAMP,
|
||||||
|
gateway_id VARCHAR(50),
|
||||||
|
protocol VARCHAR(30),
|
||||||
|
tenant_code VARCHAR(20),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_epaper_templates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
template_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
template_name VARCHAR(100) NOT NULL,
|
||||||
|
template_type VARCHAR(50),
|
||||||
|
display_size VARCHAR(20),
|
||||||
|
layout_json JSONB,
|
||||||
|
preview_base64 TEXT,
|
||||||
|
active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_qr_process_codes (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
qr_code VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
code_type VARCHAR(20),
|
||||||
|
product_code VARCHAR(50),
|
||||||
|
lot_number VARCHAR(50),
|
||||||
|
quantity INT,
|
||||||
|
current_process VARCHAR(50),
|
||||||
|
current_workstation VARCHAR(50),
|
||||||
|
status VARCHAR(20) DEFAULT 'IN_PROCESS',
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
last_scanned_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_qr_scan_events (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
qr_code VARCHAR(100) NOT NULL,
|
||||||
|
workstation_id VARCHAR(50),
|
||||||
|
operator_id VARCHAR(50),
|
||||||
|
scan_type VARCHAR(30),
|
||||||
|
result VARCHAR(10),
|
||||||
|
defect_code VARCHAR(30),
|
||||||
|
scanned_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
processing_time INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_workstations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
workstation_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
workstation_name VARCHAR(100),
|
||||||
|
line_code VARCHAR(30),
|
||||||
|
process_code VARCHAR(30),
|
||||||
|
worker_count INT DEFAULT 1,
|
||||||
|
epaper_display_id VARCHAR(50),
|
||||||
|
andon_status VARCHAR(10) DEFAULT 'GREEN',
|
||||||
|
oee DECIMAL(5,4) DEFAULT 0.85,
|
||||||
|
target_count INT DEFAULT 0,
|
||||||
|
actual_count INT DEFAULT 0,
|
||||||
|
current_order_id VARCHAR(50),
|
||||||
|
status VARCHAR(20) DEFAULT 'STOPPED',
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_production_orders (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
product_code VARCHAR(50),
|
||||||
|
product_name VARCHAR(100),
|
||||||
|
planned_qty INT,
|
||||||
|
completed_qty INT DEFAULT 0,
|
||||||
|
defect_qty INT DEFAULT 0,
|
||||||
|
status VARCHAR(20) DEFAULT 'PLANNED',
|
||||||
|
priority INT DEFAULT 5,
|
||||||
|
planned_start TIMESTAMP,
|
||||||
|
planned_end TIMESTAMP,
|
||||||
|
actual_start TIMESTAMP,
|
||||||
|
actual_end TIMESTAMP,
|
||||||
|
line_code VARCHAR(30),
|
||||||
|
batch_number VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_bom (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_code VARCHAR(50) NOT NULL,
|
||||||
|
component_code VARCHAR(50),
|
||||||
|
component_name VARCHAR(100),
|
||||||
|
required_qty DECIMAL(10,4),
|
||||||
|
unit VARCHAR(20),
|
||||||
|
level INT DEFAULT 1,
|
||||||
|
sequence INT DEFAULT 1,
|
||||||
|
process_code VARCHAR(30),
|
||||||
|
substitute_code VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_process_routes (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
route_code VARCHAR(50) NOT NULL,
|
||||||
|
product_code VARCHAR(50),
|
||||||
|
sequence INT,
|
||||||
|
process_code VARCHAR(30),
|
||||||
|
process_name VARCHAR(100),
|
||||||
|
workstation_code VARCHAR(50),
|
||||||
|
standard_time INT,
|
||||||
|
check_points JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_quality_inspections (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
inspection_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
order_id BIGINT,
|
||||||
|
lot_number VARCHAR(50),
|
||||||
|
inspection_type VARCHAR(20),
|
||||||
|
product_code VARCHAR(50),
|
||||||
|
sample_size INT,
|
||||||
|
defect_count INT DEFAULT 0,
|
||||||
|
result VARCHAR(20),
|
||||||
|
defect_details JSONB,
|
||||||
|
inspector_id VARCHAR(50),
|
||||||
|
inspected_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_quality_defects (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
inspection_id BIGINT,
|
||||||
|
defect_code VARCHAR(30),
|
||||||
|
defect_name VARCHAR(100),
|
||||||
|
defect_count INT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_equipment (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
equipment_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
equipment_name VARCHAR(100),
|
||||||
|
equipment_type VARCHAR(50),
|
||||||
|
workstation_code VARCHAR(50),
|
||||||
|
manufacturer VARCHAR(100),
|
||||||
|
model_number VARCHAR(50),
|
||||||
|
install_date DATE,
|
||||||
|
warranty_expiry DATE,
|
||||||
|
status VARCHAR(20) DEFAULT 'IDLE',
|
||||||
|
oee DECIMAL(5,4) DEFAULT 0.85,
|
||||||
|
last_maintenance_at TIMESTAMP,
|
||||||
|
next_maintenance_at TIMESTAMP,
|
||||||
|
epaper_display_id VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_andon_events (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
workstation_code VARCHAR(50),
|
||||||
|
andon_type VARCHAR(20),
|
||||||
|
severity VARCHAR(20),
|
||||||
|
description TEXT,
|
||||||
|
called_by VARCHAR(50),
|
||||||
|
response_by VARCHAR(50),
|
||||||
|
called_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
responded_at TIMESTAMP,
|
||||||
|
resolved_at TIMESTAMP,
|
||||||
|
response_time_seconds INT,
|
||||||
|
resolution_time_seconds INT,
|
||||||
|
status VARCHAR(20) DEFAULT 'OPEN',
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_inventory (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
item_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
item_name VARCHAR(100),
|
||||||
|
location_code VARCHAR(30),
|
||||||
|
quantity DECIMAL(10,4) DEFAULT 0,
|
||||||
|
safety_stock DECIMAL(10,4) DEFAULT 10,
|
||||||
|
reorder_point DECIMAL(10,4) DEFAULT 20,
|
||||||
|
unit VARCHAR(20),
|
||||||
|
status VARCHAR(20) DEFAULT 'NORMAL',
|
||||||
|
last_updated TIMESTAMP DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fa_users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role VARCHAR(30) DEFAULT 'OPERATOR',
|
||||||
|
workstation_code VARCHAR(50),
|
||||||
|
active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_epaper_status ON fa_epaper_displays(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_epaper_tenant ON fa_epaper_displays(tenant_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_qr_code ON fa_qr_process_codes(qr_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_qr_lot ON fa_qr_process_codes(lot_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_scan_qr ON fa_qr_scan_events(qr_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_scan_ws ON fa_qr_scan_events(workstation_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_order_status ON fa_production_orders(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_order_date ON fa_production_orders(planned_start);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_andon_ws ON fa_andon_events(workstation_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_andon_status ON fa_andon_events(status);
|
||||||
|
|
||||||
|
-- Seed: default admin user (password: admin123)
|
||||||
|
INSERT INTO fa_users (username, password_hash, role, active)
|
||||||
|
VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lh32', 'ADMIN', true)
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed: sample workstations
|
||||||
|
INSERT INTO fa_workstations (workstation_code, workstation_name, line_code, process_code, status, andon_status)
|
||||||
|
VALUES
|
||||||
|
('WS-001', '조립 1공정', 'LINE-A', 'ASSEMBLY', 'RUNNING', 'GREEN'),
|
||||||
|
('WS-002', '조립 2공정', 'LINE-A', 'ASSEMBLY', 'RUNNING', 'GREEN'),
|
||||||
|
('WS-003', '검사 공정', 'LINE-A', 'INSPECTION', 'STOPPED', 'GREEN'),
|
||||||
|
('WS-004', '포장 공정', 'LINE-B', 'PACKAGING', 'RUNNING', 'YELLOW')
|
||||||
|
ON CONFLICT (workstation_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed: sample e-paper displays
|
||||||
|
INSERT INTO fa_epaper_displays (display_id, workstation_id, display_size, display_type, resolution_w, resolution_h, color_mode, refresh_mode, status, battery_level, signal_strength, gateway_id, protocol, tenant_code)
|
||||||
|
VALUES
|
||||||
|
('EP-WS-001', 'WS-001', '4.2"', 'BWY', 400, 300, 'MONO', 'FULL', 'ONLINE', 87.5, -65, 'GW-001', 'ESL-BLE', 'ZIOINFO'),
|
||||||
|
('EP-WS-002', 'WS-002', '2.9"', 'BW', 296, 128, 'MONO', 'PARTIAL', 'ONLINE', 52.3, -72, 'GW-001', 'ESL-915MHz', 'ZIOINFO'),
|
||||||
|
('EP-WS-003', 'WS-003', '7.5"', 'BWR', 800, 480, 'MONO', 'FULL', 'OFFLINE', 15.0, -90, 'GW-002', 'WiFi', 'ZIOINFO'),
|
||||||
|
('EP-WS-004', 'WS-004', '2.13"', 'BW', 250, 122, 'MONO', 'PARTIAL', 'ONLINE', 91.2, -58, 'GW-002', 'ESL-BLE', 'ZIOINFO')
|
||||||
|
ON CONFLICT (display_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed: e-paper templates
|
||||||
|
INSERT INTO fa_epaper_templates (template_code, template_name, template_type, display_size, layout_json)
|
||||||
|
VALUES
|
||||||
|
('TPL-WO-001', '작업 지시서', 'WORK_ORDER', '4.2"', '{"type":"work_order","fields":["order_no","product","qty","deadline"]}'),
|
||||||
|
('TPL-ANDON-001', '안돈 현황', 'ANDON', '7.5"', '{"type":"andon","fields":["status","message","time"]}'),
|
||||||
|
('TPL-KANBAN-001', '칸반 카드', 'KANBAN', '2.9"', '{"type":"kanban","fields":["item","qty","location"]}'),
|
||||||
|
('TPL-QC-001', '품질 검사표', 'QUALITY', '4.2"', '{"type":"quality","fields":["lot","result","inspector","time"]}')
|
||||||
|
ON CONFLICT (template_code) DO NOTHING;
|
||||||
50
backend/src/main/resources/mapper/AndonEventMapper.xml
Normal file
50
backend/src/main/resources/mapper/AndonEventMapper.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.AndonEventMapper">
|
||||||
|
<resultMap id="andonMap" type="com.zioinfo.fa.domain.AndonEvent">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="workstationCode" column="workstation_code"/>
|
||||||
|
<result property="andonType" column="andon_type"/>
|
||||||
|
<result property="calledBy" column="called_by"/>
|
||||||
|
<result property="responseBy" column="response_by"/>
|
||||||
|
<result property="calledAt" column="called_at"/>
|
||||||
|
<result property="respondedAt" column="responded_at"/>
|
||||||
|
<result property="resolvedAt" column="resolved_at"/>
|
||||||
|
<result property="responseTimeSeconds" column="response_time_seconds"/>
|
||||||
|
<result property="resolutionTimeSeconds" column="resolution_time_seconds"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="andonMap">
|
||||||
|
SELECT * FROM fa_andon_events
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
<if test="severity != null">AND severity = #{severity}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY called_at DESC LIMIT 100
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="andonMap">SELECT * FROM fa_andon_events WHERE id = #{id}</select>
|
||||||
|
<select id="findByWorkstation" resultMap="andonMap">SELECT * FROM fa_andon_events WHERE workstation_code = #{workstationCode} ORDER BY called_at DESC</select>
|
||||||
|
<select id="findActive" resultMap="andonMap">SELECT * FROM fa_andon_events WHERE resolved_at IS NULL ORDER BY called_at DESC</select>
|
||||||
|
<select id="getStats" resultType="java.util.Map">
|
||||||
|
SELECT COUNT(*) as total_today,
|
||||||
|
SUM(CASE WHEN resolved_at IS NOT NULL THEN 1 ELSE 0 END) as resolved,
|
||||||
|
SUM(CASE WHEN resolved_at IS NULL THEN 1 ELSE 0 END) as active,
|
||||||
|
COALESCE(AVG(response_time_seconds), 0) as avg_response_time
|
||||||
|
FROM fa_andon_events WHERE DATE(called_at) = CURRENT_DATE
|
||||||
|
</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_andon_events (workstation_code, andon_type, severity, description, called_by, called_at, status)
|
||||||
|
VALUES (#{workstationCode}, #{andonType}, #{severity}, #{description}, #{calledBy}, #{calledAt}, #{status})
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_andon_events SET description=#{description}, severity=#{severity} WHERE id=#{id}</update>
|
||||||
|
<update id="respond">
|
||||||
|
UPDATE fa_andon_events SET response_by=#{responseBy}, responded_at=NOW(),
|
||||||
|
response_time_seconds=EXTRACT(EPOCH FROM (NOW() - called_at))::INT,
|
||||||
|
status='IN_PROGRESS' WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="resolve">
|
||||||
|
UPDATE fa_andon_events SET resolved_at=NOW(),
|
||||||
|
resolution_time_seconds=EXTRACT(EPOCH FROM (NOW() - called_at))::INT,
|
||||||
|
status='RESOLVED' WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
24
backend/src/main/resources/mapper/BomMapper.xml
Normal file
24
backend/src/main/resources/mapper/BomMapper.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.BomMapper">
|
||||||
|
<resultMap id="bomMap" type="com.zioinfo.fa.domain.Bom">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="componentCode" column="component_code"/>
|
||||||
|
<result property="componentName" column="component_name"/>
|
||||||
|
<result property="requiredQty" column="required_qty"/>
|
||||||
|
<result property="processCode" column="process_code"/>
|
||||||
|
<result property="substituteCode" column="substitute_code"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findByProductCode" resultMap="bomMap">SELECT * FROM fa_bom WHERE product_code = #{productCode} ORDER BY level, sequence</select>
|
||||||
|
<select id="findByProcessCode" resultMap="bomMap">SELECT * FROM fa_bom WHERE process_code = #{processCode}</select>
|
||||||
|
<select id="findById" resultMap="bomMap">SELECT * FROM fa_bom WHERE id = #{id}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_bom (product_code, component_code, component_name, required_qty, unit, level, process_code, substitute_code)
|
||||||
|
VALUES (#{productCode}, #{componentCode}, #{componentName}, #{requiredQty}, #{unit}, #{level}, #{processCode}, #{substituteCode})
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_bom SET required_qty=#{requiredQty}, unit=#{unit}, level=#{level} WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_bom WHERE id=#{id}</delete>
|
||||||
|
<delete id="deleteByProductCode">DELETE FROM fa_bom WHERE product_code=#{productCode}</delete>
|
||||||
|
</mapper>
|
||||||
53
backend/src/main/resources/mapper/EpaperDisplayMapper.xml
Normal file
53
backend/src/main/resources/mapper/EpaperDisplayMapper.xml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.EpaperDisplayMapper">
|
||||||
|
<resultMap id="displayMap" type="com.zioinfo.fa.domain.EpaperDisplay">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="displayId" column="display_id"/>
|
||||||
|
<result property="workstationId" column="workstation_id"/>
|
||||||
|
<result property="displaySize" column="display_size"/>
|
||||||
|
<result property="displayType" column="display_type"/>
|
||||||
|
<result property="resolutionW" column="resolution_w"/>
|
||||||
|
<result property="resolutionH" column="resolution_h"/>
|
||||||
|
<result property="colorMode" column="color_mode"/>
|
||||||
|
<result property="refreshMode" column="refresh_mode"/>
|
||||||
|
<result property="currentTemplate" column="current_template"/>
|
||||||
|
<result property="displayContent" column="display_content"/>
|
||||||
|
<result property="batteryLevel" column="battery_level"/>
|
||||||
|
<result property="signalStrength" column="signal_strength"/>
|
||||||
|
<result property="lastUpdated" column="last_updated"/>
|
||||||
|
<result property="lastSeen" column="last_seen"/>
|
||||||
|
<result property="gatewayId" column="gateway_id"/>
|
||||||
|
<result property="tenantCode" column="tenant_code"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="findAll" resultMap="displayMap">
|
||||||
|
SELECT * FROM fa_epaper_displays
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
<if test="tenantCode != null">AND tenant_code = #{tenantCode}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="displayMap">SELECT * FROM fa_epaper_displays WHERE id = #{id}</select>
|
||||||
|
<select id="findByDisplayId" resultMap="displayMap">SELECT * FROM fa_epaper_displays WHERE display_id = #{displayId}</select>
|
||||||
|
<select id="findByBatteryLow" resultMap="displayMap">SELECT * FROM fa_epaper_displays WHERE battery_level < #{threshold} AND status = 'ONLINE'</select>
|
||||||
|
<select id="findOffline" resultMap="displayMap">SELECT * FROM fa_epaper_displays WHERE status = 'OFFLINE'</select>
|
||||||
|
<select id="countTotal" resultType="long">SELECT COUNT(*) FROM fa_epaper_displays</select>
|
||||||
|
<select id="countOnline" resultType="long">SELECT COUNT(*) FROM fa_epaper_displays WHERE status = 'ONLINE'</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_epaper_displays (display_id, workstation_id, display_size, display_type, resolution_w, resolution_h,
|
||||||
|
color_mode, refresh_mode, status, gateway_id, protocol, tenant_code)
|
||||||
|
VALUES (#{displayId}, #{workstationId}, #{displaySize}, #{displayType}, #{resolutionW}, #{resolutionH},
|
||||||
|
#{colorMode}, #{refreshMode}, 'OFFLINE', #{gatewayId}, #{protocol}, #{tenantCode})
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_epaper_displays SET workstation_id=#{workstationId}, display_size=#{displaySize},
|
||||||
|
gateway_id=#{gatewayId}, protocol=#{protocol}, tenant_code=#{tenantCode} WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateStatus">UPDATE fa_epaper_displays SET status=#{status}, last_seen=#{lastSeen} WHERE display_id=#{displayId}</update>
|
||||||
|
<update id="updateContent">UPDATE fa_epaper_displays SET display_content=#{content}::jsonb, current_template=#{templateId}, last_updated=NOW() WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_epaper_displays WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
30
backend/src/main/resources/mapper/EpaperTemplateMapper.xml
Normal file
30
backend/src/main/resources/mapper/EpaperTemplateMapper.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.EpaperTemplateMapper">
|
||||||
|
<resultMap id="templateMap" type="com.zioinfo.fa.domain.EpaperTemplate">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="templateCode" column="template_code"/>
|
||||||
|
<result property="templateName" column="template_name"/>
|
||||||
|
<result property="templateType" column="template_type"/>
|
||||||
|
<result property="displaySize" column="display_size"/>
|
||||||
|
<result property="layoutJson" column="layout_json"/>
|
||||||
|
<result property="previewBase64" column="preview_base64"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<result property="updatedAt" column="updated_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="templateMap">SELECT * FROM fa_epaper_templates ORDER BY created_at DESC</select>
|
||||||
|
<select id="findByType" resultMap="templateMap">SELECT * FROM fa_epaper_templates WHERE template_type = #{templateType}</select>
|
||||||
|
<select id="findByDisplaySize" resultMap="templateMap">SELECT * FROM fa_epaper_templates WHERE display_size = #{displaySize}</select>
|
||||||
|
<select id="findById" resultMap="templateMap">SELECT * FROM fa_epaper_templates WHERE id = #{id}</select>
|
||||||
|
<select id="findByCode" resultMap="templateMap">SELECT * FROM fa_epaper_templates WHERE template_code = #{templateCode}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_epaper_templates (template_code, template_name, template_type, display_size, layout_json, active)
|
||||||
|
VALUES (#{templateCode}, #{templateName}, #{templateType}, #{displaySize}, #{layoutJson}, TRUE)
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_epaper_templates SET template_name=#{templateName}, template_type=#{templateType},
|
||||||
|
display_size=#{displaySize}, layout_json=#{layoutJson}, updated_at=NOW() WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="toggleActive">UPDATE fa_epaper_templates SET active=#{active} WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_epaper_templates WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
37
backend/src/main/resources/mapper/EquipmentMapper.xml
Normal file
37
backend/src/main/resources/mapper/EquipmentMapper.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.EquipmentMapper">
|
||||||
|
<resultMap id="eqMap" type="com.zioinfo.fa.domain.Equipment">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="equipmentCode" column="equipment_code"/>
|
||||||
|
<result property="equipmentName" column="equipment_name"/>
|
||||||
|
<result property="equipmentType" column="equipment_type"/>
|
||||||
|
<result property="workstationCode" column="workstation_code"/>
|
||||||
|
<result property="installDate" column="install_date"/>
|
||||||
|
<result property="warrantyExpiry" column="warranty_expiry"/>
|
||||||
|
<result property="lastMaintenanceAt" column="last_maintenance_at"/>
|
||||||
|
<result property="nextMaintenanceAt" column="next_maintenance_at"/>
|
||||||
|
<result property="epaperDisplayId" column="epaper_display_id"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="eqMap">
|
||||||
|
SELECT * FROM fa_equipment
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
<if test="workstationCode != null">AND workstation_code = #{workstationCode}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY equipment_code
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="eqMap">SELECT * FROM fa_equipment WHERE id = #{id}</select>
|
||||||
|
<select id="findByCode" resultMap="eqMap">SELECT * FROM fa_equipment WHERE equipment_code = #{equipmentCode}</select>
|
||||||
|
<select id="findBreakdownRisk" resultMap="eqMap">SELECT * FROM fa_equipment WHERE oee < 0.7 AND status = 'RUNNING' ORDER BY oee ASC</select>
|
||||||
|
<select id="findMaintenanceDue" resultMap="eqMap">SELECT * FROM fa_equipment WHERE next_maintenance_at <= NOW() + INTERVAL '7 days'</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_equipment (equipment_code, equipment_name, equipment_type, workstation_code, manufacturer, model_number, install_date, warranty_expiry, status, oee)
|
||||||
|
VALUES (#{equipmentCode}, #{equipmentName}, #{equipmentType}, #{workstationCode}, #{manufacturer}, #{modelNumber}, #{installDate}, #{warrantyExpiry}, 'IDLE', 0.85)
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_equipment SET equipment_name=#{equipmentName}, workstation_code=#{workstationCode}, status=#{status} WHERE id=#{id}</update>
|
||||||
|
<update id="updateStatus">UPDATE fa_equipment SET status=#{status} WHERE id=#{id}</update>
|
||||||
|
<update id="updateOee">UPDATE fa_equipment SET oee=#{oee} WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_equipment WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
21
backend/src/main/resources/mapper/FaUserMapper.xml
Normal file
21
backend/src/main/resources/mapper/FaUserMapper.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.FaUserMapper">
|
||||||
|
<resultMap id="userMap" type="com.zioinfo.fa.domain.FaUser">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="passwordHash" column="password_hash"/>
|
||||||
|
<result property="workstationCode" column="workstation_code"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<result property="updatedAt" column="updated_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="userMap">SELECT id, username, role, workstation_code, active, created_at FROM fa_users ORDER BY username</select>
|
||||||
|
<select id="findById" resultMap="userMap">SELECT * FROM fa_users WHERE id = #{id}</select>
|
||||||
|
<select id="findByUsername" resultMap="userMap">SELECT * FROM fa_users WHERE username = #{username}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_users (username, password_hash, role, workstation_code, active)
|
||||||
|
VALUES (#{username}, #{passwordHash}, #{role}, #{workstationCode}, #{active})
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_users SET role=#{role}, workstation_code=#{workstationCode}, updated_at=NOW() WHERE id=#{id}</update>
|
||||||
|
<update id="toggleActive">UPDATE fa_users SET active=#{active} WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_users WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
29
backend/src/main/resources/mapper/FactoryInventoryMapper.xml
Normal file
29
backend/src/main/resources/mapper/FactoryInventoryMapper.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.FactoryInventoryMapper">
|
||||||
|
<resultMap id="invMap" type="com.zioinfo.fa.domain.FactoryInventory">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="itemCode" column="item_code"/>
|
||||||
|
<result property="itemName" column="item_name"/>
|
||||||
|
<result property="locationCode" column="location_code"/>
|
||||||
|
<result property="safetyStock" column="safety_stock"/>
|
||||||
|
<result property="reorderPoint" column="reorder_point"/>
|
||||||
|
<result property="lastUpdated" column="last_updated"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultType="com.zioinfo.fa.domain.FactoryInventory">
|
||||||
|
SELECT * FROM fa_inventory
|
||||||
|
<where><if test="locationCode != null">AND location_code = #{locationCode}</if></where>
|
||||||
|
ORDER BY item_code
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="invMap">SELECT * FROM fa_inventory WHERE id = #{id}</select>
|
||||||
|
<select id="findByItemCode" resultMap="invMap">SELECT * FROM fa_inventory WHERE item_code = #{itemCode}</select>
|
||||||
|
<select id="findLowStock" resultMap="invMap">SELECT * FROM fa_inventory WHERE quantity <= safety_stock</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_inventory (item_code, item_name, location_code, quantity, safety_stock, reorder_point, unit, status)
|
||||||
|
VALUES (#{itemCode}, #{itemName}, #{locationCode}, #{quantity}, #{safetyStock}, #{reorderPoint}, #{unit}, 'NORMAL')
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_inventory SET item_name=#{itemName}, quantity=#{quantity}, safety_stock=#{safetyStock}, last_updated=NOW() WHERE id=#{id}</update>
|
||||||
|
<update id="updateQuantity">UPDATE fa_inventory SET quantity=quantity+#{delta}, last_updated=NOW() WHERE item_code=#{itemCode}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_inventory WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
24
backend/src/main/resources/mapper/ProcessRouteMapper.xml
Normal file
24
backend/src/main/resources/mapper/ProcessRouteMapper.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.ProcessRouteMapper">
|
||||||
|
<resultMap id="routeMap" type="com.zioinfo.fa.domain.ProcessRoute">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="routeCode" column="route_code"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="processCode" column="process_code"/>
|
||||||
|
<result property="processName" column="process_name"/>
|
||||||
|
<result property="workstationCode" column="workstation_code"/>
|
||||||
|
<result property="standardTime" column="standard_time"/>
|
||||||
|
<result property="checkPoints" column="check_points"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findByProductCode" resultMap="routeMap">SELECT * FROM fa_process_routes WHERE product_code = #{productCode} ORDER BY sequence</select>
|
||||||
|
<select id="findByRouteCode" resultMap="routeMap">SELECT * FROM fa_process_routes WHERE route_code = #{routeCode} ORDER BY sequence</select>
|
||||||
|
<select id="findById" resultMap="routeMap">SELECT * FROM fa_process_routes WHERE id = #{id}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_process_routes (route_code, product_code, sequence, process_code, process_name, workstation_code, standard_time, check_points)
|
||||||
|
VALUES (#{routeCode}, #{productCode}, #{sequence}, #{processCode}, #{processName}, #{workstationCode}, #{standardTime}, #{checkPoints})
|
||||||
|
</insert>
|
||||||
|
<update id="update">UPDATE fa_process_routes SET process_name=#{processName}, standard_time=#{standardTime} WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_process_routes WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
47
backend/src/main/resources/mapper/ProductionOrderMapper.xml
Normal file
47
backend/src/main/resources/mapper/ProductionOrderMapper.xml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.ProductionOrderMapper">
|
||||||
|
<resultMap id="orderMap" type="com.zioinfo.fa.domain.ProductionOrder">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="orderNumber" column="order_number"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="productName" column="product_name"/>
|
||||||
|
<result property="plannedQty" column="planned_qty"/>
|
||||||
|
<result property="completedQty" column="completed_qty"/>
|
||||||
|
<result property="defectQty" column="defect_qty"/>
|
||||||
|
<result property="plannedStart" column="planned_start"/>
|
||||||
|
<result property="plannedEnd" column="planned_end"/>
|
||||||
|
<result property="actualStart" column="actual_start"/>
|
||||||
|
<result property="actualEnd" column="actual_end"/>
|
||||||
|
<result property="lineCode" column="line_code"/>
|
||||||
|
<result property="batchNumber" column="batch_number"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<result property="updatedAt" column="updated_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="orderMap">
|
||||||
|
SELECT * FROM fa_production_orders
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
<if test="lineCode != null">AND line_code = #{lineCode}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY priority ASC, created_at DESC
|
||||||
|
</select>
|
||||||
|
<select id="findToday" resultMap="orderMap">
|
||||||
|
SELECT * FROM fa_production_orders WHERE DATE(planned_start) = CURRENT_DATE ORDER BY priority ASC
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="orderMap">SELECT * FROM fa_production_orders WHERE id = #{id}</select>
|
||||||
|
<select id="findByOrderNumber" resultMap="orderMap">SELECT * FROM fa_production_orders WHERE order_number = #{orderNumber}</select>
|
||||||
|
<select id="findDashboardSummary" resultMap="orderMap">SELECT * FROM fa_production_orders WHERE DATE(planned_start) = CURRENT_DATE</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_production_orders (order_number, product_code, product_name, planned_qty, completed_qty, defect_qty, status, priority, planned_start, planned_end, line_code, batch_number)
|
||||||
|
VALUES (#{orderNumber}, #{productCode}, #{productName}, #{plannedQty}, 0, 0, 'PLANNED', #{priority}, #{plannedStart}, #{plannedEnd}, #{lineCode}, #{batchNumber})
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_production_orders SET product_name=#{productName}, planned_qty=#{plannedQty},
|
||||||
|
status=#{status}, priority=#{priority}, planned_start=#{plannedStart},
|
||||||
|
planned_end=#{plannedEnd}, updated_at=NOW() WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateStatus">UPDATE fa_production_orders SET status=#{status}, updated_at=NOW() WHERE id=#{id}</update>
|
||||||
|
<update id="updateProgress">UPDATE fa_production_orders SET completed_qty=#{completedQty}, defect_qty=#{defectQty}, updated_at=NOW() WHERE id=#{id}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_production_orders WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
41
backend/src/main/resources/mapper/QrProcessCodeMapper.xml
Normal file
41
backend/src/main/resources/mapper/QrProcessCodeMapper.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.QrProcessCodeMapper">
|
||||||
|
<resultMap id="qrMap" type="com.zioinfo.fa.domain.QrProcessCode">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="qrCode" column="qr_code"/>
|
||||||
|
<result property="codeType" column="code_type"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="lotNumber" column="lot_number"/>
|
||||||
|
<result property="currentProcess" column="current_process"/>
|
||||||
|
<result property="currentWorkstation" column="current_workstation"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<result property="lastScannedAt" column="last_scanned_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="qrMap">
|
||||||
|
SELECT * FROM fa_qr_process_codes
|
||||||
|
<where>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
<if test="codeType != null">AND code_type = #{codeType}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
</select>
|
||||||
|
<select id="findByQrCode" resultMap="qrMap">SELECT * FROM fa_qr_process_codes WHERE qr_code = #{qrCode}</select>
|
||||||
|
<select id="findById" resultMap="qrMap">SELECT * FROM fa_qr_process_codes WHERE id = #{id}</select>
|
||||||
|
<select id="findByLotNumber" resultMap="qrMap">SELECT * FROM fa_qr_process_codes WHERE lot_number = #{lotNumber}</select>
|
||||||
|
<select id="findByWorkstation" resultMap="qrMap">SELECT * FROM fa_qr_process_codes WHERE current_workstation = #{workstationId}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_qr_process_codes (qr_code, code_type, product_code, lot_number, quantity, current_process, current_workstation, status)
|
||||||
|
VALUES (#{qrCode}, #{codeType}, #{productCode}, #{lotNumber}, #{quantity}, #{currentProcess}, #{currentWorkstation}, #{status})
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_qr_process_codes SET code_type=#{codeType}, product_code=#{productCode},
|
||||||
|
quantity=#{quantity}, status=#{status} WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE fa_qr_process_codes SET status=#{status}, current_workstation=#{workstation},
|
||||||
|
current_process=#{process}, last_scanned_at=NOW() WHERE qr_code=#{qrCode}
|
||||||
|
</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_qr_process_codes WHERE id=#{id}</delete>
|
||||||
|
<insert id="batchInsert"><!-- handled individually --></insert>
|
||||||
|
</mapper>
|
||||||
23
backend/src/main/resources/mapper/QrScanEventMapper.xml
Normal file
23
backend/src/main/resources/mapper/QrScanEventMapper.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.QrScanEventMapper">
|
||||||
|
<resultMap id="eventMap" type="com.zioinfo.fa.domain.QrScanEvent">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="qrCode" column="qr_code"/>
|
||||||
|
<result property="workstationId" column="workstation_id"/>
|
||||||
|
<result property="operatorId" column="operator_id"/>
|
||||||
|
<result property="scanType" column="scan_type"/>
|
||||||
|
<result property="defectCode" column="defect_code"/>
|
||||||
|
<result property="scannedAt" column="scanned_at"/>
|
||||||
|
<result property="processingTime" column="processing_time"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findByQrCode" resultMap="eventMap">SELECT * FROM fa_qr_scan_events WHERE qr_code = #{qrCode} ORDER BY scanned_at DESC</select>
|
||||||
|
<select id="findByWorkstation" resultMap="eventMap">SELECT * FROM fa_qr_scan_events WHERE workstation_id = #{workstationId} ORDER BY scanned_at DESC LIMIT #{limit}</select>
|
||||||
|
<select id="findById" resultMap="eventMap">SELECT * FROM fa_qr_scan_events WHERE id = #{id}</select>
|
||||||
|
<select id="findRecentByWorkstation" resultMap="eventMap">SELECT * FROM fa_qr_scan_events WHERE workstation_id = #{workstationId} ORDER BY scanned_at DESC LIMIT #{limit}</select>
|
||||||
|
<select id="countByResult" resultType="long">SELECT COUNT(*) FROM fa_qr_scan_events WHERE result = #{result} AND scanned_at > NOW() - INTERVAL '1 hour' * #{hours}</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_qr_scan_events (qr_code, workstation_id, operator_id, scan_type, result, defect_code, scanned_at, processing_time)
|
||||||
|
VALUES (#{qrCode}, #{workstationId}, #{operatorId}, #{scanType}, #{result}, #{defectCode}, #{scannedAt}, #{processingTime})
|
||||||
|
</insert>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.QualityInspectionMapper">
|
||||||
|
<resultMap id="qiMap" type="com.zioinfo.fa.domain.QualityInspection">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="inspectionNumber" column="inspection_number"/>
|
||||||
|
<result property="orderId" column="order_id"/>
|
||||||
|
<result property="lotNumber" column="lot_number"/>
|
||||||
|
<result property="inspectionType" column="inspection_type"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="sampleSize" column="sample_size"/>
|
||||||
|
<result property="defectCount" column="defect_count"/>
|
||||||
|
<result property="defectDetails" column="defect_details"/>
|
||||||
|
<result property="inspectorId" column="inspector_id"/>
|
||||||
|
<result property="inspectedAt" column="inspected_at"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="qiMap">
|
||||||
|
SELECT * FROM fa_quality_inspections
|
||||||
|
<where>
|
||||||
|
<if test="inspectionType != null">AND inspection_type = #{inspectionType}</if>
|
||||||
|
<if test="result != null">AND result = #{result}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="qiMap">SELECT * FROM fa_quality_inspections WHERE id = #{id}</select>
|
||||||
|
<select id="findByNumber" resultMap="qiMap">SELECT * FROM fa_quality_inspections WHERE inspection_number = #{inspectionNumber}</select>
|
||||||
|
<select id="findByOrderId" resultMap="qiMap">SELECT * FROM fa_quality_inspections WHERE order_id = #{orderId}</select>
|
||||||
|
<select id="getDefectPareto" resultType="java.util.Map">
|
||||||
|
SELECT defect_code, COUNT(*) as count FROM fa_quality_defects GROUP BY defect_code ORDER BY count DESC LIMIT 10
|
||||||
|
</select>
|
||||||
|
<select id="getDashboardSummary" resultType="java.util.Map">
|
||||||
|
SELECT COUNT(*) as total_inspections,
|
||||||
|
SUM(CASE WHEN result='PASS' THEN 1 ELSE 0 END) as passed,
|
||||||
|
SUM(CASE WHEN result='FAIL' THEN 1 ELSE 0 END) as failed,
|
||||||
|
COALESCE(AVG(defect_count * 100.0 / NULLIF(sample_size,0)), 0) as defect_rate
|
||||||
|
FROM fa_quality_inspections WHERE DATE(created_at) = CURRENT_DATE
|
||||||
|
</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_quality_inspections (inspection_number, order_id, lot_number, inspection_type, product_code, sample_size, defect_count, result, defect_details, inspector_id, inspected_at)
|
||||||
|
VALUES (#{inspectionNumber}, #{orderId}, #{lotNumber}, #{inspectionType}, #{productCode}, #{sampleSize}, #{defectCount}, #{result}, #{defectDetails}, #{inspectorId}, #{inspectedAt})
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_quality_inspections SET result=#{result}, defect_count=#{defectCount}, defect_details=#{defectDetails} WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_quality_inspections WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
42
backend/src/main/resources/mapper/WorkstationMapper.xml
Normal file
42
backend/src/main/resources/mapper/WorkstationMapper.xml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.zioinfo.fa.mapper.WorkstationMapper">
|
||||||
|
<resultMap id="wsMap" type="com.zioinfo.fa.domain.Workstation">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="workstationCode" column="workstation_code"/>
|
||||||
|
<result property="workstationName" column="workstation_name"/>
|
||||||
|
<result property="lineCode" column="line_code"/>
|
||||||
|
<result property="processCode" column="process_code"/>
|
||||||
|
<result property="workerCount" column="worker_count"/>
|
||||||
|
<result property="epaperDisplayId" column="epaper_display_id"/>
|
||||||
|
<result property="andonStatus" column="andon_status"/>
|
||||||
|
<result property="targetCount" column="target_count"/>
|
||||||
|
<result property="actualCount" column="actual_count"/>
|
||||||
|
<result property="currentOrderId" column="current_order_id"/>
|
||||||
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<result property="updatedAt" column="updated_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="findAll" resultMap="wsMap">
|
||||||
|
SELECT * FROM fa_workstations
|
||||||
|
<where>
|
||||||
|
<if test="lineCode != null">AND line_code = #{lineCode}</if>
|
||||||
|
<if test="status != null">AND status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY workstation_code
|
||||||
|
</select>
|
||||||
|
<select id="findById" resultMap="wsMap">SELECT * FROM fa_workstations WHERE id = #{id}</select>
|
||||||
|
<select id="findByCode" resultMap="wsMap">SELECT * FROM fa_workstations WHERE workstation_code = #{workstationCode}</select>
|
||||||
|
<select id="findFloorMap" resultMap="wsMap">SELECT * FROM fa_workstations ORDER BY line_code, workstation_code</select>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO fa_workstations (workstation_code, workstation_name, line_code, process_code, worker_count, status, andon_status)
|
||||||
|
VALUES (#{workstationCode}, #{workstationName}, #{lineCode}, #{processCode}, #{workerCount}, 'STOPPED', 'GREEN')
|
||||||
|
</insert>
|
||||||
|
<update id="update">
|
||||||
|
UPDATE fa_workstations SET workstation_name=#{workstationName}, line_code=#{lineCode},
|
||||||
|
process_code=#{processCode}, worker_count=#{workerCount}, updated_at=NOW() WHERE id=#{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateAndon">UPDATE fa_workstations SET andon_status=#{andonStatus}, updated_at=NOW() WHERE workstation_code=#{code}</update>
|
||||||
|
<update id="updateStatus">UPDATE fa_workstations SET status=#{status}, updated_at=NOW() WHERE workstation_code=#{code}</update>
|
||||||
|
<update id="updateCount">UPDATE fa_workstations SET actual_count=#{actualCount}, updated_at=NOW() WHERE workstation_code=#{code}</update>
|
||||||
|
<delete id="delete">DELETE FROM fa_workstations WHERE id=#{id}</delete>
|
||||||
|
</mapper>
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GUARDiA FA — Factory Automation Platform</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "guardia-fa-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^6.26.0",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"recharts": "^2.12.0",
|
||||||
|
"lucide-react": "^0.400.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"vite": "^5.3.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
111
frontend/src/App.tsx
Normal file
111
frontend/src/App.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { BrowserRouter, Routes, Route, Navigate, NavLink } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
LayoutDashboard, Map, Monitor, QrCode, Package,
|
||||||
|
AlertTriangle, ShieldCheck, Wrench, Boxes, Brain, LogOut
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Login from './pages/Login'
|
||||||
|
import Dashboard from './pages/Dashboard'
|
||||||
|
import FloorMap from './pages/FloorMap'
|
||||||
|
import EpaperManagement from './pages/EpaperManagement'
|
||||||
|
import QrTracking from './pages/QrTracking'
|
||||||
|
import ProductionOrders from './pages/ProductionOrders'
|
||||||
|
import AndonBoard from './pages/AndonBoard'
|
||||||
|
import QualityControl from './pages/QualityControl'
|
||||||
|
import EquipmentStatus from './pages/EquipmentStatus'
|
||||||
|
import AiFactory from './pages/AiFactory'
|
||||||
|
|
||||||
|
const NAV_ITEMS = [
|
||||||
|
{ to: '/dashboard', icon: LayoutDashboard, label: '대시보드' },
|
||||||
|
{ to: '/floor-map', icon: Map, label: '공장 배치도' },
|
||||||
|
{ to: '/epaper', icon: Monitor, label: 'e-Paper 관리' },
|
||||||
|
{ to: '/qr', icon: QrCode, label: 'QR WIP 추적' },
|
||||||
|
{ to: '/orders', icon: Package, label: '생산 오더' },
|
||||||
|
{ to: '/andon', icon: AlertTriangle, label: '안돈 현황판' },
|
||||||
|
{ to: '/quality', icon: ShieldCheck, label: '품질 관리' },
|
||||||
|
{ to: '/equipment', icon: Wrench, label: '설비 현황' },
|
||||||
|
{ to: '/inventory', icon: Boxes, label: '재고 관리' },
|
||||||
|
{ to: '/ai', icon: Brain, label: 'AI 공장 분석' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function Sidebar() {
|
||||||
|
const user = JSON.parse(localStorage.getItem('fa_user') || '{}')
|
||||||
|
return (
|
||||||
|
<aside className="w-60 bg-slate-900 border-r border-slate-700 flex flex-col h-screen fixed">
|
||||||
|
<div className="p-4 border-b border-slate-700">
|
||||||
|
<div className="text-blue-400 font-bold text-lg">GUARDiA FA</div>
|
||||||
|
<div className="text-slate-400 text-xs mt-1">Factory Automation Platform</div>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 overflow-y-auto py-2">
|
||||||
|
{NAV_ITEMS.map(({ to, icon: Icon, label }) => (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={16} />
|
||||||
|
{label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="p-4 border-t border-slate-700">
|
||||||
|
<div className="text-xs text-slate-400 mb-2">{user.username} ({user.role})</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { localStorage.clear(); window.location.href = '/login' }}
|
||||||
|
className="flex items-center gap-2 text-xs text-slate-400 hover:text-red-400 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut size={14} /> 로그아웃
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
|
const token = localStorage.getItem('fa_token')
|
||||||
|
return token ? <>{children}</> : <Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-60 flex-1 min-h-screen p-6">{children}</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/*" element={
|
||||||
|
<RequireAuth>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/floor-map" element={<FloorMap />} />
|
||||||
|
<Route path="/epaper" element={<EpaperManagement />} />
|
||||||
|
<Route path="/qr" element={<QrTracking />} />
|
||||||
|
<Route path="/orders" element={<ProductionOrders />} />
|
||||||
|
<Route path="/andon" element={<AndonBoard />} />
|
||||||
|
<Route path="/quality" element={<QualityControl />} />
|
||||||
|
<Route path="/equipment" element={<EquipmentStatus />} />
|
||||||
|
<Route path="/inventory" element={<AiFactory />} />
|
||||||
|
<Route path="/ai" element={<AiFactory />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</RequireAuth>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
frontend/src/api/client.ts
Normal file
28
frontend/src/api/client.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: '/api/fa',
|
||||||
|
timeout: 15000,
|
||||||
|
})
|
||||||
|
|
||||||
|
client.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('fa_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
client.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('fa_token')
|
||||||
|
localStorage.removeItem('fa_user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default client
|
||||||
10
frontend/src/index.css
Normal file
10
frontend/src/index.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
165
frontend/src/pages/AiFactory.tsx
Normal file
165
frontend/src/pages/AiFactory.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Brain, TrendingUp, Package, Wrench } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
interface AiPanel {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
action: () => Promise<string>
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AiFactory() {
|
||||||
|
const [results, setResults] = useState<Record<string, string>>({})
|
||||||
|
const [loading, setLoading] = useState<Record<string, boolean>>({})
|
||||||
|
const [inventory, setInventory] = useState<any[]>([])
|
||||||
|
const [inventoryLoaded, setInventoryLoaded] = useState(false)
|
||||||
|
|
||||||
|
const run = async (key: string, action: () => Promise<string>) => {
|
||||||
|
setLoading(prev => ({ ...prev, [key]: true }))
|
||||||
|
try {
|
||||||
|
const r = await action()
|
||||||
|
setResults(prev => ({ ...prev, [key]: r }))
|
||||||
|
} catch {
|
||||||
|
setResults(prev => ({ ...prev, [key]: 'AI 분석 일시 중단. 온프레미스 Ollama 연결을 확인하세요.' }))
|
||||||
|
}
|
||||||
|
setLoading(prev => ({ ...prev, [key]: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadInventory = async () => {
|
||||||
|
const r = await client.get('/inventory')
|
||||||
|
setInventory(r.data)
|
||||||
|
setInventoryLoaded(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const panels: AiPanel[] = [
|
||||||
|
{
|
||||||
|
title: '생산 수요 예측',
|
||||||
|
desc: '과거 생산 데이터 기반 Ollama AI 수요/생산 예측',
|
||||||
|
icon: <TrendingUp size={18} />,
|
||||||
|
color: 'text-blue-400 border-blue-700',
|
||||||
|
action: async () => {
|
||||||
|
const r = await client.post('/quality/ai-analyze', {
|
||||||
|
productCode: 'ALL',
|
||||||
|
defectDescription: '최근 생산 트렌드 분석 및 다음 주 수요 예측',
|
||||||
|
})
|
||||||
|
return r.data.analysis
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '설비 이상 감지',
|
||||||
|
desc: 'OEE 하락 패턴 분석 및 예지보전 AI 권고',
|
||||||
|
icon: <Wrench size={18} />,
|
||||||
|
color: 'text-yellow-400 border-yellow-700',
|
||||||
|
action: async () => {
|
||||||
|
const r = await client.post('/equipment/ai-predict', {
|
||||||
|
equipmentCode: 'ALL',
|
||||||
|
oeeTrend: '전체 설비 OEE 분석 및 이상 감지 요청',
|
||||||
|
})
|
||||||
|
return r.data.prediction
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '재고 최적화',
|
||||||
|
desc: '안전재고 분석 및 발주 최적화 AI 권고',
|
||||||
|
icon: <Package size={18} />,
|
||||||
|
color: 'text-green-400 border-green-700',
|
||||||
|
action: async () => {
|
||||||
|
const r = await client.post('/inventory/ai-optimize', {
|
||||||
|
lowStockItems: '재고 부족 품목 자동 감지 및 최적 발주량 계산',
|
||||||
|
})
|
||||||
|
return r.data.optimization
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '품질 불량 분석',
|
||||||
|
desc: '공정별 불량 패턴 AI 분석 및 개선 방안',
|
||||||
|
icon: <Brain size={18} />,
|
||||||
|
color: 'text-purple-400 border-purple-700',
|
||||||
|
action: async () => {
|
||||||
|
const r = await client.post('/quality/ai-analyze', {
|
||||||
|
productCode: 'ALL',
|
||||||
|
defectDescription: '전체 공정 불량 패턴 분석 및 개선 방안 도출',
|
||||||
|
})
|
||||||
|
return r.data.analysis
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">AI 공장 분석 (Ollama 온프레미스)</h1>
|
||||||
|
<div className="bg-blue-900/30 border border-blue-700 rounded-xl p-4 text-sm text-blue-300">
|
||||||
|
모든 AI 분석은 온프레미스 Ollama(localhost:11434)를 사용합니다. 외부 AI API는 절대 사용하지 않습니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{panels.map(p => (
|
||||||
|
<div key={p.title} className={`bg-slate-800 rounded-xl p-5 border ${p.color.split(' ')[1]}`}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className={p.color.split(' ')[0]}>{p.icon}</span>
|
||||||
|
<h2 className="text-sm font-medium text-white">{p.title}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mb-4">{p.desc}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => run(p.title, p.action)}
|
||||||
|
disabled={loading[p.title]}
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs disabled:opacity-50 mb-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Brain size={12} /> {loading[p.title] ? '분석 중...' : 'AI 분석 실행'}
|
||||||
|
</button>
|
||||||
|
{results[p.title] && (
|
||||||
|
<div className="bg-slate-700/50 rounded-lg p-3 text-xs text-slate-200 leading-relaxed max-h-48 overflow-y-auto">
|
||||||
|
{results[p.title]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300">재고 현황</h2>
|
||||||
|
<button onClick={loadInventory}
|
||||||
|
className="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs transition-colors">
|
||||||
|
재고 조회
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{inventoryLoaded && (
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">품목코드</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">품목명</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">위치</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">현재고</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">안전재고</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">상태</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{inventory.map((i: any) => (
|
||||||
|
<tr key={i.id} className="border-b border-slate-700/50">
|
||||||
|
<td className="px-3 py-2 font-mono text-blue-400">{i.itemCode}</td>
|
||||||
|
<td className="px-3 py-2 text-white">{i.itemName}</td>
|
||||||
|
<td className="px-3 py-2 text-slate-400">{i.locationCode}</td>
|
||||||
|
<td className="px-3 py-2 text-slate-300">{i.quantity}</td>
|
||||||
|
<td className="px-3 py-2 text-slate-400">{i.safetyStock}</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<span className={(i.quantity || 0) <= (i.safetyStock || 0) ? 'text-red-400' : 'text-green-400'}>
|
||||||
|
{(i.quantity || 0) <= (i.safetyStock || 0) ? '부족' : '정상'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
{inventoryLoaded && inventory.length === 0 && (
|
||||||
|
<div className="text-center text-slate-500 py-4">재고 데이터가 없습니다.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
134
frontend/src/pages/AndonBoard.tsx
Normal file
134
frontend/src/pages/AndonBoard.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { AlertTriangle, CheckCircle, UserCheck } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
const TYPE_COLORS: Record<string, string> = {
|
||||||
|
QUALITY: 'bg-yellow-800 border-yellow-600',
|
||||||
|
MATERIAL: 'bg-orange-800 border-orange-600',
|
||||||
|
MACHINE: 'bg-red-800 border-red-600',
|
||||||
|
SAFETY: 'bg-red-900 border-red-500',
|
||||||
|
HELP: 'bg-blue-800 border-blue-600',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEVERITY_COLORS: Record<string, string> = {
|
||||||
|
INFO: 'text-blue-400',
|
||||||
|
WARNING: 'text-yellow-400',
|
||||||
|
CRITICAL: 'text-red-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AndonBoard() {
|
||||||
|
const [board, setBoard] = useState<any[]>([])
|
||||||
|
const [events, setEvents] = useState<any[]>([])
|
||||||
|
const [stats, setStats] = useState<any>({})
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
client.get('/andon/board').then(r => setBoard(r.data))
|
||||||
|
client.get('/andon/events').then(r => setEvents(r.data))
|
||||||
|
client.get('/andon/stats').then(r => setStats(r.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { load(); const t = setInterval(load, 10000); return () => clearInterval(t) }, [])
|
||||||
|
|
||||||
|
const respond = async (id: number) => {
|
||||||
|
const user = JSON.parse(localStorage.getItem('fa_user') || '{}')
|
||||||
|
await client.put(`/andon/${id}/respond`, { responseBy: user.username })
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolve = async (id: number) => {
|
||||||
|
await client.put(`/andon/${id}/resolve`)
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold text-white">안돈 현황판</h1>
|
||||||
|
<div className="text-xs text-slate-500">10초마다 자동 갱신</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: '오늘 발생', value: stats.total_today, color: 'text-white' },
|
||||||
|
{ label: '해결 완료', value: stats.resolved, color: 'text-green-400' },
|
||||||
|
{ label: '현재 활성', value: stats.active, color: 'text-red-400' },
|
||||||
|
{ label: '평균 응답', value: `${Math.round(stats.avg_response_time || 0)}초`, color: 'text-yellow-400' },
|
||||||
|
].map(({ label, value, color }) => (
|
||||||
|
<div key={label} className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className={`text-2xl font-bold ${color}`}>{value ?? '-'}</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1">{label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{board.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-medium text-red-400 mb-3">활성 안돈 ({board.length}건)</h2>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{board.map(e => (
|
||||||
|
<div key={e.id} className={`border-2 rounded-xl p-4 ${TYPE_COLORS[e.andonType] || 'bg-slate-800 border-slate-600'}`}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<AlertTriangle size={16} className={SEVERITY_COLORS[e.severity] || 'text-yellow-400'} />
|
||||||
|
<span className="font-bold text-white">{e.workstationCode}</span>
|
||||||
|
<span className="text-xs text-white/60">{e.andonType}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/80 mb-3">{e.description}</div>
|
||||||
|
<div className="text-xs text-white/60 mb-3">호출: {e.calledBy} · {e.calledAt?.replace('T', ' ').slice(0, 16)}</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!e.respondedAt && (
|
||||||
|
<button onClick={() => respond(e.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-white/20 hover:bg-white/30 text-white rounded text-xs">
|
||||||
|
<UserCheck size={10} /> 응답
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => resolve(e.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-green-700 hover:bg-green-600 text-white rounded text-xs">
|
||||||
|
<CheckCircle size={10} /> 해결
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<div className="px-4 py-3 border-b border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300">안돈 이벤트 이력</h2>
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">작업장</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">타입</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">심각도</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">설명</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">호출 시각</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">상태</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">응답시간</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{events.slice(0, 20).map(e => (
|
||||||
|
<tr key={e.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="px-4 py-2 font-bold text-white">{e.workstationCode}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-300">{e.andonType}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={SEVERITY_COLORS[e.severity] || 'text-slate-400'}>{e.severity}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-slate-300 max-w-xs truncate">{e.description}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-400 text-xs">{e.calledAt?.replace('T', ' ').slice(0, 16)}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={e.resolvedAt ? 'text-green-400' : e.respondedAt ? 'text-yellow-400' : 'text-red-400'}>
|
||||||
|
{e.resolvedAt ? 'RESOLVED' : e.respondedAt ? 'IN_PROGRESS' : 'OPEN'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-slate-400">{e.responseTimeSeconds ? `${e.responseTimeSeconds}초` : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
119
frontend/src/pages/Dashboard.tsx
Normal file
119
frontend/src/pages/Dashboard.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { RadialBarChart, RadialBar, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [overview, setOverview] = useState<any>({})
|
||||||
|
const [oee, setOee] = useState<any>({})
|
||||||
|
const [andon, setAndon] = useState<any>({})
|
||||||
|
const [epaper, setEpaper] = useState<any>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
client.get('/dashboard/overview'),
|
||||||
|
client.get('/dashboard/oee-summary'),
|
||||||
|
client.get('/dashboard/andon-summary'),
|
||||||
|
client.get('/dashboard/epaper-status'),
|
||||||
|
]).then(([o, oe, a, ep]) => {
|
||||||
|
setOverview(o.data)
|
||||||
|
setOee(oe.data)
|
||||||
|
setAndon(a.data)
|
||||||
|
setEpaper(ep.data)
|
||||||
|
}).catch(console.error)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const oeeData = [
|
||||||
|
{ name: '가동률', value: Math.round((oee.availability || 0.92) * 100), fill: '#3b82f6' },
|
||||||
|
{ name: '성능률', value: Math.round((oee.performance || 0.88) * 100), fill: '#10b981' },
|
||||||
|
{ name: '품질률', value: Math.round((oee.quality || 0.97) * 100), fill: '#f59e0b' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const StatCard = ({ title, value, sub, color }: any) => (
|
||||||
|
<div className={`bg-slate-800 rounded-xl p-5 border border-slate-700`}>
|
||||||
|
<div className="text-slate-400 text-sm mb-1">{title}</div>
|
||||||
|
<div className={`text-3xl font-bold ${color || 'text-white'}`}>{value ?? '-'}</div>
|
||||||
|
{sub && <div className="text-slate-500 text-xs mt-1">{sub}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">공장 전체 현황 대시보드</h1>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<StatCard title="총 작업장" value={overview.workstations} color="text-blue-400" sub="운영 중" />
|
||||||
|
<StatCard title="오늘 생산 오더" value={overview.todayOrders} color="text-green-400" sub="건" />
|
||||||
|
<StatCard title="활성 안돈" value={overview.activeAndon} color="text-red-400" sub="건 대응 필요" />
|
||||||
|
<StatCard title="e-Paper 온라인" value={epaper.online} color="text-emerald-400" sub={`전체 ${epaper.total}대`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-4">OEE 지표 (종합 설비 효율)</h2>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-blue-400">
|
||||||
|
{Math.round(((oee.availability || 0.92) * (oee.performance || 0.88) * (oee.quality || 0.97)) * 100)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-500 text-xs mt-1">종합 OEE</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{oeeData.map(d => (
|
||||||
|
<div key={d.name} className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="text-xs text-slate-400 w-16">{d.name}</div>
|
||||||
|
<div className="flex-1 bg-slate-700 rounded-full h-2">
|
||||||
|
<div className="h-2 rounded-full" style={{ width: `${d.value}%`, backgroundColor: d.fill }} />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-300 w-8 text-right">{d.value}%</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-4">e-Paper 디스플레이 현황</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-center">
|
||||||
|
<div className="text-2xl font-bold text-emerald-400">{epaper.online || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">온라인</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-center">
|
||||||
|
<div className="text-2xl font-bold text-slate-400">{epaper.offline || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">오프라인</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-400">{epaper.batteryLow || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">배터리 부족</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-400">{epaper.total || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">전체</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-4">안돈 통계 (오늘)</h2>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-white">{andon?.total_today || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">총 발생</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-400">{andon?.resolved || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">해결</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-400">{andon?.active || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">활성</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-400">{Math.round(andon?.avg_response_time || 0)}초</div>
|
||||||
|
<div className="text-xs text-slate-400">평균 응답시간</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
120
frontend/src/pages/EpaperManagement.tsx
Normal file
120
frontend/src/pages/EpaperManagement.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Monitor, Battery, Wifi, Send, RefreshCw } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
|
ONLINE: 'text-green-400',
|
||||||
|
OFFLINE: 'text-slate-500',
|
||||||
|
UPDATING: 'text-yellow-400',
|
||||||
|
ERROR: 'text-red-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EpaperManagement() {
|
||||||
|
const [displays, setDisplays] = useState<any[]>([])
|
||||||
|
const [dashboard, setDashboard] = useState<any>({})
|
||||||
|
const [filter, setFilter] = useState('')
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
client.get('/epaper', { params: filter ? { status: filter } : {} }).then(r => setDisplays(r.data))
|
||||||
|
client.get('/epaper/dashboard').then(r => setDashboard(r.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { load() }, [filter])
|
||||||
|
|
||||||
|
const push = async (id: number) => {
|
||||||
|
const content = prompt('푸시할 내용 (JSON 형식):')
|
||||||
|
if (!content) return
|
||||||
|
await client.post(`/epaper/${id}/push`, { content, templateId: 'default' })
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = async (id: number) => {
|
||||||
|
await client.post(`/epaper/${id}/refresh`)
|
||||||
|
alert('갱신 요청 완료')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold text-white">e-Paper 표시기 관리</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['', 'ONLINE', 'OFFLINE', 'ERROR'].map(s => (
|
||||||
|
<button key={s} onClick={() => setFilter(s)}
|
||||||
|
className={`px-3 py-1.5 rounded text-xs transition-colors ${filter === s ? 'bg-blue-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}`}>
|
||||||
|
{s || '전체'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: '전체', value: dashboard.total, color: 'text-white' },
|
||||||
|
{ label: '온라인', value: dashboard.online, color: 'text-green-400' },
|
||||||
|
{ label: '오프라인', value: dashboard.offline, color: 'text-slate-400' },
|
||||||
|
{ label: '배터리 부족', value: dashboard.batteryLow, color: 'text-yellow-400' },
|
||||||
|
].map(({ label, value, color }) => (
|
||||||
|
<div key={label} className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className={`text-2xl font-bold ${color}`}>{value ?? '-'}</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1">{label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">디스플레이 ID</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">작업장</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">크기/타입</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">배터리</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">신호</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">상태</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">프로토콜</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{displays.map(d => (
|
||||||
|
<tr key={d.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="px-4 py-3 font-mono text-blue-400">{d.displayId}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{d.workstationId || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{d.displaySize} / {d.displayType}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`flex items-center gap-1 ${(d.batteryLevel || 0) < 20 ? 'text-red-400' : 'text-green-400'}`}>
|
||||||
|
<Battery size={12} />{d.batteryLevel?.toFixed(0) || 0}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className="flex items-center gap-1 text-slate-300">
|
||||||
|
<Wifi size={12} />{d.signalStrength || '-'} dBm
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`font-medium ${STATUS_COLORS[d.status] || 'text-slate-400'}`}>{d.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-slate-400 text-xs">{d.protocol}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={() => push(d.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded text-xs text-white transition-colors">
|
||||||
|
<Send size={10} /> 푸시
|
||||||
|
</button>
|
||||||
|
<button onClick={() => refresh(d.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-slate-600 hover:bg-slate-500 rounded text-xs text-white transition-colors">
|
||||||
|
<RefreshCw size={10} /> 갱신
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{displays.length === 0 && (
|
||||||
|
<div className="text-center text-slate-500 py-8">등록된 e-Paper 표시기가 없습니다.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
111
frontend/src/pages/EquipmentStatus.tsx
Normal file
111
frontend/src/pages/EquipmentStatus.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Wrench, Brain, AlertTriangle } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
|
RUNNING: 'text-green-400',
|
||||||
|
IDLE: 'text-slate-400',
|
||||||
|
MAINTENANCE: 'text-yellow-400',
|
||||||
|
BREAKDOWN: 'text-red-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EquipmentStatus() {
|
||||||
|
const [equipment, setEquipment] = useState<any[]>([])
|
||||||
|
const [riskList, setRiskList] = useState<any[]>([])
|
||||||
|
const [aiResult, setAiResult] = useState('')
|
||||||
|
const [predicting, setPredicting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.get('/equipment').then(r => setEquipment(r.data))
|
||||||
|
client.get('/equipment/breakdown-risk').then(r => setRiskList(r.data))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const aiPredict = async () => {
|
||||||
|
setPredicting(true)
|
||||||
|
try {
|
||||||
|
const res = await client.post('/equipment/ai-predict', {
|
||||||
|
equipmentCode: 'EQ-001',
|
||||||
|
oeeTrend: '0.85→0.78→0.71 (3일 하락 추세)',
|
||||||
|
})
|
||||||
|
setAiResult(res.data.prediction)
|
||||||
|
} catch { setAiResult('예지보전 AI 일시 중단') }
|
||||||
|
setPredicting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">설비 현황</h1>
|
||||||
|
|
||||||
|
{riskList.length > 0 && (
|
||||||
|
<div className="bg-red-900/30 border border-red-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<AlertTriangle size={16} className="text-red-400" />
|
||||||
|
<span className="text-sm font-medium text-red-300">고장 위험 설비 ({riskList.length}대)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{riskList.map(e => (
|
||||||
|
<span key={e.id} className="px-2 py-1 bg-red-800 text-red-200 rounded text-xs">
|
||||||
|
{e.equipmentCode} (OEE {Math.round((e.oee || 0) * 100)}%)
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300">AI 예지보전</h2>
|
||||||
|
<button onClick={aiPredict} disabled={predicting}
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs disabled:opacity-50">
|
||||||
|
<Brain size={12} /> {predicting ? '분석 중...' : 'AI 예측'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{aiResult ? (
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-sm text-slate-200 leading-relaxed">{aiResult}</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-slate-500 text-sm">AI 예측 버튼으로 Ollama 기반 설비 예지보전 분석을 시작합니다.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">설비 코드</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">설비명</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">작업장</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">제조사</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">OEE</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">상태</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">다음 점검</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{equipment.map(e => (
|
||||||
|
<tr key={e.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="px-4 py-3 font-mono text-blue-400">{e.equipmentCode}</td>
|
||||||
|
<td className="px-4 py-3 text-white">{e.equipmentName}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{e.workstationCode}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-400">{e.manufacturer}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-16 bg-slate-700 rounded-full h-1.5">
|
||||||
|
<div className={`h-1.5 rounded-full ${(e.oee || 0) >= 0.85 ? 'bg-green-500' : (e.oee || 0) >= 0.7 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||||
|
style={{ width: `${(e.oee || 0) * 100}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-300">{Math.round((e.oee || 0) * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`font-medium ${STATUS_COLORS[e.status] || 'text-slate-400'}`}>{e.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-slate-400 text-xs">{e.nextMaintenanceAt?.slice(0, 10) || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{equipment.length === 0 && <div className="text-center text-slate-500 py-8">등록된 설비가 없습니다.</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
74
frontend/src/pages/FloorMap.tsx
Normal file
74
frontend/src/pages/FloorMap.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
const ANDON_COLORS: Record<string, string> = {
|
||||||
|
GREEN: 'bg-green-600 border-green-500',
|
||||||
|
YELLOW: 'bg-yellow-600 border-yellow-500',
|
||||||
|
RED: 'bg-red-600 border-red-500',
|
||||||
|
BLUE: 'bg-blue-600 border-blue-500',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FloorMap() {
|
||||||
|
const [workstations, setWorkstations] = useState<any[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.get('/workstations/floor-map').then(r => setWorkstations(r.data)).catch(console.error)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const lines = Array.from(new Set(workstations.map(w => w.lineCode || 'DEFAULT')))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">공장 배치도 (Floor Map)</h1>
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||||
|
<div className="text-sm text-slate-400 mb-4">안돈 상태:
|
||||||
|
<span className="ml-2 px-2 py-0.5 bg-green-600 rounded text-xs">GREEN 정상</span>
|
||||||
|
<span className="ml-2 px-2 py-0.5 bg-yellow-600 rounded text-xs">YELLOW 주의</span>
|
||||||
|
<span className="ml-2 px-2 py-0.5 bg-red-600 rounded text-xs">RED 긴급</span>
|
||||||
|
<span className="ml-2 px-2 py-0.5 bg-blue-600 rounded text-xs">BLUE 도움요청</span>
|
||||||
|
</div>
|
||||||
|
{lines.map(line => (
|
||||||
|
<div key={line} className="mb-6">
|
||||||
|
<h3 className="text-slate-300 text-sm font-medium mb-3 border-b border-slate-700 pb-2">{line}</h3>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{workstations.filter(w => (w.lineCode || 'DEFAULT') === line).map(ws => (
|
||||||
|
<div
|
||||||
|
key={ws.id}
|
||||||
|
className={`border-2 rounded-xl p-4 w-48 ${ANDON_COLORS[ws.andonStatus] || 'bg-slate-700 border-slate-600'}`}
|
||||||
|
>
|
||||||
|
<div className="font-bold text-white text-sm">{ws.workstationCode}</div>
|
||||||
|
<div className="text-xs text-white/80 mt-1">{ws.workstationName}</div>
|
||||||
|
<div className="mt-3 space-y-1">
|
||||||
|
<div className="text-xs text-white/70">공정: {ws.processCode}</div>
|
||||||
|
<div className="text-xs text-white/70">상태: {ws.status}</div>
|
||||||
|
<div className="flex justify-between mt-2">
|
||||||
|
<span className="text-xs text-white/70">목표</span>
|
||||||
|
<span className="text-xs font-bold text-white">{ws.targetCount || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-xs text-white/70">실적</span>
|
||||||
|
<span className="text-xs font-bold text-white">{ws.actualCount || 0}</span>
|
||||||
|
</div>
|
||||||
|
{ws.oee && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex justify-between text-xs text-white/70 mb-1">
|
||||||
|
<span>OEE</span><span>{Math.round(ws.oee * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/20 rounded-full h-1.5">
|
||||||
|
<div className="bg-white h-1.5 rounded-full" style={{ width: `${ws.oee * 100}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{workstations.length === 0 && (
|
||||||
|
<div className="text-center text-slate-500 py-8">작업장 데이터가 없습니다.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
frontend/src/pages/Login.tsx
Normal file
78
frontend/src/pages/Login.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/fa/auth/login', { username, password })
|
||||||
|
localStorage.setItem('fa_token', res.data.token)
|
||||||
|
localStorage.setItem('fa_user', JSON.stringify({
|
||||||
|
username: res.data.username,
|
||||||
|
role: res.data.role,
|
||||||
|
workstationCode: res.data.workstationCode
|
||||||
|
}))
|
||||||
|
navigate('/dashboard')
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.error || '로그인 실패')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-900 flex items-center justify-center">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-8 w-96 border border-slate-700">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="text-blue-400 text-3xl font-bold mb-2">GUARDiA FA</div>
|
||||||
|
<div className="text-slate-400 text-sm">Factory Automation Platform</div>
|
||||||
|
<div className="text-slate-500 text-xs mt-1">e-Paper · QR WIP · MES 통합 플랫폼</div>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-300 mb-1">사용자명</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-300 mb-1">비밀번호</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <div className="text-red-400 text-sm">{error}</div>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-lg py-2.5 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? '로그인 중...' : '로그인'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="mt-4 text-xs text-slate-500 text-center">
|
||||||
|
기본 계정: admin / admin123
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
126
frontend/src/pages/ProductionOrders.tsx
Normal file
126
frontend/src/pages/ProductionOrders.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Plus, Play, CheckCircle } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
const STATUS_BADGE: Record<string, string> = {
|
||||||
|
PLANNED: 'bg-slate-600 text-slate-200',
|
||||||
|
RELEASED: 'bg-blue-700 text-blue-200',
|
||||||
|
IN_PROGRESS: 'bg-yellow-700 text-yellow-200',
|
||||||
|
COMPLETED: 'bg-green-700 text-green-200',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductionOrders() {
|
||||||
|
const [orders, setOrders] = useState<any[]>([])
|
||||||
|
const [dashboard, setDashboard] = useState<any>({})
|
||||||
|
const [filter, setFilter] = useState('')
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
client.get('/orders', { params: filter ? { status: filter } : {} }).then(r => setOrders(r.data))
|
||||||
|
client.get('/orders/dashboard').then(r => setDashboard(r.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { load() }, [filter])
|
||||||
|
|
||||||
|
const release = async (id: number) => {
|
||||||
|
await client.post(`/orders/${id}/release`)
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
const complete = async (id: number) => {
|
||||||
|
await client.post(`/orders/${id}/complete`)
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold text-white">생산 오더</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['', 'PLANNED', 'RELEASED', 'IN_PROGRESS', 'COMPLETED'].map(s => (
|
||||||
|
<button key={s} onClick={() => setFilter(s)}
|
||||||
|
className={`px-3 py-1.5 rounded text-xs transition-colors ${filter === s ? 'bg-blue-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}`}>
|
||||||
|
{s || '전체'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className="text-2xl font-bold text-white">{dashboard.todayTotal || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">오늘 오더</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-400">{dashboard.inProgress || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">진행 중</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-400">{dashboard.completed || 0}</div>
|
||||||
|
<div className="text-xs text-slate-400">완료</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-400">{Math.round(dashboard.completionRate || 0)}%</div>
|
||||||
|
<div className="text-xs text-slate-400">완료율</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">오더 번호</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">제품</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">라인</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">계획 수량</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">진행 수량</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">진척률</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">상태</th>
|
||||||
|
<th className="px-4 py-3 text-left text-slate-400">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map(o => {
|
||||||
|
const pct = o.plannedQty ? Math.round((o.completedQty || 0) * 100 / o.plannedQty) : 0
|
||||||
|
return (
|
||||||
|
<tr key={o.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="px-4 py-3 font-mono text-blue-400">{o.orderNumber}</td>
|
||||||
|
<td className="px-4 py-3 text-white">{o.productName}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{o.lineCode}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{o.plannedQty?.toLocaleString()}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-300">{o.completedQty?.toLocaleString() || 0}</td>
|
||||||
|
<td className="px-4 py-3 w-32">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 bg-slate-700 rounded-full h-1.5">
|
||||||
|
<div className="bg-blue-500 h-1.5 rounded-full" style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-400">{pct}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${STATUS_BADGE[o.status] || ''}`}>{o.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{o.status === 'PLANNED' && (
|
||||||
|
<button onClick={() => release(o.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-blue-700 hover:bg-blue-600 text-white rounded text-xs">
|
||||||
|
<Play size={10} /> 릴리즈
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{o.status === 'IN_PROGRESS' && (
|
||||||
|
<button onClick={() => complete(o.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 bg-green-700 hover:bg-green-600 text-white rounded text-xs">
|
||||||
|
<CheckCircle size={10} /> 완료
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{orders.length === 0 && <div className="text-center text-slate-500 py-8">생산 오더가 없습니다.</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
155
frontend/src/pages/QrTracking.tsx
Normal file
155
frontend/src/pages/QrTracking.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { QrCode, Search, Plus } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
export default function QrTracking() {
|
||||||
|
const [searchCode, setSearchCode] = useState('')
|
||||||
|
const [qrData, setQrData] = useState<any>(null)
|
||||||
|
const [history, setHistory] = useState<any[]>([])
|
||||||
|
const [route, setRoute] = useState<any[]>([])
|
||||||
|
const [lotSearch, setLotSearch] = useState('')
|
||||||
|
const [lotResults, setLotResults] = useState<any[]>([])
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
if (!searchCode.trim()) return
|
||||||
|
try {
|
||||||
|
const [code, hist, rt] = await Promise.all([
|
||||||
|
client.get(`/qr/${searchCode}`),
|
||||||
|
client.get(`/qr/${searchCode}/history`),
|
||||||
|
client.get(`/qr/${searchCode}/route`),
|
||||||
|
])
|
||||||
|
setQrData(code.data)
|
||||||
|
setHistory(hist.data)
|
||||||
|
setRoute(rt.data)
|
||||||
|
} catch { setQrData(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchLot = async () => {
|
||||||
|
if (!lotSearch.trim()) return
|
||||||
|
const res = await client.get(`/qr/lot/${lotSearch}`)
|
||||||
|
setLotResults(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
const productCode = prompt('제품 코드:')
|
||||||
|
if (!productCode) return
|
||||||
|
const lotNumber = prompt('LOT 번호:')
|
||||||
|
const qty = prompt('수량:')
|
||||||
|
const res = await client.post('/qr/generate', {
|
||||||
|
codeType: 'WIP',
|
||||||
|
productCode,
|
||||||
|
lotNumber,
|
||||||
|
quantity: parseInt(qty || '1'),
|
||||||
|
currentProcess: 'START',
|
||||||
|
})
|
||||||
|
alert(`QR 코드 생성: ${res.data.qrCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_BADGE: Record<string, string> = {
|
||||||
|
IN_PROCESS: 'bg-blue-700 text-blue-200',
|
||||||
|
WAITING: 'bg-yellow-700 text-yellow-200',
|
||||||
|
COMPLETED: 'bg-green-700 text-green-200',
|
||||||
|
HOLD: 'bg-orange-700 text-orange-200',
|
||||||
|
REJECTED: 'bg-red-700 text-red-200',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold text-white">QR WIP 추적</h1>
|
||||||
|
<button onClick={generate}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm transition-colors">
|
||||||
|
<Plus size={14} /> QR 생성
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-3">QR 코드 조회</h2>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<input value={searchCode} onChange={e => setSearchCode(e.target.value)}
|
||||||
|
placeholder="QR 코드 입력..."
|
||||||
|
className="flex-1 bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<button onClick={search}
|
||||||
|
className="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||||
|
<Search size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{qrData && (
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">제품코드</span>
|
||||||
|
<span className="text-white font-mono">{qrData.productCode}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">LOT 번호</span>
|
||||||
|
<span className="text-white font-mono">{qrData.lotNumber}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">현재 공정</span>
|
||||||
|
<span className="text-white">{qrData.currentProcess}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">현재 작업장</span>
|
||||||
|
<span className="text-white">{qrData.currentWorkstation}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">상태</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${STATUS_BADGE[qrData.status] || 'bg-slate-600 text-slate-200'}`}>{qrData.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-3">공정 경로</h2>
|
||||||
|
{route.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{route.map((r, i) => (
|
||||||
|
<div key={r.id} className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-full bg-blue-600 text-white text-xs flex items-center justify-center font-bold">{r.sequence || i + 1}</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-white">{r.processName}</div>
|
||||||
|
<div className="text-xs text-slate-400">{r.workstationCode} · {r.standardTime}분</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : <div className="text-slate-500 text-sm">QR 코드를 조회하면 공정 경로가 표시됩니다.</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-3">스캔 이력</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">시간</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">작업장</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">스캔 타입</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">결과</th>
|
||||||
|
<th className="px-3 py-2 text-left text-slate-400">작업자</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{history.map(h => (
|
||||||
|
<tr key={h.id} className="border-b border-slate-700/50">
|
||||||
|
<td className="px-3 py-2 text-slate-300 text-xs">{h.scannedAt?.replace('T', ' ').slice(0, 16)}</td>
|
||||||
|
<td className="px-3 py-2 text-slate-300">{h.workstationId}</td>
|
||||||
|
<td className="px-3 py-2 text-slate-300">{h.scanType}</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<span className={h.result === 'OK' ? 'text-green-400' : 'text-red-400'}>{h.result}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-slate-400">{h.operatorId}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{history.length === 0 && <div className="text-center text-slate-500 py-4">스캔 이력이 없습니다.</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
117
frontend/src/pages/QualityControl.tsx
Normal file
117
frontend/src/pages/QualityControl.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
|
import { Brain } from 'lucide-react'
|
||||||
|
import client from '../api/client'
|
||||||
|
|
||||||
|
export default function QualityControl() {
|
||||||
|
const [inspections, setInspections] = useState<any[]>([])
|
||||||
|
const [pareto, setPareto] = useState<any[]>([])
|
||||||
|
const [dashboard, setDashboard] = useState<any>({})
|
||||||
|
const [aiResult, setAiResult] = useState('')
|
||||||
|
const [analyzing, setAnalyzing] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.get('/quality/inspections').then(r => setInspections(r.data))
|
||||||
|
client.get('/quality/defects/pareto').then(r => setPareto(r.data))
|
||||||
|
client.get('/quality/dashboard').then(r => setDashboard(r.data))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const aiAnalyze = async () => {
|
||||||
|
setAnalyzing(true)
|
||||||
|
try {
|
||||||
|
const res = await client.post('/quality/ai-analyze', {
|
||||||
|
productCode: 'PROD-001',
|
||||||
|
defectDescription: '표면 스크래치 및 치수 불량 다수 발생',
|
||||||
|
})
|
||||||
|
setAiResult(res.data.analysis)
|
||||||
|
} catch { setAiResult('AI 분석 일시 중단') }
|
||||||
|
setAnalyzing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">품질 관리</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: '오늘 검사', value: dashboard.total_inspections, color: 'text-white' },
|
||||||
|
{ label: '합격', value: dashboard.passed, color: 'text-green-400' },
|
||||||
|
{ label: '불합격', value: dashboard.failed, color: 'text-red-400' },
|
||||||
|
{ label: '불량률', value: `${parseFloat(dashboard.defect_rate || 0).toFixed(2)}%`, color: 'text-yellow-400' },
|
||||||
|
].map(({ label, value, color }) => (
|
||||||
|
<div key={label} className="bg-slate-800 rounded-xl p-4 border border-slate-700 text-center">
|
||||||
|
<div className={`text-2xl font-bold ${color}`}>{value ?? '-'}</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1">{label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300 mb-4">불량 파레토 차트</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<BarChart data={pareto.slice(0, 8)}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||||
|
<XAxis dataKey="defect_code" tick={{ fill: '#94a3b8', fontSize: 10 }} />
|
||||||
|
<YAxis tick={{ fill: '#94a3b8', fontSize: 10 }} />
|
||||||
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155' }} />
|
||||||
|
<Bar dataKey="count" fill="#ef4444" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl p-5 border border-slate-700">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300">AI 불량 원인 분석</h2>
|
||||||
|
<button onClick={aiAnalyze} disabled={analyzing}
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs disabled:opacity-50">
|
||||||
|
<Brain size={12} /> {analyzing ? '분석 중...' : 'AI 분석'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{aiResult ? (
|
||||||
|
<div className="bg-slate-700 rounded-lg p-3 text-sm text-slate-200 leading-relaxed">{aiResult}</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-slate-500 text-sm">AI 분석 버튼을 클릭하면 Ollama가 불량 원인을 분석합니다.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<div className="px-4 py-3 border-b border-slate-700">
|
||||||
|
<h2 className="text-sm font-medium text-slate-300">검사 목록</h2>
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">검사 번호</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">제품</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">검사 유형</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">LOT</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">샘플/불량</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">결과</th>
|
||||||
|
<th className="px-4 py-2 text-left text-slate-400">검사일시</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{inspections.map(q => (
|
||||||
|
<tr key={q.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="px-4 py-2 font-mono text-blue-400">{q.inspectionNumber}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-300">{q.productCode}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-300">{q.inspectionType}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-400">{q.lotNumber}</td>
|
||||||
|
<td className="px-4 py-2 text-slate-300">{q.sampleSize} / {q.defectCount || 0}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className={q.result === 'PASS' ? 'text-green-400' : q.result === 'FAIL' ? 'text-red-400' : 'text-yellow-400'}>
|
||||||
|
{q.result || '-'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-slate-400 text-xs">{q.inspectedAt?.replace('T', ' ').slice(0, 16) || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{inspections.length === 0 && <div className="text-center text-slate-500 py-8">검사 이력이 없습니다.</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
frontend/tailwind.config.js
Normal file
6
frontend/tailwind.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: '../backend/src/main/resources/static',
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8017'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
51
setup_fa_service.sh
Normal file
51
setup_fa_service.sh
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GUARDiA FA Setup Script
|
||||||
|
# Run as root on target server
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="guardia-fa"
|
||||||
|
APP_DIR="/opt/${APP_NAME}"
|
||||||
|
DB_NAME="fa_db"
|
||||||
|
DB_USER="fa_user"
|
||||||
|
DB_PASS="fa_pass2026"
|
||||||
|
APP_PORT="8017"
|
||||||
|
|
||||||
|
echo "=== GUARDiA FA 서비스 설정 ==="
|
||||||
|
|
||||||
|
# Create DB
|
||||||
|
echo "PostgreSQL DB 생성 중..."
|
||||||
|
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" 2>/dev/null || true
|
||||||
|
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" 2>/dev/null || true
|
||||||
|
sudo -u postgres psql -d "${DB_NAME}" -f /opt/guardia-fa/schema.sql 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
mkdir -p ${APP_DIR}
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
cat > /etc/systemd/system/guardia-fa.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=GUARDiA FA Factory Automation Platform
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStart=/usr/bin/java -jar ${APP_DIR}/guardia-fa-1.0.0.jar
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable guardia-fa
|
||||||
|
|
||||||
|
echo "=== 완료 ==="
|
||||||
|
echo "서비스 시작: systemctl start guardia-fa"
|
||||||
|
echo "포트: ${APP_PORT}"
|
||||||
|
echo "URL: http://SERVER_IP:${APP_PORT}"
|
||||||
Loading…
Reference in New Issue
Block a user