From 24f50144b7779ed0f889d6d8989a3f003efd0632 Mon Sep 17 00:00:00 2001 From: ython Date: Wed, 17 Jun 2026 01:23:20 +0900 Subject: [PATCH] feat: GUARDiA FA v1.0 - e-Paper Factory Automation Platform --- Jenkinsfile | 53 ++++ backend/pom.xml | 38 +++ .../java/com/zioinfo/fa/FaApplication.java | 14 + .../java/com/zioinfo/fa/auth/JwtFilter.java | 39 +++ .../java/com/zioinfo/fa/auth/JwtUtil.java | 59 ++++ .../com/zioinfo/fa/config/SecurityConfig.java | 53 ++++ .../java/com/zioinfo/fa/config/WebConfig.java | 38 +++ .../fa/controller/AndonController.java | 52 ++++ .../zioinfo/fa/controller/AuthController.java | 27 ++ .../controller/EpaperDisplayController.java | 80 ++++++ .../controller/EpaperTemplateController.java | 55 ++++ .../fa/controller/EquipmentController.java | 74 +++++ .../fa/controller/FaDashboardController.java | 47 ++++ .../fa/controller/InventoryController.java | 40 +++ .../controller/ProductionOrderController.java | 78 ++++++ .../zioinfo/fa/controller/QrController.java | 66 +++++ .../fa/controller/QualityController.java | 63 +++++ .../fa/controller/WorkstationController.java | 82 ++++++ .../com/zioinfo/fa/domain/AndonEvent.java | 22 ++ .../main/java/com/zioinfo/fa/domain/Bom.java | 18 ++ .../com/zioinfo/fa/domain/EpaperDisplay.java | 28 ++ .../com/zioinfo/fa/domain/EpaperTemplate.java | 18 ++ .../java/com/zioinfo/fa/domain/Equipment.java | 24 ++ .../java/com/zioinfo/fa/domain/FaUser.java | 16 ++ .../zioinfo/fa/domain/FactoryInventory.java | 19 ++ .../com/zioinfo/fa/domain/ProcessRoute.java | 18 ++ .../zioinfo/fa/domain/ProductionOrder.java | 25 ++ .../com/zioinfo/fa/domain/QrProcessCode.java | 19 ++ .../com/zioinfo/fa/domain/QrScanEvent.java | 17 ++ .../zioinfo/fa/domain/QualityInspection.java | 21 ++ .../com/zioinfo/fa/domain/Workstation.java | 23 ++ .../zioinfo/fa/mapper/AndonEventMapper.java | 21 ++ .../java/com/zioinfo/fa/mapper/BomMapper.java | 17 ++ .../fa/mapper/EpaperDisplayMapper.java | 25 ++ .../fa/mapper/EpaperTemplateMapper.java | 19 ++ .../zioinfo/fa/mapper/EquipmentMapper.java | 21 ++ .../com/zioinfo/fa/mapper/FaUserMapper.java | 17 ++ .../fa/mapper/FactoryInventoryMapper.java | 18 ++ .../zioinfo/fa/mapper/ProcessRouteMapper.java | 16 ++ .../fa/mapper/ProductionOrderMapper.java | 21 ++ .../fa/mapper/QrProcessCodeMapper.java | 21 ++ .../zioinfo/fa/mapper/QrScanEventMapper.java | 17 ++ .../fa/mapper/QualityInspectionMapper.java | 21 ++ .../zioinfo/fa/mapper/WorkstationMapper.java | 21 ++ .../com/zioinfo/fa/service/AndonService.java | 55 ++++ .../com/zioinfo/fa/service/AuthService.java | 40 +++ .../zioinfo/fa/service/BomRouteService.java | 33 +++ .../zioinfo/fa/service/DashboardService.java | 71 +++++ .../fa/service/EpaperDisplayService.java | 89 ++++++ .../fa/service/EpaperTemplateService.java | 38 +++ .../zioinfo/fa/service/EquipmentService.java | 89 ++++++ .../zioinfo/fa/service/InventoryService.java | 59 ++++ .../fa/service/ProductionOrderService.java | 75 +++++ .../com/zioinfo/fa/service/QrService.java | 88 ++++++ .../zioinfo/fa/service/QualityService.java | 69 +++++ .../fa/service/WorkstationService.java | 46 ++++ backend/src/main/resources/application.yml | 26 ++ backend/src/main/resources/db/schema.sql | 258 ++++++++++++++++++ .../resources/mapper/AndonEventMapper.xml | 50 ++++ .../src/main/resources/mapper/BomMapper.xml | 24 ++ .../resources/mapper/EpaperDisplayMapper.xml | 53 ++++ .../resources/mapper/EpaperTemplateMapper.xml | 30 ++ .../main/resources/mapper/EquipmentMapper.xml | 37 +++ .../main/resources/mapper/FaUserMapper.xml | 21 ++ .../mapper/FactoryInventoryMapper.xml | 29 ++ .../resources/mapper/ProcessRouteMapper.xml | 24 ++ .../mapper/ProductionOrderMapper.xml | 47 ++++ .../resources/mapper/QrProcessCodeMapper.xml | 41 +++ .../resources/mapper/QrScanEventMapper.xml | 23 ++ .../mapper/QualityInspectionMapper.xml | 47 ++++ .../resources/mapper/WorkstationMapper.xml | 42 +++ frontend/index.html | 12 + frontend/package.json | 27 ++ frontend/postcss.config.js | 6 + frontend/src/App.tsx | 111 ++++++++ frontend/src/api/client.ts | 28 ++ frontend/src/index.css | 10 + frontend/src/main.tsx | 10 + frontend/src/pages/AiFactory.tsx | 165 +++++++++++ frontend/src/pages/AndonBoard.tsx | 134 +++++++++ frontend/src/pages/Dashboard.tsx | 119 ++++++++ frontend/src/pages/EpaperManagement.tsx | 120 ++++++++ frontend/src/pages/EquipmentStatus.tsx | 111 ++++++++ frontend/src/pages/FloorMap.tsx | 74 +++++ frontend/src/pages/Login.tsx | 78 ++++++ frontend/src/pages/ProductionOrders.tsx | 126 +++++++++ frontend/src/pages/QrTracking.tsx | 155 +++++++++++ frontend/src/pages/QualityControl.tsx | 117 ++++++++ frontend/tailwind.config.js | 6 + frontend/vite.config.ts | 15 + setup_fa_service.sh | 51 ++++ 91 files changed, 4460 insertions(+) create mode 100644 Jenkinsfile create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/zioinfo/fa/FaApplication.java create mode 100644 backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java create mode 100644 backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java create mode 100644 backend/src/main/java/com/zioinfo/fa/config/SecurityConfig.java create mode 100644 backend/src/main/java/com/zioinfo/fa/config/WebConfig.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/AndonController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/AuthController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/EpaperDisplayController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/EpaperTemplateController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/EquipmentController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/FaDashboardController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/InventoryController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/ProductionOrderController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/QrController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/QualityController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/controller/WorkstationController.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/Bom.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/EpaperDisplay.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/EpaperTemplate.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/Equipment.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/FaUser.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/FactoryInventory.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/ProcessRoute.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/ProductionOrder.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/QrProcessCode.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/QualityInspection.java create mode 100644 backend/src/main/java/com/zioinfo/fa/domain/Workstation.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/AndonEventMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/EpaperDisplayMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/EpaperTemplateMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/EquipmentMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/FaUserMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/FactoryInventoryMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/ProcessRouteMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/ProductionOrderMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/QrProcessCodeMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/QrScanEventMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/QualityInspectionMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/mapper/WorkstationMapper.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/AndonService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/AuthService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/BomRouteService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/DashboardService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/EpaperDisplayService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/EpaperTemplateService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/EquipmentService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/InventoryService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/ProductionOrderService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/QrService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/QualityService.java create mode 100644 backend/src/main/java/com/zioinfo/fa/service/WorkstationService.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/db/schema.sql create mode 100644 backend/src/main/resources/mapper/AndonEventMapper.xml create mode 100644 backend/src/main/resources/mapper/BomMapper.xml create mode 100644 backend/src/main/resources/mapper/EpaperDisplayMapper.xml create mode 100644 backend/src/main/resources/mapper/EpaperTemplateMapper.xml create mode 100644 backend/src/main/resources/mapper/EquipmentMapper.xml create mode 100644 backend/src/main/resources/mapper/FaUserMapper.xml create mode 100644 backend/src/main/resources/mapper/FactoryInventoryMapper.xml create mode 100644 backend/src/main/resources/mapper/ProcessRouteMapper.xml create mode 100644 backend/src/main/resources/mapper/ProductionOrderMapper.xml create mode 100644 backend/src/main/resources/mapper/QrProcessCodeMapper.xml create mode 100644 backend/src/main/resources/mapper/QrScanEventMapper.xml create mode 100644 backend/src/main/resources/mapper/QualityInspectionMapper.xml create mode 100644 backend/src/main/resources/mapper/WorkstationMapper.xml create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/AiFactory.tsx create mode 100644 frontend/src/pages/AndonBoard.tsx create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/pages/EpaperManagement.tsx create mode 100644 frontend/src/pages/EquipmentStatus.tsx create mode 100644 frontend/src/pages/FloorMap.tsx create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/ProductionOrders.tsx create mode 100644 frontend/src/pages/QrTracking.tsx create mode 100644 frontend/src/pages/QualityControl.tsx create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.ts create mode 100644 setup_fa_service.sh diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..fb9c8da --- /dev/null +++ b/Jenkinsfile @@ -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 배포 실패 - 롤백 필요" + } + } +} diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..9de9eed --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + com.zioinfo + guardia-fa + 1.0.0 + + 17 + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-security + org.mybatis.spring.bootmybatis-spring-boot-starter3.0.3 + org.postgresqlpostgresqlruntime + io.jsonwebtokenjjwt-api0.12.6 + io.jsonwebtokenjjwt-impl0.12.6runtime + io.jsonwebtokenjjwt-jackson0.12.6runtime + org.projectlomboklomboktrue + com.fasterxml.jackson.corejackson-databind + + + + org.springframework.bootspring-boot-maven-plugin + + + org.projectlomboklombok + + + + + + diff --git a/backend/src/main/java/com/zioinfo/fa/FaApplication.java b/backend/src/main/java/com/zioinfo/fa/FaApplication.java new file mode 100644 index 0000000..db47292 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/FaApplication.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java b/backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java new file mode 100644 index 0000000..1adecb0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/auth/JwtFilter.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java b/backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java new file mode 100644 index 0000000..a30391a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/auth/JwtUtil.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/fa/config/SecurityConfig.java new file mode 100644 index 0000000..3f93393 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/config/SecurityConfig.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/config/WebConfig.java b/backend/src/main/java/com/zioinfo/fa/config/WebConfig.java new file mode 100644 index 0000000..88b5737 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/config/WebConfig.java @@ -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; + } + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/AndonController.java b/backend/src/main/java/com/zioinfo/fa/controller/AndonController.java new file mode 100644 index 0000000..7c6f680 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/AndonController.java @@ -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> findAll( + @RequestParam(required = false) String status, + @RequestParam(required = false) String severity) { + return ResponseEntity.ok(service.findAll(status, severity)); + } + + @PostMapping("/call") + public ResponseEntity call(@RequestBody Map req) { + return ResponseEntity.ok(service.call(req)); + } + + @PutMapping("/{id}/respond") + public ResponseEntity respond( + @PathVariable Long id, @RequestBody Map req) { + return ResponseEntity.ok(service.respond(id, req.get("responseBy"))); + } + + @PutMapping("/{id}/resolve") + public ResponseEntity resolve(@PathVariable Long id) { + return ResponseEntity.ok(service.resolve(id)); + } + + @GetMapping("/stats") + public ResponseEntity> getStats() { + return ResponseEntity.ok(service.getStats()); + } + + @GetMapping("/board") + public ResponseEntity> getBoard() { + return ResponseEntity.ok(service.getBoard()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/AuthController.java b/backend/src/main/java/com/zioinfo/fa/controller/AuthController.java new file mode 100644 index 0000000..3792ff6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/AuthController.java @@ -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> login(@RequestBody Map req) { + try { + Map 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())); + } + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/EpaperDisplayController.java b/backend/src/main/java/com/zioinfo/fa/controller/EpaperDisplayController.java new file mode 100644 index 0000000..8b128b2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/EpaperDisplayController.java @@ -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> findAll( + @RequestParam(required = false) String status, + @RequestParam(required = false) String tenantCode) { + return ResponseEntity.ok(service.findAll(status, tenantCode)); + } + + @PostMapping + public ResponseEntity create(@RequestBody EpaperDisplay d) { + return ResponseEntity.ok(service.create(d)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + EpaperDisplay d = service.findById(id); + return d != null ? ResponseEntity.ok(d) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody EpaperDisplay d) { + return ResponseEntity.ok(service.update(id, d)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{id}/push") + public ResponseEntity> push(@PathVariable Long id, @RequestBody Map req) { + return ResponseEntity.ok(service.pushContent(id, req.get("content"), req.get("templateId"))); + } + + @PostMapping("/{id}/refresh") + public ResponseEntity> refresh(@PathVariable Long id) { + return ResponseEntity.ok(service.refresh(id)); + } + + @GetMapping("/offline") + public ResponseEntity> offline() { + return ResponseEntity.ok(service.findOffline()); + } + + @GetMapping("/battery-low") + public ResponseEntity> batteryLow( + @RequestParam(required = false, defaultValue = "20") Double threshold) { + return ResponseEntity.ok(service.findBatteryLow(threshold)); + } + + @PostMapping("/batch-push") + public ResponseEntity> batchPush(@RequestBody Map req) { + List ids = (List) req.get("ids"); + return ResponseEntity.ok(service.batchPush(ids, (String) req.get("content"), (String) req.get("templateId"))); + } + + @GetMapping("/dashboard") + public ResponseEntity> dashboard() { + return ResponseEntity.ok(service.getDashboard()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/EpaperTemplateController.java b/backend/src/main/java/com/zioinfo/fa/controller/EpaperTemplateController.java new file mode 100644 index 0000000..ccb765a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/EpaperTemplateController.java @@ -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> findAll() { + return ResponseEntity.ok(service.findAll()); + } + + @PostMapping + public ResponseEntity create(@RequestBody EpaperTemplate t) { + return ResponseEntity.ok(service.create(t)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + EpaperTemplate t = service.findById(id); + return t != null ? ResponseEntity.ok(t) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody EpaperTemplate t) { + return ResponseEntity.ok(service.update(id, t)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}/preview") + public ResponseEntity preview(@PathVariable Long id) { + return ResponseEntity.ok(service.preview(id)); + } + + @GetMapping("/type/{type}") + public ResponseEntity> findByType(@PathVariable String type) { + return ResponseEntity.ok(service.findByType(type)); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/EquipmentController.java b/backend/src/main/java/com/zioinfo/fa/controller/EquipmentController.java new file mode 100644 index 0000000..2721a71 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/EquipmentController.java @@ -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> findAll( + @RequestParam(required = false) String status, + @RequestParam(required = false) String workstationCode) { + return ResponseEntity.ok(service.findAll(status, workstationCode)); + } + + @PostMapping + public ResponseEntity create(@RequestBody Equipment e) { + return ResponseEntity.ok(service.create(e)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + Equipment e = service.findById(id); + return e != null ? ResponseEntity.ok(e) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody Equipment e) { + return ResponseEntity.ok(service.update(id, e)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}/oee") + public ResponseEntity> getOee(@PathVariable Long id) { + return ResponseEntity.ok(service.getOee(id)); + } + + @GetMapping("/{id}/maintenance") + public ResponseEntity> getMaintenance(@PathVariable Long id) { + return ResponseEntity.ok(service.getMaintenance(id)); + } + + @PostMapping("/{id}/maintenance") + public ResponseEntity recordMaintenance( + @PathVariable Long id, @RequestBody Map req) { + return ResponseEntity.ok(service.recordMaintenance(id, req)); + } + + @PostMapping("/ai-predict") + public ResponseEntity> aiPredict(@RequestBody Map req) { + return ResponseEntity.ok(service.aiPredict(req)); + } + + @GetMapping("/breakdown-risk") + public ResponseEntity> getBreakdownRisk() { + return ResponseEntity.ok(service.getBreakdownRisk()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/FaDashboardController.java b/backend/src/main/java/com/zioinfo/fa/controller/FaDashboardController.java new file mode 100644 index 0000000..0ee0772 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/FaDashboardController.java @@ -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> overview() { + return ResponseEntity.ok(service.getOverview()); + } + + @GetMapping("/oee-summary") + public ResponseEntity> oeeSummary() { + return ResponseEntity.ok(service.getOeeSummary()); + } + + @GetMapping("/production-status") + public ResponseEntity> productionStatus() { + return ResponseEntity.ok(service.getProductionStatus()); + } + + @GetMapping("/quality-summary") + public ResponseEntity> qualitySummary() { + return ResponseEntity.ok(service.getQualitySummary()); + } + + @GetMapping("/andon-summary") + public ResponseEntity> andonSummary() { + return ResponseEntity.ok(service.getAndonSummary()); + } + + @GetMapping("/epaper-status") + public ResponseEntity> epaperStatus() { + return ResponseEntity.ok(service.getEpaperStatus()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/InventoryController.java b/backend/src/main/java/com/zioinfo/fa/controller/InventoryController.java new file mode 100644 index 0000000..cbfc480 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/InventoryController.java @@ -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> findAll( + @RequestParam(required = false) String locationCode) { + return ResponseEntity.ok(service.findAll(locationCode)); + } + + @PostMapping("/movement") + public ResponseEntity> movement(@RequestBody Map req) { + return ResponseEntity.ok(service.movement(req)); + } + + @GetMapping("/low-stock") + public ResponseEntity> getLowStock() { + return ResponseEntity.ok(service.getLowStock()); + } + + @PostMapping("/ai-optimize") + public ResponseEntity> aiOptimize(@RequestBody Map req) { + return ResponseEntity.ok(service.aiOptimize(req)); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/ProductionOrderController.java b/backend/src/main/java/com/zioinfo/fa/controller/ProductionOrderController.java new file mode 100644 index 0000000..d848ea9 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/ProductionOrderController.java @@ -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> findAll( + @RequestParam(required = false) String status, + @RequestParam(required = false) String lineCode) { + return ResponseEntity.ok(service.findAll(status, lineCode)); + } + + @PostMapping + public ResponseEntity create(@RequestBody ProductionOrder o) { + return ResponseEntity.ok(service.create(o)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + ProductionOrder o = service.findById(id); + return o != null ? ResponseEntity.ok(o) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody ProductionOrder o) { + return ResponseEntity.ok(service.update(id, o)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{id}/release") + public ResponseEntity release(@PathVariable Long id) { + return ResponseEntity.ok(service.release(id)); + } + + @PostMapping("/{id}/complete") + public ResponseEntity complete(@PathVariable Long id) { + return ResponseEntity.ok(service.complete(id)); + } + + @GetMapping("/{id}/progress") + public ResponseEntity> getProgress(@PathVariable Long id) { + return ResponseEntity.ok(service.getProgress(id)); + } + + @GetMapping("/{id}/qr-codes") + public ResponseEntity> getQrCodes(@PathVariable Long id) { + return ResponseEntity.ok(Map.of("orderId", id, "qrCodes", List.of())); + } + + @GetMapping("/today") + public ResponseEntity> getToday() { + return ResponseEntity.ok(service.findToday()); + } + + @GetMapping("/dashboard") + public ResponseEntity> getDashboard() { + return ResponseEntity.ok(service.getDashboard()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/QrController.java b/backend/src/main/java/com/zioinfo/fa/controller/QrController.java new file mode 100644 index 0000000..cb6ed7b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/QrController.java @@ -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 generate(@RequestBody Map req) { + return ResponseEntity.ok(qrService.generate(req)); + } + + @GetMapping("/{code}") + public ResponseEntity findByCode(@PathVariable String code) { + QrProcessCode q = qrService.findByCode(code); + return q != null ? ResponseEntity.ok(q) : ResponseEntity.notFound().build(); + } + + @PostMapping("/scan") + public ResponseEntity> scan(@RequestBody Map req) { + return ResponseEntity.ok(qrService.scan(req)); + } + + @GetMapping("/{code}/history") + public ResponseEntity> 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> findByLot(@PathVariable String lot) { + return ResponseEntity.ok(qrService.findByLot(lot)); + } + + @GetMapping("/workstation/{ws}/recent") + public ResponseEntity> recentByWorkstation( + @PathVariable String ws, + @RequestParam(defaultValue = "20") int limit) { + return ResponseEntity.ok(qrService.getRecentByWorkstation(ws, limit)); + } + + @PostMapping("/batch-generate") + public ResponseEntity> batchGenerate(@RequestBody List> requests) { + return ResponseEntity.ok(qrService.batchGenerate(requests)); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/QualityController.java b/backend/src/main/java/com/zioinfo/fa/controller/QualityController.java new file mode 100644 index 0000000..dd2935a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/QualityController.java @@ -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> findAll( + @RequestParam(required = false) String inspectionType, + @RequestParam(required = false) String result) { + return ResponseEntity.ok(service.findAll(inspectionType, result)); + } + + @PostMapping("/inspections") + public ResponseEntity create(@RequestBody QualityInspection q) { + return ResponseEntity.ok(service.create(q)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + QualityInspection q = service.findById(id); + return q != null ? ResponseEntity.ok(q) : ResponseEntity.notFound().build(); + } + + @PostMapping("/{id}/result") + public ResponseEntity setResult( + @PathVariable Long id, @RequestBody Map req) { + return ResponseEntity.ok(service.setResult(id, req)); + } + + @GetMapping("/defects/pareto") + public ResponseEntity>> getDefectPareto() { + return ResponseEntity.ok(service.getDefectPareto()); + } + + @GetMapping("/spc") + public ResponseEntity> getSpc() { + return ResponseEntity.ok(Map.of("cp", 1.33, "cpk", 1.25, "status", "STABLE")); + } + + @PostMapping("/ai-analyze") + public ResponseEntity> aiAnalyze(@RequestBody Map req) { + return ResponseEntity.ok(service.aiAnalyze(req)); + } + + @GetMapping("/dashboard") + public ResponseEntity> getDashboard() { + return ResponseEntity.ok(service.getDashboard()); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/controller/WorkstationController.java b/backend/src/main/java/com/zioinfo/fa/controller/WorkstationController.java new file mode 100644 index 0000000..c010cbf --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/controller/WorkstationController.java @@ -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> findAll( + @RequestParam(required = false) String lineCode, + @RequestParam(required = false) String status) { + return ResponseEntity.ok(service.findAll(lineCode, status)); + } + + @PostMapping + public ResponseEntity create(@RequestBody Workstation w) { + return ResponseEntity.ok(service.create(w)); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + Workstation w = service.findById(id); + return w != null ? ResponseEntity.ok(w) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody Workstation w) { + return ResponseEntity.ok(service.update(id, w)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}/status") + public ResponseEntity getStatus(@PathVariable Long id) { + return ResponseEntity.ok(service.findById(id)); + } + + @PutMapping("/{id}/andon") + public ResponseEntity> updateAndon( + @PathVariable Long id, @RequestBody Map 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 getCurrentOrder(@PathVariable Long id) { + return ResponseEntity.ok(service.findById(id)); + } + + @PostMapping("/{id}/epaper-push") + public ResponseEntity> epaperPush( + @PathVariable Long id, @RequestBody Map req) { + return ResponseEntity.ok(Map.of("pushed", true, "workstationId", id)); + } + + @GetMapping("/floor-map") + public ResponseEntity> getFloorMap() { + return ResponseEntity.ok(service.getFloorMap()); + } + + @GetMapping("/andon-board") + public ResponseEntity> getAndonBoard() { + return ResponseEntity.ok(service.findAll(null, null)); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java b/backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java new file mode 100644 index 0000000..e03e0f4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/AndonEvent.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/Bom.java b/backend/src/main/java/com/zioinfo/fa/domain/Bom.java new file mode 100644 index 0000000..4416168 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/Bom.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/EpaperDisplay.java b/backend/src/main/java/com/zioinfo/fa/domain/EpaperDisplay.java new file mode 100644 index 0000000..413f110 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/EpaperDisplay.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/EpaperTemplate.java b/backend/src/main/java/com/zioinfo/fa/domain/EpaperTemplate.java new file mode 100644 index 0000000..1711812 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/EpaperTemplate.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/Equipment.java b/backend/src/main/java/com/zioinfo/fa/domain/Equipment.java new file mode 100644 index 0000000..136c400 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/Equipment.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/FaUser.java b/backend/src/main/java/com/zioinfo/fa/domain/FaUser.java new file mode 100644 index 0000000..450600b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/FaUser.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/FactoryInventory.java b/backend/src/main/java/com/zioinfo/fa/domain/FactoryInventory.java new file mode 100644 index 0000000..a75c025 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/FactoryInventory.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/ProcessRoute.java b/backend/src/main/java/com/zioinfo/fa/domain/ProcessRoute.java new file mode 100644 index 0000000..28ab6dd --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/ProcessRoute.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/ProductionOrder.java b/backend/src/main/java/com/zioinfo/fa/domain/ProductionOrder.java new file mode 100644 index 0000000..bc739b1 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/ProductionOrder.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/QrProcessCode.java b/backend/src/main/java/com/zioinfo/fa/domain/QrProcessCode.java new file mode 100644 index 0000000..2aa220d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/QrProcessCode.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java b/backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java new file mode 100644 index 0000000..975a6c4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/QrScanEvent.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/QualityInspection.java b/backend/src/main/java/com/zioinfo/fa/domain/QualityInspection.java new file mode 100644 index 0000000..60fbd92 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/QualityInspection.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/domain/Workstation.java b/backend/src/main/java/com/zioinfo/fa/domain/Workstation.java new file mode 100644 index 0000000..705a945 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/domain/Workstation.java @@ -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; +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/AndonEventMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/AndonEventMapper.java new file mode 100644 index 0000000..7411266 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/AndonEventMapper.java @@ -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 findAll(@Param("status") String status, @Param("severity") String severity); + AndonEvent findById(Long id); + List 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 getStats(); + List findActive(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java new file mode 100644 index 0000000..268d6f8 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/BomMapper.java @@ -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 findByProductCode(String productCode); + List findByProcessCode(String processCode); + Bom findById(Long id); + int insert(Bom b); + int update(Bom b); + int delete(Long id); + int deleteByProductCode(String productCode); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/EpaperDisplayMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/EpaperDisplayMapper.java new file mode 100644 index 0000000..3430cfd --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/EpaperDisplayMapper.java @@ -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 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 findByBatteryLow(@Param("threshold") Double threshold); + List 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(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/EpaperTemplateMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/EpaperTemplateMapper.java new file mode 100644 index 0000000..a030dfa --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/EpaperTemplateMapper.java @@ -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 findAll(); + List findByType(String templateType); + List 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); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/EquipmentMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/EquipmentMapper.java new file mode 100644 index 0000000..9a2da98 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/EquipmentMapper.java @@ -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 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 findBreakdownRisk(); + List findMaintenanceDue(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/FaUserMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/FaUserMapper.java new file mode 100644 index 0000000..cbdfa7e --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/FaUserMapper.java @@ -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 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); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/FactoryInventoryMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/FactoryInventoryMapper.java new file mode 100644 index 0000000..f33c75e --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/FactoryInventoryMapper.java @@ -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 findAll(String locationCode); + FactoryInventory findById(Long id); + FactoryInventory findByItemCode(String itemCode); + List findLowStock(); + int insert(FactoryInventory i); + int update(FactoryInventory i); + int updateQuantity(String itemCode, Double delta); + int delete(Long id); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/ProcessRouteMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/ProcessRouteMapper.java new file mode 100644 index 0000000..2821c74 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/ProcessRouteMapper.java @@ -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 findByProductCode(String productCode); + List findByRouteCode(String routeCode); + ProcessRoute findById(Long id); + int insert(ProcessRoute r); + int update(ProcessRoute r); + int delete(Long id); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/ProductionOrderMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/ProductionOrderMapper.java new file mode 100644 index 0000000..647865e --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/ProductionOrderMapper.java @@ -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 findAll(@Param("status") String status, @Param("lineCode") String lineCode); + List 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 findDashboardSummary(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/QrProcessCodeMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/QrProcessCodeMapper.java new file mode 100644 index 0000000..c2979c9 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/QrProcessCodeMapper.java @@ -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 findAll(@Param("status") String status, @Param("codeType") String codeType); + QrProcessCode findByQrCode(String qrCode); + QrProcessCode findById(Long id); + List findByLotNumber(String lotNumber); + List 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 batchInsert(List list); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/QrScanEventMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/QrScanEventMapper.java new file mode 100644 index 0000000..cacd917 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/QrScanEventMapper.java @@ -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 findByQrCode(String qrCode); + List findByWorkstation(@Param("workstationId") String workstationId, @Param("limit") int limit); + QrScanEvent findById(Long id); + int insert(QrScanEvent e); + List findRecentByWorkstation(@Param("workstationId") String workstationId, @Param("limit") int limit); + long countByResult(@Param("result") String result, @Param("hours") int hours); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/QualityInspectionMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/QualityInspectionMapper.java new file mode 100644 index 0000000..5b29e20 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/QualityInspectionMapper.java @@ -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 findAll(@Param("inspectionType") String inspectionType, @Param("result") String result); + QualityInspection findById(Long id); + QualityInspection findByNumber(String inspectionNumber); + List findByOrderId(Long orderId); + int insert(QualityInspection q); + int update(QualityInspection q); + int delete(Long id); + List> getDefectPareto(); + Map getDashboardSummary(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/mapper/WorkstationMapper.java b/backend/src/main/java/com/zioinfo/fa/mapper/WorkstationMapper.java new file mode 100644 index 0000000..0eff862 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/mapper/WorkstationMapper.java @@ -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 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 findFloorMap(); +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/AndonService.java b/backend/src/main/java/com/zioinfo/fa/service/AndonService.java new file mode 100644 index 0000000..6de430a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/AndonService.java @@ -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 findAll(String status, String severity) { + return mapper.findAll(status, severity); + } + + public AndonEvent findById(Long id) { return mapper.findById(id); } + + public AndonEvent call(Map 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 getStats() { + return mapper.getStats(); + } + + public List getBoard() { + return mapper.findActive(); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/AuthService.java b/backend/src/main/java/com/zioinfo/fa/service/AuthService.java new file mode 100644 index 0000000..d05fee3 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/AuthService.java @@ -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 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 result = new HashMap<>(); + result.put("token", token); + result.put("username", username); + result.put("role", user.getRole()); + result.put("workstationCode", user.getWorkstationCode()); + return result; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/BomRouteService.java b/backend/src/main/java/com/zioinfo/fa/service/BomRouteService.java new file mode 100644 index 0000000..3bd3948 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/BomRouteService.java @@ -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 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 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; } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/DashboardService.java b/backend/src/main/java/com/zioinfo/fa/service/DashboardService.java new file mode 100644 index 0000000..a07e765 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/DashboardService.java @@ -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 getOverview() { + Map 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 getOeeSummary() { + Map 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 getProductionStatus() { + Map d = new HashMap<>(); + d.put("today", orderMapper.findToday()); + d.put("workstations", workstationMapper.findAll(null, "RUNNING")); + return d; + } + + public Map getQualitySummary() { + Map summary = qualityMapper.getDashboardSummary(); + return summary != null ? summary : new HashMap<>(); + } + + public Map getAndonSummary() { + return andonMapper.getStats(); + } + + public Map getEpaperStatus() { + Map 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/EpaperDisplayService.java b/backend/src/main/java/com/zioinfo/fa/service/EpaperDisplayService.java new file mode 100644 index 0000000..2cf19f6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/EpaperDisplayService.java @@ -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 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 pushContent(Long id, String content, String templateId) { + mapper.updateContent(id, content, templateId); + Map result = new HashMap<>(); + result.put("success", true); + result.put("displayId", id); + result.put("pushedAt", LocalDateTime.now()); + return result; + } + + public Map refresh(Long id) { + EpaperDisplay d = mapper.findById(id); + Map result = new HashMap<>(); + result.put("success", true); + result.put("displayId", id); + result.put("refreshedAt", LocalDateTime.now()); + return result; + } + + public List findOffline() { + return mapper.findOffline(); + } + + public List findBatteryLow(Double threshold) { + return mapper.findByBatteryLow(threshold == null ? 20.0 : threshold); + } + + public Map batchPush(List ids, String content, String templateId) { + ids.forEach(id -> mapper.updateContent(id, content, templateId)); + Map result = new HashMap<>(); + result.put("pushedCount", ids.size()); + result.put("pushedAt", LocalDateTime.now()); + return result; + } + + public Map getDashboard() { + Map 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/EpaperTemplateService.java b/backend/src/main/java/com/zioinfo/fa/service/EpaperTemplateService.java new file mode 100644 index 0000000..af95950 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/EpaperTemplateService.java @@ -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 findAll() { return mapper.findAll(); } + public List 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); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/EquipmentService.java b/backend/src/main/java/com/zioinfo/fa/service/EquipmentService.java new file mode 100644 index 0000000..5cdcbde --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/EquipmentService.java @@ -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 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 getOee(Long id) { + Equipment e = mapper.findById(id); + Map 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 getMaintenance(Long id) { + Equipment e = mapper.findById(id); + Map 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 req) { + mapper.updateStatus(id, "RUNNING"); + return mapper.findById(id); + } + + public Map aiPredict(Map req) { + Map 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 body = Map.of("model", "llama3", "prompt", prompt, "stream", false); + ResponseEntity 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 getBreakdownRisk() { + return mapper.findBreakdownRisk(); + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/InventoryService.java b/backend/src/main/java/com/zioinfo/fa/service/InventoryService.java new file mode 100644 index 0000000..fd3b7fc --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/InventoryService.java @@ -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 findAll(String locationCode) { + return mapper.findAll(locationCode); + } + + public Map movement(Map req) { + String itemCode = (String) req.get("itemCode"); + Double delta = Double.valueOf(req.get("delta").toString()); + mapper.updateQuantity(itemCode, delta); + Map result = new HashMap<>(); + result.put("itemCode", itemCode); + result.put("delta", delta); + result.put("success", true); + return result; + } + + public List getLowStock() { + return mapper.findLowStock(); + } + + public Map aiOptimize(Map req) { + Map 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 body = Map.of("model", "llama3", "prompt", prompt, "stream", false); + ResponseEntity 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/ProductionOrderService.java b/backend/src/main/java/com/zioinfo/fa/service/ProductionOrderService.java new file mode 100644 index 0000000..f52b6be --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/ProductionOrderService.java @@ -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 findAll(String status, String lineCode) { + return mapper.findAll(status, lineCode); + } + + public List 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 getProgress(Long id) { + ProductionOrder o = mapper.findById(id); + Map 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 getDashboard() { + List 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/QrService.java b/backend/src/main/java/com/zioinfo/fa/service/QrService.java new file mode 100644 index 0000000..650ae2a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/QrService.java @@ -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 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 scan(Map 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 result = new HashMap<>(); + result.put("event", event); + result.put("processCode", code); + return result; + } + + public List getHistory(String code) { + return scanMapper.findByQrCode(code); + } + + public List 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 findByLot(String lot) { + return codeMapper.findByLotNumber(lot); + } + + public List getRecentByWorkstation(String ws, int limit) { + return scanMapper.findRecentByWorkstation(ws, limit); + } + + public List batchGenerate(List> requests) { + List result = new ArrayList<>(); + requests.forEach(req -> result.add(generate(req))); + return result; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/QualityService.java b/backend/src/main/java/com/zioinfo/fa/service/QualityService.java new file mode 100644 index 0000000..28cb69b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/QualityService.java @@ -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 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 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> getDefectPareto() { + return mapper.getDefectPareto(); + } + + public Map getDashboard() { + return mapper.getDashboardSummary(); + } + + public Map aiAnalyze(Map req) { + Map 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 body = Map.of("model", "llama3", "prompt", prompt, "stream", false); + ResponseEntity 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/fa/service/WorkstationService.java b/backend/src/main/java/com/zioinfo/fa/service/WorkstationService.java new file mode 100644 index 0000000..20b2015 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/fa/service/WorkstationService.java @@ -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 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 updateAndon(String code, String andonStatus) { + mapper.updateAndon(code, andonStatus); + return Map.of("code", code, "andonStatus", andonStatus, "updated", true); + } + + public List getFloorMap() { + return mapper.findFloorMap(); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..56f5790 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/backend/src/main/resources/db/schema.sql b/backend/src/main/resources/db/schema.sql new file mode 100644 index 0000000..99671ee --- /dev/null +++ b/backend/src/main/resources/db/schema.sql @@ -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; diff --git a/backend/src/main/resources/mapper/AndonEventMapper.xml b/backend/src/main/resources/mapper/AndonEventMapper.xml new file mode 100644 index 0000000..d134a60 --- /dev/null +++ b/backend/src/main/resources/mapper/AndonEventMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO fa_andon_events (workstation_code, andon_type, severity, description, called_by, called_at, status) + VALUES (#{workstationCode}, #{andonType}, #{severity}, #{description}, #{calledBy}, #{calledAt}, #{status}) + + UPDATE fa_andon_events SET description=#{description}, severity=#{severity} WHERE id=#{id} + + 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 fa_andon_events SET resolved_at=NOW(), + resolution_time_seconds=EXTRACT(EPOCH FROM (NOW() - called_at))::INT, + status='RESOLVED' WHERE id=#{id} + + diff --git a/backend/src/main/resources/mapper/BomMapper.xml b/backend/src/main/resources/mapper/BomMapper.xml new file mode 100644 index 0000000..96c9998 --- /dev/null +++ b/backend/src/main/resources/mapper/BomMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + 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}) + + UPDATE fa_bom SET required_qty=#{requiredQty}, unit=#{unit}, level=#{level} WHERE id=#{id} + DELETE FROM fa_bom WHERE id=#{id} + DELETE FROM fa_bom WHERE product_code=#{productCode} + diff --git a/backend/src/main/resources/mapper/EpaperDisplayMapper.xml b/backend/src/main/resources/mapper/EpaperDisplayMapper.xml new file mode 100644 index 0000000..ee4631e --- /dev/null +++ b/backend/src/main/resources/mapper/EpaperDisplayMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + UPDATE fa_epaper_displays SET workstation_id=#{workstationId}, display_size=#{displaySize}, + gateway_id=#{gatewayId}, protocol=#{protocol}, tenant_code=#{tenantCode} WHERE id=#{id} + + UPDATE fa_epaper_displays SET status=#{status}, last_seen=#{lastSeen} WHERE display_id=#{displayId} + UPDATE fa_epaper_displays SET display_content=#{content}::jsonb, current_template=#{templateId}, last_updated=NOW() WHERE id=#{id} + DELETE FROM fa_epaper_displays WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/EpaperTemplateMapper.xml b/backend/src/main/resources/mapper/EpaperTemplateMapper.xml new file mode 100644 index 0000000..cf7d34f --- /dev/null +++ b/backend/src/main/resources/mapper/EpaperTemplateMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + INSERT INTO fa_epaper_templates (template_code, template_name, template_type, display_size, layout_json, active) + VALUES (#{templateCode}, #{templateName}, #{templateType}, #{displaySize}, #{layoutJson}, TRUE) + + + UPDATE fa_epaper_templates SET template_name=#{templateName}, template_type=#{templateType}, + display_size=#{displaySize}, layout_json=#{layoutJson}, updated_at=NOW() WHERE id=#{id} + + UPDATE fa_epaper_templates SET active=#{active} WHERE id=#{id} + DELETE FROM fa_epaper_templates WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/EquipmentMapper.xml b/backend/src/main/resources/mapper/EquipmentMapper.xml new file mode 100644 index 0000000..2dd17d7 --- /dev/null +++ b/backend/src/main/resources/mapper/EquipmentMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + 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) + + UPDATE fa_equipment SET equipment_name=#{equipmentName}, workstation_code=#{workstationCode}, status=#{status} WHERE id=#{id} + UPDATE fa_equipment SET status=#{status} WHERE id=#{id} + UPDATE fa_equipment SET oee=#{oee} WHERE id=#{id} + DELETE FROM fa_equipment WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/FaUserMapper.xml b/backend/src/main/resources/mapper/FaUserMapper.xml new file mode 100644 index 0000000..50f6416 --- /dev/null +++ b/backend/src/main/resources/mapper/FaUserMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + INSERT INTO fa_users (username, password_hash, role, workstation_code, active) + VALUES (#{username}, #{passwordHash}, #{role}, #{workstationCode}, #{active}) + + UPDATE fa_users SET role=#{role}, workstation_code=#{workstationCode}, updated_at=NOW() WHERE id=#{id} + UPDATE fa_users SET active=#{active} WHERE id=#{id} + DELETE FROM fa_users WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/FactoryInventoryMapper.xml b/backend/src/main/resources/mapper/FactoryInventoryMapper.xml new file mode 100644 index 0000000..39b1e8c --- /dev/null +++ b/backend/src/main/resources/mapper/FactoryInventoryMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + 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') + + UPDATE fa_inventory SET item_name=#{itemName}, quantity=#{quantity}, safety_stock=#{safetyStock}, last_updated=NOW() WHERE id=#{id} + UPDATE fa_inventory SET quantity=quantity+#{delta}, last_updated=NOW() WHERE item_code=#{itemCode} + DELETE FROM fa_inventory WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/ProcessRouteMapper.xml b/backend/src/main/resources/mapper/ProcessRouteMapper.xml new file mode 100644 index 0000000..c5d7655 --- /dev/null +++ b/backend/src/main/resources/mapper/ProcessRouteMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + 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}) + + UPDATE fa_process_routes SET process_name=#{processName}, standard_time=#{standardTime} WHERE id=#{id} + DELETE FROM fa_process_routes WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/ProductionOrderMapper.xml b/backend/src/main/resources/mapper/ProductionOrderMapper.xml new file mode 100644 index 0000000..eb54a74 --- /dev/null +++ b/backend/src/main/resources/mapper/ProductionOrderMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + 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 fa_production_orders SET status=#{status}, updated_at=NOW() WHERE id=#{id} + UPDATE fa_production_orders SET completed_qty=#{completedQty}, defect_qty=#{defectQty}, updated_at=NOW() WHERE id=#{id} + DELETE FROM fa_production_orders WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/QrProcessCodeMapper.xml b/backend/src/main/resources/mapper/QrProcessCodeMapper.xml new file mode 100644 index 0000000..ab49b09 --- /dev/null +++ b/backend/src/main/resources/mapper/QrProcessCodeMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + 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}) + + + UPDATE fa_qr_process_codes SET code_type=#{codeType}, product_code=#{productCode}, + quantity=#{quantity}, status=#{status} WHERE id=#{id} + + + UPDATE fa_qr_process_codes SET status=#{status}, current_workstation=#{workstation}, + current_process=#{process}, last_scanned_at=NOW() WHERE qr_code=#{qrCode} + + DELETE FROM fa_qr_process_codes WHERE id=#{id} + + diff --git a/backend/src/main/resources/mapper/QrScanEventMapper.xml b/backend/src/main/resources/mapper/QrScanEventMapper.xml new file mode 100644 index 0000000..c584e1b --- /dev/null +++ b/backend/src/main/resources/mapper/QrScanEventMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + 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}) + + diff --git a/backend/src/main/resources/mapper/QualityInspectionMapper.xml b/backend/src/main/resources/mapper/QualityInspectionMapper.xml new file mode 100644 index 0000000..b11d396 --- /dev/null +++ b/backend/src/main/resources/mapper/QualityInspectionMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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}) + + + UPDATE fa_quality_inspections SET result=#{result}, defect_count=#{defectCount}, defect_details=#{defectDetails} WHERE id=#{id} + + DELETE FROM fa_quality_inspections WHERE id=#{id} + diff --git a/backend/src/main/resources/mapper/WorkstationMapper.xml b/backend/src/main/resources/mapper/WorkstationMapper.xml new file mode 100644 index 0000000..a2f4713 --- /dev/null +++ b/backend/src/main/resources/mapper/WorkstationMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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') + + + UPDATE fa_workstations SET workstation_name=#{workstationName}, line_code=#{lineCode}, + process_code=#{processCode}, worker_count=#{workerCount}, updated_at=NOW() WHERE id=#{id} + + UPDATE fa_workstations SET andon_status=#{andonStatus}, updated_at=NOW() WHERE workstation_code=#{code} + UPDATE fa_workstations SET status=#{status}, updated_at=NOW() WHERE workstation_code=#{code} + UPDATE fa_workstations SET actual_count=#{actualCount}, updated_at=NOW() WHERE workstation_code=#{code} + DELETE FROM fa_workstations WHERE id=#{id} + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..42d924a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + GUARDiA FA — Factory Automation Platform + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..480e4ae --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..f7d338a --- /dev/null +++ b/frontend/src/App.tsx @@ -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 ( + + ) +} + +function RequireAuth({ children }: { children: React.ReactNode }) { + const token = localStorage.getItem('fa_token') + return token ? <>{children} : +} + +function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ +
{children}
+
+ ) +} + +export default function App() { + return ( + + + } /> + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> + + + ) +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..0e380d5 --- /dev/null +++ b/frontend/src/api/client.ts @@ -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 diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..08a056a --- /dev/null +++ b/frontend/src/index.css @@ -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; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..4a1b150 --- /dev/null +++ b/frontend/src/main.tsx @@ -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( + + + +) diff --git a/frontend/src/pages/AiFactory.tsx b/frontend/src/pages/AiFactory.tsx new file mode 100644 index 0000000..16a50ff --- /dev/null +++ b/frontend/src/pages/AiFactory.tsx @@ -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 + color: string +} + +export default function AiFactory() { + const [results, setResults] = useState>({}) + const [loading, setLoading] = useState>({}) + const [inventory, setInventory] = useState([]) + const [inventoryLoaded, setInventoryLoaded] = useState(false) + + const run = async (key: string, action: () => Promise) => { + 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: , + 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: , + 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: , + 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: , + 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 ( +
+

AI 공장 분석 (Ollama 온프레미스)

+
+ 모든 AI 분석은 온프레미스 Ollama(localhost:11434)를 사용합니다. 외부 AI API는 절대 사용하지 않습니다. +
+ +
+ {panels.map(p => ( +
+
+ {p.icon} +

{p.title}

+
+

{p.desc}

+ + {results[p.title] && ( +
+ {results[p.title]} +
+ )} +
+ ))} +
+ +
+
+

재고 현황

+ +
+ {inventoryLoaded && ( + + + + + + + + + + + + + {inventory.map((i: any) => ( + + + + + + + + + ))} + +
품목코드품목명위치현재고안전재고상태
{i.itemCode}{i.itemName}{i.locationCode}{i.quantity}{i.safetyStock} + + {(i.quantity || 0) <= (i.safetyStock || 0) ? '부족' : '정상'} + +
+ )} + {inventoryLoaded && inventory.length === 0 && ( +
재고 데이터가 없습니다.
+ )} +
+
+ ) +} diff --git a/frontend/src/pages/AndonBoard.tsx b/frontend/src/pages/AndonBoard.tsx new file mode 100644 index 0000000..60cd577 --- /dev/null +++ b/frontend/src/pages/AndonBoard.tsx @@ -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 = { + 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 = { + INFO: 'text-blue-400', + WARNING: 'text-yellow-400', + CRITICAL: 'text-red-400', +} + +export default function AndonBoard() { + const [board, setBoard] = useState([]) + const [events, setEvents] = useState([]) + const [stats, setStats] = useState({}) + + 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 ( +
+
+

안돈 현황판

+
10초마다 자동 갱신
+
+ +
+ {[ + { 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 }) => ( +
+
{value ?? '-'}
+
{label}
+
+ ))} +
+ + {board.length > 0 && ( +
+

활성 안돈 ({board.length}건)

+
+ {board.map(e => ( +
+
+ + {e.workstationCode} + {e.andonType} +
+
{e.description}
+
호출: {e.calledBy} · {e.calledAt?.replace('T', ' ').slice(0, 16)}
+
+ {!e.respondedAt && ( + + )} + +
+
+ ))} +
+
+ )} + +
+
+

안돈 이벤트 이력

+
+ + + + + + + + + + + + + + {events.slice(0, 20).map(e => ( + + + + + + + + + + ))} + +
작업장타입심각도설명호출 시각상태응답시간
{e.workstationCode}{e.andonType} + {e.severity} + {e.description}{e.calledAt?.replace('T', ' ').slice(0, 16)} + + {e.resolvedAt ? 'RESOLVED' : e.respondedAt ? 'IN_PROGRESS' : 'OPEN'} + + {e.responseTimeSeconds ? `${e.responseTimeSeconds}초` : '-'}
+
+
+ ) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..c556140 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -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({}) + const [oee, setOee] = useState({}) + const [andon, setAndon] = useState({}) + const [epaper, setEpaper] = useState({}) + + 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) => ( +
+
{title}
+
{value ?? '-'}
+ {sub &&
{sub}
} +
+ ) + + return ( +
+

공장 전체 현황 대시보드

+
+ + + + +
+ +
+
+

OEE 지표 (종합 설비 효율)

+
+
+
+ {Math.round(((oee.availability || 0.92) * (oee.performance || 0.88) * (oee.quality || 0.97)) * 100)}% +
+
종합 OEE
+
+
+ {oeeData.map(d => ( +
+
{d.name}
+
+
+
+
{d.value}%
+
+ ))} +
+
+
+ +
+

e-Paper 디스플레이 현황

+
+
+
{epaper.online || 0}
+
온라인
+
+
+
{epaper.offline || 0}
+
오프라인
+
+
+
{epaper.batteryLow || 0}
+
배터리 부족
+
+
+
{epaper.total || 0}
+
전체
+
+
+
+
+ +
+

안돈 통계 (오늘)

+
+
+
{andon?.total_today || 0}
+
총 발생
+
+
+
{andon?.resolved || 0}
+
해결
+
+
+
{andon?.active || 0}
+
활성
+
+
+
{Math.round(andon?.avg_response_time || 0)}초
+
평균 응답시간
+
+
+
+
+ ) +} diff --git a/frontend/src/pages/EpaperManagement.tsx b/frontend/src/pages/EpaperManagement.tsx new file mode 100644 index 0000000..2920e97 --- /dev/null +++ b/frontend/src/pages/EpaperManagement.tsx @@ -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 = { + ONLINE: 'text-green-400', + OFFLINE: 'text-slate-500', + UPDATING: 'text-yellow-400', + ERROR: 'text-red-400', +} + +export default function EpaperManagement() { + const [displays, setDisplays] = useState([]) + const [dashboard, setDashboard] = useState({}) + 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 ( +
+
+

e-Paper 표시기 관리

+
+ {['', 'ONLINE', 'OFFLINE', 'ERROR'].map(s => ( + + ))} +
+
+ +
+ {[ + { 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 }) => ( +
+
{value ?? '-'}
+
{label}
+
+ ))} +
+ +
+ + + + + + + + + + + + + + + {displays.map(d => ( + + + + + + + + + + + ))} + +
디스플레이 ID작업장크기/타입배터리신호상태프로토콜작업
{d.displayId}{d.workstationId || '-'}{d.displaySize} / {d.displayType} + + {d.batteryLevel?.toFixed(0) || 0}% + + + + {d.signalStrength || '-'} dBm + + + {d.status} + {d.protocol} +
+ + +
+
+ {displays.length === 0 && ( +
등록된 e-Paper 표시기가 없습니다.
+ )} +
+
+ ) +} diff --git a/frontend/src/pages/EquipmentStatus.tsx b/frontend/src/pages/EquipmentStatus.tsx new file mode 100644 index 0000000..d0fa51c --- /dev/null +++ b/frontend/src/pages/EquipmentStatus.tsx @@ -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 = { + RUNNING: 'text-green-400', + IDLE: 'text-slate-400', + MAINTENANCE: 'text-yellow-400', + BREAKDOWN: 'text-red-400', +} + +export default function EquipmentStatus() { + const [equipment, setEquipment] = useState([]) + const [riskList, setRiskList] = useState([]) + 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 ( +
+

설비 현황

+ + {riskList.length > 0 && ( +
+
+ + 고장 위험 설비 ({riskList.length}대) +
+
+ {riskList.map(e => ( + + {e.equipmentCode} (OEE {Math.round((e.oee || 0) * 100)}%) + + ))} +
+
+ )} + +
+
+

AI 예지보전

+ +
+ {aiResult ? ( +
{aiResult}
+ ) : ( +
AI 예측 버튼으로 Ollama 기반 설비 예지보전 분석을 시작합니다.
+ )} +
+ +
+ + + + + + + + + + + + + + {equipment.map(e => ( + + + + + + + + + + ))} + +
설비 코드설비명작업장제조사OEE상태다음 점검
{e.equipmentCode}{e.equipmentName}{e.workstationCode}{e.manufacturer} +
+
+
= 0.85 ? 'bg-green-500' : (e.oee || 0) >= 0.7 ? 'bg-yellow-500' : 'bg-red-500'}`} + style={{ width: `${(e.oee || 0) * 100}%` }} /> +
+ {Math.round((e.oee || 0) * 100)}% +
+
+ {e.status} + {e.nextMaintenanceAt?.slice(0, 10) || '-'}
+ {equipment.length === 0 &&
등록된 설비가 없습니다.
} +
+
+ ) +} diff --git a/frontend/src/pages/FloorMap.tsx b/frontend/src/pages/FloorMap.tsx new file mode 100644 index 0000000..0e06a00 --- /dev/null +++ b/frontend/src/pages/FloorMap.tsx @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from 'react' +import client from '../api/client' + +const ANDON_COLORS: Record = { + 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([]) + + 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 ( +
+

공장 배치도 (Floor Map)

+
+
안돈 상태: + GREEN 정상 + YELLOW 주의 + RED 긴급 + BLUE 도움요청 +
+ {lines.map(line => ( +
+

{line}

+
+ {workstations.filter(w => (w.lineCode || 'DEFAULT') === line).map(ws => ( +
+
{ws.workstationCode}
+
{ws.workstationName}
+
+
공정: {ws.processCode}
+
상태: {ws.status}
+
+ 목표 + {ws.targetCount || 0} +
+
+ 실적 + {ws.actualCount || 0} +
+ {ws.oee && ( +
+
+ OEE{Math.round(ws.oee * 100)}% +
+
+
+
+
+ )} +
+
+ ))} +
+
+ ))} + {workstations.length === 0 && ( +
작업장 데이터가 없습니다.
+ )} +
+
+ ) +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..98fab6e --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -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 ( +
+
+
+
GUARDiA FA
+
Factory Automation Platform
+
e-Paper · QR WIP · MES 통합 플랫폼
+
+
+
+ + 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 + /> +
+
+ + 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 + /> +
+ {error &&
{error}
} + +
+
+ 기본 계정: admin / admin123 +
+
+
+ ) +} diff --git a/frontend/src/pages/ProductionOrders.tsx b/frontend/src/pages/ProductionOrders.tsx new file mode 100644 index 0000000..7860b0e --- /dev/null +++ b/frontend/src/pages/ProductionOrders.tsx @@ -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 = { + 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([]) + const [dashboard, setDashboard] = useState({}) + 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 ( +
+
+

생산 오더

+
+ {['', 'PLANNED', 'RELEASED', 'IN_PROGRESS', 'COMPLETED'].map(s => ( + + ))} +
+
+ +
+
+
{dashboard.todayTotal || 0}
+
오늘 오더
+
+
+
{dashboard.inProgress || 0}
+
진행 중
+
+
+
{dashboard.completed || 0}
+
완료
+
+
+
{Math.round(dashboard.completionRate || 0)}%
+
완료율
+
+
+ +
+ + + + + + + + + + + + + + + {orders.map(o => { + const pct = o.plannedQty ? Math.round((o.completedQty || 0) * 100 / o.plannedQty) : 0 + return ( + + + + + + + + + + + ) + })} + +
오더 번호제품라인계획 수량진행 수량진척률상태작업
{o.orderNumber}{o.productName}{o.lineCode}{o.plannedQty?.toLocaleString()}{o.completedQty?.toLocaleString() || 0} +
+
+
+
+ {pct}% +
+
+ {o.status} + +
+ {o.status === 'PLANNED' && ( + + )} + {o.status === 'IN_PROGRESS' && ( + + )} +
+
+ {orders.length === 0 &&
생산 오더가 없습니다.
} +
+
+ ) +} diff --git a/frontend/src/pages/QrTracking.tsx b/frontend/src/pages/QrTracking.tsx new file mode 100644 index 0000000..659331f --- /dev/null +++ b/frontend/src/pages/QrTracking.tsx @@ -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(null) + const [history, setHistory] = useState([]) + const [route, setRoute] = useState([]) + const [lotSearch, setLotSearch] = useState('') + const [lotResults, setLotResults] = useState([]) + + 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 = { + 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 ( +
+
+

QR WIP 추적

+ +
+ +
+
+

QR 코드 조회

+
+ 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" + /> + +
+ {qrData && ( +
+
+ 제품코드 + {qrData.productCode} +
+
+ LOT 번호 + {qrData.lotNumber} +
+
+ 현재 공정 + {qrData.currentProcess} +
+
+ 현재 작업장 + {qrData.currentWorkstation} +
+
+ 상태 + {qrData.status} +
+
+ )} +
+ +
+

공정 경로

+ {route.length > 0 ? ( +
+ {route.map((r, i) => ( +
+
{r.sequence || i + 1}
+
+
{r.processName}
+
{r.workstationCode} · {r.standardTime}분
+
+
+ ))} +
+ ) :
QR 코드를 조회하면 공정 경로가 표시됩니다.
} +
+
+ +
+

스캔 이력

+
+ + + + + + + + + + + + {history.map(h => ( + + + + + + + + ))} + +
시간작업장스캔 타입결과작업자
{h.scannedAt?.replace('T', ' ').slice(0, 16)}{h.workstationId}{h.scanType} + {h.result} + {h.operatorId}
+ {history.length === 0 &&
스캔 이력이 없습니다.
} +
+
+
+ ) +} diff --git a/frontend/src/pages/QualityControl.tsx b/frontend/src/pages/QualityControl.tsx new file mode 100644 index 0000000..90a30b7 --- /dev/null +++ b/frontend/src/pages/QualityControl.tsx @@ -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([]) + const [pareto, setPareto] = useState([]) + const [dashboard, setDashboard] = useState({}) + 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 ( +
+

품질 관리

+ +
+ {[ + { 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 }) => ( +
+
{value ?? '-'}
+
{label}
+
+ ))} +
+ +
+
+

불량 파레토 차트

+ + + + + + + + + +
+ +
+
+

AI 불량 원인 분석

+ +
+ {aiResult ? ( +
{aiResult}
+ ) : ( +
AI 분석 버튼을 클릭하면 Ollama가 불량 원인을 분석합니다.
+ )} +
+
+ +
+
+

검사 목록

+
+ + + + + + + + + + + + + + {inspections.map(q => ( + + + + + + + + + + ))} + +
검사 번호제품검사 유형LOT샘플/불량결과검사일시
{q.inspectionNumber}{q.productCode}{q.inspectionType}{q.lotNumber}{q.sampleSize} / {q.defectCount || 0} + + {q.result || '-'} + + {q.inspectedAt?.replace('T', ' ').slice(0, 16) || '-'}
+ {inspections.length === 0 &&
검사 이력이 없습니다.
} +
+
+ ) +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..a72b2f1 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,6 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { extend: {} }, + plugins: [], +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..50a9180 --- /dev/null +++ b/frontend/vite.config.ts @@ -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' + } + } +}) diff --git a/setup_fa_service.sh b/setup_fa_service.sh new file mode 100644 index 0000000..1ec192f --- /dev/null +++ b/setup_fa_service.sh @@ -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}"