using Application.Abstractions.Data; using Microsoft.EntityFrameworkCore; namespace Application.Abstractions.Channels; /// /// URL 파라미터(@handle 또는 YouTube Channel ID)를 내부 Channel.SID 로 변환하는 헬퍼 /// - `@` 로 시작하면 현재 핸들 → SID 로 조회 /// - 그 외에는 SID 로 간주 (YouTube Channel ID 24자) /// - 현재 핸들로 일치하는 채널이 없으면 이전 핸들 이력 조회 후 현재 핸들/SID 반환 /// public static class ChannelIdentifierResolver { public enum IdentifierKind { Handle, Sid } /// /// 가 null 이면 해당 식별자에 대응하는 채널이 없음을 의미. /// 이 입력 핸들과 다르면 이전 핸들에서 변경된 채널임을 의미 (리다이렉트 대상). /// public readonly record struct ResolveResult( string? ChannelSID, string? CanonicalHandle, IdentifierKind Kind ); public static IdentifierKind DetectKind(string identifier) { if (!string.IsNullOrEmpty(identifier) && identifier[0] == '@') { return IdentifierKind.Handle; } return IdentifierKind.Sid; } public static string NormalizeHandle(string identifier) { if (string.IsNullOrEmpty(identifier)) { return string.Empty; } return identifier[0] == '@' ? identifier[1..] : identifier; } /// /// 입력 식별자를 Channel.SID 로 정규화. /// - SID 입력: DB 조회 없이 그대로 반환 (존재 여부는 후속 Handler 가 검증) /// - Handle 입력: 현재 핸들과 일치하는 채널 조회, 없으면 이력 테이블 조회하여 현재 핸들 반환 /// public static async Task ResolveAsync(IAppDbContext db, string identifier, CancellationToken ct) { if (string.IsNullOrWhiteSpace(identifier)) { return new ResolveResult(null, null, IdentifierKind.Sid); } var kind = DetectKind(identifier); if (kind == IdentifierKind.Sid) { return new ResolveResult(identifier, null, IdentifierKind.Sid); } var handle = NormalizeHandle(identifier); var current = await db.Channel.AsNoTracking() .Where(c => c.Handle == handle) .Select(c => new { c.SID, c.Handle }) .FirstOrDefaultAsync(ct); if (current is not null) { return new ResolveResult(current.SID, current.Handle, IdentifierKind.Handle); } // 핸들 변경 이력 조회 — 이전 핸들이면 현재 핸들/SID 반환 var historical = await db.ChannelHandleHistory.AsNoTracking() .Where(h => h.OldHandle == handle) .OrderByDescending(h => h.ChangedAt) .Select(h => new { h.Channel.SID, h.Channel.Handle }) .FirstOrDefaultAsync(ct); if (historical is not null) { return new ResolveResult(historical.SID, historical.Handle, IdentifierKind.Handle); } return new ResolveResult(null, null, IdentifierKind.Handle); } }