package com.hlab.yanalyst.domain.channel;
import com.hlab.yanalyst.domain.channel.dto.MyChannelSummaryDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/**
* 내 채널(OWN) 분석.
*
*
내가 이미 발행한 영상은 소재가 아니라 성과다. 수집함·칸반·발굴·피드 어디에도 섞이지 않도록
* 출처를 OWN 으로 분리해 두고(자세한 건 {@link ChannelRole}), 이 화면에서만 다룬다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MyChannelService {
/** 랭킹에 올릴 소재의 최소 편수. 1편짜리 우연을 상위에 띄우지 않기 위함. */
private static final int MIN_TOPIC_VIDEOS = 2;
private final ChannelRepository channelRepository;
private final ChannelVideoRepository channelVideoRepository;
private final ChannelSnapshotRepository channelSnapshotRepository;
private final ChannelService channelService;
/** 등록된 내 채널. 없으면 empty. */
public Optional findOwnChannel() {
List own = channelRepository.findOwnChannels();
return own.isEmpty() ? Optional.empty() : Optional.of(own.get(0));
}
public MyChannelSummaryDto summary() {
Channel c = findOwnChannel().orElse(null);
if (c == null) return MyChannelSummaryDto.empty();
List videos = channelVideoRepository.findByChannelId(c.getId());
long avg = TopicPerformance.channelAverage(toEntries(videos));
LocalDateTime since = LocalDateTime.now().minusDays(30);
long last30 = videos.stream()
.filter(v -> v.getPublishedAt() != null && v.getPublishedAt().isAfter(since))
.mapToLong(v -> v.getViewCount() == null ? 0 : v.getViewCount())
.sum();
return new MyChannelSummaryDto(true, c.getId(), c.getChannelId(), c.getTitle(), c.getThumbnailUrl(),
c.getSubscriberCount(), c.getViewCount(), c.getVideoCount(), videos.size(), avg, last30);
}
/** 해시태그(소재)별 성과 랭킹. */
public List topics() {
Channel c = findOwnChannel().orElse(null);
if (c == null) return List.of();
return TopicPerformance.aggregate(toEntries(channelVideoRepository.findByChannelId(c.getId())),
MIN_TOPIC_VIDEOS);
}
/** 내 영상 목록(최신순). */
public List videos() {
Channel c = findOwnChannel().orElse(null);
if (c == null) return List.of();
List list = new ArrayList<>(channelVideoRepository.findByChannelId(c.getId()));
list.sort(Comparator.comparing(ChannelVideo::getPublishedAt,
Comparator.nullsLast(Comparator.reverseOrder())));
return list;
}
/** 성장 추이(일별 스냅샷, 오래된 순). */
public List growth() {
Channel c = findOwnChannel().orElse(null);
if (c == null) return List.of();
return channelSnapshotRepository.findByChannelIdOrderBySnapshotDateAsc(c.getId());
}
/**
* 내 채널을 등록한다. 이미 등록돼 있으면 교체한다(내 채널은 하나만 둔다).
* 등록 즉시 영상을 수집해 분석할 데이터를 채운다.
*/
@Transactional
public MyChannelSummaryDto register(String url) {
findOwnChannel().ifPresent(prev -> {
log.info("[MyChannel] 기존 내 채널 {} 을(를) 교체합니다", prev.getTitle());
channelService.deleteChannel(prev.getId());
});
Channel channel = channelService.saveChannelFromUrl(url);
channel.changeRole(ChannelRole.OWN);
channelRepository.save(channel);
channelService.collectChannelVideos(channel.getId());
return summary();
}
/** 내 채널 영상을 다시 동기화한다. */
@Transactional
public MyChannelSummaryDto sync() {
Channel c = findOwnChannel()
.orElseThrow(() -> new IllegalArgumentException("등록된 내 채널이 없습니다."));
channelService.collectChannelVideos(c.getId());
return summary();
}
/** 내 채널 등록을 해제하고 수집된 영상도 함께 지운다. */
@Transactional
public void unregister() {
Channel c = findOwnChannel()
.orElseThrow(() -> new IllegalArgumentException("등록된 내 채널이 없습니다."));
channelService.deleteChannel(c.getId());
}
private List toEntries(List videos) {
List out = new ArrayList<>(videos.size());
for (ChannelVideo v : videos) {
out.add(new TopicPerformance.Entry(v.getHashtags(), v.getViewCount(), v.getTitle(), v.getVideoId()));
}
return out;
}
}