using Dapper; using FastEndpoints; using KBX.Shared.Experience; using Npgsql; namespace KBX.Modules.Common.Suggestions.Submit; public sealed record Request( string Category, string Message, bool IncludeScreenContext, SuggestionContext Context); public sealed record Response(Guid SuggestionId, DateTimeOffset ReceivedAt); public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint { public override void Configure() { Post("/api/common/suggestions"); Permissions("common.suggestion.create"); } public override async Task HandleAsync(Request req, CancellationToken ct) { if (req.Message.Trim().Length is < 3 or > 2000) { AddError(r => r.Message, "의견 내용은 3~2000자로 입력하세요."); await Send.ErrorsAsync(cancellation: ct); return; } if (req.Category is not ("inconvenience" or "bug" or "improvement")) { AddError(r => r.Category, "지원하지 않는 의견 유형입니다."); await Send.ErrorsAsync(cancellation: ct); return; } // Host application must replace these claims adapters with its authenticated tenant/user resolver. var tenantId = Guid.Empty; var userId = Guid.Empty; var id = Guid.NewGuid(); var now = DateTimeOffset.UtcNow; object? context = null; if (req.IncludeScreenContext) { // Deliberately persist diagnostic keys only, not arbitrary screen/business values. context = new { req.Context.ActiveFilters, req.Context.GridLayoutVersion, }; } await using var connection = await dataSource.OpenConnectionAsync(ct); await connection.ExecuteAsync(new CommandDefinition(""" insert into kbx.user_suggestions (id, tenant_id, user_id, category, message, screen_id, screen_version, route, app_version, user_role, context, status, created_at) values (@Id, @TenantId, @UserId, @Category, @Message, @ScreenId, @ScreenVersion, @Route, @AppVersion, @UserRole, cast(@Context as jsonb), 'NEW', @CreatedAt) """, new { Id = id, TenantId = tenantId, UserId = userId, req.Category, Message = req.Message.Trim(), req.Context.ScreenId, req.Context.ScreenVersion, req.Context.Route, req.Context.AppVersion, req.Context.UserRole, Context = context is null ? null : System.Text.Json.JsonSerializer.Serialize(context), CreatedAt = now, }, cancellationToken: ct)); await Send.OkAsync(new Response(id, now), ct); } }