diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml
index c9dbf70..ea18f0b 100644
--- a/.github/workflows/deploy-backend.yml
+++ b/.github/workflows/deploy-backend.yml
@@ -17,8 +17,8 @@ on:
workflow_dispatch:
concurrency:
- group: backend-${{ github.ref_name }}
- cancel-in-progress: true
+ group: backend-gitops-dev
+ cancel-in-progress: false
env:
AWS_REGION: ap-northeast-2
@@ -26,7 +26,9 @@ env:
ECR_REPOSITORY: aims/backend
NAMESPACE: aims-project
BACKEND_CONTEXT: .
+
INFRA_REPOSITORY: SK-Rookies-AIMS/infra
+ INFRA_BRANCH: dev
INFRA_MANIFEST_PATH: k8s/backend/kustomization.yaml
jobs:
@@ -53,47 +55,142 @@ jobs:
- name: Ensure ECR repository exists
run: |
- aws ecr describe-repositories --repository-names "$ECR_REPOSITORY" --region "$AWS_REGION" >/dev/null 2>&1 || aws ecr create-repository --repository-name "$ECR_REPOSITORY" --region "$AWS_REGION" --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE
+ set -euo pipefail
+
+ if ! aws ecr describe-repositories \
+ --repository-names "$ECR_REPOSITORY" \
+ --region "$AWS_REGION" \
+ > /dev/null 2>&1; then
+
+ aws ecr create-repository \
+ --repository-name "$ECR_REPOSITORY" \
+ --region "$AWS_REGION" \
+ --image-scanning-configuration scanOnPush=true \
+ --image-tag-mutability MUTABLE
+ fi
- name: Build and push Docker image
id: build-image
run: |
+ set -euo pipefail
+
IMAGE_TAG="${GITHUB_REF_NAME}-${GITHUB_SHA::12}"
IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/${ECR_REPOSITORY}:${IMAGE_TAG}"
BRANCH_IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/${ECR_REPOSITORY}:${GITHUB_REF_NAME}"
- docker build -t "$IMAGE_URI" -t "$BRANCH_IMAGE_URI" "$BACKEND_CONTEXT"
+ echo "Building image: $IMAGE_URI"
+
+ docker build \
+ -t "$IMAGE_URI" \
+ -t "$BRANCH_IMAGE_URI" \
+ "$BACKEND_CONTEXT"
+
docker push "$IMAGE_URI"
docker push "$BRANCH_IMAGE_URI"
echo "image_uri=$IMAGE_URI" >> "$GITHUB_OUTPUT"
+ echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
- name: Checkout infra repository
uses: actions/checkout@v4
with:
repository: ${{ env.INFRA_REPOSITORY }}
- ref: ${{ github.ref_name }}
+ ref: ${{ env.INFRA_BRANCH }}
token: ${{ secrets.INFRA_REPO_TOKEN }}
path: infra
fetch-depth: 0
- name: Update kubeconfig and ensure namespace
- if: github.ref_name == 'dev'
run: |
- aws eks update-kubeconfig --region "$AWS_REGION" --name "$EKS_CLUSTER_NAME"
- kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
+ set -euo pipefail
+
+ aws eks update-kubeconfig \
+ --region "$AWS_REGION" \
+ --name "$EKS_CLUSTER_NAME"
+
+ kubectl create namespace "$NAMESPACE" \
+ --dry-run=client \
+ -o yaml |
+ kubectl apply -f -
- name: Load backend runtime parameters from SSM
- if: github.ref_name == 'dev'
run: |
- RDS_SECRET_ARN=$(aws ssm get-parameter --name "/aims/dev/rds/secret-arn" --region "$AWS_REGION" --query "Parameter.Value" --output text)
- RDS_HOST=$(aws ssm get-parameter --name "/aims/dev/backend/rds-host" --region "$AWS_REGION" --query "Parameter.Value" --output text)
- RDS_PORT=$(aws ssm get-parameter --name "/aims/dev/backend/rds-port" --region "$AWS_REGION" --query "Parameter.Value" --output text)
- MAIN_DB_NAME=$(aws ssm get-parameter --name "/aims/dev/backend/main-db-name" --region "$AWS_REGION" --query "Parameter.Value" --output text)
- SAMPLE_DB_NAME=$(aws ssm get-parameter --name "/aims/dev/backend/sample-db-name" --region "$AWS_REGION" --query "Parameter.Value" --output text)
- JWT_SECRET_KEY=$(aws ssm get-parameter --name "/aims/dev/backend/jwt-secret-key" --region "$AWS_REGION" --with-decryption --query "Parameter.Value" --output text)
-
- for VALUE in "$RDS_SECRET_ARN" "$RDS_HOST" "$RDS_PORT" "$MAIN_DB_NAME" "$SAMPLE_DB_NAME" "$JWT_SECRET_KEY"; do
+ set -euo pipefail
+
+ RDS_SECRET_ARN=$(aws ssm get-parameter \
+ --name "/aims/dev/rds/secret-arn" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ RDS_HOST=$(aws ssm get-parameter \
+ --name "/aims/dev/backend/rds-host" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ RDS_PORT=$(aws ssm get-parameter \
+ --name "/aims/dev/backend/rds-port" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ MAIN_DB_NAME=$(aws ssm get-parameter \
+ --name "/aims/dev/backend/main-db-name" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ SAMPLE_DB_NAME=$(aws ssm get-parameter \
+ --name "/aims/dev/backend/sample-db-name" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ JWT_SECRET_KEY=$(aws ssm get-parameter \
+ --name "/aims/dev/backend/jwt-secret-key" \
+ --region "$AWS_REGION" \
+ --with-decryption \
+ --query "Parameter.Value" \
+ --output text)
+
+ REDIS_HOST=$(aws ssm get-parameter \
+ --name "/aims/dev/redis/host" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ REDIS_PORT=$(aws ssm get-parameter \
+ --name "/aims/dev/redis/port" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ KAFKA_BOOTSTRAP_SERVER_1=$(aws ssm get-parameter \
+ --name "/aims/dev/msk/bootstrap-server-1" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+
+ KAFKA_BOOTSTRAP_SERVER_2=$(aws ssm get-parameter \
+ --name "/aims/dev/msk/bootstrap-server-2" \
+ --region "$AWS_REGION" \
+ --query "Parameter.Value" \
+ --output text)
+
+ for VALUE in \
+ "$RDS_SECRET_ARN" \
+ "$RDS_HOST" \
+ "$RDS_PORT" \
+ "$MAIN_DB_NAME" \
+ "$SAMPLE_DB_NAME" \
+ "$JWT_SECRET_KEY" \
+ "$REDIS_HOST" \
+ "$REDIS_PORT" \
+ "$KAFKA_BOOTSTRAP_SERVER_1" \
+ "$KAFKA_BOOTSTRAP_SERVER_2"; do
+
if [ -z "$VALUE" ] || [ "$VALUE" = "None" ]; then
echo "Required backend SSM parameter is empty"
exit 1
@@ -109,21 +206,36 @@ jobs:
echo "MAIN_DB_NAME=$MAIN_DB_NAME" >> "$GITHUB_ENV"
echo "SAMPLE_DB_NAME=$SAMPLE_DB_NAME" >> "$GITHUB_ENV"
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> "$GITHUB_ENV"
+ echo "REDIS_HOST=$REDIS_HOST" >> "$GITHUB_ENV"
+ echo "REDIS_PORT=$REDIS_PORT" >> "$GITHUB_ENV"
+ echo "KAFKA_BOOTSTRAP_SERVER_1=$KAFKA_BOOTSTRAP_SERVER_1" >> "$GITHUB_ENV"
+ echo "KAFKA_BOOTSTRAP_SERVER_2=$KAFKA_BOOTSTRAP_SERVER_2" >> "$GITHUB_ENV"
- name: Create or update backend Kubernetes Secret
- if: github.ref_name == 'dev'
run: |
- SECRET_JSON=$(aws secretsmanager get-secret-value --secret-id "$RDS_SECRET_ARN" --region "$AWS_REGION" --query "SecretString" --output text)
+ set -euo pipefail
- DB_USERNAME=$(echo "$SECRET_JSON" | jq -r '.username')
- DB_PASSWORD=$(echo "$SECRET_JSON" | jq -r '.password')
+ SECRET_JSON=$(aws secretsmanager get-secret-value \
+ --secret-id "$RDS_SECRET_ARN" \
+ --region "$AWS_REGION" \
+ --query "SecretString" \
+ --output text)
- if [ -z "$DB_USERNAME" ] || [ "$DB_USERNAME" = "null" ] || [ -z "$DB_PASSWORD" ] || [ "$DB_PASSWORD" = "null" ]; then
- echo "RDS username or password is missing"
+ DB_USERNAME=$(echo "$SECRET_JSON" | jq -r ".username")
+ DB_PASSWORD=$(echo "$SECRET_JSON" | jq -r ".password")
+
+ if [ -z "$DB_USERNAME" ] || [ "$DB_USERNAME" = "null" ]; then
+ echo "RDS username is missing"
+ exit 1
+ fi
+
+ if [ -z "$DB_PASSWORD" ] || [ "$DB_PASSWORD" = "null" ]; then
+ echo "RDS password is missing"
exit 1
fi
MAIN_DB_JDBC_URL="jdbc:mysql://$RDS_HOST:$RDS_PORT/$MAIN_DB_NAME?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8"
+
SAMPLE_DB_JDBC_URL="jdbc:mysql://$RDS_HOST:$RDS_PORT/$SAMPLE_DB_NAME?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8"
echo "::add-mask::$DB_USERNAME"
@@ -131,22 +243,85 @@ jobs:
echo "::add-mask::$MAIN_DB_JDBC_URL"
echo "::add-mask::$SAMPLE_DB_JDBC_URL"
- kubectl create secret generic backend-rds-secret -n "$NAMESPACE" --from-literal=MAIN_DB_JDBC_URL="$MAIN_DB_JDBC_URL" --from-literal=MAIN_DB_USERNAME="$DB_USERNAME" --from-literal=MAIN_DB_PASSWORD="$DB_PASSWORD" --from-literal=SAMPLE_DB_JDBC_URL="$SAMPLE_DB_JDBC_URL" --from-literal=SAMPLE_DB_USERNAME="$DB_USERNAME" --from-literal=SAMPLE_DB_PASSWORD="$DB_PASSWORD" --from-literal=JWT_SECRET_KEY="$JWT_SECRET_KEY" --dry-run=client -o yaml | kubectl apply -f -
+ kubectl create secret generic backend-rds-secret \
+ --namespace "$NAMESPACE" \
+ --from-literal=MAIN_DB_JDBC_URL="$MAIN_DB_JDBC_URL" \
+ --from-literal=MAIN_DB_USERNAME="$DB_USERNAME" \
+ --from-literal=MAIN_DB_PASSWORD="$DB_PASSWORD" \
+ --from-literal=SAMPLE_DB_JDBC_URL="$SAMPLE_DB_JDBC_URL" \
+ --from-literal=SAMPLE_DB_USERNAME="$DB_USERNAME" \
+ --from-literal=SAMPLE_DB_PASSWORD="$DB_PASSWORD" \
+ --from-literal=JWT_SECRET_KEY="$JWT_SECRET_KEY" \
+ --from-literal=REDIS_HOST="$REDIS_HOST" \
+ --from-literal=REDIS_PORT="$REDIS_PORT" \
+ --from-literal=KAFKA_BOOTSTRAP_SERVER_1="$KAFKA_BOOTSTRAP_SERVER_1" \
+ --from-literal=KAFKA_BOOTSTRAP_SERVER_2="$KAFKA_BOOTSTRAP_SERVER_2" \
+ --dry-run=client \
+ -o yaml |
+ kubectl apply -f -
- name: Update backend image in GitOps repository
env:
IMAGE_URI: ${{ steps.build-image.outputs.image_uri }}
run: |
+ set -euo pipefail
+
MANIFEST="infra/${INFRA_MANIFEST_PATH}"
- test -f "$MANIFEST"
+ if [ ! -f "$MANIFEST" ]; then
+ echo "Manifest file not found: $MANIFEST"
+ exit 1
+ fi
+
IMAGE_TAG="${IMAGE_URI##*:}"
-
- sed -i -E \
- "s|^([[:space:]]*)newTag:.*$|\1newTag: ${IMAGE_TAG}|" \
- "$MANIFEST"
-
- grep -n "newTag:" "$MANIFEST"
+ IMAGE_NAME="${IMAGE_URI%:*}"
+ TEMP_FILE="${MANIFEST}.tmp"
+
+ echo "Manifest: $MANIFEST"
+ echo "Image name: $IMAGE_NAME"
+ echo "Image tag: $IMAGE_TAG"
+
+ awk \
+ -v image_name="$IMAGE_NAME" \
+ -v image_tag="$IMAGE_TAG" \
+ '
+ BEGIN {
+ in_target = 0
+ updated = 0
+ }
+
+ {
+ if ($1 == "-" && $2 == "name:") {
+ in_target = ($3 == image_name)
+ }
+
+ if (in_target && $1 == "newTag:") {
+ match($0, /^[[:space:]]*/)
+ indent = substr($0, 1, RLENGTH)
+
+ print indent "newTag: " image_tag
+
+ updated = 1
+ in_target = 0
+ next
+ }
+
+ print
+ }
+
+ END {
+ if (!updated) {
+ print "Backend image block or newTag was not found" > "/dev/stderr"
+ exit 42
+ }
+ }
+ ' \
+ "$MANIFEST" > "$TEMP_FILE"
+
+ mv "$TEMP_FILE" "$MANIFEST"
+
+ echo "Updated backend image:"
+ grep -n -A 2 -B 1 "$IMAGE_NAME" "$MANIFEST"
cd infra
@@ -160,12 +335,18 @@ jobs:
exit 0
fi
- git commit -m "chore(backend): deploy ${GITHUB_REF_NAME}-${GITHUB_SHA::12}"
+ git diff --cached
+
+ git commit \
+ -m "chore(backend): deploy ${GITHUB_REF_NAME}-${GITHUB_SHA::12}"
for ATTEMPT in 1 2 3; do
- git pull --rebase origin "$GITHUB_REF_NAME"
+ echo "GitOps push attempt: $ATTEMPT"
+
+ git pull --rebase origin "$INFRA_BRANCH"
- if git push origin "HEAD:$GITHUB_REF_NAME"; then
+ if git push origin "HEAD:$INFRA_BRANCH"; then
+ echo "GitOps image update pushed successfully"
exit 0
fi
diff --git a/README.md b/README.md
index b665cbf..a9c620a 100644
--- a/README.md
+++ b/README.md
@@ -1,225 +1,311 @@
-# backend-main
+# AIMS - Backend
+### AIMS (Auto Intelligence Manufacturing System) - AI 기반 자동차 스마트팩토리 관제 시스템
+
+`backend`는 AIMS의 **Spring Boot 기반 Main Backend 서비스**입니다. 제조/분석 서비스가 Kafka로 전달한 알림 및 공정 분석 이벤트를 소비하고, 알림 이력·분석 결과·AGV 운영 상태를 운영 화면에서 조회할 수 있도록 API와 실시간 WebSocket 메시지를 제공합니다.
+
+이 Backend가 담당하는 핵심 기능은 다음과 같습니다.
+
+- Kafka의 `factory.manufacturing.alert` 이벤트를 검증·중복 제거·점수화하여 `AlertEvent`로 저장합니다.
+- Kafka의 `factory.manufacturing.analysis` 이벤트 중 `PROCESS_RISK_ANALYSIS` 결과를 이용해 정상 공정의 AGV 배차를 요청합니다.
+- 알림 조치 이력과 공정별 분석 결과를 바탕으로 유사 장애 조치 추천을 제공합니다. 이는 현재 코드상 별도 AI 모델 호출이 아니라 Main DB의 유사 분석 결과와 조치 타임라인을 조회하는 방식입니다.
+- Redis에 AGV 대기열 및 실시간 운행 상태를 관리하고, `/topic/alerts`와 `/topic/agv`로 운영 화면에 변경 사항을 전달합니다.
+- OpenSearch Client 연결 설정은 존재하지만, 이 저장소에서 OpenSearch로 분석 결과를 색인하거나 조회하는 서비스 로직은 확인되지 않습니다. 현재 주된 영속화는 Main DB와 Redis입니다.
+
+## 핵심 구조
+
+```mermaid
+flowchart LR
+ Producer[제조 assembly-service]
+ Kafka[(Kafka / MSK)]
+ Backend[AIMS Backend]
+ MainDB[(MySQL Main DB)]
+ Redis[(Redis)]
+ Client[Dashboard Client]
+ Assembly[Assembly Service]
+
+ Producer -->|factory.manufacturing.alert| Kafka
+ Producer -->|factory.manufacturing.analysis| Kafka
+ Kafka -->|AlertEventConsumer| Backend
+ Kafka -->|ManufacturingAnalysisConsumer| Backend
+ Backend --> MainDB
+ Backend --> Redis
+ Backend -->|STOMP /topic/alerts, /topic/agv| Client
+ Backend -->|POST /api/internal/agv-arrivals| Assembly
+```
-`backend-main`은 AIMS 알림/이벤트 처리 API를 제공하는 Spring Boot 백엔드 서비스입니다. 알림 조회, 상세 확인, 조치 처리, AI 대응 추천, 인증, Gateway 연동을 담당합니다.
+현재 Backend 코드에는 `KafkaTemplate` Producer Bean이 정의되어 있지만, 애플리케이션 서비스에서 Kafka로 메시지를 발행하는 호출은 확인되지 않습니다. 따라서 실제 입력 Producer는 외부 제조/분석 서비스로 보는 것이 맞습니다.
+
+## 알림 처리
+
+
+ | 알림 우선순위 구조 |
+ 알림 목록 |
+
+
+
+
+ |
+
+
+ |
+
+
+
+ | 알림 조치 |
+ 알림 상세 |
+
+
+
+
+ |
+
+
+ |
+
+
+
+알림은 `factory.manufacturing.alert` 토픽을 `AlertEventConsumer`가 소비합니다. `AlertEventSaveService`는 JSON을 정규화하고 중복 이벤트를 걸러낸 뒤 점수를 계산하여 DB에 저장합니다.
+
+### 처리 순서
+
+```mermaid
+sequenceDiagram
+ participant A as Alert Producer
+ participant K as Kafka
factory.manufacturing.alert
+ participant C as AlertEventConsumer
+ participant S as AlertEventSaveService
+ participant W as STOMP WebSocket
+ participant DB as Main DB
+ participant UI as Dashboard
+
+ A->>K: alert JSON(eventId, alertType, processCode, riskScore)
+ K->>C: consume(message)
+ C->>S: save(message)
+ S->>S: eventId 중복 확인
+ S->>S: occurrence / detection / priority 계산
+ S->>W: /topic/alerts 실시간 알림 publish
+ S->>DB: AlertEvent saveAndFlush
+ W-->>UI: AlertRealtimeMessage
+```
-## 실행 프로파일
+### 점수와 상태
-`backend-main`은 `local`, `dev`, `prod` 프로파일을 사용합니다. 기본 프로파일은 `local`이며, `SPRING_PROFILES_ACTIVE` 환경 변수로 변경합니다.
+- 지원 공정: `PRESS`, `BODY`, `PAINT`, `ASSEMBLY`
+- 알림 유형: `PROCESS`, `EQUIPMENT`
+- `riskScore`는 0~100 범위이며 필수입니다.
+- `occurrenceScore`: 같은 `eventKey`의 최근 30일 발생 빈도 기반
+- `detectionScore`: 과거 조치 상태(`COMPLETED`, `INCOMPLETE`, `NOT_NEEDED`) 기반
+- `priorityScore`: `riskScore × (1 + occurrenceScore) × (1 + detectionScore)`
+- `priorityScore >= 250`이면 `DANGER`, 그 미만이면 `CAUTION`
+- 같은 `eventId`가 이미 저장되어 있으면 중복 처리하지 않습니다.
-| 프로파일 | 용도 | 주요 설정 |
-| --- | --- | --- |
-| `local` | 로컬 개발 환경 | `.env` 기반 로컬 DB/Redis/Kafka/OpenSearch 접속, 상세 SQL 로그 활성화 |
-| `dev` | 개발 서버 환경 | 개발 서버용 외부 의존성 접속, Kafka group 기본값 `backend-dev`, 개발 로그 레벨 |
-| `prod` | 운영 환경 | 운영 서버용 외부 의존성 접속, Kafka group 기본값 `backend-prod`, 운영 로그 레벨 |
+알림 실시간 메시지는 `/topic/alerts`로 발행되며, 알림 이력과 조치 정보는 Main DB의 `alert_event`, `action_timeline`을 통해 조회합니다. WebSocket publish가 실패하더라도 DB 저장 흐름 자체는 계속 진행하도록 처리되어 있습니다.
-프로파일 변경 예시:
+### 알림 API
-```properties
-SPRING_PROFILES_ACTIVE=local
+| 기능 | HTTP API |
+| --- | --- |
+| 알림 목록/검색 | `GET /api/event` |
+| 우선순위 요약 | `GET /api/event/priority-summary?days=7` |
+| 알림 상세 | `GET /api/event/{logNo}` |
+| eventId로 조회 | `GET /api/event/by-event-id/{eventId}` |
+| 조치 상태 변경 | `PATCH /api/event/{logNo}/action` |
+| 조치 타임라인 조회 | `GET /api/event/{logNo}/action-timeline` |
+| 조치 타임라인 등록 | `POST /api/event/{logNo}/action-timeline` |
+| 유사 장애 조치 추천 | `GET /api/event/{logNo}/recommendation` |
+
+## AGV 배차 및 운반
+
+
+AGV는 분석 결과를 직접 Kafka로 재발행하지 않고, 분석 이벤트를 소비한 뒤 Redis 대기열과 DB 상태를 조합하여 시뮬레이션합니다.
+
+### 분석 이벤트 → 배차 흐름
+
+`ManufacturingAnalysisConsumer`는 `factory.manufacturing.analysis`를 소비하고, `analysisType == PROCESS_RISK_ANALYSIS`인 이벤트만 처리합니다. 동일 `eventId`는 Redis의 `agv:analysis:processed:{eventId}` 키로 1분 동안 중복 방지합니다. 분석 결과가 abnormal이면 AGV를 배차하지 않습니다.
+
+```mermaid
+flowchart TD
+ E[factory.manufacturing.analysis] --> C[ManufacturingAnalysisConsumer]
+ C --> T{analysisType == PROCESS_RISK_ANALYSIS?}
+ T -- 아니오 --> I[무시]
+ T -- 예 --> D["Redis 중복 확인
agv:analysis:processed:{eventId}"]
+ D --> X{isAbnormal?}
+ X -- 예 --> N[AGV 배차하지 않음]
+ X -- 아니오 --> Q[route별 Redis Queue 적재]
+ Q --> S[AgvDispatchScheduler
1초 주기 + ShedLock]
+ S --> A{대기 AGV 존재?}
+ A -- 아니오 --> R[Queue 선두 재적재 후 대기]
+ A -- 예 --> DB[agv_operation을 MOVING으로 변경]
+ DB --> RT[Redis realtime 상태 저장]
+ RT --> WS["/topic/agv publish"]
```
-Windows PowerShell:
+### Route와 Redis 자료구조
-```powershell
-$env:SPRING_PROFILES_ACTIVE = "dev"
-.\gradlew.bat bootRun
-```
+| 출발 공정 | 도착 공정 | routeCode | 배차 Queue 키 |
+| --- | --- | --- | --- |
+| `PRESS` | `BODY` | `PRESS_BODY` | `agv:dispatch:queue:PRESS_BODY` |
+| `BODY` | `PAINT` | `BODY_PAINT` | `agv:dispatch:queue:BODY_PAINT` |
+| `PAINT` | `ASSEMBLY` | `PAINT_ASSEMBLY` | `agv:dispatch:queue:PAINT_ASSEMBLY` |
+| `ASSEMBLY` | `INSPECTION` | `ASSEMBLY_INSPECTION` | `agv:dispatch:queue:ASSEMBLY_INSPECTION` |
-프로파일별 설정 파일:
+각 route에는 중복 방지용 Set(`agv:dispatch:event-ids:{routeCode}`)도 함께 사용합니다. 사용 가능한 AGV가 없거나 배차 중 예외가 발생하면 요청을 Queue 앞에 다시 넣습니다.
-- 공통 설정: `src/main/resources/application.yaml`
-- 로컬 설정: `src/main/resources/application-local.yaml`
-- 개발 설정: `src/main/resources/application-dev.yaml`
-- 운영 설정: `src/main/resources/application-prod.yaml`
+### AGV 상태 머신
-## 주요 기능
+```mermaid
+stateDiagram-v2
+ [*] --> WAITING
+ WAITING --> MOVING: 배차 성공
+ MOVING --> UNLOADING: 이동 30초 만료
+ UNLOADING --> RETURNING: 하역 5초 만료
+ RETURNING --> WAITING: 복귀 30초 만료
+```
-### event
+- 영속 상태: Main DB `agv_operation`
+- 실시간 진행률/예상 도착: Redis `agv:realtime:{agvId}`
+- 상태 전이 확인: `AgvTransportStateScheduler`가 1초마다 만료 상태를 검사
+- 다중 Pod 중복 실행 방지: Redis 기반 ShedLock
+- Pod 재시작 시 DB 상태와 Redis 상태를 비교하여 하역/복귀 세션을 복구
+- `MOVING` 도착 시 Assembly Service에 `POST {ASSEMBLY_SERVICE_URL}/api/internal/agv-arrivals`로 `eventId`를 전달
+- Assembly 도착 API가 실패해도 AGV 시뮬레이션은 계속 진행
-알림과 이벤트 처리 흐름을 담당합니다.
+AGV 상태가 변경될 때 전체 AGV 목록을 `/topic/agv`로 publish합니다. REST 조회는 다음 API를 사용합니다.
-| 기능 | 설명 | 권장 API |
-| --- | --- | --- |
-| 알림 목록 조회 | 조건, 상태, 기간, 키워드 기반 필터 검색 | `GET /api/events/alerts` |
-| 알림 상세 조회 | 단일 알림의 상세 정보 조회 | `GET /api/events/alerts/{alertId}` |
-| 조치 이력 조회 | 알림별 조치 이력 조회 | `GET /api/events/alerts/{alertId}/actions` |
-| AI 대응 추천 조회 | 알림 원인과 상황 기반 AI 대응 추천 조회 | `GET /api/events/alerts/{alertId}/ai/recommendation` |
-| AI 매뉴얼 | 알림 유형별 AI 매뉴얼 조회 | `GET /api/events/alerts/{alertId}/ai/manual` |
-| AI 신뢰성 평가 | AI 추천 결과에 대한 신뢰도/근거 평가 조회 | `GET /api/events/alerts/{alertId}/ai/reliability` |
-| 알림 요약 조회 | 알림 상태, 유형, 심각도 기준 요약 조회 | `GET /api/events/alerts/summary` |
-| 오늘 이벤트 현황 | 당일 발생 이벤트 통계 조회 | `GET /api/events/today` |
-| 알림 조치 처리 | 담당자 조치 내용 등록 및 상태 변경 | `POST /api/events/alerts/{alertId}/actions` |
-| 조치 불필요 처리 | 알림을 조치 불필요 상태로 변경 | `PATCH /api/events/alerts/{alertId}/dismiss` |
-
-## 인증
-
-인증은 JWT Bearer Token 기반입니다.
-
-- 인증 헤더: `Authorization: Bearer {accessToken}`
-- Access Token 만료 시간: `JWT_ACCESS_EXPIRATION`
-- Refresh Token 만료 시간: `JWT_REFRESH_EXPIRATION`
-- JWT secret: `JWT_SECRET_KEY`
-
-현재 Security 설정:
-
-- Stateless session
-- CSRF, form login, HTTP basic 비활성화
-- JWT 필터: `JwtAuthenticationFilter`
-- 토큰 생성/검증: `TokenProvider`
-- 공개 경로:
- - `/`
- - `/api/test`
- - `/actuator/health`
- - `/swagger-ui/**`
- - `/v3/api-docs/**`
- - `/swagger-resources/**`
- - `/webjars/**`
-- 그 외 API는 인증 필요
-
-## Gateway 설정
-
-Gateway는 외부 요청을 `backend-main`으로 라우팅하고 JWT 인증 헤더를 전달합니다.
-
-권장 라우팅:
-
-```yaml
-spring:
- cloud:
- gateway:
- routes:
- - id: backend-main
- uri: http://backend-main:8080
- predicates:
- - Path=/api/events/**,/api/test/**,/v3/api-docs/backend/**
- filters:
- - StripPrefix=0
+| 기능 | HTTP API |
+| --- | --- |
+| AGV 상태 요약 | `GET /api/main/agv-status` |
+| 공정 흐름 및 AGV 상세 | `GET /api/main/process-flow` |
+
+## Kafka 설계
+
+### 토픽과 Consumer Group
+
+| 토픽 | 기본 Partition 수 | Backend Consumer | Consumer Group | 목적 |
+| --- | ---: | --- | --- | --- |
+| `factory.manufacturing.alert` | 2 | `AlertEventConsumer` | `app.kafka.group-id` | 알림 저장 및 WebSocket 전달 |
+| `factory.manufacturing.analysis` | 2 | `ManufacturingAnalysisConsumer` | `app.kafka.consumer.agv-group-id` | 정상 공정 분석 이벤트 기반 AGV 배차 |
+| `factory.manufacturing.raw` | 4 | Assembly Service 측 처리 | Assembly Service Group | 제조 원천 이벤트 수집·분석 입력 |
+| `factory.manufacturing.equipment` | 2 | Assembly Service 측 처리 | Assembly Service Group | 설비 이벤트 수집·분석 입력 |
+
+`raw`와 `equipment`는 Assembly Service에서 제조 원천·설비 정보를 처리하는 채널입니다. 이 Backend는 해당 원천 이벤트를 직접 소비하기보다, 분석 결과가 발행된 `analysis`와 알림 결과가 발행된 `alert` 토픽을 소비합니다.
+
+```mermaid
+flowchart LR
+ subgraph K["Kafka / AWS MSK"]
+ RAW[factory.manufacturing.raw
4 partitions]
+ ANALYSIS[factory.manufacturing.analysis
2 partitions]
+ ALERT[factory.manufacturing.alert
2 partitions]
+ EQUIP[factory.manufacturing.equipment
2 partitions]
+ end
+
+ AS[Assembly Service]
+ RAW -->|Assembly Service Group| AS
+ EQUIP -->|Assembly Service Group| AS
+ ANALYSIS -->|AGV group
main-agv-group*| AC[ManufacturingAnalysisConsumer
concurrency=2]
+ ALERT -->|일반 backend group
main-*/backend-*| NC[AlertEventConsumer]
+ AC --> RQ[Redis AGV Queue]
+ NC --> DB[MySQL AlertEvent]
+ NC --> WS["STOMP /topic/alerts"]
```
-Gateway 연동 시 유지해야 하는 헤더:
-
-- `Authorization`
-- `Content-Type`
-- `X-Request-Id`
+### Kafka Client 동작
-CORS는 `WebConfig`에서 처리하며, 허용 origin은 `cors.allowed-origins` 설정으로 관리합니다.
+`KafkaConfig`에서 다음과 같이 구성합니다.
-## 기술 스택
+- Producer/Consumer payload: `String`
+- Producer: `acks=all`, `enable.idempotence=true`
+- Consumer: auto commit 비활성화, `AckMode.RECORD`
+- 기본 offset: `earliest` (local profile은 `latest`로 덮어씀)
+- 운영/개발 MSK: `SASL_SSL` + `AWS_MSK_IAM`
+- 로컬 기본값: `localhost:9092`, `PLAINTEXT`
+- Listener 전체 비활성화: `app.kafka.listeners-enabled=false`
-- Java 17
-- Spring Boot 4.0.6
-- Spring WebMVC
-- Spring Security
-- Spring Data JPA
-- QueryDSL
-- MySQL, H2
-- Redis Cache
-- Kafka
-- OpenSearch Java Client
-- Spring Cloud OpenFeign
-- Springdoc OpenAPI
-- JWT
-- Spring Boot Actuator
-- Spring Boot Admin Client
+`AlertEventConsumer`는 `app.kafka.listeners-enabled`가 true일 때만 등록됩니다. 반면 AGV 분석 Consumer는 코드상 `app.kafka.consumer.agv-group-id`를 사용하므로 해당 프로퍼티와 Kafka 접속 정보가 실행 환경에 있어야 합니다.
-## 패키지 구조
+### 데이터 흐름도
+
-현재 기본 패키지는 `com.aims.backend`입니다.
+## WebSocket / STOMP
-```text
-src/main/java/com/aims/backend
-+-- BackendApplication.java
-+-- common
-| +-- code
-| +-- response
-| +-- status
-+-- config
-| +-- jwt
-| +-- security
-+-- controller
-+-- domain
-| +-- commons
-+-- dto
-| +-- auth
-| +-- test
-+-- exception
-| +-- handler
-+-- mapper
-+-- properties
-+-- repository
-+-- service
-+-- utils
-```
+STOMP endpoint는 `/ws`와 `/api/ws`이며 SockJS를 지원합니다. 서버 브로커 prefix는 `/topic`입니다.
-권장 event 패키지 확장:
-
-```text
-com.aims.backend
-+-- controller/event
-+-- dto/event
-+-- domain/event
-+-- repository/event
-+-- service/event
-+-- mapper/event
-```
+| 구독 destination | 메시지 | 발생 시점 |
+| --- | --- | --- |
+| `/topic/alerts` | `AlertRealtimeMessage` | 새로운 알림을 Kafka에서 수신하고 점수 계산 후 |
+| `/topic/agv` | AGV 전체 상태 목록 | 배차, 이동 도착, 하역 시작, 복귀 시작/완료 시 |
## 설정
-프로젝트 루트의 `.env`를 자동으로 읽습니다.
+설정 파일은 다음 순서로 관리합니다.
-```yaml
-spring:
- config:
- import: optional:file:.env[.properties]
-```
+- 공통: `src/main/resources/application.yaml`
+- local: `application-local.yaml`
+- dev: `application-dev.yaml`
+- prod: `application-prod.yaml`
-주요 환경 변수:
+주요 환경 변수는 다음과 같습니다.
| 변수 | 설명 |
| --- | --- |
-| `APP_NAME` | Spring application name |
-| `SPRING_PROFILES_ACTIVE` | 실행 profile |
-| `MAIN_DB_JDBC_URL` | Main DB JDBC URL |
-| `MAIN_DB_USERNAME` | Main DB 계정 |
-| `MAIN_DB_PASSWORD` | Main DB 비밀번호 |
-| `SAMPLE_DB_JDBC_URL` | Sample DB JDBC URL |
-| `REDIS_HOST` | Redis host |
-| `REDIS_PORT` | Redis port |
-| `KAFKA_BOOTSTRAP_SERVERS` | Kafka bootstrap servers |
-| `KAFKA_GROUP_ID` | Kafka consumer group id |
-| `OPENSEARCH_HOST` | OpenSearch host |
-| `OPENSEARCH_PORT` | OpenSearch port |
-| `JWT_SECRET_KEY` | JWT signing key |
+| `SPRING_PROFILES_ACTIVE` | 실행 profile (`local`, `dev`, `prod`) |
+| `KAFKA_BOOTSTRAP_SERVER_1`, `KAFKA_BOOTSTRAP_SERVER_2` | Kafka/MSK Broker 주소 |
+| `KAFKA_GROUP_ID` | 알림 Consumer Group |
+| `AGV_CONSUMER_GROUP_ID` | AGV 분석 Consumer Group |
+| `KAFKA_LISTENERS_ENABLED` | Kafka Listener 활성화 여부 |
+| `REDIS_HOST`, `REDIS_PORT` | Redis 접속 정보 |
+| `MAIN_DB_JDBC_URL`, `MAIN_DB_USERNAME`, `MAIN_DB_PASSWORD` | Main DB 접속 정보 |
+| `ASSEMBLY_SERVICE_URL` | Assembly Service 주소 |
+| `JWT_SECRET_KEY` | JWT 서명 키 |
## 실행
-```bash
-./gradlew bootRun
-```
-
-Windows:
+프로젝트 루트의 `.env`를 준비한 후 실행합니다.
```powershell
+$env:SPRING_PROFILES_ACTIVE = "local"
.\gradlew.bat bootRun
```
-## 테스트
-
-```bash
-./gradlew test
-```
-
-Windows:
+테스트:
```powershell
.\gradlew.bat test
```
-테스트 profile은 H2 in-memory DB를 사용합니다.
+Swagger UI:
-## API 문서
+```text
+http://localhost:8081/swagger-ui/index.html
+```
-로컬 실행 후 Swagger UI에서 API 문서를 확인할 수 있습니다.
+## 주요 패키지
```text
-http://localhost:8080/swagger-ui/index.html
+src/main/java/com/aims/backend
+├─ config/ # Kafka, WebSocket, Security, DB 설정
+├─ controller/alert # 알림 REST API
+├─ controller/dashboard # 대시보드·AGV REST API
+├─ service/alert # Kafka 알림 소비, 저장, WebSocket publish
+├─ service/dashboard # 분석 소비, AGV Queue·Scheduler·상태 전이
+├─ domain/alert # AlertEvent, ActionTimeline
+├─ domain/dashboard # AgvOperation 및 공정 도메인
+├─ repository/alert
+└─ repository/dashboard
```
diff --git a/build.gradle b/build.gradle
index 4132fda..8b79147 100644
--- a/build.gradle
+++ b/build.gradle
@@ -43,6 +43,10 @@ dependencies {
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ // ShedLock
+ implementation "net.javacrumbs.shedlock:shedlock-spring:7.7.0"
+ implementation "net.javacrumbs.shedlock:shedlock-provider-redis-spring:7.7.0"
+
// Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
diff --git a/src/main/java/com/aims/backend/client/AssemblyArrivalClient.java b/src/main/java/com/aims/backend/client/AssemblyArrivalClient.java
new file mode 100644
index 0000000..8f3dcb1
--- /dev/null
+++ b/src/main/java/com/aims/backend/client/AssemblyArrivalClient.java
@@ -0,0 +1,64 @@
+package com.aims.backend.client;
+
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+import com.aims.backend.dto.dashboard.AgvArrivalRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClient;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AssemblyArrivalClient {
+
+ private final RestClient restClient;
+
+ @Value("${external.assembly-url}")
+ private String assemblyUrl;
+
+ /**
+ * AGV 도착 정보를 Assembly Service로 전달
+ */
+ public void notifyAgvArrived(
+ String eventId
+ ) {
+
+ AgvArrivalRequest request =
+ AgvArrivalRequest.builder()
+ .eventId(eventId)
+ .build();
+
+ String url =
+ assemblyUrl + "/api/internal/agv-arrivals";
+
+ log.info("""
+
+ ==============================
+ Assembly 도착 API 요청
+
+ url={}
+ {}
+
+ ==============================
+
+ """,
+ url,
+ eventId
+ );
+
+ restClient.post()
+ .uri(url)
+ .body(request)
+ .retrieve()
+ .toBodilessEntity();
+
+ log.info(
+ "[ASSEMBLY ARRIVAL SUCCESS] eventId={}",
+ eventId
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/common/status/ErrorStatus.java b/src/main/java/com/aims/backend/common/status/ErrorStatus.java
index df09a71..2cd2858 100644
--- a/src/main/java/com/aims/backend/common/status/ErrorStatus.java
+++ b/src/main/java/com/aims/backend/common/status/ErrorStatus.java
@@ -10,6 +10,37 @@
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
+
+ RECOMMENDATION_ANALYSIS_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "RECOMMEND404_1",
+ "분석 결과를 찾을 수 없습니다."
+ ),
+
+ RECOMMENDATION_PRESS_ANALYSIS_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "RECOMMEND404_2",
+ "PRESS 분석 결과를 찾을 수 없습니다."
+ ),
+
+ RECOMMENDATION_SIMILAR_EVENT_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "RECOMMEND404_3",
+ "유사 이벤트를 찾을 수 없습니다."
+ ),
+
+ RECOMMENDATION_ALERT_EVENT_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "RECOMMEND404_4",
+ "유사 이벤트 정보를 찾을 수 없습니다."
+ ),
+
+ RECOMMENDATION_TIMELINE_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "RECOMMEND404_5",
+ "유사 이벤트의 조치 이력이 존재하지 않습니다."
+ ),
+
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 내부 오류가 발생했습니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
diff --git a/src/main/java/com/aims/backend/config/AlertImageProperties.java b/src/main/java/com/aims/backend/config/AlertImageProperties.java
new file mode 100644
index 0000000..32d67e1
--- /dev/null
+++ b/src/main/java/com/aims/backend/config/AlertImageProperties.java
@@ -0,0 +1,61 @@
+package com.aims.backend.config;
+
+import com.aims.backend.domain.alert.AlertSeverity;
+import com.aims.backend.domain.alert.AlertType;
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+import jakarta.annotation.PostConstruct;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+@Slf4j
+@Getter
+@Setter
+@Component
+@ConfigurationProperties(prefix = "app.alert.image")
+public class AlertImageProperties {
+
+ private Map processImages = new EnumMap<>(ProcessCode.class);
+
+ @PostConstruct
+ public void logLoadedImages() {
+ log.info("AlertImageProperties loaded. size={}, keys={}",
+ processImages.size(), processImages.keySet());
+ processImages.forEach((code, set) ->
+ log.info(" {} -> dangerEquipment={}, dangerProcess={}, warning={}",
+ code, set.getDangerEquipment(), set.getDangerProcess(), set.getWarning()));
+ }
+
+ public String resolve(ProcessCode processCode, AlertType alertType, AlertSeverity severity) {
+
+ ImageSet imageSet = processImages.get(processCode);
+
+ log.info("resolve() called. processCode={}, alertType={}, severity={}, imageSetFound={}",
+ processCode, alertType, severity, imageSet != null);
+
+ if (imageSet == null || severity == null) {
+ return null;
+ }
+
+ if (severity == AlertSeverity.CAUTION) {
+ return imageSet.getWarning();
+ }
+
+ return alertType == AlertType.EQUIPMENT
+ ? imageSet.getDangerEquipment()
+ : imageSet.getDangerProcess();
+ }
+
+ @Getter
+ @Setter
+ public static class ImageSet {
+ private String dangerEquipment;
+ private String dangerProcess;
+ private String warning;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/config/KafkaConfig.java b/src/main/java/com/aims/backend/config/KafkaConfig.java
index b7d9b07..96a6105 100644
--- a/src/main/java/com/aims/backend/config/KafkaConfig.java
+++ b/src/main/java/com/aims/backend/config/KafkaConfig.java
@@ -28,6 +28,14 @@ public class KafkaConfig {
private final KafkaCustomProperties kafkaCustomProperties;
+ private String getBootstrapServers() {
+
+ return String.join(
+ ",",
+ kafkaCustomProperties.getBootstrapServers()
+ );
+ }
+
@Bean
public ProducerFactory producerFactory() {
@@ -35,7 +43,7 @@ public ProducerFactory producerFactory() {
properties.put(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
- kafkaCustomProperties.getBootstrapServers()
+ getBootstrapServers()
);
properties.put(
diff --git a/src/main/java/com/aims/backend/config/RestClientConfig.java b/src/main/java/com/aims/backend/config/RestClientConfig.java
new file mode 100644
index 0000000..01c01af
--- /dev/null
+++ b/src/main/java/com/aims/backend/config/RestClientConfig.java
@@ -0,0 +1,14 @@
+package com.aims.backend.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class RestClientConfig {
+
+ @Bean
+ public RestClient restClient() {
+ return RestClient.create();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/config/SecurityConfig.java b/src/main/java/com/aims/backend/config/SecurityConfig.java
index 7c08353..fa365f6 100644
--- a/src/main/java/com/aims/backend/config/SecurityConfig.java
+++ b/src/main/java/com/aims/backend/config/SecurityConfig.java
@@ -51,7 +51,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/api/auth/login",
"/api/auth/signup",
"/api/auth/refresh",
- "/api/event/**"
+ "/api/event/**",
+ "/api/main/process-flow",
+ "/ws/**",
+ "/api/ws/**"
).permitAll()
.anyRequest().authenticated()
)
diff --git a/src/main/java/com/aims/backend/config/ShedLockConfig.java b/src/main/java/com/aims/backend/config/ShedLockConfig.java
new file mode 100644
index 0000000..5f280bc
--- /dev/null
+++ b/src/main/java/com/aims/backend/config/ShedLockConfig.java
@@ -0,0 +1,25 @@
+package com.aims.backend.config;
+
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+
+@Configuration
+@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
+public class ShedLockConfig {
+
+ private static final String ENVIRONMENT = "aims-backend";
+
+ @Bean
+ public LockProvider lockProvider(
+ RedisConnectionFactory redisConnectionFactory
+ ) {
+ return new RedisLockProvider(
+ redisConnectionFactory,
+ ENVIRONMENT
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/config/WebSocketConfig.java b/src/main/java/com/aims/backend/config/WebSocketConfig.java
index 5fa5226..73155ad 100644
--- a/src/main/java/com/aims/backend/config/WebSocketConfig.java
+++ b/src/main/java/com/aims/backend/config/WebSocketConfig.java
@@ -9,13 +9,20 @@
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
- public void registerStompEndpoints(StompEndpointRegistry registry) {
- registry.addEndpoint("/ws")
- .setAllowedOriginPatterns("*");
+ public void registerStompEndpoints(
+ StompEndpointRegistry registry
+ ) {
+
+ registry.addEndpoint("/ws","api/ws")
+ .setAllowedOriginPatterns("*")
+ .withSockJS();
+
}
+
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
+
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
diff --git a/src/main/java/com/aims/backend/config/jpa/MainJpaConfig.java b/src/main/java/com/aims/backend/config/jpa/MainJpaConfig.java
index 725d535..061c22d 100644
--- a/src/main/java/com/aims/backend/config/jpa/MainJpaConfig.java
+++ b/src/main/java/com/aims/backend/config/jpa/MainJpaConfig.java
@@ -44,7 +44,13 @@ public DataSource mainDataSource() {
public LocalContainerEntityManagerFactoryBean mainEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(mainDataSource());
- em.setPackagesToScan("com.aims.backend.domain.commons", "com.aims.backend.domain.mainpage", "com.aims.backend.domain.user", "com.aims.backend.domain.dashboard", "com.aims.backend.domain.alert");
+ em.setPackagesToScan(
+ "com.aims.backend.domain.commons",
+ "com.aims.backend.domain.mainpage",
+ "com.aims.backend.domain.user",
+ "com.aims.backend.domain.dashboard",
+ "com.aims.backend.domain.alert",
+ "com.aims.backend.domain.eventAnalysis");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return em;
}
diff --git a/src/main/java/com/aims/backend/controller/alert/AlertEventController.java b/src/main/java/com/aims/backend/controller/alert/AlertEventController.java
index f84d7e2..ee52a3e 100644
--- a/src/main/java/com/aims/backend/controller/alert/AlertEventController.java
+++ b/src/main/java/com/aims/backend/controller/alert/AlertEventController.java
@@ -1,33 +1,66 @@
package com.aims.backend.controller.alert;
import com.aims.backend.common.response.ApiResponse;
+import com.aims.backend.config.jwt.TokenProvider;
import com.aims.backend.dto.alert.AlertActionUpdateRequest;
import com.aims.backend.dto.alert.AlertEventResponse;
+import com.aims.backend.dto.alert.AlertPrioritySummaryResponse;
import com.aims.backend.dto.alert.AlertSearchRequest;
+import com.aims.backend.dto.eventAnalysis.RecommendationResponse;
+import com.aims.backend.service.eventAnalysis.AlertRecommendationService;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import com.aims.backend.dto.alert.ActionTimelineResponse;
+import com.aims.backend.service.alert.AlertPrioritySummaryService;
+import com.aims.backend.dto.alert.ActionTimelineCreateRequest;
import com.aims.backend.service.alert.AlertEventQueryService;
+import com.aims.backend.service.alert.ActionTimelineService;
+import com.aims.backend.service.alert.ActionTimelineService;
+import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
+import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
@RestController
@RequestMapping("/api/event")
@RequiredArgsConstructor
+@Validated
public class AlertEventController {
private final AlertEventQueryService alertEventQueryService;
+ private final AlertPrioritySummaryService alertPrioritySummaryService;
+ private final ActionTimelineService actionTimelineService;
+ private final TokenProvider tokenProvider;
+ private final AlertRecommendationService alertRecommendationService;
@GetMapping
public ApiResponse> getAlerts(@ModelAttribute AlertSearchRequest request) {
return ApiResponse.success(alertEventQueryService.getAlerts(request));
}
+ @GetMapping("/priority-summary")
+ public ApiResponse getPrioritySummary(
+ @RequestParam(defaultValue = "7") @Min(1) @Max(365) int days
+ ) {
+ return ApiResponse.success(alertPrioritySummaryService.getPrioritySummary(days));
+ }
+
@GetMapping("/{logNo}")
public ApiResponse getAlert(@PathVariable String logNo) {
return ApiResponse.success(alertEventQueryService.getAlert(logNo));
@@ -45,4 +78,47 @@ public ApiResponse updateAction(
) {
return ApiResponse.success(alertEventQueryService.updateAction(logNo, request));
}
+
+ @GetMapping("/{logNo}/action-timeline")
+ public ApiResponse> getActionTimeline(@PathVariable String logNo) {
+ return ApiResponse.success(actionTimelineService.getTimeline(logNo));
+ }
+
+ @PostMapping("/{logNo}/action-timeline")
+ public ApiResponse createActionTimeline(
+ @PathVariable String logNo,
+ @Valid @RequestBody ActionTimelineCreateRequest request,
+ HttpServletRequest httpServletRequest
+ ) {
+
+
+ String accessToken = TokenProvider.resolveToken(httpServletRequest);
+ if (accessToken == null) {
+ return ApiResponse.failure("Access token is missing or invalid.", null);
+ }
+
+ Authentication authentication = tokenProvider.getAuthentication(accessToken);
+ if (authentication == null || !(authentication.getPrincipal() instanceof TokenProvider.JwtPrincipal principal)) {
+ return ApiResponse.failure("Invalid authentication principal.", null);
+ }
+
+ return ApiResponse.success(actionTimelineService.createTimeline(logNo, request, principal));
+ }
+
+ @Operation(
+ summary = "유사 장애 조치 추천",
+ description = "현재 이벤트와 가장 유사한 과거 이벤트를 찾아 추천 조치 방법을 반환합니다."
+ )
+ @GetMapping("/{logNo}/recommendation")
+ public ApiResponse getRecommendation(
+ @PathVariable String logNo) {
+
+ RecommendationResponse response =
+ alertRecommendationService.getRecommendation(logNo);
+
+ return ApiResponse.success(
+ response,
+ "유사 장애 추천 조회 성공"
+ );
+ }
}
diff --git a/src/main/java/com/aims/backend/controller/dashboard/AgvTestController.java b/src/main/java/com/aims/backend/controller/dashboard/AgvTestController.java
deleted file mode 100644
index 8db17f3..0000000
--- a/src/main/java/com/aims/backend/controller/dashboard/AgvTestController.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.aims.backend.controller.dashboard;
-
-import com.aims.backend.domain.dashboard.enums.ProcessCode;
-import com.aims.backend.service.dashboard.AgvSimulationService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-@RequestMapping("/api/test/agv")
-@RestController
-@RequiredArgsConstructor
-public class AgvTestController {
-
- private final AgvSimulationService agvSimulationService;
-
- @PostMapping("/dispatch")
- public ResponseEntity dispatch(
- @RequestParam Long carMasterId,
- @RequestParam ProcessCode processCode
- ) {
- agvSimulationService.dispatchAgv(
- carMasterId,
- processCode
- );
-
- return ResponseEntity.ok().build();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/controller/dashboard/ManufacturingEventTestController.java b/src/main/java/com/aims/backend/controller/dashboard/ManufacturingEventTestController.java
deleted file mode 100644
index 838a8e2..0000000
--- a/src/main/java/com/aims/backend/controller/dashboard/ManufacturingEventTestController.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.aims.backend.controller.dashboard;
-
-import com.aims.backend.dto.dashboard.ManufacturingEventRequest;
-import com.aims.backend.service.dashboard.AgvSimulationService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/api/test/manufacturing-events") /* 테스트 */
-@RequiredArgsConstructor
-public class ManufacturingEventTestController {
-
- private final AgvSimulationService agvSimulationService;
-
- @PostMapping
- public ResponseEntity receiveManufacturingEvent(
- @RequestBody ManufacturingEventRequest request
- ) {
- agvSimulationService.handleManufacturingEvent(request);
- return ResponseEntity.ok().build();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/alert/ActionCategory.java b/src/main/java/com/aims/backend/domain/alert/ActionCategory.java
new file mode 100644
index 0000000..14970c1
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/alert/ActionCategory.java
@@ -0,0 +1,10 @@
+package com.aims.backend.domain.alert;
+
+public enum ActionCategory {
+ CHECK,
+ REPAIR,
+ CHANGE,
+ CLEAN,
+ ADJUST,
+ RESTART
+}
diff --git a/src/main/java/com/aims/backend/domain/alert/ActionTimeline.java b/src/main/java/com/aims/backend/domain/alert/ActionTimeline.java
new file mode 100644
index 0000000..947b537
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/alert/ActionTimeline.java
@@ -0,0 +1,64 @@
+package com.aims.backend.domain.alert;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Table;
+import jakarta.persistence.Id;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import com.aims.backend.domain.user.UserRole;
+
+import java.time.LocalDateTime;
+
+import com.aims.backend.domain.alert.ActionCategory;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "action_timeline")
+public class ActionTimeline {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "action_id")
+ private Long actionId;
+
+ @Column(name = "log_no", nullable = false, length = 20)
+ private String logNo;
+
+ @Column(name = "emp_no", nullable = false, length = 20)
+ private String empNo;
+
+ @Column(name = "emp_name", nullable = false, length = 20)
+ private String empName;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "emp_role", nullable = false, length = 20)
+ private UserRole empRole;
+
+ @Column(name = "action_time", nullable = false)
+ private LocalDateTime actionTime;
+
+ @Column(name = "action_content", nullable = false, length = 20)
+ private String actionContent;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "action_category", nullable = false, length = 20)
+ private ActionCategory actionCategory;
+
+ @Column(name = "action_result", nullable = false, length = 20)
+ private String actionResult;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/alert/AlertEvent.java b/src/main/java/com/aims/backend/domain/alert/AlertEvent.java
index ab97bf7..ee9a935 100644
--- a/src/main/java/com/aims/backend/domain/alert/AlertEvent.java
+++ b/src/main/java/com/aims/backend/domain/alert/AlertEvent.java
@@ -81,6 +81,9 @@ public class AlertEvent {
@Column(name = "score_calculated_at")
private LocalDateTime scoreCalculatedAt;
+ @Column(name = "image_url")
+ private String imageUrl;
+
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
diff --git a/src/main/java/com/aims/backend/domain/dashboard/AgvOperation.java b/src/main/java/com/aims/backend/domain/dashboard/AgvOperation.java
index c1d2dd1..6a1a468 100644
--- a/src/main/java/com/aims/backend/domain/dashboard/AgvOperation.java
+++ b/src/main/java/com/aims/backend/domain/dashboard/AgvOperation.java
@@ -18,6 +18,9 @@ public class AgvOperation extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
+ @Column(name = "event_id")
+ private String eventId;
+
@Column(name = "car_master_id")
private Long carMasterId;
@@ -40,11 +43,13 @@ public class AgvOperation extends BaseEntity {
private Integer laneNo;
public void dispatch(
+ String eventId,
Long carMasterId,
ProcessCode from,
ProcessCode to,
String routeCode
) {
+ this.eventId = eventId;
this.carMasterId = carMasterId;
this.currentProcess = from;
this.targetProcess = to;
@@ -52,11 +57,15 @@ public void dispatch(
this.agvStatus = AgvStatus.MOVING;
}
+ public void changeToUnloading() {
+ this.currentProcess = this.targetProcess;
+ this.agvStatus = AgvStatus.UNLOADING;
+ }
+
public void changeToReturning() {
- ProcessCode arrivedProcess = this.targetProcess;
- ProcessCode homeProcess = this.currentProcess;
+ ProcessCode arrivedProcess = this.currentProcess;
+ ProcessCode homeProcess = getHomeProcessByRouteCode(this.routeCode);
- this.carMasterId = null;
this.currentProcess = arrivedProcess;
this.targetProcess = homeProcess;
this.agvStatus = AgvStatus.RETURNING;
@@ -64,13 +73,31 @@ public void changeToReturning() {
public void changeToWaiting(
ProcessCode homeProcess,
- ProcessCode nextProcess,
+ ProcessCode nextTarget,
String routeCode
) {
+ this.eventId = null;
this.carMasterId = null;
this.currentProcess = homeProcess;
- this.targetProcess = nextProcess;
+ this.targetProcess = nextTarget;
this.routeCode = routeCode;
this.agvStatus = AgvStatus.WAITING;
}
+
+ private ProcessCode getHomeProcessByRouteCode(String routeCode) {
+ if ("PRESS_BODY".equals(routeCode)) {
+ return ProcessCode.PRESS;
+ }
+ if ("BODY_PAINT".equals(routeCode)) {
+ return ProcessCode.BODY;
+ }
+ if ("PAINT_ASSEMBLY".equals(routeCode)) {
+ return ProcessCode.PAINT;
+ }
+ if ("ASSEMBLY_INSPECTION".equals(routeCode)) {
+ return ProcessCode.ASSEMBLY;
+ }
+
+ return this.currentProcess;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/dashboard/Equipment.java b/src/main/java/com/aims/backend/domain/dashboard/Equipment.java
index fbdd368..b997a20 100644
--- a/src/main/java/com/aims/backend/domain/dashboard/Equipment.java
+++ b/src/main/java/com/aims/backend/domain/dashboard/Equipment.java
@@ -42,9 +42,6 @@ public class Equipment {
@Column(name = "current_status")
private OperationStatus currentStatus;
- @Column(name = "health_status")
- private String healthStatus;
-
@Column(name = "last_fault_time")
private LocalDateTime lastFaultTime;
diff --git a/src/main/java/com/aims/backend/domain/dashboard/enums/AgvStatus.java b/src/main/java/com/aims/backend/domain/dashboard/enums/AgvStatus.java
index d83afa6..7a949ce 100644
--- a/src/main/java/com/aims/backend/domain/dashboard/enums/AgvStatus.java
+++ b/src/main/java/com/aims/backend/domain/dashboard/enums/AgvStatus.java
@@ -1,7 +1,8 @@
package com.aims.backend.domain.dashboard.enums;
public enum AgvStatus {
- WAITING, /** 대기중 */
- MOVING, /** 이동중 */
- RETURNING /** 복귀중 */
-}
+ WAITING,
+ MOVING,
+ UNLOADING,
+ RETURNING
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/dashboard/enums/OperationStatus.java b/src/main/java/com/aims/backend/domain/dashboard/enums/OperationStatus.java
index 4260117..ce44500 100644
--- a/src/main/java/com/aims/backend/domain/dashboard/enums/OperationStatus.java
+++ b/src/main/java/com/aims/backend/domain/dashboard/enums/OperationStatus.java
@@ -12,13 +12,12 @@ public enum OperationStatus {
RUNNING("가동"),
- IDLE("대기"),
STOPPED("정지"),
FAULT("고장"),
-
- MAINTENANCE("정비");
+
+ WARNING("경고");
/**
diff --git a/src/main/java/com/aims/backend/domain/dashboard/scheduler/AgvSimulatorScheduler.java b/src/main/java/com/aims/backend/domain/dashboard/scheduler/AgvSimulatorScheduler.java
deleted file mode 100644
index 998c1c6..0000000
--- a/src/main/java/com/aims/backend/domain/dashboard/scheduler/AgvSimulatorScheduler.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.aims.backend.domain.dashboard.scheduler;
-
-import com.aims.backend.service.dashboard.AgvSimulationService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class AgvSimulatorScheduler {
-
- private final AgvSimulationService agvSimulationService;
-
- @Scheduled(fixedRate = 3000)
- public void run() {
- agvSimulationService.updateAgvProgress();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/dashboard/scheduler/SimulationClock.java b/src/main/java/com/aims/backend/domain/dashboard/scheduler/SimulationClock.java
deleted file mode 100644
index 988dcf5..0000000
--- a/src/main/java/com/aims/backend/domain/dashboard/scheduler/SimulationClock.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.aims.backend.domain.dashboard.scheduler;
-
-import lombok.Getter;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDateTime;
-
-@Getter
-@Component
-public class SimulationClock {
-
- private LocalDateTime currentTime = LocalDateTime.of(
- 2026,
- 6,
- 1,
- 8,
- 0,
- 0
- );
-
- public void tickSeconds(long seconds) {
- this.currentTime = this.currentTime.plusSeconds(seconds);
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/eventAnalysis/AssemblyAnalysisResult.java b/src/main/java/com/aims/backend/domain/eventAnalysis/AssemblyAnalysisResult.java
new file mode 100644
index 0000000..2f16aa6
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/eventAnalysis/AssemblyAnalysisResult.java
@@ -0,0 +1,41 @@
+package com.aims.backend.domain.eventAnalysis;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "assembly_analysis_result")
+
+public class AssemblyAnalysisResult {
+
+ @Id
+ @Column(name = "analysis_result_id")
+ private Long analysisResultId;
+
+ @Column(name = "expected_sequence")
+ private String expectedSequence;
+
+ @Column(name = "actual_sequence")
+ private String actualSequence;
+
+ @Column(name = "sequence_error_count")
+ private Integer sequenceErrorCount;
+
+ @Column(name = "missing_part_count")
+ private Integer missingPartCount;
+
+ @Column(name = "fastening_error_count")
+ private Integer fasteningErrorCount;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/domain/eventAnalysis/BodyAnalysisResult.java b/src/main/java/com/aims/backend/domain/eventAnalysis/BodyAnalysisResult.java
new file mode 100644
index 0000000..61706cc
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/eventAnalysis/BodyAnalysisResult.java
@@ -0,0 +1,41 @@
+package com.aims.backend.domain.eventAnalysis;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "body_analysis_result")
+
+public class BodyAnalysisResult {
+
+ @Id
+ @Column(name = "analysis_result_id")
+ private Long analysisResultId;
+
+ @Column(name = "robot_motion_status")
+ private String robotMotionStatus;
+
+ @Column(name = "robot_operation_mode")
+ private String robotOperationMode;
+
+ @Column(name = "robot_vibration_score")
+ private Double robotVibrationScore;
+
+ @Column(name = "frequency_peak_band")
+ private String frequencyPeakBand;
+
+ @Column(name = "frequency_peak_value")
+ private Double frequencyPeakValue;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/domain/eventAnalysis/ManufacturingAnalysisResult.java b/src/main/java/com/aims/backend/domain/eventAnalysis/ManufacturingAnalysisResult.java
new file mode 100644
index 0000000..4b536e1
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/eventAnalysis/ManufacturingAnalysisResult.java
@@ -0,0 +1,61 @@
+package com.aims.backend.domain.eventAnalysis;
+
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "manufacturing_analysis_result")
+
+public class ManufacturingAnalysisResult {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "event_id", nullable = false, unique = true)
+ private String eventId;
+
+ @Column(name = "car_master_id")
+ private Long carMasterId;
+
+ @Column(name = "equipment_id")
+ private Long equipmentId;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "process_code", nullable = false)
+ private ProcessCode processCode;
+
+ @Column(name = "event_time")
+ private LocalDateTime eventTime;
+
+ @Column(name = "is_abnormal")
+ private Integer isAbnormal;
+
+ @Column(name = "abnormal_type")
+ private String abnormalType;
+
+ @Column(name = "severity")
+ private String severity;
+
+ @Column(name = "risk_score")
+ private Double riskScore;
+
+ @Column(name = "analysis_message")
+ private String analysisMessage;
+
+ @Column(name = "analyzed_at")
+ private LocalDateTime analyzedAt;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/domain/eventAnalysis/PaintAnalysisResult.java b/src/main/java/com/aims/backend/domain/eventAnalysis/PaintAnalysisResult.java
new file mode 100644
index 0000000..7ee2a73
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/eventAnalysis/PaintAnalysisResult.java
@@ -0,0 +1,43 @@
+package com.aims.backend.domain.eventAnalysis;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "paint_analysis_result")
+public class PaintAnalysisResult {
+
+ @Id
+ @Column(name = "analysis_result_id")
+ private Long analysisResultId;
+
+ @Column(name = "image_position")
+ private String imagePosition;
+
+ @Column(name = "thermal_std_temp")
+ private Double thermalStdTemp;
+
+ @Column(name = "thickness_value")
+ private Double thicknessValue;
+
+ @Column(name = "defeat_score")
+ private Double defeatScore;
+
+ @Column(name = "vision_label")
+ private String visionLabel;
+
+ @Column(name = "surface_quality_score")
+ private Double surfaceQualityScore;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/domain/eventAnalysis/PressAnalysisResult.java b/src/main/java/com/aims/backend/domain/eventAnalysis/PressAnalysisResult.java
new file mode 100644
index 0000000..e226fa7
--- /dev/null
+++ b/src/main/java/com/aims/backend/domain/eventAnalysis/PressAnalysisResult.java
@@ -0,0 +1,40 @@
+package com.aims.backend.domain.eventAnalysis;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Table(name = "press_analysis_result")
+
+public class PressAnalysisResult {
+ @Id
+ @Column(name = "analysis_result_id")
+ private Long analysisResultId;
+
+ @Column(name = "count_increase_yn")
+ private Integer countIncreaseYn;
+
+ @Column(name = "target_cycle_time_sec")
+ private Double targetCycleTimeSec;
+
+ @Column(name = "actual_cycle_time_sec")
+ private Double actualCycleTimeSec;
+
+ @Column(name = "cycle_time_gap_sec")
+ private Double cycleTimeGapSec;
+
+ @Column(name = "timestamp_delay_sec")
+ private Double timestampDelaySec;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/domain/mainpage/TaskStatus.java b/src/main/java/com/aims/backend/domain/mainpage/TaskStatus.java
deleted file mode 100644
index 687be62..0000000
--- a/src/main/java/com/aims/backend/domain/mainpage/TaskStatus.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.aims.backend.domain.mainpage;
-
-public enum TaskStatus {
- TODO,
- PROGRESS,
- DONE
-}
diff --git a/src/main/java/com/aims/backend/domain/mainpage/UserTask.java b/src/main/java/com/aims/backend/domain/mainpage/UserTask.java
index 407252a..97af479 100644
--- a/src/main/java/com/aims/backend/domain/mainpage/UserTask.java
+++ b/src/main/java/com/aims/backend/domain/mainpage/UserTask.java
@@ -35,9 +35,6 @@ public class UserTask extends BaseEntity {
@Column(name = "task_title", nullable = false, length = 255)
private String taskTitle;
- @Enumerated(EnumType.STRING)
- @Column(name = "task_status", nullable = false, length = 20)
- private TaskStatus taskStatus;
@Column(name = "scheduled_at", nullable = false)
private LocalDateTime scheduledAt;
diff --git a/src/main/java/com/aims/backend/dto/alert/ActionTimelineCreateRequest.java b/src/main/java/com/aims/backend/dto/alert/ActionTimelineCreateRequest.java
new file mode 100644
index 0000000..2f821d8
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/alert/ActionTimelineCreateRequest.java
@@ -0,0 +1,33 @@
+package com.aims.backend.dto.alert;
+
+import com.aims.backend.domain.alert.ActionCategory;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ActionTimelineCreateRequest {
+
+ @NotNull(message = "조치 시간은 필수 입력 값입니다.")
+ private LocalDateTime actionTime;
+
+ @NotBlank(message = "조치 내용은 필수 입력 값입니다.")
+ @Size(max = 20, message = "조치 내용은 최대 20자까지 입력 가능합니다.")
+ private String actionContent;
+
+ @NotNull(message = "조치 카테고리는 필수 입력 값입니다.")
+ private ActionCategory actionCategory;
+
+ @NotBlank(message = "조치 결과는 필수 입력 값입니다.")
+ @Size(max = 20, message = "조치 결과는 최대 20자까지 입력 가능합니다.")
+ private String actionResult;
+}
diff --git a/src/main/java/com/aims/backend/dto/alert/ActionTimelineResponse.java b/src/main/java/com/aims/backend/dto/alert/ActionTimelineResponse.java
new file mode 100644
index 0000000..45af99d
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/alert/ActionTimelineResponse.java
@@ -0,0 +1,36 @@
+package com.aims.backend.dto.alert;
+
+import java.time.LocalDateTime;
+
+import com.aims.backend.domain.alert.ActionCategory;
+import com.aims.backend.domain.alert.ActionTimeline;
+import com.aims.backend.domain.user.UserRole;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public class ActionTimelineResponse {
+ private Long actionId;
+ private String empNo;
+ private String empName;
+ private UserRole empRole;
+ private LocalDateTime actionTime;
+ private String actionContent;
+ private ActionCategory actionCategory;
+ private String actionResult;
+
+ public static ActionTimelineResponse from(ActionTimeline entity) {
+ return ActionTimelineResponse.builder()
+ .actionId(entity.getActionId())
+ .empNo(entity.getEmpNo())
+ .empName(entity.getEmpName())
+ .empRole(entity.getEmpRole())
+ .actionTime(entity.getActionTime())
+ .actionContent(entity.getActionContent())
+ .actionCategory(entity.getActionCategory())
+ .actionResult(entity.getActionResult())
+ .build();
+ }
+}
diff --git a/src/main/java/com/aims/backend/dto/alert/AlertEventResponse.java b/src/main/java/com/aims/backend/dto/alert/AlertEventResponse.java
index 42aedc6..ceb4a24 100644
--- a/src/main/java/com/aims/backend/dto/alert/AlertEventResponse.java
+++ b/src/main/java/com/aims/backend/dto/alert/AlertEventResponse.java
@@ -30,6 +30,7 @@ public class AlertEventResponse {
private String actionStatus;
private String reason;
private LocalDateTime scoreCalculatedAt;
+ private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime resolvedAt;
@@ -52,6 +53,7 @@ public static AlertEventResponse from(AlertEvent alertEvent) {
.actionStatus(alertEvent.getActionStatus() == null ? null : alertEvent.getActionStatus().name())
.reason(alertEvent.getReason())
.scoreCalculatedAt(alertEvent.getScoreCalculatedAt())
+ .imageUrl(alertEvent.getImageUrl())
.createdAt(alertEvent.getCreatedAt())
.resolvedAt(alertEvent.getResolvedAt())
.build();
diff --git a/src/main/java/com/aims/backend/dto/alert/AlertPrioritySummaryResponse.java b/src/main/java/com/aims/backend/dto/alert/AlertPrioritySummaryResponse.java
new file mode 100644
index 0000000..42561a8
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/alert/AlertPrioritySummaryResponse.java
@@ -0,0 +1,18 @@
+package com.aims.backend.dto.alert;
+
+import java.math.BigDecimal;
+
+public record AlertPrioritySummaryResponse(
+
+ int periodDays,
+
+ BigDecimal averagePriorityScore,
+
+ BigDecimal averageRiskScore,
+
+ BigDecimal averageOccurrencePercentage,
+
+ BigDecimal actionCompletionRate
+
+) {
+}
diff --git a/src/main/java/com/aims/backend/dto/dashboard/AgvArrivalRequest.java b/src/main/java/com/aims/backend/dto/dashboard/AgvArrivalRequest.java
new file mode 100644
index 0000000..2143a6c
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/dashboard/AgvArrivalRequest.java
@@ -0,0 +1,17 @@
+package com.aims.backend.dto.dashboard;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AgvArrivalRequest {
+
+ private String eventId;
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/dashboard/AgvOperationResponse.java b/src/main/java/com/aims/backend/dto/dashboard/AgvOperationResponse.java
index c849fe5..b9affb2 100644
--- a/src/main/java/com/aims/backend/dto/dashboard/AgvOperationResponse.java
+++ b/src/main/java/com/aims/backend/dto/dashboard/AgvOperationResponse.java
@@ -1,17 +1,38 @@
package com.aims.backend.dto.dashboard;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.Instant;
import java.time.LocalDateTime;
-public record AgvOperationResponse(
- Long id,
- Long carMasterId,
- String agvStatus,
- String currentProcess,
- String targetProcess,
- Double progressRate,
- Integer delaySeconds,
- String routeCode,
- Integer laneNo,
- LocalDateTime updatedAt
-) {
+@Getter
+@AllArgsConstructor
+public class AgvOperationResponse {
+
+ private Long agvId;
+
+ private String eventId;
+
+ private Long carMasterId;
+
+ private String agvStatus;
+
+ private String currentProcess;
+
+ private String targetProcess;
+
+ private Double progressRate;
+
+ private Integer delaySeconds;
+
+ private Instant startedAt;
+
+ private Instant expectedArrivalTime;
+
+ private String routeCode;
+
+ private Integer laneNo;
+
+ private LocalDateTime updatedAt;
}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/dashboard/AgvRealtimeState.java b/src/main/java/com/aims/backend/dto/dashboard/AgvRealtimeState.java
index db5f53f..50dfd70 100644
--- a/src/main/java/com/aims/backend/dto/dashboard/AgvRealtimeState.java
+++ b/src/main/java/com/aims/backend/dto/dashboard/AgvRealtimeState.java
@@ -1,7 +1,13 @@
package com.aims.backend.dto.dashboard;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Duration;
+import java.time.Instant;
@Getter
@Setter
@@ -12,17 +18,65 @@ public class AgvRealtimeState {
private Long agvId;
+ private String eventId;
+
+ private Long carMasterId;
+
+ /**
+ * MOVING / RETURNING
+ */
+ private String status;
+
+ private String currentProcess;
+
+ private String targetProcess;
+
+ /**
+ * 누적 증가값이 아니라 startedAt, expectedArrivalTime 기준으로 계산된 값
+ */
private Double progressRate;
private Integer delaySeconds;
- public void increaseProgress(double amount) {
- double current = progressRate == null ? 0.0 : progressRate;
- this.progressRate = Math.min(100.0, current + amount);
+ private Instant startedAt;
+
+ private Instant expectedArrivalTime;
+
+ public static AgvRealtimeState empty(Long agvId) {
+ return AgvRealtimeState.builder()
+ .agvId(agvId)
+ .progressRate(0.0)
+ .delaySeconds(0)
+ .build();
}
- @JsonIgnore
- public boolean isArrived() {
- return progressRate != null && progressRate >= 100.0;
+ public void calculateProgress(Instant now) {
+ if (startedAt == null || expectedArrivalTime == null) {
+ this.progressRate = 0.0;
+ return;
+ }
+
+ long totalMillis = Duration.between(
+ startedAt,
+ expectedArrivalTime
+ ).toMillis();
+
+ long elapsedMillis = Duration.between(
+ startedAt,
+ now
+ ).toMillis();
+
+ if (totalMillis <= 0) {
+ this.progressRate = 100.0;
+ return;
+ }
+
+ double progress =
+ ((double) elapsedMillis / totalMillis) * 100.0;
+
+ this.progressRate = Math.max(
+ 0.0,
+ Math.min(100.0, progress)
+ );
}
}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/dashboard/DispatchRequest.java b/src/main/java/com/aims/backend/dto/dashboard/DispatchRequest.java
new file mode 100644
index 0000000..02e7d26
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/dashboard/DispatchRequest.java
@@ -0,0 +1,10 @@
+package com.aims.backend.dto.dashboard;
+
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+
+public record DispatchRequest(
+ String eventId,
+ Long carMasterId,
+ ProcessCode processCode
+) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/ActionTimelineResponse.java b/src/main/java/com/aims/backend/dto/eventAnalysis/ActionTimelineResponse.java
new file mode 100644
index 0000000..784ed8e
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/ActionTimelineResponse.java
@@ -0,0 +1,33 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import com.aims.backend.domain.alert.ActionCategory;
+import com.aims.backend.domain.user.UserRole;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ActionTimelineResponse {
+
+ private Long actionId;
+
+ private LocalDateTime actionTime;
+
+ private String empNo;
+
+ private String empName;
+
+ private UserRole empRole;
+
+ private ActionCategory actionCategory;
+
+ private String actionContent;
+
+ private String actionResult;
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/AssemblyAnalysisResultDto.java b/src/main/java/com/aims/backend/dto/eventAnalysis/AssemblyAnalysisResultDto.java
new file mode 100644
index 0000000..1ba988d
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/AssemblyAnalysisResultDto.java
@@ -0,0 +1,31 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AssemblyAnalysisResultDto {
+
+ private Long analysisResultId;
+
+ private String expectedSequence;
+
+ private String actualSequence;
+
+ private Integer sequenceErrorCount;
+
+ private Integer missingPartCount;
+
+ private Integer fasteningErrorCount;
+
+ private LocalDateTime createdAt;
+
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/BodyAnalysisResultDto.java b/src/main/java/com/aims/backend/dto/eventAnalysis/BodyAnalysisResultDto.java
new file mode 100644
index 0000000..6fb81c5
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/BodyAnalysisResultDto.java
@@ -0,0 +1,31 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BodyAnalysisResultDto {
+
+ private Long analysisResultId;
+
+ private String robotMotionStatus;
+
+ private String robotOperationMode;
+
+ private Double robotVibrationScore;
+
+ private String frequencyPeakBand;
+
+ private Double frequencyPeakValue;
+
+ private LocalDateTime createdAt;
+
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/ManufacturingAnalysisResultDto.java b/src/main/java/com/aims/backend/dto/eventAnalysis/ManufacturingAnalysisResultDto.java
new file mode 100644
index 0000000..d30bfe3
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/ManufacturingAnalysisResultDto.java
@@ -0,0 +1,44 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ManufacturingAnalysisResultDto {
+
+ private Long id;
+
+ private String eventId;
+
+ private Long carMasterId;
+
+ private Long equipmentId;
+
+ private ProcessCode processCode;
+
+ private LocalDateTime eventTime;
+
+ private Integer isAbnormal;
+
+ private String abnormalType;
+
+ private String severity;
+
+ private Double riskScore;
+
+ private String analysisMessage;
+
+ private LocalDateTime analyzedAt;
+
+ private LocalDateTime createdAt;
+
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/PaintAnalysisResultDto.java b/src/main/java/com/aims/backend/dto/eventAnalysis/PaintAnalysisResultDto.java
new file mode 100644
index 0000000..b820e58
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/PaintAnalysisResultDto.java
@@ -0,0 +1,33 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PaintAnalysisResultDto {
+
+ private Long analysisResultId;
+
+ private String imagePosition;
+
+ private Double thermalStdTemp;
+
+ private Double thicknessValue;
+
+ private Double defeatScore;
+
+ private String visionLabel;
+
+ private Double surfaceQualityScore;
+
+ private LocalDateTime createdAt;
+
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/PressAnalysisResultDto.java b/src/main/java/com/aims/backend/dto/eventAnalysis/PressAnalysisResultDto.java
new file mode 100644
index 0000000..28c7749
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/PressAnalysisResultDto.java
@@ -0,0 +1,31 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PressAnalysisResultDto {
+
+ private Long analysisResultId;
+
+ private Integer countIncreaseYn;
+
+ private Double targetCycleTimeSec;
+
+ private Double actualCycleTimeSec;
+
+ private Double cycleTimeGapSec;
+
+ private Double timestampDelaySec;
+
+ private LocalDateTime createdAt;
+
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/RecommendationResponse.java b/src/main/java/com/aims/backend/dto/eventAnalysis/RecommendationResponse.java
new file mode 100644
index 0000000..ede5d14
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/RecommendationResponse.java
@@ -0,0 +1,28 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RecommendationResponse {
+
+ private String similarLogNo;
+
+ private Double confidence;
+
+ private String handler;
+
+ private String recommendedAction;
+
+ private String recommendationReason;
+
+ private List actionTimeline;
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/eventAnalysis/SimilarResult.java b/src/main/java/com/aims/backend/dto/eventAnalysis/SimilarResult.java
new file mode 100644
index 0000000..8945213
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/eventAnalysis/SimilarResult.java
@@ -0,0 +1,14 @@
+package com.aims.backend.dto.eventAnalysis;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Builder;
+
+@Builder
+@Getter
+@AllArgsConstructor
+public class SimilarResult {
+ private String similarLogNo;
+ private double confidence;
+ private String logNo;
+}
diff --git a/src/main/java/com/aims/backend/dto/kafka/ManufacturingAnalysisEvent.java b/src/main/java/com/aims/backend/dto/kafka/ManufacturingAnalysisEvent.java
new file mode 100644
index 0000000..0aa6756
--- /dev/null
+++ b/src/main/java/com/aims/backend/dto/kafka/ManufacturingAnalysisEvent.java
@@ -0,0 +1,39 @@
+package com.aims.backend.dto.kafka;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record ManufacturingAnalysisEvent(
+
+ String analysisId,
+
+ String eventId,
+
+ Long carMasterId,
+
+ String processCode,
+
+ String riskLevel,
+
+ String analysisType,
+
+ AnalysisResult analysisResult
+
+) {
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record AnalysisResult(
+
+ boolean isAbnormal,
+
+ boolean isBottleneck,
+
+ boolean isQualityDefect,
+
+ boolean isEquipmentFault,
+
+ boolean isSequenceError
+
+ ) {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/dto/mainpage/UserTaskResponse.java b/src/main/java/com/aims/backend/dto/mainpage/UserTaskResponse.java
index 19d8b1f..7c6ecf7 100644
--- a/src/main/java/com/aims/backend/dto/mainpage/UserTaskResponse.java
+++ b/src/main/java/com/aims/backend/dto/mainpage/UserTaskResponse.java
@@ -1,6 +1,5 @@
package com.aims.backend.dto.mainpage;
-import com.aims.backend.domain.mainpage.TaskStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -21,10 +20,7 @@ public static class MainPageTaskDTO {
@Schema(description = "업무명", example = "프레스 라인 점검")
private String taskTitle;
- @Schema(description = "업무 상태", example = "TODO")
- private TaskStatus taskStatus;
-
@Schema(description = "업무 시간", example = "2026-06-11T09:00:00")
- private LocalDateTime scheduledAt;
+ private String scheduledAt;
}
}
diff --git a/src/main/java/com/aims/backend/repository/alert/ActionTimelineRepository.java b/src/main/java/com/aims/backend/repository/alert/ActionTimelineRepository.java
new file mode 100644
index 0000000..80515c8
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/alert/ActionTimelineRepository.java
@@ -0,0 +1,13 @@
+package com.aims.backend.repository.alert;
+
+import org.springframework.stereotype.Repository;
+
+import com.aims.backend.domain.alert.ActionTimeline;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+
+@Repository
+public interface ActionTimelineRepository extends JpaRepository {
+ List findByLogNoOrderByActionTimeAsc(String logNo);
+}
diff --git a/src/main/java/com/aims/backend/repository/alert/AlertEventRepository.java b/src/main/java/com/aims/backend/repository/alert/AlertEventRepository.java
index eabc340..299822c 100644
--- a/src/main/java/com/aims/backend/repository/alert/AlertEventRepository.java
+++ b/src/main/java/com/aims/backend/repository/alert/AlertEventRepository.java
@@ -7,11 +7,31 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;
public interface AlertEventRepository extends JpaRepository, JpaSpecificationExecutor {
+ interface PrioritySummaryProjection {
+
+ long getTotalCount();
+
+ BigDecimal getPriorityScoreSum();
+
+ long getPriorityScoreCount();
+
+ BigDecimal getRiskScoreSum();
+
+ long getRiskScoreCount();
+
+ BigDecimal getOccurrenceScoreSum();
+
+ long getOccurrenceScoreCount();
+
+ long getCompletedCount();
+ }
+
boolean existsByEventId(String eventId);
Optional findByEventId(String eventId);
@@ -39,4 +59,24 @@ long countByEventKeyAndActionStatus(
String eventKey,
AlertActionStatus actionStatus
);
+
+ @Query("""
+ SELECT COUNT(e) AS totalCount,
+ SUM(CASE WHEN e.actionStatus = :incompleteStatus THEN e.priorityScore ELSE 0 END) AS priorityScoreSum,
+ COUNT(CASE WHEN e.actionStatus = :incompleteStatus THEN e.priorityScore ELSE NULL END) AS priorityScoreCount,
+ SUM(CASE WHEN e.actionStatus = :incompleteStatus THEN e.riskScore ELSE 0 END) AS riskScoreSum,
+ COUNT(CASE WHEN e.actionStatus = :incompleteStatus THEN e.riskScore ELSE NULL END) AS riskScoreCount,
+ SUM(CASE WHEN e.actionStatus = :incompleteStatus THEN e.occurrenceScore ELSE 0 END) AS occurrenceScoreSum,
+ COUNT(CASE WHEN e.actionStatus = :incompleteStatus THEN e.occurrenceScore ELSE NULL END) AS occurrenceScoreCount,
+ SUM(CASE WHEN e.actionStatus = :completedStatus THEN 1 ELSE 0 END) AS completedCount
+ FROM AlertEvent e
+ WHERE e.createdAt >= :from
+ AND e.createdAt < :to
+ """)
+ PrioritySummaryProjection findPrioritySummary(
+ @Param("from") LocalDateTime from,
+ @Param("to") LocalDateTime to,
+ @Param("incompleteStatus") AlertActionStatus incompleteStatus,
+ @Param("completedStatus") AlertActionStatus completedStatus
+ );
}
diff --git a/src/main/java/com/aims/backend/repository/dashboard/AgvOperationRepository.java b/src/main/java/com/aims/backend/repository/dashboard/AgvOperationRepository.java
index 8e32541..3439683 100644
--- a/src/main/java/com/aims/backend/repository/dashboard/AgvOperationRepository.java
+++ b/src/main/java/com/aims/backend/repository/dashboard/AgvOperationRepository.java
@@ -2,11 +2,9 @@
import com.aims.backend.domain.dashboard.AgvOperation;
import com.aims.backend.domain.dashboard.enums.AgvStatus;
-import com.aims.backend.dto.dashboard.AgvStatusCountResponse;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
-
-import java.util.List;
+import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
@@ -21,4 +19,21 @@ Optional findFirstByRouteCodeAndAgvStatusOrderByLaneNoAsc(
String routeCode,
AgvStatus agvStatus
);
-}
\ No newline at end of file
+
+ @Query(
+ value = """
+ SELECT *
+ FROM agv_operation
+ WHERE route_code = :routeCode
+ AND agv_status = :agvStatus
+ ORDER BY lane_no ASC
+ LIMIT 1
+ FOR UPDATE SKIP LOCKED
+ """,
+ nativeQuery = true
+ )
+ Optional findFirstWaitingAgvForUpdateSkipLocked(
+ @Param("routeCode") String routeCode,
+ @Param("agvStatus") String agvStatus
+ );
+}
diff --git a/src/main/java/com/aims/backend/repository/eventAnalysis/AssemblyAnalysisResultRepository.java b/src/main/java/com/aims/backend/repository/eventAnalysis/AssemblyAnalysisResultRepository.java
new file mode 100644
index 0000000..235e490
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/eventAnalysis/AssemblyAnalysisResultRepository.java
@@ -0,0 +1,31 @@
+package com.aims.backend.repository.eventAnalysis;
+
+import com.aims.backend.domain.eventAnalysis.AssemblyAnalysisResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.List;
+
+public interface AssemblyAnalysisResultRepository
+ extends JpaRepository {
+
+ Optional findByAnalysisResultId(Long id);
+
+ List findAll();
+
+ @Query("""
+ SELECT a
+ FROM AssemblyAnalysisResult a
+ WHERE a.analysisResultId <> :currentId
+ ORDER BY ABS(a.sequenceErrorCount - :sequenceErrorCount)
+ """)
+ List findTop20Similar(
+ @Param("currentId") Long currentId,
+ @Param("sequenceErrorCount") Integer sequenceErrorCount,
+ Pageable pageable
+ );
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/repository/eventAnalysis/BodyAnalysisResultRepository.java b/src/main/java/com/aims/backend/repository/eventAnalysis/BodyAnalysisResultRepository.java
new file mode 100644
index 0000000..096c62e
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/eventAnalysis/BodyAnalysisResultRepository.java
@@ -0,0 +1,30 @@
+package com.aims.backend.repository.eventAnalysis;
+
+import com.aims.backend.domain.eventAnalysis.BodyAnalysisResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.List;
+
+public interface BodyAnalysisResultRepository
+ extends JpaRepository {
+
+ Optional findByAnalysisResultId(Long id);
+
+ List findAll();
+
+ @Query("""
+ SELECT b
+ FROM BodyAnalysisResult b
+ WHERE b.analysisResultId <> :currentId
+ ORDER BY ABS(b.robotVibrationScore - :robotVibrationScore)
+ """)
+ List findTop20Similar(
+ @Param("currentId") Long currentId,
+ @Param("robotVibrationScore") Double robotVibrationScore,
+ Pageable pageable
+ );
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/repository/eventAnalysis/ManufacturingAnalysisResultRepository.java b/src/main/java/com/aims/backend/repository/eventAnalysis/ManufacturingAnalysisResultRepository.java
new file mode 100644
index 0000000..5b53a94
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/eventAnalysis/ManufacturingAnalysisResultRepository.java
@@ -0,0 +1,20 @@
+package com.aims.backend.repository.eventAnalysis;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.aims.backend.domain.dashboard.enums.ProcessCode;
+import com.aims.backend.domain.eventAnalysis.ManufacturingAnalysisResult;
+
+import java.util.Optional;
+import java.util.List;
+
+public interface ManufacturingAnalysisResultRepository
+ extends JpaRepository {
+
+ Optional findByEventId(String eventId);
+
+ Optional findById(Long id);
+
+ List findByProcessCode(ProcessCode processCode);
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/repository/eventAnalysis/PaintAnalysisResultRepository.java b/src/main/java/com/aims/backend/repository/eventAnalysis/PaintAnalysisResultRepository.java
new file mode 100644
index 0000000..7ba6362
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/eventAnalysis/PaintAnalysisResultRepository.java
@@ -0,0 +1,31 @@
+package com.aims.backend.repository.eventAnalysis;
+
+import com.aims.backend.domain.eventAnalysis.PaintAnalysisResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.List;
+
+public interface PaintAnalysisResultRepository
+ extends JpaRepository {
+ Optional findByAnalysisResultId(Long id);
+
+ List findAll();
+
+ @Query("""
+ SELECT p
+ FROM PaintAnalysisResult p
+ WHERE p.analysisResultId <> :currentId
+ AND p.visionLabel = :visionLabel
+ ORDER BY ABS(p.surfaceQualityScore - :surfaceQualityScore)
+ """)
+ List findTop20Similar(
+ @Param("currentId") Long currentId,
+ @Param("visionLabel") String visionLabel,
+ @Param("surfaceQualityScore") Double surfaceQualityScore,
+ Pageable pageable
+ );
+}
\ No newline at end of file
diff --git a/src/main/java/com/aims/backend/repository/eventAnalysis/PressAnalysisResultRepository.java b/src/main/java/com/aims/backend/repository/eventAnalysis/PressAnalysisResultRepository.java
new file mode 100644
index 0000000..72e5a1c
--- /dev/null
+++ b/src/main/java/com/aims/backend/repository/eventAnalysis/PressAnalysisResultRepository.java
@@ -0,0 +1,36 @@
+package com.aims.backend.repository.eventAnalysis;
+
+import com.aims.backend.domain.eventAnalysis.PressAnalysisResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.List;
+
+
+public interface PressAnalysisResultRepository
+ extends JpaRepository {
+
+ Optional findByAnalysisResultId(Long id);
+
+ List findAll();
+
+ @Query("""
+ SELECT p
+ FROM PressAnalysisResult p
+ WHERE p.analysisResultId <> :currentId
+ AND p.countIncreaseYn = :countIncreaseYn
+ ORDER BY
+ ABS(p.cycleTimeGapSec - :cycleGap),
+ ABS(COALESCE(p.timestampDelaySec, 0.0) - COALESCE(:timestampDelay, 0.0))
+ """)
+ List findMostSimilar(
+ @Param("currentId") Long currentId,
+ @Param("countIncreaseYn") Integer countIncreaseYn,
+ @Param("cycleGap") Double cycleGap,
+ @Param("timestampDelay") Double timestampDelay,
+ Pageable pageable
+ );
+}
diff --git a/src/main/java/com/aims/backend/repository/sample/EquipmentRepository.java b/src/main/java/com/aims/backend/repository/sample/EquipmentRepository.java
index f9e1445..afa8c1f 100644
--- a/src/main/java/com/aims/backend/repository/sample/EquipmentRepository.java
+++ b/src/main/java/com/aims/backend/repository/sample/EquipmentRepository.java
@@ -14,7 +14,7 @@ public interface EquipmentRepository extends JpaRepository {
@Query("SELECT e.currentStatus, COUNT(e) FROM Equipment e GROUP BY e.currentStatus")
List