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.Sns.Disconnect; internal sealed class Handler( IAppDbContext db, IGoogleOAuthService oauthService, ILogger logger ) : ICommandHandler> { private static readonly string[] AllowedProviders = ["Google"]; private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(500); public async Task> Handle(Command request, CancellationToken ct) { if (!AllowedProviders.Contains(request.Provider)) { return Result.Failure(Error.Problem("Sns.UnsupportedProvider", "지원하지 않는 SNS provider 입니다.")); } var token = await db.MemberOAuthToken.FirstOrDefaultAsync(t => t.MemberID == request.MemberID && t.Provider == request.Provider, ct); if (token is null) { // idempotent: 이미 연동되어 있지 않아도 성공으로 처리 return Result.Success(new Response(request.Provider, true)); } // Google revoke 시도 — refresh_token 우선 (refresh_token revoke 는 access_token 도 함께 무효화) var revokeTarget = !string.IsNullOrEmpty(token.RefreshTokenEnc) ? token.RefreshTokenEnc : token.AccessTokenEnc; var revoked = await oauthService.RevokeTokenAsync(revokeTarget, ct); if (!revoked) { // 1회 재시도 (네트워크 일시 장애/Google 5xx 대비) try { await Task.Delay(RetryDelay, ct); revoked = await oauthService.RevokeTokenAsync(revokeTarget, ct); } catch (OperationCanceledException) { throw; } } // DB 삭제 — revoke 성공/실패 무관 (DB 삭제는 항상 진행) db.MemberOAuthToken.Remove(token); await db.SaveChangesAsync(ct); if (!revoked) { logger.LogWarning("[Sns.Disconnect] revoke 실패 — DB 만 삭제. memberID={MemberID}, provider={Provider}", request.MemberID, request.Provider); } else { logger.LogInformation("[Sns.Disconnect] 연동 해제 완료: memberID={MemberID}, provider={Provider}", request.MemberID, request.Provider); } return Result.Success(new Response(request.Provider, revoked)); } }