Handler.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.YouTube;
  4. using SharedKernel.Results;
  5. using Domain.Entities.Members;
  6. using Microsoft.EntityFrameworkCore;
  7. using Microsoft.Extensions.Logging;
  8. namespace Application.Features.Api.Studio.ConnectYouTube;
  9. internal sealed class Handler(
  10. IAppDbContext db,
  11. IGoogleOAuthService oauthService,
  12. IYouTubeApiService youtubeApiService,
  13. IYouTubeChannelCache channelCache,
  14. IYouTubePubSubService pubSubService,
  15. ILogger<Handler> logger
  16. ) : ICommandHandler<Command, Result>
  17. {
  18. public async Task<Result> Handle(Command request, CancellationToken ct)
  19. {
  20. var config = await db.Config.AsNoTracking().OrderByDescending(x => x.ID).FirstOrDefaultAsync(ct);
  21. if (config is null
  22. || string.IsNullOrEmpty(config.External.GoogleClientId)
  23. || string.IsNullOrEmpty(config.External.GoogleClientSecretEnc
  24. )) {
  25. return Result.Failure(Error.Problem("YouTube.OAuthConfigMissing", "Google OAuth 설정이 없습니다."));
  26. }
  27. var tokens = await oauthService.ExchangeCodeAsync(
  28. request.Code,
  29. request.RedirectUri,
  30. config.External.GoogleClientId,
  31. config.External.GoogleClientSecretEnc,
  32. ct
  33. );
  34. if (tokens is null)
  35. {
  36. return Result.Failure(Error.Problem("YouTube.OAuthExchangeFailed", "Google 인증 코드 교환에 실패했습니다."));
  37. }
  38. var ytInfo = await youtubeApiService.GetMyChannelAsync(tokens.AccessToken, ct);
  39. if (ytInfo is null)
  40. {
  41. return Result.Failure(Error.Problem("YouTube.ChannelInfoUnavailable", "YouTube 채널 정보를 가져올 수 없습니다."));
  42. }
  43. var youtubeUrl = ytInfo.CustomUrl is not null
  44. ? $"https://youtube.com/{ytInfo.CustomUrl}"
  45. : $"https://youtube.com/channel/{ytInfo.ChannelID}";
  46. // Member 크리에이터 전환
  47. var member = await db.Member.FirstAsync(m => m.ID == request.MemberID, ct);
  48. if (!member.IsCreator)
  49. {
  50. member.SetCreator(true);
  51. }
  52. // Channel 생성 또는 업데이트
  53. var channel = await db.Channel.FirstOrDefaultAsync(c => c.MemberID == request.MemberID, ct);
  54. if (channel is null)
  55. {
  56. channel = Domain.Entities.Members.Channel.Create(request.MemberID, ytInfo.ChannelID, ytInfo.Title, youtubeUrl);
  57. db.Channel.Add(channel);
  58. }
  59. // YouTube 상세 정보 DB 저장
  60. channel.UpdateYouTubeInfo(
  61. ytInfo.ChannelID,
  62. ytInfo.Title,
  63. ytInfo.CustomUrl,
  64. ytInfo.Description,
  65. ytInfo.ThumbnailUrl,
  66. ytInfo.BannerUrl,
  67. ytInfo.SubscriberCount,
  68. ytInfo.VideoCount,
  69. ytInfo.ViewCount,
  70. ytInfo.Email,
  71. ytInfo.PublishedAt
  72. );
  73. // MemberOAuthToken 생성 또는 업데이트 (Provider = "YouTube")
  74. var oauthToken = await db.MemberOAuthToken.FirstOrDefaultAsync(t => t.MemberID == request.MemberID && t.Provider == "YouTube", ct);
  75. var scopesStr = string.Join(" ", tokens.Scopes);
  76. if (oauthToken is null)
  77. {
  78. oauthToken = MemberOAuthToken.Create(
  79. request.MemberID,
  80. "YouTube",
  81. tokens.AccessToken,
  82. tokens.RefreshToken,
  83. tokens.ExpiresAt,
  84. scopesStr
  85. );
  86. db.MemberOAuthToken.Add(oauthToken);
  87. }
  88. else
  89. {
  90. oauthToken.UpdateTokens(tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt, scopesStr);
  91. }
  92. await db.SaveChangesAsync(ct);
  93. // 캐시 갱신
  94. await channelCache.SetAsync(ytInfo);
  95. // PubSub 구독 (실패해도 OAuth 연동은 성공 처리)
  96. try
  97. {
  98. var subscribed = await pubSubService.SubscribeAsync(ytInfo.ChannelID, ct);
  99. if (!subscribed)
  100. {
  101. logger.LogWarning("[ConnectYouTube] PubSub 구독 실패 (YouTube에서 거부): channelId={ChannelId}", ytInfo.ChannelID);
  102. }
  103. }
  104. catch (Exception ex)
  105. {
  106. logger.LogError(ex, "[ConnectYouTube] PubSub 구독 중 예외: channelId={ChannelId}", ytInfo.ChannelID);
  107. }
  108. return Result.Success();
  109. }
  110. }