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

965 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
**요청 파라미터**:
```json
{
"status": "IN_PROGRESS", // "", "IN_PROGRESS", "COMPLETED"
"modelNo": "G1", // "G1", "G2", "G3", "G4"
"page": 0,
"size": 20
}
```
**응답 예시**:
```json
{
"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 컬러 가이드**
```javascript
// 상태별 배지 색상 매핑
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>
);
}
```
#### 📊 **상세 진행 상황 표시**
```javascript
// 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}` | 최초 학습 시작 | `READY``IN_PROGRESS` | `statusCd == 'READY'` |
| **학습 재실행** | POST | `/api/train/restart/{uuid}` | 중단/오류 후 처음부터 재실행 | `STOPPED/ERROR``IN_PROGRESS` | `statusCd == 'STOPPED' \|\| statusCd == 'ERROR'` |
| **학습 이어하기** | POST | `/api/train/resume/{uuid}` | 중단된 지점부터 계속 실행 | `STOPPED``IN_PROGRESS` | `statusCd == 'STOPPED'` |
| **학습 취소** | POST | `/api/train/cancel/{uuid}` | 실행 중인 학습 중단 | `IN_PROGRESS``STOPPED` | `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: READY``IN_PROGRESS` | `step1Status == 'COMPLETED' && step2Status != 'IN_PROGRESS'` |
| **테스트 취소** | POST | `/api/train/test/cancel/{uuid}` | 실행 중인 테스트 중단 | `step2_state: IN_PROGRESS``STOPPED` | `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)
```javascript
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 호출 함수
```javascript
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 주기적 상태 업데이트 (폴링)
```javascript
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: 학습 중단 후 재시작
**현재 상태**:
```json
{
"statusCd": "STOPPED",
"step1Status": "STOPPED",
"lastError": "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE"
}
```
**프론트 표시**:
- ⚠️ 중단됨 - 산출물 부족으로 중단됨
- 버튼: [재실행] [이어하기]
**액션**:
**옵션 A) 재실행** (`POST /api/train/restart/{uuid}`):
```javascript
// 처음부터 다시 시작
await axios.post(`/api/train/restart/${uuid}`);
// 결과 상태
{
"statusCd": "IN_PROGRESS",
"step1Status": "IN_PROGRESS",
"currentAttemptId": 새로운JobId
}
```
**옵션 B) 이어하기** (`POST /api/train/resume/{uuid}`):
```javascript
// 중단된 지점부터 계속
await axios.post(`/api/train/resume/${uuid}`);
// 결과 상태
{
"statusCd": "IN_PROGRESS",
"step1Status": "IN_PROGRESS",
"currentAttemptId": 새로운JobId,
// paramsJson에 resumeFrom 설정됨
}
```
---
### 시나리오 2: 학습 완료 후 테스트 실행
**현재 상태**:
```json
{
"statusCd": "COMPLETED",
"step1Status": "COMPLETED",
"step2Status": "READY",
"bestEpoch": 45
}
```
**프론트 표시**:
- ✅ 완료 - 학습 완료 (테스트 대기)
- 버튼: [테스트 실행]
**액션**:
```javascript
// 테스트 실행 (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: 실행 중인 학습 취소
**현재 상태**:
```json
{
"statusCd": "IN_PROGRESS",
"step1Status": "IN_PROGRESS",
"currentAttemptId": 123
}
```
**프론트 표시**:
- 🔄 진행중 - 학습 진행 중...
- 버튼: [학습 취소]
**액션**:
```javascript
// 학습 취소
await axios.post(`/api/train/cancel/${uuid}`);
// 결과 상태
{
"statusCd": "STOPPED",
"step1Status": "STOPPED",
"currentAttemptId": 123
}
```
**프론트 표시 변경**:
- ⚠️ 중단됨 - 사용자가 학습을 취소함
- 버튼: [재실행] [이어하기]
---
### 시나리오 4: 서버 재시작 후 자동 상태 복구
**서버 재시작 전**:
```json
{
"statusCd": "IN_PROGRESS",
"step1Status": "IN_PROGRESS"
}
```
**서버 재시작 후 API 호출**:
```javascript
// 주기적 폴링 또는 수동 호출
await axios.post(`/api/train/status/${uuid}`);
```
**케이스 A) 학습 완료된 경우**:
```json
{
"statusCd": "COMPLETED",
"step1Status": "COMPLETED",
"step1EndDttm": "2026-04-06 15:00:00",
"step1MetricSaveYn": true
}
```
- ✅ 완료 - 학습 완료
- 버튼: [테스트 실행] [결과 보기]
**케이스 B) 산출물 부족한 경우**:
```json
{
"statusCd": "STOPPED",
"step1Status": "STOPPED",
"lastError": "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE: val.csv-lines-mismatch"
}
```
- ⚠️ 중단됨 - 산출물 부족으로 중단됨
- 버튼: [재실행] [이어하기]
**케이스 C) 여전히 실행 중인 경우**:
```json
{
"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 에러 표시 컴포넌트
```javascript
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 상태 동기화
```javascript
// ❌ 나쁜 예: 프론트에서 임의로 상태 변경
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 업데이트
```javascript
// ✅ 사용자 경험 향상: 즉시 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 일관성
```css
/* 상태별 일관된 색상 스타일 */
.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초 간격 폴링
---