From db6a9831b69f973018304274b4ebe0329dfe9013 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Mon, 3 Aug 2026 12:00:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=88=8F=ED=8F=BC=20=ED=81=90=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=C2=B7REST=20API=20=E2=80=94=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D/=EC=A1=B0=ED=9A=8C/=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=A0=80=EC=9E=A5/import/=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit status=PENDING 필터는 shortform-queue 스킬의 큐 조회용. Co-Authored-By: Claude Fable 5 --- .../domain/shortform/ShortformController.java | 51 ++++++++++++++ .../domain/shortform/ShortformService.java | 67 +++++++++++++++++++ .../domain/shortform/dto/ShortformDtos.java | 46 +++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 src/main/java/com/hlab/yanalyst/domain/shortform/ShortformController.java create mode 100644 src/main/java/com/hlab/yanalyst/domain/shortform/ShortformService.java create mode 100644 src/main/java/com/hlab/yanalyst/domain/shortform/dto/ShortformDtos.java diff --git a/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformController.java b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformController.java new file mode 100644 index 0000000..fd27a34 --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformController.java @@ -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 register(@RequestBody RegisterRequest request) { + return ApiResponse.created(shortformService.register(request.youtubeUrl())); + } + + @GetMapping("/jobs") + public ApiResponse> list(@RequestParam(required = false) ShortformJobStatus status) { + return ApiResponse.ok(shortformService.list(status)); + } + + @GetMapping("/jobs/{id}") + public ApiResponse detail(@PathVariable Long id) { + return ApiResponse.ok(shortformService.detail(id)); + } + + @PostMapping("/jobs/{id}/result") + public ApiResponse saveResult(@PathVariable Long id, @RequestBody ResultRequest request) { + return ApiResponse.ok(shortformService.saveResult(id, request.rawText())); + } + + @PostMapping("/import") + public ApiResponse importResult(@RequestBody ImportRequest request) { + return ApiResponse.ok(shortformService.importResult(request.youtubeUrl(), request.rawText())); + } + + @DeleteMapping("/jobs/{id}") + public ApiResponse delete(@PathVariable Long id) { + shortformService.delete(id); + return ApiResponse.ok(id); + } +} diff --git a/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformService.java b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformService.java new file mode 100644 index 0000000..d3f89d2 --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformService.java @@ -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 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 list(ShortformJobStatus status) { + List 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); + } +} diff --git a/src/main/java/com/hlab/yanalyst/domain/shortform/dto/ShortformDtos.java b/src/main/java/com/hlab/yanalyst/domain/shortform/dto/ShortformDtos.java new file mode 100644 index 0000000..c6953cf --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/domain/shortform/dto/ShortformDtos.java @@ -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 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()); + } + } +}