feat: 숏폼 큐 서비스·REST API — 등록/조회/결과저장/import/삭제
status=PENDING 필터는 shortform-queue 스킬의 큐 조회용. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
005df3225b
commit
db6a9831b6
@ -0,0 +1,51 @@
|
||||
package com.hlab.yanalyst.domain.shortform;
|
||||
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.ImportRequest;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobDetail;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobSummary;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.RegisterRequest;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.ResultRequest;
|
||||
import com.hlab.yanalyst.global.common.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/shortform")
|
||||
@RequiredArgsConstructor
|
||||
public class ShortformController {
|
||||
|
||||
private final ShortformService shortformService;
|
||||
|
||||
@PostMapping("/jobs")
|
||||
public ApiResponse<JobDetail> register(@RequestBody RegisterRequest request) {
|
||||
return ApiResponse.created(shortformService.register(request.youtubeUrl()));
|
||||
}
|
||||
|
||||
@GetMapping("/jobs")
|
||||
public ApiResponse<List<JobSummary>> list(@RequestParam(required = false) ShortformJobStatus status) {
|
||||
return ApiResponse.ok(shortformService.list(status));
|
||||
}
|
||||
|
||||
@GetMapping("/jobs/{id}")
|
||||
public ApiResponse<JobDetail> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(shortformService.detail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/jobs/{id}/result")
|
||||
public ApiResponse<JobDetail> saveResult(@PathVariable Long id, @RequestBody ResultRequest request) {
|
||||
return ApiResponse.ok(shortformService.saveResult(id, request.rawText()));
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
public ApiResponse<JobDetail> importResult(@RequestBody ImportRequest request) {
|
||||
return ApiResponse.ok(shortformService.importResult(request.youtubeUrl(), request.rawText()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/jobs/{id}")
|
||||
public ApiResponse<Long> delete(@PathVariable Long id) {
|
||||
shortformService.delete(id);
|
||||
return ApiResponse.ok(id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.hlab.yanalyst.domain.shortform;
|
||||
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobDetail;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobSummary;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ShortformService {
|
||||
|
||||
private final ShortformJobRepository jobRepository;
|
||||
private final ShortformOutputParser parser;
|
||||
|
||||
/** URL 등록. 같은 videoId가 이미 있으면 그 작업을 그대로 반환한다(중복 방지). */
|
||||
@Transactional
|
||||
public JobDetail register(String youtubeUrl) {
|
||||
String videoId = YoutubeVideoId.from(youtubeUrl)
|
||||
.orElseThrow(() -> new IllegalArgumentException("유튜브 URL이 아닙니다: " + youtubeUrl));
|
||||
ShortformJob job = jobRepository.findByVideoId(videoId)
|
||||
.orElseGet(() -> jobRepository.save(ShortformJob.create(youtubeUrl.trim(), videoId)));
|
||||
return JobDetail.from(job);
|
||||
}
|
||||
|
||||
/** Opal 출력 원문을 파싱해 저장. 클립이 하나도 안 나오면 FAILED로 남긴다. */
|
||||
@Transactional
|
||||
public JobDetail saveResult(Long jobId, String rawText) {
|
||||
ShortformJob job = jobRepository.findById(jobId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("작업이 없습니다: " + jobId));
|
||||
List<ParsedClip> parsed = parser.parse(rawText);
|
||||
if (parsed.isEmpty()) {
|
||||
job.markFailed(rawText);
|
||||
} else {
|
||||
job.applyResult(rawText, parsed);
|
||||
}
|
||||
return JobDetail.from(job);
|
||||
}
|
||||
|
||||
/** 등록 + 결과 저장 한 번에 (Opal 수동 실행 후 붙여넣기 경로). */
|
||||
@Transactional
|
||||
public JobDetail importResult(String youtubeUrl, String rawText) {
|
||||
JobDetail registered = register(youtubeUrl);
|
||||
return saveResult(registered.id(), rawText);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<JobSummary> list(ShortformJobStatus status) {
|
||||
List<ShortformJob> jobs = (status == null)
|
||||
? jobRepository.findAllByOrderByCreatedAtDesc()
|
||||
: jobRepository.findByStatusOrderByCreatedAtAsc(status);
|
||||
return jobs.stream().map(JobSummary::from).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public JobDetail detail(Long jobId) {
|
||||
return jobRepository.findById(jobId).map(JobDetail::from)
|
||||
.orElseThrow(() -> new IllegalArgumentException("작업이 없습니다: " + jobId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long jobId) {
|
||||
jobRepository.deleteById(jobId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.hlab.yanalyst.domain.shortform.dto;
|
||||
|
||||
import com.hlab.yanalyst.domain.shortform.ShortformClip;
|
||||
import com.hlab.yanalyst.domain.shortform.ShortformJob;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class ShortformDtos {
|
||||
|
||||
private ShortformDtos() {}
|
||||
|
||||
public record RegisterRequest(String youtubeUrl) {}
|
||||
|
||||
public record ResultRequest(String rawText) {}
|
||||
|
||||
public record ImportRequest(String youtubeUrl, String rawText) {}
|
||||
|
||||
public record JobSummary(Long id, String youtubeUrl, String videoId, String title,
|
||||
String status, int clipCount,
|
||||
LocalDateTime createdAt, LocalDateTime completedAt) {
|
||||
public static JobSummary from(ShortformJob job) {
|
||||
return new JobSummary(job.getId(), job.getYoutubeUrl(), job.getVideoId(),
|
||||
job.getTitle(), job.getStatus().name(), job.getClips().size(),
|
||||
job.getCreatedAt(), job.getCompletedAt());
|
||||
}
|
||||
}
|
||||
|
||||
public record ClipDto(int clipNo, String titleTop, String titleMain,
|
||||
String capcutJson, String titleCandidates, String durationTable) {
|
||||
public static ClipDto from(ShortformClip clip) {
|
||||
return new ClipDto(clip.getClipNo(), clip.getTitleTop(), clip.getTitleMain(),
|
||||
clip.getCapcutJson(), clip.getTitleCandidates(), clip.getDurationTable());
|
||||
}
|
||||
}
|
||||
|
||||
public record JobDetail(Long id, String youtubeUrl, String videoId, String title,
|
||||
String status, LocalDateTime createdAt, LocalDateTime completedAt,
|
||||
List<ClipDto> clips) {
|
||||
public static JobDetail from(ShortformJob job) {
|
||||
return new JobDetail(job.getId(), job.getYoutubeUrl(), job.getVideoId(),
|
||||
job.getTitle(), job.getStatus().name(), job.getCreatedAt(),
|
||||
job.getCompletedAt(), job.getClips().stream().map(ClipDto::from).toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user