34 KiB
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} |
최초 학습 시작 | 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)
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; }
📝 요약
상태 확인 방법
- API 호출:
POST /api/train/status/{uuid} - 응답 확인:
statusCd,step1Status,step2Status - UI 반영: 상태별 배지/아이콘 표시
상태 변경 방법
-
적절한 제어 API 호출:
- 실행:
POST /api/train/run/{uuid} - 취소:
POST /api/train/cancel/{uuid} - 재실행:
POST /api/train/restart/{uuid} - 이어하기:
POST /api/train/resume/{uuid}
- 실행:
-
버튼 조건부 표시:
statusCd,step1Status,step2Status조합 확인
-
주기적 폴링:
- 진행 중(
IN_PROGRESS) 상태일 때만 10초 간격 폴링
- 진행 중(