From 051082665db85b1523e63cad5f1781b1eae6ed7c Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 6 Apr 2026 21:32:24 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=81=ED=83=9C=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 상태전의 STATUS 가이드.md | 964 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 964 insertions(+) create mode 100644 상태전의 STATUS 가이드.md diff --git a/상태전의 STATUS 가이드.md b/상태전의 STATUS 가이드.md new file mode 100644 index 0000000..80a5214 --- /dev/null +++ b/상태전의 STATUS 가이드.md @@ -0,0 +1,964 @@ +# 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 ( + + {icon} {name} + + ); +} +``` + +#### 📊 **상세 진행 상황 표시** + +```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 ( +
+ {steps.map((step, index) => ( +
+
+ {step.name} + +
+
+ {step.startTime && ( + <> + 시작: {step.startTime} + {step.endTime && ~ 종료: {step.endTime}} + {step.duration && (소요시간: {step.duration})} + + )} +
+ {step.status === 'IN_PROGRESS' && ( +
+
+
+ )} +
+ ))} +
+ ); +} +``` + +--- + +## 🔄 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 ( +
+ {canRun && ( + + )} + + {canCancel && ( + + )} + + {canRestart && ( + + )} + + {canResume && ( + + )} + + {canTest && ( + + )} + + {canCancelTest && ( + + )} + + {canViewResult && ( + + )} +
+ ); +} +``` + +### 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 ( +
+
+ {errorInfo.icon} + {errorInfo.title} +
+

{errorInfo.message}

+
+ ); +} +``` + +--- + +## 🔍 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초 간격 폴링 + +--- + + +