using Application.Abstractions.Crypto; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.YouTube; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedKernel.Results; namespace Application.Features.Api.MyPage.UnlinkYouTube; /// /// YouTube 연동 해제 처리: /// 1. 회원의 MemberOAuthToken (Google provider) revoke + DB row 삭제 /// 2. 회원의 Channel.WipeYouTubeData() 호출 — YT 출처 필드 모두 null·0 /// 3. Redis YouTubeChannel:{channelID} 캐시 삭제 /// /// 회원 본인 또는 Google Security 설정에서 권한 회수 시 호출됨. /// 탈퇴 Handler 와 동일한 wipe 절차, member.Withdraw() 호출만 제외. /// internal sealed class Handler( IAppDbContext db, IGoogleOAuthService oauthService, IYouTubeChannelCache youTubeChannelCache, IFieldEncryptor fieldEncryptor, ILogger logger ) : ICommandHandler { public async Task Handle(Command request, CancellationToken ct) { var memberExists = await db.Member.AnyAsync(m => m.ID == request.MemberID, ct); if (!memberExists) { return Result.Failure(Error.NotFound("MyPage.MemberNotFound", "회원 정보를 찾을 수 없습니다.")); } var oauthTokens = await db.MemberOAuthToken.Where(t => t.MemberID == request.MemberID).ToListAsync(ct); var purgedChannelIds = new List(); foreach (var token in oauthTokens) { var encrypted = token.RefreshTokenEnc ?? token.AccessTokenEnc; if (!string.IsNullOrEmpty(encrypted)) { try { var plaintext = fieldEncryptor.Decrypt(encrypted, 1); if (!string.IsNullOrEmpty(plaintext)) { await oauthService.RevokeTokenAsync(plaintext, ct); } } catch (Exception ex) { logger.LogWarning(ex, "[UnlinkYouTube] OAuth revoke 실패 (memberID={MemberID}) — row 삭제는 진행", request.MemberID); } } db.MemberOAuthToken.Remove(token); } var channels = await db.Channel.Where(c => c.MemberID == request.MemberID).ToListAsync(ct); foreach (var channel in channels) { if (!string.IsNullOrEmpty(channel.YouTubeChannelID)) { purgedChannelIds.Add(channel.YouTubeChannelID); } channel.WipeYouTubeData(); } if (oauthTokens.Count == 0 && channels.Count == 0) { return Result.Success(); } await db.SaveChangesAsync(ct); foreach (var channelID in purgedChannelIds) { try { await youTubeChannelCache.RemoveAsync(channelID); } catch (Exception ex) { logger.LogWarning(ex, "[UnlinkYouTube] Redis YouTube 캐시 제거 실패 (channelID={ChannelID})", channelID); } } logger.LogInformation( "[UnlinkYouTube] memberID={MemberID} — tokens={Tokens}, channels={Channels}, cache={Cache}", request.MemberID, oauthTokens.Count, channels.Count, purgedChannelIds.Count ); return Result.Success(); } }