daniel 작업본 추가

This commit is contained in:
2026-04-14 08:31:53 +09:00
parent afb3f57ef6
commit 419b3ccdfc
7 changed files with 575 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ package com.kamco.cd.training.train;
import com.kamco.cd.training.config.api.ApiResponseDto; import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.train.dto.TrainingMetricsDto; import com.kamco.cd.training.train.dto.TrainingMetricsDto;
import com.kamco.cd.training.train.dto.TrainingProgressDto;
import com.kamco.cd.training.train.service.DataSetCountersService; import com.kamco.cd.training.train.service.DataSetCountersService;
import com.kamco.cd.training.train.service.TestJobService; import com.kamco.cd.training.train.service.TestJobService;
import com.kamco.cd.training.train.service.TrainJobService; import com.kamco.cd.training.train.service.TrainJobService;
@@ -298,4 +299,26 @@ public class TrainApiController {
trainingMetricsService.getTrainingMetricsByModelUuid(modelUuid); trainingMetricsService.getTrainingMetricsByModelUuid(modelUuid);
return ApiResponseDto.ok(response); return ApiResponseDto.ok(response);
} }
@Operation(
summary = "학습 진행률 조회",
description = "UUID로 학습 진행률을 실시간 조회합니다. 기존 DB 구조를 활용하여 진행률을 계산합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingProgressDto.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/progress/{uuid}")
public ApiResponseDto<TrainingProgressDto> getTrainingProgress(
@Parameter(description = "모델 UUID", required = true) @PathVariable UUID uuid) {
TrainingProgressDto progress = trainJobService.getTrainingProgress(uuid);
return ApiResponseDto.ok(progress);
}
} }

View File

@@ -0,0 +1,44 @@
package com.kamco.cd.training.train.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
/** 학습 진행률 조회 응답 DTO */
@Getter
@Builder
public class TrainingProgressDto {
// 기본 정보
private Long jobId;
private UUID modelUuid;
private String statusCd; // QUEUED/RUNNING/SUCCESS/FAILED
private String currentPhase; // 현재 단계 (계산된 값)
private Double progressPercent; // 진행률 (0.00 ~ 100.00, 계산된 값)
// Epoch 정보 (기존 DB 컬럼)
private Integer currentEpoch; // 현재 Epoch
private Integer totalEpoch; // 전체 Epoch
// 시간 정보 (기존 DB 컬럼) - ISO 8601 형식으로 직렬화
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private ZonedDateTime queuedDttm; // 큐 등록 시각
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private ZonedDateTime startedDttm; // 시작 시각
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private ZonedDateTime finishedDttm; // 종료 시각 (완료/실패 시)
// 계산된 시간 정보
private Long elapsedSeconds; // 경과 시간 (초)
private Long estimatedRemainingSeconds; // 예상 남은 시간 (초)
// 상태 메시지
private String message; // 현재 상태 메시지 (계산된 값)
// 에러 정보 (실패 시)
private String errorMessage; // 에러 메시지 (기존 DB 컬럼)
}

View File

@@ -158,12 +158,42 @@ public class DockerTrainService {
lastEpoch.set(epoch); lastEpoch.set(epoch);
lastIter.set(iter); lastIter.set(iter);
// Step 구분 (컨테이너 이름으로 판별)
boolean isStep1 = containerName.startsWith("train-");
boolean isStep2 = containerName.startsWith("eval-");
// 진행률 계산 (Step별 구분)
double progress = 0.0;
if (maxEpochs > 0) {
// Epoch + Iteration 기반 정밀 진행률 계산
double epochProgress =
((double) (epoch - 1) + ((double) iter / totalIter)) / maxEpochs;
// Step1 (학습): 7.5% ~ 42.5% (0% ~ 50% 중 15% ~ 85%)
// Step2 (테스트): 57.5% ~ 92.5% (50% ~ 100% 중 15% ~ 85%)
if (isStep1) {
// Step1: 0% ~ 50% 범위, 그 중 15% ~ 85% = 7.5% ~ 42.5%
progress = 7.5 + (epochProgress * 35.0);
} else if (isStep2) {
// Step2: 50% ~ 100% 범위, 그 중 15% ~ 85% = 57.5% ~ 92.5%
progress = 57.5 + (epochProgress * 35.0);
} else {
// 기본값 (하위 호환)
progress = 15.0 + (epochProgress * 70.0);
}
}
String stepLabel = isStep1 ? "STEP1" : isStep2 ? "STEP2" : "UNKNOWN";
log.info( log.info(
"[TRAIN] container={} epoch={} iter={}/{}", "[TRAIN] {} container={} epoch={}/{} iter={}/{} | Progress: {}%",
stepLabel,
containerName, containerName,
epoch, epoch,
maxEpochs,
iter, iter,
totalIter); totalIter,
String.format("%.2f", progress));
modelTrainJobCoreService.updateEpoch(containerName, epoch); modelTrainJobCoreService.updateEpoch(containerName, epoch);
} }

View File

@@ -300,7 +300,7 @@ public class ModelTestMetricsJobService {
zipFiles(zipFileList, individualZipPath); zipFiles(zipFileList, individualZipPath);
log.info( log.info(
"개별 ZIP 생성 완료: fileName={}, pthFile={}, size={} bytes", "개별 ZIP 생성 완료: fileName={}, pthFile={}, size={} bytes",
individualZipName, individualZipName,
pthFileName, pthFileName,
Files.size(individualZipPath)); Files.size(individualZipPath));

View File

@@ -13,6 +13,8 @@ import com.kamco.cd.training.train.dto.ModelTrainJobQueuedEvent;
import com.kamco.cd.training.train.dto.ModelTrainLinkDto; import com.kamco.cd.training.train.dto.ModelTrainLinkDto;
import com.kamco.cd.training.train.dto.OutputResult; import com.kamco.cd.training.train.dto.OutputResult;
import com.kamco.cd.training.train.dto.TrainRunRequest; import com.kamco.cd.training.train.dto.TrainRunRequest;
import com.kamco.cd.training.train.dto.TrainingProgressDto;
import com.kamco.cd.training.train.util.TrainingProgressCalculator;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -466,4 +468,136 @@ public class TrainJobService {
modelTrainMngCoreService.markStep1Stop(job.getModelId(), msg); modelTrainMngCoreService.markStep1Stop(job.getModelId(), msg);
} }
} }
/**
* UUID로 학습 진행률 조회 기존 DB 컬럼만을 활용하여 실시간으로 진행률 계산
*
* @param uuid 모델 UUID
* @return 학습 진행률 정보
*/
@Transactional(readOnly = true)
public TrainingProgressDto getTrainingProgress(UUID uuid) {
// 1. UUID로 모델 조회
Long modelId = getModelIdByUuid(uuid);
ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelById(modelId);
// 2. Step 상태 확인 (step1Status, step2Status)
String step1Status = model.getStep1Status(); // READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR
String step2Status = model.getStep2Status(); // READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR
// 3. 현재 실행중인 Job ID 확인
Long jobId = model.getCurrentAttemptId();
if (jobId == null) {
// Job이 없는 경우 모델 상태만 반환
// Step1 완료 여부에 따라 진행률 결정
double progressPercent = 0.0;
String currentPhase = "NOT_STARTED";
if ("COMPLETED".equals(step1Status) && "COMPLETED".equals(step2Status)) {
progressPercent = 100.0;
currentPhase = "COMPLETED";
} else if ("COMPLETED".equals(step1Status)) {
progressPercent = 50.0;
currentPhase = "STEP1_COMPLETED";
}
return TrainingProgressDto.builder()
.modelUuid(uuid)
.statusCd(model.getStatusCd())
.currentPhase(currentPhase)
.progressPercent(progressPercent)
.message(getMessageForPhase(currentPhase, null, null))
.build();
}
// 4. Job 정보 조회 (기존 컬럼만 사용)
ModelTrainJobDto job =
modelTrainJobCoreService
.findById(jobId)
.orElseThrow(() -> new IllegalStateException("Job을 찾을 수 없습니다: " + jobId));
// 5. totalEpoch 추출 (DB 컬럼 우선, 없으면 params_json에서 추출)
Integer totalEpoch = job.getTotalEpoch();
if (totalEpoch == null && job.getParamsJson() != null) {
Object totalEpochObj = job.getParamsJson().get("totalEpoch");
if (totalEpochObj != null) {
totalEpoch = Integer.valueOf(String.valueOf(totalEpochObj));
}
}
// 6. jobType 추출 (TRAIN/EVAL)
String jobType = "TRAIN"; // 기본값
if (job.getParamsJson() != null) {
Object jobTypeObj = job.getParamsJson().get("jobType");
if (jobTypeObj != null) {
jobType = String.valueOf(jobTypeObj);
}
}
// 7. 진행률 계산 (Step 구분하여 계산)
double progressPercent =
TrainingProgressCalculator.calculateProgress(
job.getStatusCd(),
job.getCurrentEpoch(),
totalEpoch,
job.getStartedDttm(),
job.getFinishedDttm(),
jobType,
step1Status,
step2Status);
// 8. 현재 Phase 추정
String currentPhase =
TrainingProgressCalculator.estimateCurrentPhase(
job.getStatusCd(), job.getCurrentEpoch(), totalEpoch, job.getStartedDttm(), jobType);
// 9. 경과 시간 계산
Long elapsedSeconds = TrainingProgressCalculator.calculateElapsedSeconds(job.getStartedDttm());
// 10. 예상 남은 시간 계산
Long estimatedRemaining =
TrainingProgressCalculator.estimateRemainingSeconds(progressPercent, elapsedSeconds);
// 11. 상태 메시지 생성
String message =
TrainingProgressCalculator.generateProgressMessage(
currentPhase, job.getCurrentEpoch(), totalEpoch);
// 12. DTO 생성 및 반환
return TrainingProgressDto.builder()
.jobId(job.getId())
.modelUuid(uuid)
.statusCd(job.getStatusCd())
.currentPhase(currentPhase)
.progressPercent(progressPercent)
.currentEpoch(job.getCurrentEpoch())
.totalEpoch(totalEpoch)
.queuedDttm(job.getQueuedDttm())
.startedDttm(job.getStartedDttm())
.finishedDttm(job.getFinishedDttm())
.elapsedSeconds(elapsedSeconds)
.estimatedRemainingSeconds(estimatedRemaining)
.message(message)
.errorMessage(job.getErrorMessage())
.build();
}
/**
* Phase에 맞는 메시지 반환 (Job이 없는 경우)
*
* @param phase 현재 Phase
* @param currentEpoch 현재 Epoch
* @param totalEpoch 전체 Epoch
* @return 상태 메시지
*/
private String getMessageForPhase(String phase, Integer currentEpoch, Integer totalEpoch) {
if ("COMPLETED".equals(phase)) {
return "모든 학습 및 테스트가 완료되었습니다.";
} else if ("STEP1_COMPLETED".equals(phase)) {
return "학습이 완료되었습니다. 테스트를 시작할 수 있습니다.";
} else {
return "학습 작업이 시작되지 않았습니다.";
}
}
} }

View File

@@ -58,6 +58,7 @@ public class TrainJobWorker {
(isEval ? "eval-" : "train-") + jobId + "-" + params.get("uuid").toString().substring(0, 8); (isEval ? "eval-" : "train-") + jobId + "-" + params.get("uuid").toString().substring(0, 8);
String type = isEval ? "TEST" : "TRAIN"; String type = isEval ? "TEST" : "TRAIN";
String step = isEval ? "STEP2" : "STEP1";
Integer totalEpoch = null; Integer totalEpoch = null;
if (params.containsKey("totalEpoch")) { if (params.containsKey("totalEpoch")) {
@@ -65,6 +66,21 @@ public class TrainJobWorker {
totalEpoch = Integer.parseInt(params.get("totalEpoch").toString()); totalEpoch = Integer.parseInt(params.get("totalEpoch").toString());
} }
} }
// Phase 1: 준비 단계
// Step1: 0% ~ 2.5%, Step2: 50% ~ 52.5%
double preparingProgress = isEval ? 50.0 : 0.0;
log.info(
"[JOB] {} jobId={} | Phase: PREPARING | Progress: {}%",
step, jobId, String.format("%.1f", preparingProgress + 2.5));
// Phase 2: 컨테이너 시작
// Step1: 2.5% ~ 5%, Step2: 52.5% ~ 55%
double containerStartProgress = isEval ? 52.5 : 2.5;
log.info(
"[JOB] {} jobId={} | Phase: CONTAINER_STARTING | Progress: {}%",
step, jobId, String.format("%.1f", containerStartProgress + 2.5));
log.info("[JOB] markRunning start jobId={}, containerName={}", jobId, containerName); log.info("[JOB] markRunning start jobId={}, containerName={}", jobId, containerName);
// 실행 시작 처리 // 실행 시작 처리
modelTrainJobCoreService.markRunning( modelTrainJobCoreService.markRunning(
@@ -90,12 +106,21 @@ public class TrainJobWorker {
evalReq.setReqTmpYn((Boolean) params.get("reqTmpYn")); evalReq.setReqTmpYn((Boolean) params.get("reqTmpYn"));
log.info("[JOB] selected test epoch={}", epoch); log.info("[JOB] selected test epoch={}", epoch);
// Phase 3: 테스트 시작
// Step2: 55% ~ 57.5%
log.info("[JOB] STEP2 jobId={} | Phase: EVAL_STARTED | Progress: 57.5%", jobId);
// 도커 실행 후 로그 수집 // 도커 실행 후 로그 수집
result = dockerTrainService.runEvalSync(containerName, evalReq); result = dockerTrainService.runEvalSync(containerName, evalReq);
} else { } else {
// step1 진행중 처리 // step1 진행중 처리
modelTrainMngCoreService.markStep1InProgress(modelId, jobId); modelTrainMngCoreService.markStep1InProgress(modelId, jobId);
TrainRunRequest trainReq = toTrainRunRequest(params); TrainRunRequest trainReq = toTrainRunRequest(params);
// Phase 3: 학습 시작
// Step1: 5% ~ 7.5%
log.info("[JOB] STEP1 jobId={} | Phase: TRAINING_STARTED | Progress: 7.5%", jobId);
// 도커 실행 후 로그 수집 // 도커 실행 후 로그 수집
result = dockerTrainService.runTrainSync(trainReq, containerName); result = dockerTrainService.runTrainSync(trainReq, containerName);
} }
@@ -109,11 +134,25 @@ public class TrainJobWorker {
return; return;
} }
// Phase 4: 학습/테스트 완료
// Step1: 42.5%, Step2: 92.5%
double completedProgress = isEval ? 92.5 : 42.5;
log.info(
"[JOB] {} jobId={} | Phase: TRAINING_COMPLETED | Progress: {}%",
step, jobId, String.format("%.1f", completedProgress));
/** /**
* 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부 * 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부
* 강제 중단 STOP * 강제 중단 STOP
*/ */
if (result.getExitCode() == 0) { if (result.getExitCode() == 0) {
// Phase 5: 결과 처리 중
// Step1: 45%, Step2: 95%
double processingProgress = isEval ? 95.0 : 45.0;
log.info(
"[JOB] {} jobId={} | Phase: PROCESSING_RESULTS | Progress: {}%",
step, jobId, String.format("%.1f", processingProgress));
// 성공 처리 // 성공 처리
modelTrainJobCoreService.markSuccess(jobId, result.getExitCode()); modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());
@@ -128,6 +167,13 @@ public class TrainJobWorker {
modelTrainMetricsJobService.findTrainValidMetricCsvFiles(); modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
} }
// Phase 6: 완료
// Step1: 50%, Step2: 100%
double finalProgress = isEval ? 100.0 : 50.0;
log.info(
"[JOB] {} jobId={} | Phase: COMPLETED | Progress: {}%",
step, jobId, String.format("%.1f", finalProgress));
} else { } else {
String failMsg = result.getStatus() + "\n" + result.getLogs(); String failMsg = result.getStatus() + "\n" + result.getLogs();

View File

@@ -0,0 +1,295 @@
package com.kamco.cd.training.train.util;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import lombok.experimental.UtilityClass;
/**
* 학습 진행률 계산 유틸리티 기존 DB 컬럼만을 활용하여 진행률을 실시간 계산
*
* <p>상태전의 STATUS 가이드 기준: - Step1 (학습): TRAIN jobType → 0% ~ 50% - Step2 (테스트): EVAL jobType → 50% ~
* 100%
*/
@UtilityClass
public class TrainingProgressCalculator {
/**
* 학습 진행률 계산 (Step 구분)
*
* @param statusCd 작업 상태 (QUEUED/RUNNING/SUCCESS/FAILED/STOPPED/CANCELED)
* @param currentEpoch 현재 Epoch
* @param totalEpoch 전체 Epoch
* @param startedDttm 시작 시각
* @param finishedDttm 종료 시각
* @param jobType 작업 타입 (TRAIN/EVAL)
* @param step1State Step1 상태 (READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR)
* @param step2State Step2 상태 (READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR)
* @return 진행률 (0.00 ~ 100.00)
*/
public static double calculateProgress(
String statusCd,
Integer currentEpoch,
Integer totalEpoch,
ZonedDateTime startedDttm,
ZonedDateTime finishedDttm,
String jobType,
String step1State,
String step2State) {
if (statusCd == null) {
return 0.0;
}
// Step별 진행률 계산
boolean isStep1 = "TRAIN".equals(jobType);
boolean isStep2 = "EVAL".equals(jobType) || "TEST".equals(jobType);
// Step1 완료, Step2 완료 여부 확인
boolean isStep1Completed = "COMPLETED".equals(step1State);
boolean isStep2Completed = "COMPLETED".equals(step2State);
switch (statusCd) {
case "QUEUED":
// Step1 대기 중: 0%, Step2 대기 중: 50%
return isStep2 ? 50.0 : 0.0;
case "RUNNING":
return calculateRunningProgress(
currentEpoch, totalEpoch, startedDttm, isStep1, isStep2, isStep1Completed);
case "SUCCESS":
// Step1 성공: 50%, Step2 성공: 100%
if (isStep2 || isStep2Completed) {
return 100.0;
} else if (isStep1 || isStep1Completed) {
return 50.0;
}
return 100.0; // 기본값
case "FAILED":
case "STOPPED":
case "CANCELED":
// 실패/취소된 경우 마지막 진행률 반환
return calculateRunningProgress(
currentEpoch, totalEpoch, startedDttm, isStep1, isStep2, isStep1Completed);
default:
return 0.0;
}
}
/**
* 실행 중인 학습의 진행률 계산 (Step 구분)
*
* <p>Step1 (학습): 0% ~ 50% - PREPARING: 0% ~ 2.5% - CONTAINER_STARTING: 2.5% ~ 5% - DATA_LOADING:
* 5% ~ 7.5% - TRAINING: 7.5% ~ 42.5% (35% 비중) - PROCESSING: 42.5% ~ 50%
*
* <p>Step2 (테스트): 50% ~ 100% - PREPARING: 50% ~ 52.5% - CONTAINER_STARTING: 52.5% ~ 55% -
* DATA_LOADING: 55% ~ 57.5% - TESTING: 57.5% ~ 92.5% (35% 비중) - PROCESSING: 92.5% ~ 100%
*/
private static double calculateRunningProgress(
Integer currentEpoch,
Integer totalEpoch,
ZonedDateTime startedDttm,
boolean isStep1,
boolean isStep2,
boolean isStep1Completed) {
// Step 기준 계산
double baseProgress = 0.0;
double maxProgress = 50.0;
if (isStep2 || isStep1Completed) {
// Step2 진행 중 또는 Step1 완료 후
baseProgress = 50.0;
maxProgress = 100.0;
} else if (isStep1) {
// Step1 진행 중
baseProgress = 0.0;
maxProgress = 50.0;
}
double stepRange = maxProgress - baseProgress; // 50%
// 시작 직후 (Epoch 정보 없음)
if (currentEpoch == null || totalEpoch == null || totalEpoch == 0) {
return baseProgress + estimateInitialPhaseProgress(startedDttm, stepRange);
}
// Epoch 0 또는 1 (학습 시작 단계)
if (currentEpoch <= 1) {
return baseProgress + (stepRange * 0.15); // 7.5% (Step1) 또는 57.5% (Step2)
}
// 학습 진행 중 (15% ~ 85% of step range)
double epochProgress = (double) currentEpoch / totalEpoch;
double trainingProgress = 0.15 + (epochProgress * 0.70); // 15% ~ 85%
// 학습 완료 (마지막 Epoch)
if (currentEpoch >= totalEpoch) {
// 85% ~ 100% 사이로 추정
return baseProgress + (stepRange * 0.90);
}
return baseProgress + (stepRange * trainingProgress);
}
/**
* 초기 단계 진행률 추정 (시작 시간 기반)
*
* @param startedDttm 시작 시각
* @param stepRange Step 범위 (50%)
* @return 초기 단계 진행률 (0 ~ 0.15 비율)
*/
private static double estimateInitialPhaseProgress(ZonedDateTime startedDttm, double stepRange) {
if (startedDttm == null) {
return stepRange * 0.05; // 5% of step range
}
long elapsedSeconds = ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
// 시작 후 30초 이내: PREPARING (0% ~ 5% of step)
if (elapsedSeconds < 30) {
return Math.min(stepRange * 0.05, (elapsedSeconds / 30.0) * stepRange * 0.05);
}
// 30초 ~ 60초: CONTAINER_STARTING (5% ~ 10% of step)
else if (elapsedSeconds < 60) {
return stepRange * 0.05 + ((elapsedSeconds - 30) / 30.0) * stepRange * 0.05;
}
// 60초 이상: DATA_LOADING (10% ~ 15% of step)
else {
return Math.min(
stepRange * 0.15, stepRange * 0.10 + ((elapsedSeconds - 60) / 60.0) * stepRange * 0.05);
}
}
/**
* 현재 Phase 추정
*
* @param statusCd 작업 상태
* @param currentEpoch 현재 Epoch
* @param totalEpoch 전체 Epoch
* @param startedDttm 시작 시각
* @param jobType 작업 타입 (TRAIN/EVAL)
* @return 현재 Phase
*/
public static String estimateCurrentPhase(
String statusCd,
Integer currentEpoch,
Integer totalEpoch,
ZonedDateTime startedDttm,
String jobType) {
if ("SUCCESS".equals(statusCd)) {
return "COMPLETED";
}
if ("FAILED".equals(statusCd)) {
return "FAILED";
}
if ("STOPPED".equals(statusCd) || "CANCELED".equals(statusCd)) {
return "CANCELED";
}
if ("QUEUED".equals(statusCd)) {
return "QUEUED";
}
// RUNNING 상태
boolean isStep2 = "EVAL".equals(jobType) || "TEST".equals(jobType);
String prefix = isStep2 ? "STEP2_" : "STEP1_";
if (currentEpoch == null || totalEpoch == null) {
if (startedDttm == null) {
return prefix + "PREPARING";
}
long elapsed = ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
if (elapsed < 30) return prefix + "PREPARING";
if (elapsed < 60) return prefix + "CONTAINER_STARTING";
return prefix + "DATA_LOADING";
}
if (currentEpoch >= totalEpoch) {
return prefix + "PROCESSING_RESULTS";
}
return prefix + "TRAINING";
}
/** 경과 시간 계산 (초) */
public static Long calculateElapsedSeconds(ZonedDateTime startedDttm) {
if (startedDttm == null) {
return null;
}
return ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
}
/** 예상 남은 시간 계산 (초) */
public static Long estimateRemainingSeconds(double progressPercent, Long elapsedSeconds) {
if (elapsedSeconds == null || progressPercent <= 0.0) {
return null;
}
double remaining = (100.0 - progressPercent) / progressPercent;
return (long) (elapsedSeconds * remaining);
}
/**
* 상태 메시지 생성
*
* @param phase 현재 Phase
* @param currentEpoch 현재 Epoch
* @param totalEpoch 전체 Epoch
* @return 상태 메시지
*/
public static String generateProgressMessage(
String phase, Integer currentEpoch, Integer totalEpoch) {
if (phase == null) {
return "대기중";
}
// Step 구분
boolean isStep1 = phase.startsWith("STEP1_");
boolean isStep2 = phase.startsWith("STEP2_");
String stepName = isStep1 ? "학습" : isStep2 ? "테스트" : "";
switch (phase) {
case "QUEUED":
return "학습 작업이 큐에 등록되었습니다.";
case "STEP1_PREPARING":
return "학습 준비 중입니다.";
case "STEP1_CONTAINER_STARTING":
return "학습용 Docker 컨테이너를 시작하는 중입니다.";
case "STEP1_DATA_LOADING":
return "학습 데이터를 로딩하는 중입니다.";
case "STEP1_TRAINING":
if (currentEpoch != null && totalEpoch != null) {
return String.format("학습 진행 중 (Epoch %d/%d)", currentEpoch, totalEpoch);
}
return "학습 진행 중";
case "STEP1_PROCESSING_RESULTS":
return "학습 결과를 처리하는 중입니다.";
case "STEP2_PREPARING":
return "테스트 준비 중입니다.";
case "STEP2_CONTAINER_STARTING":
return "테스트용 Docker 컨테이너를 시작하는 중입니다.";
case "STEP2_DATA_LOADING":
return "테스트 데이터를 로딩하는 중입니다.";
case "STEP2_TRAINING":
if (currentEpoch != null && totalEpoch != null) {
return String.format("테스트 진행 중 (Epoch %d/%d)", currentEpoch, totalEpoch);
}
return "테스트 진행 중";
case "STEP2_PROCESSING_RESULTS":
return "테스트 결과를 처리하는 중입니다.";
case "COMPLETED":
return "학습이 성공적으로 완료되었습니다.";
case "FAILED":
return "학습이 실패했습니다.";
case "CANCELED":
return "학습이 취소되었습니다.";
default:
return stepName + " 진행 중";
}
}
}