Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public enum ErrorStatus implements BaseErrorCode {
INVALID_IMAGE_PUBLISH_POLICY(HttpStatus.BAD_REQUEST, "ADMIN4025", "잘못된 이미지 게시 정책입니다. 허용값: AUTO_AFTER_QA, MANUAL"),
IMAGE_REFERENCE_CHARACTER_REQUIRED(HttpStatus.BAD_REQUEST, "ADMIN4026", "대표 이미지 후보로 사용할 인물을 찾을 수 없습니다."),
BOOK_DELETE_JOB_IN_PROGRESS(HttpStatus.CONFLICT, "ADMIN4027", "진행 중인 처리 job이 있어 책을 삭제할 수 없습니다."),
IMAGE_FANOUT_JOB_ACTIVE(HttpStatus.CONFLICT, "ADMIN4028", "해당 책의 캐릭터 이미지 fan-out job이 이미 진행 중입니다."),
IMAGE_FANOUT_JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "ADMIN4029", "캐릭터 이미지 fan-out job을 찾을 수 없습니다."),
IMAGE_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ADMIN5020", "캐릭터 이미지 생성에 실패했습니다."),

// Bookmark
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/kw/readwith/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
Expand All @@ -15,6 +16,7 @@
*/
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig implements AsyncConfigurer {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
@ConfigurationProperties(prefix = "character-image")
public class CharacterImageProperties {

private String model = "gpt-image-1";
private String editModel = "gpt-image-1";
private String model = "gpt-image-2";
private String editModel = "gpt-image-2";
private String quality = "medium";
private int width = 1024;
private int height = 1024;
Expand Down Expand Up @@ -48,5 +48,10 @@ public class CharacterImageProperties {
* 기본값: 10명
*/
private int batchSize = 10;

/**
* OpenAI Batch 상태를 다시 확인하는 간격입니다.
*/
private long batchPollIntervalMs = 30000;
}

14 changes: 12 additions & 2 deletions src/main/java/com/kw/readwith/domain/CharacterImageAsset.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.kw.readwith.domain.enums.CharacterImageAssetRole;
import com.kw.readwith.domain.enums.CharacterImageAssetStatus;
import com.kw.readwith.domain.enums.CharacterImageGenerationMode;
import com.kw.readwith.domain.processing.ProcessingJob;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand Down Expand Up @@ -56,6 +57,10 @@ public class CharacterImageAsset extends BaseEntity {
@JoinColumn(name = "source_reference_asset_id")
private CharacterImageAsset sourceReferenceAsset;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "processing_job_id")
private ProcessingJob processingJob;

@Column(name = "reference_version", nullable = false)
@Builder.Default
private int referenceVersion = 0;
Expand Down Expand Up @@ -147,7 +152,6 @@ public void beginReferenceCandidate(Character referenceCharacter, int slotNo) {
this.referenceVersion = 0;
this.slotNo = slotNo;
this.status = CharacterImageAssetStatus.GENERATING;
this.s3Url = null;
this.model = null;
this.promptHash = null;
this.qaResultJson = null;
Expand All @@ -164,10 +168,10 @@ public void beginCharacterImage(Character targetCharacter,
this.assetRole = CharacterImageAssetRole.CHARACTER_IMAGE;
this.generationMode = CharacterImageGenerationMode.REFERENCE_EDIT;
this.sourceReferenceAsset = sourceReferenceAsset;
this.processingJob = null;
this.referenceVersion = referenceVersion;
this.slotNo = null;
this.status = CharacterImageAssetStatus.GENERATING;
this.s3Url = null;
this.model = null;
this.promptHash = null;
this.qaResultJson = null;
Expand All @@ -176,4 +180,10 @@ public void beginCharacterImage(Character targetCharacter,
this.publishedAt = null;
this.attemptNo += 1;
}

public void assignProcessingJob(ProcessingJob processingJob, String model, String promptHash) {
this.processingJob = processingJob;
this.model = model;
this.promptHash = promptHash;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public enum ProcessingPipelineType {
NORMALIZATION,
AI_ANALYSIS
AI_ANALYSIS,
IMAGE_GENERATION
}
39 changes: 39 additions & 0 deletions src/main/java/com/kw/readwith/domain/processing/ProcessingJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public class ProcessingJob extends BaseEntity {
@Column(name = "artifact_path", length = 255)
private String artifactPath; // 결과물이 저장된 루트 경로

@Column(name = "external_job_id", length = 120)
private String externalJobId;

@Column(name = "input_file_id", length = 120)
private String inputFileId;

@Column(name = "output_file_id", length = 120)
private String outputFileId;

@Column(name = "error_file_id", length = 120)
private String errorFileId;

@Column(name = "rule_version", length = 50)
private String ruleVersion; // 이 job이 사용한 정규화 규칙 버전

Expand Down Expand Up @@ -114,4 +126,31 @@ public void assignNormalizationVersion(String ruleVersion, String locatorVersion
this.ruleVersion = ruleVersion;
this.locatorVersion = locatorVersion;
}

public void markExternalJobSubmitted(String externalJobId, String inputFileId, String externalStatus) {
this.externalJobId = externalJobId;
this.inputFileId = inputFileId;
this.status = ProcessingJobStatus.PROCESSING;
this.currentStep = "openai_" + externalStatus;
if (this.startedAt == null) {
this.startedAt = LocalDateTime.now();
}
this.finishedAt = null;
this.failureCode = null;
this.failureMessage = null;
}

public void updateExternalJobStatus(String externalStatus, String outputFileId, String errorFileId) {
this.currentStep = "openai_" + externalStatus;
this.outputFileId = outputFileId;
this.errorFileId = errorFileId;
}

public void advanceProcessing(String currentStep) {
this.status = ProcessingJobStatus.PROCESSING;
this.currentStep = currentStep;
if (this.startedAt == null) {
this.startedAt = LocalDateTime.now();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class AdminImageGenerationStatusResponseDTO {
)
private String nextAction;

@Schema(description = "가장 최근 캐릭터 이미지 fan-out job입니다. 대표 후보 선택 전에는 null입니다.", nullable = true)
private ProcessingJobResponseDTO fanoutJob;

@Schema(description = "서버가 자동 지정한 대표 캐릭터입니다. 후보사진 4장은 이 캐릭터 기준으로 생성됩니다.", nullable = true)
private CharacterSummary referenceCharacter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public class CharacterImageAssetDTO {
@Schema(description = "S3 공개 URL", nullable = true)
private String s3Url;

@Schema(description = "사용 모델", nullable = true, example = "gpt-image-1")
@Schema(description = "사용 모델", nullable = true, example = "gpt-image-2")
private String model;

@Schema(description = "참조 asset ID", nullable = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.kw.readwith.domain.enums.ProcessingJobStatus;
import com.kw.readwith.domain.enums.ProcessingPipelineType;
import com.kw.readwith.domain.processing.ProcessingJob;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -14,6 +15,7 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "서버 비동기 처리 job 상태")
public class ProcessingJobResponseDTO {
private Long id;
private Long bookId;
Expand All @@ -22,6 +24,14 @@ public class ProcessingJobResponseDTO {
private String runId;
private String sourceVersion;
private String artifactPath;
@Schema(description = "외부 처리 시스템의 job ID. 이미지 fan-out에서는 OpenAI Batch ID입니다.", nullable = true, example = "batch_abc123")
private String externalJobId;
@Schema(description = "OpenAI Batch 입력 JSONL file ID", nullable = true, example = "file-input123")
private String inputFileId;
@Schema(description = "OpenAI Batch 성공 결과 JSONL file ID", nullable = true, example = "file-output123")
private String outputFileId;
@Schema(description = "OpenAI Batch 실패 결과 JSONL file ID", nullable = true, example = "file-error123")
private String errorFileId;
private ProcessingJobStatus status;
private String currentStep;
private String failureCode;
Expand All @@ -42,6 +52,10 @@ public static ProcessingJobResponseDTO from(ProcessingJob job) {
.runId(job.getRunId())
.sourceVersion(job.getSourceVersion())
.artifactPath(job.getArtifactPath())
.externalJobId(job.getExternalJobId())
.inputFileId(job.getInputFileId())
.outputFileId(job.getOutputFileId())
.errorFileId(job.getErrorFileId())
.status(job.getStatus())
.currentStep(job.getCurrentStep())
.failureCode(job.getFailureCode())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.kw.readwith.domain.CharacterImageAsset;
import com.kw.readwith.domain.enums.CharacterImageAssetRole;
import com.kw.readwith.domain.enums.CharacterImageAssetStatus;
import com.kw.readwith.domain.processing.ProcessingJob;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -54,6 +55,8 @@ Optional<CharacterImageAsset> findFirstByCharacterAndStatusOrderByCreatedAtDesc(

List<CharacterImageAsset> findByBookAndStatusIn(Book book, Collection<CharacterImageAssetStatus> statuses);

List<CharacterImageAsset> findByProcessingJobOrderByIdAsc(ProcessingJob processingJob);

@Query("SELECT COALESCE(MAX(a.attemptNo), 0) FROM CharacterImageAsset a " +
"WHERE a.character = :character AND a.assetRole = :assetRole")
int findMaxAttemptNo(@Param("character") Character character,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
import org.springframework.data.domain.Pageable;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

public interface ProcessingJobLogRepository extends JpaRepository<ProcessingJobLog, Long> {

long countByJobId(Long jobId);

List<ProcessingJobLog> findAllByJobIdOrderBySeqAsc(Long jobId);

Optional<ProcessingJobLog> findTopByJobIdOrderBySeqDesc(Long jobId);

List<ProcessingJobLog> findAllByOrderByIdDesc(Pageable pageable);

@Modifying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import com.kw.readwith.domain.processing.ProcessingJob;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import jakarta.persistence.LockModeType;
import org.springframework.data.domain.Pageable;
import java.util.Collection;
import java.util.List;
Expand All @@ -30,6 +34,15 @@ List<ProcessingJob> findAllByPipelineTypeOrderByCreatedAtDesc(
Pageable pageable
);

List<ProcessingJob> findAllByPipelineTypeAndStatusInOrderByCreatedAtAsc(
ProcessingPipelineType pipelineType,
Collection<ProcessingJobStatus> statuses
);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT job FROM ProcessingJob job WHERE job.id = :id")
Optional<ProcessingJob> findByIdForUpdate(@Param("id") Long id);

boolean existsByBookIdAndStatusIn(Long bookId, Collection<ProcessingJobStatus> statuses);

List<ProcessingJob> findAllByBook(Book book);
Expand Down
Loading
Loading