package com.hlab.yanalyst.domain.channel; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Entity @Table(name = "youtube_channels") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @EntityListeners(AuditingEntityListener.class) public class Channel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String channelId; // YouTube Channel ID @Column(nullable = false) private String title; @Column(columnDefinition = "TEXT") private String description; @Column(length = 2083) private String thumbnailUrl; private Long subscriberCount; private Long viewCount; private Long videoCount; @Column(name = "uploads_playlist_id") private String uploadsPlaylistId; @Column(length = 8) private String country; // YouTube snippet.country (ISO-3166 alpha-2, 예: KR/JP/US). 없으면 null /** * 채널 역할: MY(내 채널) / SOURCE(소재 원본 웹예능) / RIVAL(경쟁 쇼츠). * ddl-auto:update 특성상 기존 행은 null 이므로 조회 시 null 은 MY 로 간주한다({@link ChannelRole#normalize}). */ @Column(length = 10) private String role = ChannelRole.MY; /** * 피드 수집 연속 실패 횟수. 3회 이상이면 자동 스킵해 쿼터 낭비를 막는다. * 성공하면 0으로 리셋된다. */ @Column(name = "feed_fail_count") private Integer feedFailCount = 0; private LocalDateTime publishedAt; @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; @Builder public Channel(String channelId, String title, String description, String thumbnailUrl, Long subscriberCount, Long viewCount, Long videoCount, LocalDateTime publishedAt, String uploadsPlaylistId) { this.channelId = channelId; this.title = title; this.description = description; this.thumbnailUrl = thumbnailUrl; this.subscriberCount = subscriberCount; this.viewCount = viewCount; this.videoCount = videoCount; this.publishedAt = publishedAt; this.uploadsPlaylistId = uploadsPlaylistId; } public void update(String title, String description, String thumbnailUrl, Long subscriberCount, Long viewCount, Long videoCount) { this.title = title; this.description = description; this.thumbnailUrl = thumbnailUrl; this.subscriberCount = subscriberCount; this.viewCount = viewCount; this.videoCount = videoCount; } public void setUploadsPlaylistId(String uploadsPlaylistId) { this.uploadsPlaylistId = uploadsPlaylistId; } public void setCountry(String country) { this.country = country; } /** 역할 변경. null/미인식 값은 MY 로 보정된다. */ public void changeRole(String role) { this.role = ChannelRole.normalize(role); } /** null 을 MY 로 보정한 실제 역할. */ public String roleOrDefault() { return ChannelRole.normalize(this.role); } public int feedFailCountOrZero() { return this.feedFailCount == null ? 0 : this.feedFailCount; } /** 피드 수집 실패 누적. */ public void recordFeedFailure() { this.feedFailCount = feedFailCountOrZero() + 1; } /** 피드 수집 성공 — 실패 카운터 리셋. */ public void resetFeedFailure() { this.feedFailCount = 0; } /** 연속 실패로 자동 비활성화된 상태인지. */ public boolean isFeedDisabled() { return feedFailCountOrZero() >= 3; } }