Files
kamco-train-api/상태전의 STATUS 가이드.md
2026-04-06 21:32:24 +09:00

34 KiB

POST /api/train/status/{uuid} - 상태 전이 다이어그램 완전 가이드

프론트엔드 표시 방법 및 상태 제어 API 포함


📊 1. DB 테이블 상태 필드

1.1 Job 테이블 (tb_model_train_job)

필드명 설명 가능한 값 UI 표시명
status_cd Job 실행 상태 QUEUED, RUNNING, SUCCESS, FAILED, STOPPED, CANCELED 대기중, 실행중, 성공, 실패, 중단됨, 취소
exit_code 종료 코드 정수 (0: 정상, -1: 중단, 기타: 에러) -
error_message 에러 메시지 문자열 에러 상세 내용
finished_dttm 완료 시간 timestamp YYYY-MM-DD HH:mm:ss

1.2 Model Master 테이블 (tb_model_master)

필드명 설명 가능한 값 UI 표시명
status_cd 전체 모델 상태 READY, IN_PROGRESS, COMPLETED, STOPPED, ERROR 대기, 진행중, 완료, 중단됨, 오류
step1_state Step1(학습) 상태 READY, IN_PROGRESS, COMPLETED, STOPPED, ERROR 대기, 진행중, 완료, 중단됨, 오류
step2_state Step2(테스트) 상태 READY, IN_PROGRESS, COMPLETED, STOPPED, ERROR 대기, 진행중, 완료, 중단됨, 오류
step1_strt_dttm Step1 시작 시간 timestamp YYYY-MM-DD HH:mm:ss
step1_end_dttm Step1 종료 시간 timestamp YYYY-MM-DD HH:mm:ss
step2_strt_dttm Step2 시작 시간 timestamp YYYY-MM-DD HH:mm:ss
step2_end_dttm Step2 종료 시간 timestamp YYYY-MM-DD HH:mm:ss
step1_metric_save_yn Step1 메트릭 저장 여부 true, false -
step2_metric_save_yn Step2 메트릭 저장 여부 true, false -
current_attempt_id 현재 실행 중인 Job ID Long -
best_epoch 최적 에폭 Integer Best Epoch: {값}
last_error 마지막 에러 메시지 문자열 에러 상세 내용

🎨 2. 프론트엔드 API 응답 및 표시

2.1 모델 목록 조회 API

엔드포인트: GET /api/models/list

요청 파라미터:

{
  "status": "IN_PROGRESS",  // "", "IN_PROGRESS", "COMPLETED"
  "modelNo": "G1",           // "G1", "G2", "G3", "G4"
  "page": 0,
  "size": 20
}

응답 예시:

{
  "success": true,
  "data": {
    "content": [
      {
        "id": 123,
        "uuid": "e22181eb-2ac4-4100-9941-d06efce25c49",
        "modelVer": "v1.0.0",
        "statusCd": "IN_PROGRESS",
        "statusName": "진행중",
        "step1Status": "COMPLETED",
        "step1StatusName": "완료",
        "step2Status": "IN_PROGRESS",
        "step2StatusName": "진행중",
        "step1StrtDttm": "2026-04-05 10:30:00",
        "step1EndDttm": "2026-04-05 12:45:00",
        "step1Duration": "2시간 15분 0초",
        "step2StrtDttm": "2026-04-05 12:50:00",
        "step2EndDttm": null,
        "step2Duration": null,
        "modelNo": "G1",
        "trainType": "GENERAL",
        "trainTypeName": "일반",
        "currentAttemptId": 456,
        "memo": "테스트 학습"
      }
    ],
    "totalElements": 100,
    "totalPages": 5
  }
}

2.2 프론트엔드 표시 예시

📋 모델 목록 테이블

모델 번호 버전 전체 상태 학습(Step1) 테스트(Step2) 학습 시간 테스트 시간 작업
G1 v1.0.0 🟢 진행중 완료 (2시간 15분) 🔄 진행중 2026-04-05 10:30 ~ 12:45 2026-04-05 12:50 ~ [취소]
G2 v1.0.1 완료 완료 (1시간 30분) 완료 (45분) 2026-04-04 14:00 ~ 15:30 2026-04-04 15:35 ~ 16:20 [결과보기]
G3 v1.0.2 ⚠️ 중단됨 ⚠️ 중단됨 - 2026-04-03 09:00 ~ - [재시작] [이어하기]
G4 v1.0.3 오류 오류 - 2026-04-02 11:00 ~ - [재시작]

🎯 상태별 UI 컬러 가이드

// 상태별 배지 색상 매핑
const STATUS_COLORS = {
  'READY': { bg: '#e3f2fd', text: '#1976d2', icon: '⏸️' },        // 파란색 - 대기
  'IN_PROGRESS': { bg: '#e8f5e9', text: '#388e3c', icon: '🔄' },  // 녹색 - 진행중
  'COMPLETED': { bg: '#f1f8e9', text: '#689f38', icon: '✅' },    // 연두색 - 완료
  'STOPPED': { bg: '#fff3e0', text: '#f57c00', icon: '⚠️' },      // 주황색 - 중단됨
  'ERROR': { bg: '#ffebee', text: '#d32f2f', icon: '❌' }         // 빨간색 - 오류
};

// 상태명 한글 변환
const STATUS_NAMES = {
  'READY': '대기',
  'IN_PROGRESS': '진행중',
  'COMPLETED': '완료',
  'STOPPED': '중단됨',
  'ERROR': '오류'
};

// React 컴포넌트 예시
function StatusBadge({ statusCd }) {
  const { bg, text, icon } = STATUS_COLORS[statusCd] || {};
  const name = STATUS_NAMES[statusCd] || statusCd;
  
  return (
    <span style={{ backgroundColor: bg, color: text, padding: '4px 12px', borderRadius: '12px' }}>
      {icon} {name}
    </span>
  );
}

📊 상세 진행 상황 표시

// Step별 진행률 계산
function ModelProgressBar({ model }) {
  const steps = [
    {
      name: '학습 (Step1)',
      status: model.step1Status,
      statusName: model.step1StatusName,
      startTime: model.step1StrtDttm,
      endTime: model.step1EndDttm,
      duration: model.step1Duration
    },
    {
      name: '테스트 (Step2)',
      status: model.step2Status,
      statusName: model.step2StatusName,
      startTime: model.step2StrtDttm,
      endTime: model.step2EndDttm,
      duration: model.step2Duration
    }
  ];

  return (
    <div className="progress-container">
      {steps.map((step, index) => (
        <div key={index} className="step-item">
          <div className="step-header">
            <span className="step-name">{step.name}</span>
            <StatusBadge statusCd={step.status} />
          </div>
          <div className="step-time">
            {step.startTime && (
              <>
                <span>시작: {step.startTime}</span>
                {step.endTime && <span> ~ 종료: {step.endTime}</span>}
                {step.duration && <span> (소요시간: {step.duration})</span>}
              </>
            )}
          </div>
          {step.status === 'IN_PROGRESS' && (
            <div className="loading-bar">
              <div className="loading-animation"></div>
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

🔄 3. 상태 전이 플로우차트 (상태값 명시)

┌─────────────────────────────────────────────────────────────────┐
│          POST /api/train/status/{uuid} API 호출                  │
└────────────────────────────┬────────────────────────────────────┘
                             ↓
                    ┌────────────────────┐
                    │  UUID → ModelID    │
                    │  조회 및 Job 조회   │
                    └────────┬───────────┘
                             ↓
                    ┌────────────────────┐
                    │ Docker Inspect 실행 │
                    │ (containerName)     │
                    └────────┬───────────┘
                             ↓
              ┌──────────────┴──────────────┐
              │                             │
         [exists=false]                [exists=true]
         컨테이너 없음                   컨테이너 존재
              ↓                             ↓
    ┌─────────────────────┐      ┌──────────────────────┐
    │ TrainUtilService    │      │   jobType 확인        │
    │ .probeOutputs()     │      │   (paramsJson)        │
    │                     │      └──────────┬───────────┘
    │ 1. total_epoch 추출 │                 │
    │ 2. valInterval 추출 │      ┌──────────┴──────────┐
    │ 3. val.csv 존재확인 │      │                     │
    │ 4. 라인수 검증       │   [TRAIN]              [EVAL]
    └─────────┬───────────┘      │                     │
              │                  ↓                     ↓
    ┌─────────┴─────────┐   ┌──────────────┐   ┌──────────────┐
    │                   │   │ Step1 진행중  │   │ Step2 진행중  │
 [completed=true]   [completed=false]  └──────────────┘   └──────────────┘
    │                   │        │                     │
    │                   │   ┌────▼─────────────────────▼────────┐
    │                   │   │ ✅ tb_model_train_job             │
    │                   │   │   status_cd = "RUNNING"           │
    │                   │   │                                   │
    │                   │   │ ✅ tb_model_master                │
    │                   │   │   status_cd = "IN_PROGRESS"      │
    │                   │   │   step1_state = "IN_PROGRESS"    │ [TRAIN]
    │                   │   │   step1_strt_dttm = now()        │
    │                   │   │   OR                             │
    │                   │   │   step2_state = "IN_PROGRESS"    │ [EVAL]
    │                   │   │   step2_strt_dttm = now()        │
    │                   │   │   current_attempt_id = jobId     │
    │                   │   └──────────────────────────────────┘
    │                   │   【프론트 표시】
    │                   │   🔄 진행중 - 학습 중... / 테스트 중...
    │                   │
    │                   ↓
    │          ┌─────────────────────┐
    │          │ ⚠️  산출물 부족       │
    │          │ (val.csv 부족 등)    │
    │          └─────────┬───────────┘
    │                    │
    │          ┌─────────▼────────────────────────────────┐
    │          │ ⚠️ tb_model_train_job                    │
    │          │   status_cd = "STOPPED"                  │
    │          │   exit_code = -1                         │
    │          │   error_message = "SERVER_RESTART_..."   │
    │          │                                          │
    │          │ ⚠️ tb_model_master                       │
    │          │   status_cd = "STOPPED"                  │
    │          │   step1_state = "STOPPED" [TRAIN]        │
    │          │   OR                                     │
    │          │   step2_state = "STOPPED" [EVAL]         │
    │          │   last_error = "컨테이너 없음, 산출물 부족" │
    │          └──────────────────────────────────────────┘
    │          【프론트 표시】
    │          ⚠️ 중단됨 - 산출물 부족으로 중단됨
    │          [재시작] [이어하기] 버튼 활성화
    │
    ↓
┌──────────────────┐
│ existsZipFile()  │
│ ZIP 파일 존재?    │
└────┬────────┬────┘
     │        │
  [YES]     [NO]
     │        │
     │        ↓
     │   ┌──────────────────────────────────────────────┐
     │   │ 📁 학습 완료 (Step1만)                        │
     │   └─────────┬────────────────────────────────────┘
     │             │
     │   ┌─────────▼──────────────────────────────────┐
     │   │ ✅ tb_model_train_job                      │
     │   │   status_cd = "SUCCESS"                    │
     │   │   exit_code = 0                            │
     │   │   finished_dttm = now()                    │
     │   │                                            │
     │   │ ✅ tb_model_master (Step1 미완료 시)        │
     │   │   status_cd = "COMPLETED"                  │
     │   │   step1_state = "COMPLETED"                │
     │   │   step1_end_dttm = now()                   │
     │   │   step1_metric_save_yn = true              │
     │   │                                            │
     │   │ 📊 Metrics 저장                             │
     │   │   - train.csv → tb_model_metrics_train    │
     │   │   - val.csv → tb_model_metrics_validation │
     │   └────────────────────────────────────────────┘
     │   【프론트 표시】
     │   ✅ 완료 - 학습 완료 (테스트 대기)
     │   [테스트 실행] 버튼 활성화
     │
     ↓
┌──────────────────────────────────────────────┐
│ 📦 학습+테스트 완료 (Step1 + Step2)            │
└─────────┬────────────────────────────────────┘
          │
┌─────────▼──────────────────────────────────┐
│ ✅ tb_model_train_job                      │
│   status_cd = "SUCCESS"                    │
│   exit_code = 0                            │
│   finished_dttm = now()                    │
│                                            │
│ ✅ tb_model_master (Step1 미완료 시)        │
│   status_cd = "COMPLETED"                  │
│   step1_state = "COMPLETED"                │
│   step1_end_dttm = now()                   │
│   step1_metric_save_yn = true              │
│                                            │
│ ✅ tb_model_master (Step2 미완료 시)        │
│   step2_state = "COMPLETED"                │
│   step2_end_dttm = now()                   │
│   step2_metric_save_yn = true              │
│   best_epoch = 최적값                       │
│                                            │
│ 📊 Metrics 저장                             │
│   - train.csv → tb_model_metrics_train    │
│   - val.csv → tb_model_metrics_validation │
│   - test.csv → 테스트 메트릭 테이블           │
│   - *.zip → 테스트 결과 파일 생성             │
└────────────────────────────────────────────┘
【프론트 표시】
✅ 완료 - 모든 학습 및 테스트 완료
[결과 보기] [다운로드] 버튼 활성화

🛠️ 4. 상태 제어 API 목록

4.1 학습 실행 제어 API

API HTTP 엔드포인트 설명 상태 전환 프론트 버튼 표시 조건
학습 실행 POST /api/train/run/{uuid} 최초 학습 시작 READYIN_PROGRESS statusCd == 'READY'
학습 재실행 POST /api/train/restart/{uuid} 중단/오류 후 처음부터 재실행 STOPPED/ERRORIN_PROGRESS statusCd == 'STOPPED' || statusCd == 'ERROR'
학습 이어하기 POST /api/train/resume/{uuid} 중단된 지점부터 계속 실행 STOPPEDIN_PROGRESS statusCd == 'STOPPED'
학습 취소 POST /api/train/cancel/{uuid} 실행 중인 학습 중단 IN_PROGRESSSTOPPED statusCd == 'IN_PROGRESS' && step1Status == 'IN_PROGRESS'
학습 상태 확인 POST /api/train/status/{uuid} 현재 상태 동기화 현재 상태 유지/갱신 주기적 호출 (폴링)

4.2 테스트 실행 제어 API

API HTTP 엔드포인트 설명 상태 전환 프론트 버튼 표시 조건
테스트 실행 POST /api/train/test/run/{epoch}/{uuid} 특정 에폭으로 테스트 실행 step2_state: READYIN_PROGRESS step1Status == 'COMPLETED' && step2Status != 'IN_PROGRESS'
테스트 취소 POST /api/train/test/cancel/{uuid} 실행 중인 테스트 중단 step2_state: IN_PROGRESSSTOPPED step2Status == 'IN_PROGRESS'

4.3 기타 API

API HTTP 엔드포인트 설명
데이터셋 임시 파일 생성 POST /api/train/create-tmp/{uuid} 학습용 임시 데이터셋 생성
데이터셋 카운트 조회 GET /api/train/counts/{uuid} 데이터셋 통계 정보 조회

📋 5. 상태별 프론트엔드 액션 매트릭스

5.1 전체 상태 (statusCd) 기준

상태 코드 상태명 활성화 버튼 비활성화 버튼 표시 메시지 UI 색상
READY 대기 [학습 실행] [취소] [재실행] [이어하기] "학습 준비 완료" 파란색
IN_PROGRESS 진행중 [취소] [학습 실행] [재실행] [이어하기] "학습 진행 중..." 녹색
COMPLETED 완료 [결과 보기] [다운로드] [학습 실행] [취소] "학습 완료" 연두색
STOPPED 중단됨 [재실행] [이어하기] [취소] "학습 중단됨 - 재시작 가능" 주황색
ERROR 오류 [재실행] [취소] [이어하기] "오류 발생 - 재시작 필요" 빨간색

5.2 Step1 상태 (step1Status) 기준

상태 코드 버튼/액션 조건 설명
READY [학습 실행] statusCd == 'READY' 최초 학습 시작
IN_PROGRESS [학습 취소] statusCd == 'IN_PROGRESS' 진행 중인 학습 중단
COMPLETED [테스트 실행] step2Status == 'READY' 학습 완료 후 테스트 가능
STOPPED [재실행] [이어하기] statusCd == 'STOPPED' 중단된 학습 복구
ERROR [재실행] statusCd == 'ERROR' 오류 발생 후 재시도

5.3 Step2 상태 (step2Status) 기준

상태 코드 버튼/액션 조건 설명
READY [테스트 실행] step1Status == 'COMPLETED' 학습 완료 후 테스트 가능
IN_PROGRESS [테스트 취소] statusCd == 'IN_PROGRESS' 진행 중인 테스트 중단
COMPLETED [결과 보기] [다운로드] 항상 테스트 완료 후 결과 확인
STOPPED [테스트 재실행] step1Status == 'COMPLETED' 중단된 테스트 재시작

💻 6. 프론트엔드 구현 예시

6.1 상태별 버튼 렌더링 (React)

function ModelActionButtons({ model }) {
  const { uuid, statusCd, step1Status, step2Status } = model;

  // 학습 실행 버튼
  const canRun = statusCd === 'READY';
  
  // 학습 취소 버튼
  const canCancel = statusCd === 'IN_PROGRESS' && step1Status === 'IN_PROGRESS';
  
  // 재실행 버튼
  const canRestart = ['STOPPED', 'ERROR'].includes(statusCd);
  
  // 이어하기 버튼
  const canResume = statusCd === 'STOPPED';
  
  // 테스트 실행 버튼
  const canTest = step1Status === 'COMPLETED' && 
                  step2Status !== 'IN_PROGRESS' && 
                  step2Status !== 'COMPLETED';
  
  // 테스트 취소 버튼
  const canCancelTest = step2Status === 'IN_PROGRESS';
  
  // 결과 보기 버튼
  const canViewResult = statusCd === 'COMPLETED' && 
                        step1Status === 'COMPLETED' && 
                        step2Status === 'COMPLETED';

  return (
    <div className="action-buttons">
      {canRun && (
        <button onClick={() => runTrain(uuid)} className="btn-primary">
          🚀 학습 실행
        </button>
      )}
      
      {canCancel && (
        <button onClick={() => cancelTrain(uuid)} className="btn-danger">
          ⏹️ 학습 취소
        </button>
      )}
      
      {canRestart && (
        <button onClick={() => restartTrain(uuid)} className="btn-warning">
          🔄 재실행
        </button>
      )}
      
      {canResume && (
        <button onClick={() => resumeTrain(uuid)} className="btn-info">
          ▶️ 이어하기
        </button>
      )}
      
      {canTest && (
        <button onClick={() => runTest(uuid, model.bestEpoch)} className="btn-success">
          🧪 테스트 실행
        </button>
      )}
      
      {canCancelTest && (
        <button onClick={() => cancelTest(uuid)} className="btn-danger">
          ⏹️ 테스트 취소
        </button>
      )}
      
      {canViewResult && (
        <button onClick={() => viewResult(uuid)} className="btn-primary">
          📊 결과 보기
        </button>
      )}
    </div>
  );
}

6.2 API 호출 함수

import axios from 'axios';

const API_BASE = '/api/train';

// 학습 실행
async function runTrain(uuid) {
  try {
    const response = await axios.post(`${API_BASE}/run/${uuid}`);
    if (response.data.success) {
      alert('학습이 시작되었습니다.');
      refreshModelList(); // 목록 갱신
    }
  } catch (error) {
    alert('학습 실행 실패: ' + error.message);
  }
}

// 학습 취소
async function cancelTrain(uuid) {
  if (!confirm('학습을 취소하시겠습니까?')) return;
  
  try {
    const response = await axios.post(`${API_BASE}/cancel/${uuid}`);
    if (response.data.success) {
      alert('학습이 취소되었습니다.');
      refreshModelList();
    }
  } catch (error) {
    alert('학습 취소 실패: ' + error.message);
  }
}

// 학습 재실행
async function restartTrain(uuid) {
  if (!confirm('처음부터 다시 학습을 시작하시겠습니까?')) return;
  
  try {
    const response = await axios.post(`${API_BASE}/restart/${uuid}`);
    if (response.data.success) {
      alert('학습이 재시작되었습니다.');
      refreshModelList();
    }
  } catch (error) {
    alert('학습 재시작 실패: ' + error.message);
  }
}

// 학습 이어하기
async function resumeTrain(uuid) {
  if (!confirm('중단된 지점부터 학습을 이어가시겠습니까?')) return;
  
  try {
    const response = await axios.post(`${API_BASE}/resume/${uuid}`);
    if (response.data.success) {
      alert('학습이 재개되었습니다.');
      refreshModelList();
    }
  } catch (error) {
    alert('학습 이어하기 실패: ' + error.message);
  }
}

// 테스트 실행
async function runTest(uuid, epoch) {
  if (!confirm(`Epoch ${epoch}으로 테스트를 실행하시겠습니까?`)) return;
  
  try {
    const response = await axios.post(`${API_BASE}/test/run/${epoch}/${uuid}`);
    if (response.data.success) {
      alert('테스트가 시작되었습니다.');
      refreshModelList();
    }
  } catch (error) {
    alert('테스트 실행 실패: ' + error.message);
  }
}

// 테스트 취소
async function cancelTest(uuid) {
  if (!confirm('테스트를 취소하시겠습니까?')) return;
  
  try {
    const response = await axios.post(`${API_BASE}/test/cancel/${uuid}`);
    if (response.data.success) {
      alert('테스트가 취소되었습니다.');
      refreshModelList();
    }
  } catch (error) {
    alert('테스트 취소 실패: ' + error.message);
  }
}

// 학습 상태 확인 (폴링용)
async function checkTrainStatus(uuid) {
  try {
    const response = await axios.post(`${API_BASE}/status/${uuid}`);
    return response.data.success;
  } catch (error) {
    console.error('상태 확인 실패:', error);
    return false;
  }
}

6.3 주기적 상태 업데이트 (폴링)

import { useEffect, useState } from 'react';

function ModelMonitor({ uuid }) {
  const [isPolling, setIsPolling] = useState(false);

  useEffect(() => {
    let intervalId;

    // 진행 중인 모델만 폴링
    if (isPolling) {
      intervalId = setInterval(async () => {
        const success = await checkTrainStatus(uuid);
        if (success) {
          refreshModelList(); // 상태 업데이트 후 목록 갱신
        }
      }, 10000); // 10초마다 상태 확인
    }

    return () => {
      if (intervalId) clearInterval(intervalId);
    };
  }, [uuid, isPolling]);

  // 모델 상태에 따라 폴링 활성화/비활성화
  useEffect(() => {
    const shouldPoll = model.statusCd === 'IN_PROGRESS';
    setIsPolling(shouldPoll);
  }, [model.statusCd]);

  return null; // 백그라운드 작업
}

🎯 7. 상태 수정 시나리오별 가이드

시나리오 1: 학습 중단 후 재시작

현재 상태:

{
  "statusCd": "STOPPED",
  "step1Status": "STOPPED",
  "lastError": "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE"
}

프론트 표시:

  • ⚠️ 중단됨 - 산출물 부족으로 중단됨
  • 버튼: [재실행] [이어하기]

액션:

옵션 A) 재실행 (POST /api/train/restart/{uuid}):

// 처음부터 다시 시작
await axios.post(`/api/train/restart/${uuid}`);

// 결과 상태
{
  "statusCd": "IN_PROGRESS",
  "step1Status": "IN_PROGRESS",
  "currentAttemptId": 새로운JobId
}

옵션 B) 이어하기 (POST /api/train/resume/{uuid}):

// 중단된 지점부터 계속
await axios.post(`/api/train/resume/${uuid}`);

// 결과 상태
{
  "statusCd": "IN_PROGRESS",
  "step1Status": "IN_PROGRESS",
  "currentAttemptId": 새로운JobId,
  // paramsJson에 resumeFrom 설정됨
}

시나리오 2: 학습 완료 후 테스트 실행

현재 상태:

{
  "statusCd": "COMPLETED",
  "step1Status": "COMPLETED",
  "step2Status": "READY",
  "bestEpoch": 45
}

프론트 표시:

  • 완료 - 학습 완료 (테스트 대기)
  • 버튼: [테스트 실행]

액션:

// 테스트 실행 (bestEpoch 사용)
await axios.post(`/api/train/test/run/45/${uuid}`);

// 결과 상태
{
  "statusCd": "IN_PROGRESS",
  "step1Status": "COMPLETED",
  "step2Status": "IN_PROGRESS",
  "step2StrtDttm": "2026-04-06 14:30:00"
}

프론트 표시 변경:

  • 🔄 진행중 - 테스트 진행 중...
  • 버튼: [테스트 취소]

시나리오 3: 실행 중인 학습 취소

현재 상태:

{
  "statusCd": "IN_PROGRESS",
  "step1Status": "IN_PROGRESS",
  "currentAttemptId": 123
}

프론트 표시:

  • 🔄 진행중 - 학습 진행 중...
  • 버튼: [학습 취소]

액션:

// 학습 취소
await axios.post(`/api/train/cancel/${uuid}`);

// 결과 상태
{
  "statusCd": "STOPPED",
  "step1Status": "STOPPED",
  "currentAttemptId": 123
}

프론트 표시 변경:

  • ⚠️ 중단됨 - 사용자가 학습을 취소함
  • 버튼: [재실행] [이어하기]

시나리오 4: 서버 재시작 후 자동 상태 복구

서버 재시작 전:

{
  "statusCd": "IN_PROGRESS",
  "step1Status": "IN_PROGRESS"
}

서버 재시작 후 API 호출:

// 주기적 폴링 또는 수동 호출
await axios.post(`/api/train/status/${uuid}`);

케이스 A) 학습 완료된 경우:

{
  "statusCd": "COMPLETED",
  "step1Status": "COMPLETED",
  "step1EndDttm": "2026-04-06 15:00:00",
  "step1MetricSaveYn": true
}
  • 완료 - 학습 완료
  • 버튼: [테스트 실행] [결과 보기]

케이스 B) 산출물 부족한 경우:

{
  "statusCd": "STOPPED",
  "step1Status": "STOPPED",
  "lastError": "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE: val.csv-lines-mismatch"
}
  • ⚠️ 중단됨 - 산출물 부족으로 중단됨
  • 버튼: [재실행] [이어하기]

케이스 C) 여전히 실행 중인 경우:

{
  "statusCd": "IN_PROGRESS",
  "step1Status": "IN_PROGRESS"
}
  • 🔄 진행중 - 학습 진행 중...
  • 버튼: [학습 취소]

📊 8. 에러 메시지 처리

8.1 last_error 필드 해석

에러 메시지 의미 프론트 표시 권장 액션
SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE 서버 재시작 후 컨테이너 없음 + 산출물 부족 "서버 재시작으로 인한 중단 - 산출물 확인 필요" [재실행]
val.csv-lines-mismatch val.csv 라인 수 부족 "검증 데이터 부족 - 학습이 완료되지 않음" [재실행] [이어하기]
val.csv-missing val.csv 파일 없음 "검증 파일 없음 - 학습 실패" [재실행]
total-epoch-missing paramsJson에 total_epoch 없음 "설정 오류 - 에폭 정보 없음" [재실행]
output-dir-missing 산출물 디렉토리 없음 "산출물 디렉토리 없음 - 학습 실패" [재실행]

8.2 에러 표시 컴포넌트

function ErrorMessage({ lastError }) {
  if (!lastError) return null;

  const errorMessages = {
    'SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE': {
      icon: '⚠️',
      title: '서버 재시작으로 인한 중단',
      message: '서버가 재시작되면서 학습이 중단되었습니다. 산출물을 확인하여 학습 상태를 복구합니다.',
      severity: 'warning'
    },
    'val.csv-lines-mismatch': {
      icon: '📊',
      title: '검증 데이터 부족',
      message: '검증 데이터 라인 수가 예상보다 적습니다. 학습이 완료되지 않았을 가능성이 있습니다.',
      severity: 'warning'
    },
    'val.csv-missing': {
      icon: '❌',
      title: '검증 파일 없음',
      message: 'val.csv 파일을 찾을 수 없습니다. 학습이 실패했을 가능성이 높습니다.',
      severity: 'error'
    }
  };

  // 에러 메시지 파싱
  const errorKey = Object.keys(errorMessages).find(key => lastError.includes(key));
  const errorInfo = errorMessages[errorKey] || {
    icon: '⚠️',
    title: '오류 발생',
    message: lastError,
    severity: 'error'
  };

  return (
    <div className={`error-message severity-${errorInfo.severity}`}>
      <div className="error-header">
        <span className="error-icon">{errorInfo.icon}</span>
        <span className="error-title">{errorInfo.title}</span>
      </div>
      <p className="error-description">{errorInfo.message}</p>
    </div>
  );
}

🔍 9. 상태 일관성 체크리스트

프론트엔드 개발 시 확인사항

상태 표시

  • statusCd를 UI에 정확히 매핑 (READY, IN_PROGRESS, COMPLETED, STOPPED, ERROR)
  • step1Status, step2Status 개별 표시
  • 한글 상태명 표시 (statusName, step1StatusName, step2StatusName 활용)
  • 상태별 적절한 색상/아이콘 사용

버튼 활성화/비활성화

  • statusCd에 따른 버튼 조건 확인
  • step1Status, step2Status 조합 고려
  • 중복 실행 방지 (disabled 속성)

시간 정보 표시

  • step1StrtDttm, step1EndDttm 표시
  • step2StrtDttm, step2EndDttm 표시
  • step1Duration, step2Duration 계산 표시

에러 처리

  • lastError 메시지 파싱 및 표시
  • 사용자 친화적 에러 메시지 변환
  • 에러 발생 시 복구 방법 안내

주기적 업데이트

  • 진행 중인 모델 폴링 (10초 간격 권장)
  • POST /api/train/status/{uuid} 주기 호출
  • 목록 자동 갱신

💡 10. 베스트 프랙티스

10.1 상태 동기화

// ❌ 나쁜 예: 프론트에서 임의로 상태 변경
function handleCancel() {
  model.statusCd = 'STOPPED'; // 직접 변경 X
  updateUI();
}

// ✅ 좋은 예: API 호출 후 서버 응답 기준으로 업데이트
async function handleCancel() {
  await axios.post(`/api/train/cancel/${uuid}`);
  const updated = await axios.get(`/api/models/list`);
  setModels(updated.data.content);
}

10.2 낙관적 UI 업데이트

// ✅ 사용자 경험 향상: 즉시 UI 업데이트 + 백그라운드 검증
async function handleRun() {
  // 1. 즉시 UI 업데이트 (낙관적)
  setModel(prev => ({ ...prev, statusCd: 'IN_PROGRESS' }));
  
  try {
    // 2. API 호출
    await axios.post(`/api/train/run/${uuid}`);
    
    // 3. 실제 상태 확인
    setTimeout(() => checkTrainStatus(uuid), 2000);
  } catch (error) {
    // 4. 실패 시 롤백
    setModel(prev => ({ ...prev, statusCd: 'READY' }));
    alert('실행 실패: ' + error.message);
  }
}

10.3 상태별 UI 일관성

/* 상태별 일관된 색상 스타일 */
.status-badge.ready { background: #e3f2fd; color: #1976d2; }
.status-badge.in-progress { background: #e8f5e9; color: #388e3c; }
.status-badge.completed { background: #f1f8e9; color: #689f38; }
.status-badge.stopped { background: #fff3e0; color: #f57c00; }
.status-badge.error { background: #ffebee; color: #d32f2f; }

/* 버튼 스타일 */
.btn-primary { background: #1976d2; color: white; }
.btn-success { background: #388e3c; color: white; }
.btn-warning { background: #f57c00; color: white; }
.btn-danger { background: #d32f2f; color: white; }
.btn-info { background: #0288d1; color: white; }

📝 요약

상태 확인 방법

  1. API 호출: POST /api/train/status/{uuid}
  2. 응답 확인: statusCd, step1Status, step2Status
  3. UI 반영: 상태별 배지/아이콘 표시

상태 변경 방법

  1. 적절한 제어 API 호출:

    • 실행: POST /api/train/run/{uuid}
    • 취소: POST /api/train/cancel/{uuid}
    • 재실행: POST /api/train/restart/{uuid}
    • 이어하기: POST /api/train/resume/{uuid}
  2. 버튼 조건부 표시:

    • statusCd, step1Status, step2Status 조합 확인
  3. 주기적 폴링:

    • 진행 중(IN_PROGRESS) 상태일 때만 10초 간격 폴링