V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,8 @@
using Dapper;using FastEndpoints;using Npgsql;using Kbx.Shared.Runtime;using Shared.Problems;
namespace Kbx.Modules.Common.Integrations.Attempts.Get;
public sealed record Response(Guid Id,string IntegrationId,string MessageId,string AggregateType,string AggregateId,string State,int AttemptNo,string? FailureCode,string? Detail,DateTimeOffset StartedAt,DateTimeOffset? CompletedAt,DateTimeOffset? NextRetryAt,string CorrelationId);
public sealed class Endpoint(NpgsqlDataSource dataSource):EndpointWithoutRequest<object>
{
public override void Configure(){Get("/api/integrations/attempts/{attemptId:guid}");Permissions("common.integration.read");}
public override async Task HandleAsync(CancellationToken ct){var attemptId=Route<Guid>("attemptId");await using var c=await dataSource.OpenConnectionAsync(ct);const string sql="""select id, integration_id as IntegrationId, message_id::text as MessageId, aggregate_type as AggregateType, aggregate_id as AggregateId, state, attempt_no as AttemptNo, failure_code as FailureCode, detail, started_at as StartedAt, completed_at as CompletedAt, next_retry_at as NextRetryAt, correlation_id as CorrelationId from kbx.integration_attempts where id=@Id and tenant_id=@TenantId""";var row=await c.QuerySingleOrDefaultAsync<Response>(new CommandDefinition(sql,new{Id=attemptId,TenantId=RuntimeIdentity.TenantId(User)},cancellationToken:ct));if(row is null){await Send.ResponseAsync(KbxNotFoundProblem.Create("INTEGRATION_ATTEMPT_NOT_FOUND","연계 이력을 찾을 수 없습니다."),404,cancellation:ct);return;}await Send.OkAsync(row,ct);}
}
@@ -0,0 +1,47 @@
using System.Text.Json;
using Dapper;
using FastEndpoints;
using Npgsql;
using Kbx.Shared.Runtime;
using Shared.Problems;
namespace Kbx.Modules.Common.Integrations.Attempts.Retry;
public sealed record Request(string? IdempotencyKey = null);
public sealed record Response(Guid AttemptId,string State,string Message);
public sealed class Endpoint(NpgsqlDataSource dataSource):Endpoint<Request,object>
{
private const string OperationId = "common.integrations.retryAttempt";
public override void Configure(){Post("/api/integrations/attempts/{attemptId:guid}/retry");Permissions("common.integration.retry");}
public override async Task HandleAsync(Request req,CancellationToken ct)
{
var attemptId=Route<Guid>("attemptId");
var key=HttpContext.Request.Headers["Idempotency-Key"].FirstOrDefault()??req.IdempotencyKey;
if(string.IsNullOrWhiteSpace(key)){
await Send.ResponseAsync(KbxValidationProblem.Create(new KbxValidationError(null,null,"IDEMPOTENCY_KEY_REQUIRED","안전한 재처리를 위해 Idempotency-Key가 필요합니다.")),400,cancellation:ct);return;
}
await using var c=await dataSource.OpenConnectionAsync(ct);
await using var tx=await c.BeginTransactionAsync(ct);
var replay=await c.QuerySingleOrDefaultAsync<string?>(new CommandDefinition(
"select response_json::text from kbx.command_receipts where operation_id=@OperationId and idempotency_key=@Key",
new { OperationId, Key=key },tx,cancellationToken:ct));
if(replay is not null){await tx.RollbackAsync(ct);await Send.OkAsync(JsonSerializer.Deserialize<Response>(replay)!,ct);return;}
const string sql="""
update kbx.integration_attempts
set state='retrying', next_retry_at=now(), manual_retry_key=@Key
where id=@Id and tenant_id=@TenantId and state='failed'
returning id;
""";
var id=await c.ExecuteScalarAsync<Guid?>(new CommandDefinition(sql,new{Id=attemptId,TenantId=RuntimeIdentity.TenantId(User),Key=key},tx,cancellationToken:ct));
if(id is null){await tx.RollbackAsync(ct);await Send.ResponseAsync(KbxNotFoundProblem.Create("INTEGRATION_ATTEMPT_NOT_RETRYABLE","재처리할 수 있는 연계 실패를 찾지 못했습니다."),404,cancellation:ct);return;}
var response=new Response(id.Value,"retrying","재처리를 예약했습니다.");
await c.ExecuteAsync(new CommandDefinition("insert into kbx.command_receipts(operation_id,idempotency_key,response_json) values(@OperationId,@Key,cast(@Json as jsonb))",
new { OperationId, Key=key, Json=JsonSerializer.Serialize(response) },tx,cancellationToken:ct));
await tx.CommitAsync(ct);
await Send.OkAsync(response,ct);
}
}