| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Microsoft.EntityFrameworkCore;
- using SharedKernel.Results;
- namespace Application.Features.Api.Stocks.Watch.Toggle;
- /// <summary>
- /// 관심종목 토글 — 회원×종목코드가 있으면 해제, 없으면 등록. Stock.Code 활성 존재를 AnyAsync 로 사전 검증(FK 없이 denorm). (MemberID,StockCode) UNIQUE.
- /// </summary>
- public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<Response>>
- {
- public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
- {
- var code = request.StockCode?.Trim() ?? "";
- var existing = await db.StockWatch.FirstOrDefaultAsync(c => c.MemberID == request.MemberID && c.StockCode == code, ct);
- bool isWatched;
- if (existing is not null)
- {
- // 이미 관심 등록된 종목은 상장폐지(IsActive=false)돼도 항상 해제 가능해야 하므로 존재검증을 건너뛴다
- db.StockWatch.Remove(existing);
- isWatched = false;
- }
- else
- {
- // 신규 등록만 종목 존재(활성) 검증 — FK 없이 denorm 이라 사전 AnyAsync
- var exists = await db.Stock.AsNoTracking().AnyAsync(c => c.Code == code && c.IsActive, ct);
- if (!exists)
- {
- return Result.Failure<Response>(Error.NotFound("StockWatch.StockNotFound", "종목을 찾을 수 없습니다."));
- }
- var watch = Domain.Entities.Stocks.StockWatch.Create(request.MemberID, code);
- await db.StockWatch.AddAsync(watch, ct);
- isWatched = true;
- }
- try
- {
- await db.SaveChangesAsync(ct);
- }
- catch (DbUpdateConcurrencyException) when (existing is not null)
- {
- // 동시 해제 경합(대상 행이 이미 삭제됨) — idempotent 성공 처리
- return new Response(false);
- }
- catch (DbUpdateException) when (existing is null)
- {
- // 동시 등록 경합(UNIQUE 위반) — 다른 요청이 이미 등록 완료 — idempotent 성공 처리
- return new Response(true);
- }
- return new Response(isWatched);
- }
- }
|