Handler.cs 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. using Application.Abstractions.Crypto;
  2. using Application.Abstractions.Data;
  3. using Application.Abstractions.Messaging;
  4. using Application.Abstractions.YouTube;
  5. using Microsoft.EntityFrameworkCore;
  6. using Microsoft.Extensions.Logging;
  7. using SharedKernel.Results;
  8. namespace Application.Features.Api.MyPage.UnlinkYouTube;
  9. /// <summary>
  10. /// YouTube 연동 해제 처리:
  11. /// 1. 회원의 MemberOAuthToken (Google provider) revoke + DB row 삭제
  12. /// 2. 회원의 Channel.WipeYouTubeData() 호출 — YT 출처 필드 모두 null·0
  13. /// 3. Redis YouTubeChannel:{channelID} 캐시 삭제
  14. ///
  15. /// 회원 본인 또는 Google Security 설정에서 권한 회수 시 호출됨.
  16. /// 탈퇴 Handler 와 동일한 wipe 절차, member.Withdraw() 호출만 제외.
  17. /// </summary>
  18. internal sealed class Handler(
  19. IAppDbContext db,
  20. IGoogleOAuthService oauthService,
  21. IYouTubeChannelCache youTubeChannelCache,
  22. IFieldEncryptor fieldEncryptor,
  23. ILogger<Handler> logger
  24. ) : ICommandHandler<Command, Result>
  25. {
  26. public async Task<Result> Handle(Command request, CancellationToken ct)
  27. {
  28. var memberExists = await db.Member.AnyAsync(m => m.ID == request.MemberID, ct);
  29. if (!memberExists)
  30. {
  31. return Result.Failure(Error.NotFound("MyPage.MemberNotFound", "회원 정보를 찾을 수 없습니다."));
  32. }
  33. var oauthTokens = await db.MemberOAuthToken.Where(t => t.MemberID == request.MemberID).ToListAsync(ct);
  34. var purgedChannelIds = new List<string>();
  35. foreach (var token in oauthTokens)
  36. {
  37. var encrypted = token.RefreshTokenEnc ?? token.AccessTokenEnc;
  38. if (!string.IsNullOrEmpty(encrypted))
  39. {
  40. try
  41. {
  42. var plaintext = fieldEncryptor.Decrypt(encrypted, 1);
  43. if (!string.IsNullOrEmpty(plaintext))
  44. {
  45. await oauthService.RevokeTokenAsync(plaintext, ct);
  46. }
  47. }
  48. catch (Exception ex)
  49. {
  50. logger.LogWarning(ex, "[UnlinkYouTube] OAuth revoke 실패 (memberID={MemberID}) — row 삭제는 진행", request.MemberID);
  51. }
  52. }
  53. db.MemberOAuthToken.Remove(token);
  54. }
  55. var channels = await db.Channel.Where(c => c.MemberID == request.MemberID).ToListAsync(ct);
  56. foreach (var channel in channels)
  57. {
  58. if (!string.IsNullOrEmpty(channel.YouTubeChannelID))
  59. {
  60. purgedChannelIds.Add(channel.YouTubeChannelID);
  61. }
  62. channel.WipeYouTubeData();
  63. }
  64. if (oauthTokens.Count == 0 && channels.Count == 0)
  65. {
  66. return Result.Success();
  67. }
  68. await db.SaveChangesAsync(ct);
  69. foreach (var channelID in purgedChannelIds)
  70. {
  71. try
  72. {
  73. await youTubeChannelCache.RemoveAsync(channelID);
  74. }
  75. catch (Exception ex)
  76. {
  77. logger.LogWarning(ex, "[UnlinkYouTube] Redis YouTube 캐시 제거 실패 (channelID={ChannelID})", channelID);
  78. }
  79. }
  80. logger.LogInformation(
  81. "[UnlinkYouTube] memberID={MemberID} — tokens={Tokens}, channels={Channels}, cache={Cache}",
  82. request.MemberID, oauthTokens.Count, channels.Count, purgedChannelIds.Count
  83. );
  84. return Result.Success();
  85. }
  86. }