diff --git a/backend/src/main/java/com/worlabel/domain/model/service/AiModelService.java b/backend/src/main/java/com/worlabel/domain/model/service/AiModelService.java index af27141..47b500c 100644 --- a/backend/src/main/java/com/worlabel/domain/model/service/AiModelService.java +++ b/backend/src/main/java/com/worlabel/domain/model/service/AiModelService.java @@ -127,7 +127,7 @@ public class AiModelService { } @CheckPrivilege(PrivilegeType.EDITOR) - public void train(final Integer projectId, Integer modelId) { + public void train(final Integer projectId, final Integer modelId) { trainProgressCheck(projectId); // FastAPI 서버로 학습 요청을 전송 diff --git a/backend/src/main/java/com/worlabel/domain/report/controller/ReportController.java b/backend/src/main/java/com/worlabel/domain/report/controller/ReportController.java new file mode 100644 index 0000000..8652b37 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/report/controller/ReportController.java @@ -0,0 +1,24 @@ +package com.worlabel.domain.report.controller; + +import com.worlabel.domain.report.entity.dto.ReportResponse; +import com.worlabel.domain.report.service.ReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/reports") +@RequiredArgsConstructor +public class ReportController { + + private final ReportService reportService; + + @GetMapping("/model/{model_id}") + public List getReportsByModelId(@PathVariable("model_id") final Integer modelId) { + return reportService.getReportsByModelId(modelId); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/report/entity/Report.java b/backend/src/main/java/com/worlabel/domain/report/entity/Report.java new file mode 100644 index 0000000..55172c7 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/report/entity/Report.java @@ -0,0 +1,51 @@ +package com.worlabel.domain.report.entity; + +import com.worlabel.domain.model.entity.AiModel; +import com.worlabel.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "report") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Report extends BaseEntity { + + @Id + @Column(name = "report_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 소속된 모델 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "model_id", nullable = false) + private AiModel aiModel; + + /** + * 전체 에포크 + */ + @Column(name = "total_epochs", nullable = false) + private Integer totalEpochs; + + /** + * 현재 에포크 + */ + @Column(name = "epoch", nullable = false) + private Integer epoch; + + @Column(name = "box_loss", nullable = false) + private double boxLoss; + + @Column(name = "cls_loss", nullable = false) + private double clsLoss; + + @Column(name = "dfl_loss", nullable = false) + private double dflLoss; + + @Column(name = "fitness", nullable = false) + private double fitness; +} diff --git a/backend/src/main/java/com/worlabel/domain/report/entity/dto/ReportResponse.java b/backend/src/main/java/com/worlabel/domain/report/entity/dto/ReportResponse.java new file mode 100644 index 0000000..6ad1d3d --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/report/entity/dto/ReportResponse.java @@ -0,0 +1,29 @@ +package com.worlabel.domain.report.entity.dto; + +import com.worlabel.domain.report.entity.Report; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ReportResponse { + private Integer id; + private Integer totalEpochs; + private Integer epoch; + private double boxLoss; + private double clsLoss; + private double dflLoss; + private double fitness; + + public static ReportResponse from(final Report report) { + return new ReportResponse( + report.getId(), + report.getTotalEpochs(), + report.getEpoch(), + report.getBoxLoss(), + report.getClsLoss(), + report.getDflLoss(), + report.getFitness()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/report/repository/ReportRepository.java b/backend/src/main/java/com/worlabel/domain/report/repository/ReportRepository.java new file mode 100644 index 0000000..025bf44 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/report/repository/ReportRepository.java @@ -0,0 +1,11 @@ +package com.worlabel.domain.report.repository; + +import com.worlabel.domain.report.entity.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReportRepository extends JpaRepository { + + List findByAiModelId(Integer modelId); +} diff --git a/backend/src/main/java/com/worlabel/domain/report/service/ReportService.java b/backend/src/main/java/com/worlabel/domain/report/service/ReportService.java new file mode 100644 index 0000000..d0315ee --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/report/service/ReportService.java @@ -0,0 +1,25 @@ +package com.worlabel.domain.report.service; + +import com.worlabel.domain.report.entity.Report; +import com.worlabel.domain.report.entity.dto.ReportResponse; +import com.worlabel.domain.report.repository.ReportRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReportService { + + private final ReportRepository reportRepository; + + public List getReportsByModelId(final Integer modelId) { + List reports = reportRepository.findByAiModelId(modelId); + return reports.stream() + .map(ReportResponse::from) + .toList(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/result/controller/ResultController.java b/backend/src/main/java/com/worlabel/domain/result/controller/ResultController.java new file mode 100644 index 0000000..fd1366a --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/controller/ResultController.java @@ -0,0 +1,33 @@ +package com.worlabel.domain.result.controller; + +import com.worlabel.domain.result.entity.dto.ResultResponse; +import com.worlabel.domain.result.service.ResultService; +import com.worlabel.global.config.swagger.SwaggerApiError; +import com.worlabel.global.config.swagger.SwaggerApiSuccess; +import com.worlabel.global.exception.ErrorCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "결과 관련 API") +@RestController +@RequestMapping("/api/results") +@RequiredArgsConstructor +public class ResultController { + + private final ResultService resultService; + + @Operation(summary = "모델 결과 조회", description = "모델 결과를 조회합니다.") + @SwaggerApiSuccess(description = "모델 결과를 성공적으로 조회합니다") + @SwaggerApiError({ErrorCode.EMPTY_REQUEST_PARAMETER, ErrorCode.SERVER_ERROR}) + @GetMapping("/model/{model_id}") + public List getResultsByModelId(@PathVariable("model_id") final Integer modelId) { + return resultService.getResultsByModelId(modelId); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/result/entity/Optimizer.java b/backend/src/main/java/com/worlabel/domain/result/entity/Optimizer.java new file mode 100644 index 0000000..f31fd92 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/entity/Optimizer.java @@ -0,0 +1,36 @@ +package com.worlabel.domain.result.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Optimizer { + + AUTO("AUTO"), + SGD("SGD"), + ADAM("ADAM"), + ADAMW("ADAMW"), + NADAM("NADAM"), + RADAM("RADAM"), + RMSPROP("RMSPROP"); + + private final String value; + + Optimizer(String value) { + this.value = value; + } + + @JsonCreator + public static Optimizer from(String value) { + for (Optimizer status : Optimizer.values()) { + if (status.getValue().equals(value.toUpperCase())) { + return status; + } + } + return null; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/src/main/java/com/worlabel/domain/result/entity/Result.java b/backend/src/main/java/com/worlabel/domain/result/entity/Result.java new file mode 100644 index 0000000..bf5f854 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/entity/Result.java @@ -0,0 +1,61 @@ +package com.worlabel.domain.result.entity; + +import com.worlabel.domain.model.entity.AiModel; +import com.worlabel.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "result") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Result extends BaseEntity { + + @Id + @Column(name = "result_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 소속된 모델 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "model_id", nullable = false) + private AiModel aiModel; + + @Column(name = "precision", nullable = false) + private double precision; + + @Column(name = "recall", nullable = false) + private double recall; + + @Column(name = "mAP50", nullable = false) + private double mAP50; + + @Column(name = "mAP50-95", nullable = false) + private double mAP5095; + + @Column(name = "fitness", nullable = false) + private double fitness; + + @Column(name = "ratio", nullable = false) + private double ratio; + + @Column(name = "epochs", nullable = false) + private double epochs; + + @Column(name = "batch", nullable = false) + private double batch; + + @Column(name = "ir0", nullable = false) + private double ir0; + + @Column(name = "irf", nullable = false) + private double irf; + + @Column(name = "optimizer", nullable = false) + @Enumerated(EnumType.STRING) + private Optimizer optimizer; +} diff --git a/backend/src/main/java/com/worlabel/domain/result/entity/dto/ResultResponse.java b/backend/src/main/java/com/worlabel/domain/result/entity/dto/ResultResponse.java new file mode 100644 index 0000000..3afba70 --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/entity/dto/ResultResponse.java @@ -0,0 +1,41 @@ +package com.worlabel.domain.result.entity.dto; + +import com.worlabel.domain.result.entity.Optimizer; +import com.worlabel.domain.result.entity.Result; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ResultResponse { + private Integer id; + private double precision; + private double recall; + private double mAP50; + private double mAP5095; + private double fitness; + private double ratio; + private double epochs; + private double batch; + private double ir0; + private double irf; + private Optimizer optimizer; + + public static ResultResponse fromResult(final Result result) { + return new ResultResponse( + result.getId(), + result.getPrecision(), + result.getRecall(), + result.getMAP50(), + result.getMAP5095(), + result.getFitness(), + result.getRatio(), + result.getEpochs(), + result.getBatch(), + result.getIr0(), + result.getIrf(), + result.getOptimizer()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/worlabel/domain/result/repository/ResultRepository.java b/backend/src/main/java/com/worlabel/domain/result/repository/ResultRepository.java new file mode 100644 index 0000000..cca499b --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/repository/ResultRepository.java @@ -0,0 +1,11 @@ +package com.worlabel.domain.result.repository; + +import com.worlabel.domain.result.entity.Result; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ResultRepository extends JpaRepository { + + List findByAiModelId(Integer modelId); +} diff --git a/backend/src/main/java/com/worlabel/domain/result/service/ResultService.java b/backend/src/main/java/com/worlabel/domain/result/service/ResultService.java new file mode 100644 index 0000000..aeb6a7e --- /dev/null +++ b/backend/src/main/java/com/worlabel/domain/result/service/ResultService.java @@ -0,0 +1,25 @@ +package com.worlabel.domain.result.service; + +import com.worlabel.domain.result.entity.Result; +import com.worlabel.domain.result.entity.dto.ResultResponse; +import com.worlabel.domain.result.repository.ResultRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ResultService { + + private final ResultRepository resultRepository; + + public List getResultsByModelId(final Integer modelId) { + List results = resultRepository.findByAiModelId(modelId); + return results.stream() + .map(ResultResponse::fromResult) + .toList(); + } +}