diff --git a/src/main/java/com/hlab/yanalyst/domain/category/CategoryService.java b/src/main/java/com/hlab/yanalyst/domain/category/CategoryService.java index 5b311b8..dc93a5b 100644 --- a/src/main/java/com/hlab/yanalyst/domain/category/CategoryService.java +++ b/src/main/java/com/hlab/yanalyst/domain/category/CategoryService.java @@ -63,4 +63,21 @@ public class CategoryService { public long countVideos(Long categoryId) { return channelVideoRepository.countByCategoryId(categoryId); } + + /** 대시보드용 카테고리 분포: 카테고리별 영상 수 + 미분류 수. */ + public java.util.Map distribution() { + java.util.List> cats = new java.util.ArrayList<>(); + for (Category c : getAll()) { + java.util.Map m = new java.util.LinkedHashMap<>(); + m.put("id", c.getId()); + m.put("name", c.getName()); + m.put("color", c.getColor()); + m.put("count", channelVideoRepository.countByCategoryId(c.getId())); + cats.add(m); + } + java.util.Map result = new java.util.LinkedHashMap<>(); + result.put("categories", cats); + result.put("uncategorized", channelVideoRepository.countByCategoryIdIsNull()); + return result; + } } diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java index 4136780..1a942fe 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java @@ -190,7 +190,12 @@ public class ChannelVideoCurationService { /** 대시보드/칸반용 파이프라인 통계: 총계 + 상태별/출처별 분포. */ public java.util.Map pipelineStats() { java.util.Map stats = new java.util.LinkedHashMap<>(); - stats.put("total", channelVideoRepository.count()); + long total = channelVideoRepository.count(); + stats.put("total", total); + + long shorts = channelVideoRepository.countByIsShortsTrue(); + stats.put("shorts", shorts); + stats.put("longForm", Math.max(0, total - shorts)); java.util.Map byStatus = new java.util.LinkedHashMap<>(); for (String s : ALLOWED_STATUS) { diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java index 410788d..b04ff1d 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java @@ -17,6 +17,8 @@ public interface ChannelVideoRepository extends JpaRepository { Optional findByChannelVideoId(Long channelVideoId); List findByStatus(String status, Sort sort); + long countByStatus(String status); + + /** + * 전체 조회(정렬 포함). {@code findAll(Sort)}는 Criteria 기반이라 nullsLast/First 같은 + * null precedence를 지원하지 않으므로(UnsupportedOperationException), Sort를 HQL ORDER BY로 + * 붙이는 @Query 방식을 사용한다. + */ + @Query("select p from PublishPackage p") + List findAllSorted(Sort sort); } diff --git a/src/main/java/com/hlab/yanalyst/domain/publish/PublishService.java b/src/main/java/com/hlab/yanalyst/domain/publish/PublishService.java index a2f5131..18ed926 100644 --- a/src/main/java/com/hlab/yanalyst/domain/publish/PublishService.java +++ b/src/main/java/com/hlab/yanalyst/domain/publish/PublishService.java @@ -55,12 +55,27 @@ public class PublishService { return repository.save(p); } + /** 대시보드용 발행 요약: 상태별 카운트 + 최근 5건. */ + public java.util.Map dashboardSummary() { + java.util.Map byStatus = new java.util.LinkedHashMap<>(); + for (String s : ALLOWED_STATUS) { + byStatus.put(s, repository.countByStatus(s)); + } + List all = list(null); + List recent = all.size() > 5 ? all.subList(0, 5) : all; + java.util.Map result = new java.util.LinkedHashMap<>(); + result.put("byStatus", byStatus); + result.put("total", (long) all.size()); + result.put("recent", recent); + return result; + } + /** 발행 큐: 상태(null이면 전체)로 필터, 예약일 → 수정일 순. */ public List list(String status) { Sort sort = Sort.by(Sort.Order.asc("scheduledAt").nullsLast(), Sort.Order.desc("updatedAt")); if (StringUtils.hasText(status)) { return repository.findByStatus(status, sort); } - return repository.findAll(sort); + return repository.findAllSorted(sort); } } diff --git a/src/main/java/com/hlab/yanalyst/service/DashboardService.java b/src/main/java/com/hlab/yanalyst/service/DashboardService.java new file mode 100644 index 0000000..5744259 --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/service/DashboardService.java @@ -0,0 +1,38 @@ +package com.hlab.yanalyst.service; + +import com.hlab.yanalyst.domain.category.CategoryService; +import com.hlab.yanalyst.domain.channel.ChannelVideo; +import com.hlab.yanalyst.domain.channel.ChannelVideoCurationService; +import com.hlab.yanalyst.domain.publish.PublishService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 홈 대시보드용 단일 집계 — 파이프라인(수집→큐레이션→발행) 현황을 한 번에 묶어 반환한다. + * 각 도메인 서비스의 집계를 조합만 한다(리포지토리 직접 접근 없음). + */ +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DashboardService { + + private final ChannelVideoCurationService curationService; + private final CategoryService categoryService; + private final PublishService publishService; + + public Map summary() { + Map result = new LinkedHashMap<>(); + result.put("pipeline", curationService.pipelineStats()); // total, byStatus, bySource, shorts, longForm + result.put("categories", categoryService.distribution()); // categories[], uncategorized + result.put("publish", publishService.dashboardSummary()); // byStatus, total, recent + List outperformers = curationService.findOutperformers(5, BigDecimal.ONE); + result.put("outperformers", outperformers); + return result; + } +} diff --git a/src/main/java/com/hlab/yanalyst/web/DashboardApiController.java b/src/main/java/com/hlab/yanalyst/web/DashboardApiController.java new file mode 100644 index 0000000..f2ca583 --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/web/DashboardApiController.java @@ -0,0 +1,28 @@ +package com.hlab.yanalyst.web; + +import com.hlab.yanalyst.global.common.ApiResponse; +import com.hlab.yanalyst.service.DashboardService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/dashboard") +@RequiredArgsConstructor +@Tag(name = "Dashboard API", description = "홈 대시보드 집계") +public class DashboardApiController { + + private final DashboardService dashboardService; + + @GetMapping("/summary") + @Operation(summary = "대시보드 요약", + description = "파이프라인(수집/상태/출처/포맷) + 카테고리 분포 + 발행 현황 + 떡상 후보 TOP5 를 한 번에 반환.") + public ApiResponse> summary() { + return ApiResponse.ok(dashboardService.summary()); + } +} diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 9950b44..8b346ef 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -9,112 +9,222 @@
-

Dashboard

-

Overview of your YouTube analytics and tracking.

+
+
+

Dashboard

+

수집 → 큐레이션 → 재가공 → 발행 파이프라인 현황

+
+ +
- -
+ +
-
-
-

수집 영상

-

-

-
-
- -
+
+

수집 영상

-

+
-
채널 + 검색 수집
+
채널 - · 검색 -
-
-
-
-

작업대상 (TARGET)

-

-

-
-
- -
+
+

미검토 (NEW)

-

+
-
검토중 -
+
검토중 -
-
-
-
-

미검토 (NEW)

-

-

-
-
- -
+
+

작업대상 (TARGET)

-

+
-
제외 -
+
재가공 대상
+
+
+
+

완료 (DONE)

-

+
+
+
제외 -
+
+
+
+

발행완료

-

+
+
+
대기 - · 작성중 -
-
+ +
+
+

파이프라인 현황

+ +
+
+

로딩 중...

+
+
+ + +

🚀 떡상 후보 TOP

- 수집함 → -
-
-

로딩 중...

+ 발굴 →
+

로딩 중...

-

수집 출처

-
-

로딩 중...

+
+

발행 현황

+ 발행 큐 →
+
+
+ +
+
+
+

카테고리 분포

+ 수집함 → +
+

로딩 중...

+
+
+

수집 출처 & 포맷

+

로딩 중...

+
+
+ + +
- \ No newline at end of file +