feat: 숏폼 큐 엔티티(ShortformJob/Clip)와 리포지토리

rawOutput은 파싱 성공 여부와 무관하게 원문 보존. videoId 유니크로 중복 등록 방지.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-03 11:59:12 +09:00
parent 3fda9077a6
commit 005df3225b
4 changed files with 145 additions and 0 deletions

View File

@ -0,0 +1,51 @@
package com.hlab.yanalyst.domain.shortform;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
/** 작업 하나에서 나온 구간(ID 1~5) 하나. capcutJson이 실제 편집 프로그램에 붙여넣는 값. */
@Entity
@Table(name = "shortform_clip")
@Getter
@NoArgsConstructor
public class ShortformClip {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "job_id")
private ShortformJob job;
@Column(nullable = false)
private Integer clipNo;
@Column(length = 100)
private String titleTop;
@Column(length = 200)
private String titleMain;
@Column(columnDefinition = "TEXT")
private String capcutJson;
@Column(columnDefinition = "TEXT")
private String titleCandidates;
@Column(columnDefinition = "TEXT")
private String durationTable;
static ShortformClip of(ShortformJob job, ParsedClip p) {
ShortformClip clip = new ShortformClip();
clip.job = job;
clip.clipNo = p.clipNo();
clip.titleTop = p.titleTop();
clip.titleMain = p.titleMain();
clip.capcutJson = p.capcutJson();
clip.titleCandidates = p.titleCandidates();
clip.durationTable = p.durationTable();
return clip;
}
}

View File

@ -0,0 +1,79 @@
package com.hlab.yanalyst.domain.shortform;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 숏폼 작업 하나 = 유튜브 영상 하나. Opal "숏폼 생성기" 실행 결과를 담는다.
*
* <p>rawOutput은 파싱 성공 여부와 무관하게 항상 원문을 보존한다 파서가 놓친 정보는
* 여기서 복구할 있다.
*/
@Entity
@Table(name = "shortform_job",
indexes = @Index(name = "idx_sfj_video_id", columnList = "videoId", unique = true))
@Getter
@NoArgsConstructor
public class ShortformJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 500)
private String youtubeUrl;
@Column(nullable = false, length = 20)
private String videoId;
/** 결과의 첫 클립 title_main. 목록 표시에 쓴다. */
@Column(length = 300)
private String title;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ShortformJobStatus status = ShortformJobStatus.PENDING;
@Column(columnDefinition = "TEXT")
private String rawOutput;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
private LocalDateTime completedAt;
@OneToMany(mappedBy = "job", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("clipNo ASC")
private List<ShortformClip> clips = new ArrayList<>();
public static ShortformJob create(String youtubeUrl, String videoId) {
ShortformJob job = new ShortformJob();
job.youtubeUrl = youtubeUrl;
job.videoId = videoId;
return job;
}
public void applyResult(String raw, List<ParsedClip> parsed) {
this.rawOutput = raw;
this.clips.clear();
for (ParsedClip p : parsed) {
this.clips.add(ShortformClip.of(this, p));
}
this.title = parsed.isEmpty() ? this.title : parsed.get(0).titleMain();
this.status = ShortformJobStatus.DONE;
this.completedAt = LocalDateTime.now();
}
public void markFailed(String raw) {
this.rawOutput = raw;
this.status = ShortformJobStatus.FAILED;
this.completedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,12 @@
package com.hlab.yanalyst.domain.shortform;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface ShortformJobRepository extends JpaRepository<ShortformJob, Long> {
Optional<ShortformJob> findByVideoId(String videoId);
List<ShortformJob> findAllByOrderByCreatedAtDesc();
List<ShortformJob> findByStatusOrderByCreatedAtAsc(ShortformJobStatus status);
}

View File

@ -0,0 +1,3 @@
package com.hlab.yanalyst.domain.shortform;
public enum ShortformJobStatus { PENDING, DONE, FAILED }