Handler.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.YouTube;
  4. using Domain.Entities.Members;
  5. using Microsoft.EntityFrameworkCore;
  6. namespace Application.Features.Api.Studio.ConnectYouTube;
  7. internal sealed class Handler(
  8. IAppDbContext db,
  9. IGoogleOAuthService oauthService,
  10. IYouTubeApiService youtubeApiService,
  11. IYouTubeChannelCache channelCache
  12. ) : ICommandHandler<Command>
  13. {
  14. public async Task Handle(Command request, CancellationToken ct)
  15. {
  16. var config = await db.Config.AsNoTracking().OrderByDescending(x => x.ID).FirstOrDefaultAsync(ct);
  17. if (config is null
  18. || string.IsNullOrEmpty(config.External.GoogleClientId)
  19. || string.IsNullOrEmpty(config.External.GoogleClientSecretEnc
  20. )) {
  21. throw new InvalidOperationException("Google OAuth 설정이 없습니다.");
  22. }
  23. var tokens = await oauthService.ExchangeCodeAsync(
  24. request.Code,
  25. request.RedirectUri,
  26. config.External.GoogleClientId,
  27. config.External.GoogleClientSecretEnc,
  28. ct
  29. );
  30. if (tokens is null)
  31. {
  32. throw new InvalidOperationException("Google 인증 코드 교환에 실패했습니다.");
  33. }
  34. var ytInfo = await youtubeApiService.GetMyChannelAsync(tokens.AccessToken, ct);
  35. if (ytInfo is null)
  36. {
  37. throw new InvalidOperationException("YouTube 채널 정보를 가져올 수 없습니다.");
  38. }
  39. var youtubeUrl = ytInfo.CustomUrl is not null
  40. ? $"https://youtube.com/{ytInfo.CustomUrl}"
  41. : $"https://youtube.com/channel/{ytInfo.ChannelID}";
  42. // Member 크리에이터 전환
  43. var member = await db.Member.FirstAsync(m => m.ID == request.MemberID, ct);
  44. if (!member.IsCreator)
  45. {
  46. member.SetCreator(true);
  47. }
  48. // Channel 생성 또는 업데이트
  49. var channel = await db.Channel.FirstOrDefaultAsync(c => c.MemberID == request.MemberID, ct);
  50. if (channel is null)
  51. {
  52. channel = Domain.Entities.Members.Channel.Create(request.MemberID, ytInfo.ChannelID, ytInfo.Title, youtubeUrl);
  53. db.Channel.Add(channel);
  54. }
  55. // YouTube 상세 정보 DB 저장
  56. channel.UpdateYouTubeInfo(
  57. ytInfo.ChannelID,
  58. ytInfo.Title,
  59. ytInfo.CustomUrl,
  60. ytInfo.Description,
  61. ytInfo.ThumbnailUrl,
  62. ytInfo.BannerUrl,
  63. ytInfo.SubscriberCount,
  64. ytInfo.VideoCount,
  65. ytInfo.ViewCount,
  66. ytInfo.Email,
  67. ytInfo.PublishedAt
  68. );
  69. // MemberOAuthToken 생성 또는 업데이트 (Provider = "YouTube")
  70. var oauthToken = await db.MemberOAuthToken.FirstOrDefaultAsync(t => t.MemberID == request.MemberID && t.Provider == "YouTube", ct);
  71. var scopesStr = string.Join(" ", tokens.Scopes);
  72. if (oauthToken is null)
  73. {
  74. oauthToken = MemberOAuthToken.Create(
  75. request.MemberID,
  76. "YouTube",
  77. tokens.AccessToken,
  78. tokens.RefreshToken,
  79. tokens.ExpiresAt,
  80. scopesStr
  81. );
  82. db.MemberOAuthToken.Add(oauthToken);
  83. }
  84. else
  85. {
  86. oauthToken.UpdateTokens(tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresAt, scopesStr);
  87. }
  88. await db.SaveChangesAsync(ct);
  89. // 캐시 갱신
  90. await channelCache.SetAsync(ytInfo);
  91. }
  92. }