using System.Text.Json; using System.Text.Json.Serialization; namespace Web.Api.Common; /// /// DB 의 모든 DateTime 컬럼은 UTC 로 저장된다(Domain Entity 가 일관되게 DateTime.UtcNow 사용). /// SQL Server datetime2 컬럼은 timezone 정보가 없어 EF Core 가 Kind = Unspecified 로 materialize 한다. /// System.Text.Json 의 기본 동작은 Unspecified DateTime 을 Z/offset 없이 출력해서, 클라이언트 JS 가 /// 로컬 시간으로 오해 → 9시간(KST 오프셋) 차이 발생. 이 converter 가 Z 마커를 강제해서 문제를 해결. /// public sealed class UtcDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var dt = reader.GetDateTime(); return dt.Kind == DateTimeKind.Utc ? dt : DateTime.SpecifyKind(dt, DateTimeKind.Utc); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { var utc = value.Kind switch { DateTimeKind.Utc => value, DateTimeKind.Local => value.ToUniversalTime(), _ => DateTime.SpecifyKind(value, DateTimeKind.Utc) }; writer.WriteStringValue(utc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); } } public sealed class UtcNullableDateTimeConverter : JsonConverter { private static readonly UtcDateTimeConverter Inner = new(); public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } return Inner.Read(ref reader, typeof(DateTime), options); } public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { if (value is null) { writer.WriteNullValue(); return; } Inner.Write(writer, value.Value, options); } }