using ExperionCrawler.Core.Application.Services; using Xunit; namespace ExperionCrawler.Tests; /// /// KoreanTimeRangeExtractor 단위 테스트 /// 새로운 한글 시간 패턴 파싱 로직을 검증합니다. /// public class KoreanTimeRangeExtractorTests { private readonly KoreanTimeRangeExtractor _extractor; private readonly KstClock _kstClock; public KoreanTimeRangeExtractorTests() { // 테스트용 고정 시계: KST 2026-04-23 12:00:00 = UTC 2026-04-23 03:00:00 var fixedClock = new FixedClock(new DateTimeOffset(2026, 4, 23, 3, 0, 0, TimeSpan.Zero)); _kstClock = new KstClock(fixedClock); _extractor = new KoreanTimeRangeExtractor(_kstClock); } #region 절대 범위: 부터 ~ 까지 [Fact] public void AbsoluteRange_FromTo_KoreanDate() { // Arrange & Act var result = _extractor.Extract("4월 3일 부터 4월 5일 까지"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); Assert.Null(result.PostgresInterval); // KST 2026-04-03 00:00 ~ 2026-04-06 00:00 (까지의 다음날) Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(4, result.KstFrom.Value.Month); Assert.Equal(3, result.KstFrom.Value.Day); Assert.Equal(2026, result.KstTo!.Value.Year); Assert.Equal(4, result.KstTo.Value.Month); Assert.Equal(6, result.KstTo.Value.Day); // 다음날 00:00 } [Fact] public void AbsoluteRange_FromTo_IsoDate() { // Arrange & Act var result = _extractor.Extract("2026-04-03 부터 2026-04-05 까지"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); } [Fact] public void AbsoluteRange_FromTo_WithTime() { // Arrange & Act var result = _extractor.Extract("어제 09:00 부터 오늘 18:00 까지"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); // KST 2026-04-22 09:00 ~ 2026-04-23 18:00 Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(4, result.KstFrom.Value.Month); Assert.Equal(22, result.KstFrom.Value.Day); Assert.Equal(9, result.KstFrom.Value.Hour); Assert.Equal(2026, result.KstTo!.Value.Year); Assert.Equal(4, result.KstTo.Value.Month); Assert.Equal(23, result.KstTo.Value.Day); Assert.Equal(18, result.KstTo.Value.Hour); } [Fact] public void AbsoluteRange_FromTo_WithAmPm() { // Arrange & Act var result = _extractor.Extract("오전 9시부터 오후 6시까지"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); // KST 2026-04-23 09:00 ~ 18:00 Assert.Equal(9, result.KstFrom!.Value.Hour); Assert.Equal(18, result.KstTo!.Value.Hour); } [Fact] public void AbsoluteRange_WithTimeComponent_PreservesTime() { // Arrange & Act var result = _extractor.Extract("2026-03-01 14:00 부터 2026-03-01 16:30 까지"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); Assert.Equal(14, result.KstFrom!.Value.Hour); Assert.Equal(0, result.KstFrom.Value.Minute); Assert.Equal(16, result.KstTo!.Value.Hour); Assert.Equal(30, result.KstTo.Value.Minute); } #endregion #region 단방향: 이후/이전 [Fact] public void OneDirection_After() { // Arrange & Act var result = _extractor.Extract("오늘 오후 2시 이후"); // Assert Assert.NotNull(result.KstFrom); Assert.Null(result.KstTo); Assert.Null(result.PostgresInterval); // KST 2026-04-23 14:00 Assert.Equal(14, result.KstFrom!.Value.Hour); } [Fact] public void OneDirection_Before() { // Arrange & Act var result = _extractor.Extract("2026-05-05 이전"); // Assert Assert.Null(result.KstFrom); Assert.NotNull(result.KstTo); Assert.Null(result.PostgresInterval); // KST 2026-05-05 Assert.Equal(2026, result.KstTo!.Value.Year); Assert.Equal(5, result.KstTo.Value.Month); Assert.Equal(5, result.KstTo.Value.Day); } [Fact] public void OneDirection_FromOnly() { // Arrange & Act var result = _extractor.Extract("4월 3일 부터"); // Assert Assert.NotNull(result.KstFrom); Assert.Null(result.KstTo); Assert.Null(result.PostgresInterval); } #endregion #region 상대 범위: 최근/지난 N시간|분|일 [Theory] [InlineData("최근 3시간", "3 hours")] [InlineData("지난 2시간", "2 hours")] [InlineData("최근 30분", "30 minutes")] [InlineData("지난 15분", "15 minutes")] [InlineData("최근 7일", "7 days")] [InlineData("지난 30일", "30 days")] [InlineData("최근 2주", "14 days")] public void RelativeRange_CorrectInterval(string input, string expectedInterval) { // Arrange & Act var result = _extractor.Extract(input); // Assert Assert.Equal(expectedInterval, result.PostgresInterval); Assert.Null(result.KstFrom); Assert.Null(result.KstTo); } #endregion #region 지정 날짜: 오늘/어제/이번 주 [Fact] public void NamedDay_Today() { // Arrange & Act var result = _extractor.Extract("오늘"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); // KST 2026-04-23 00:00 ~ 2026-04-24 00:00 Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(4, result.KstFrom.Value.Month); Assert.Equal(23, result.KstFrom.Value.Day); Assert.Equal(2026, result.KstTo!.Value.Year); Assert.Equal(4, result.KstTo.Value.Month); Assert.Equal(24, result.KstTo.Value.Day); } [Fact] public void NamedDay_Yesterday() { // Arrange & Act var result = _extractor.Extract("어제"); // Assert Assert.NotNull(result.KstFrom); Assert.NotNull(result.KstTo); // KST 2026-04-22 00:00 ~ 2026-04-23 00:00 Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(4, result.KstFrom.Value.Month); Assert.Equal(22, result.KstFrom.Value.Day); Assert.Equal(2026, result.KstTo!.Value.Year); Assert.Equal(4, result.KstTo.Value.Month); Assert.Equal(23, result.KstTo.Value.Day); } [Fact] public void NamedDay_ThisWeek() { // Arrange & Act var result = _extractor.Extract("이번 주"); // Assert Assert.NotNull(result.KstFrom); Assert.Null(result.KstTo); // KST 2026-04-20 (월요일 - 2026-04-23은 목요일이므로 3일 전) Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(4, result.KstFrom.Value.Month); // 2026-04-23은 목요일(Thursday = 4), 월요일은 4-3 = 4-20 Assert.Equal(20, result.KstFrom.Value.Day); } #endregion #region 기본값 [Fact] public void Default_InvalidInput_ReturnsOneHourInterval() { // Arrange & Act var result = _extractor.Extract("PV_101.PV 평균값"); // Assert Assert.Equal("1 hour", result.PostgresInterval); Assert.Null(result.KstFrom); Assert.Null(result.KstTo); } [Fact] public void Default_EmptyInput_ReturnsOneHourInterval() { // Arrange & Act var result = _extractor.Extract(""); // Assert Assert.Equal("1 hour", result.PostgresInterval); Assert.Null(result.KstFrom); Assert.Null(result.KstTo); } #endregion #region ToSqlCondition Tests [Fact] public void ToSqlCondition_RelativeRange_UsesInterval() { // Arrange var result = _extractor.Extract("최근 3시간"); // Act var sql = result.ToSqlCondition("\"time\"", _kstClock); // Assert Assert.Equal("\"time\" >= NOW() - INTERVAL '3 hours'", sql); } [Fact] public void ToSqlCondition_AbsoluteRange_BothSides() { // Arrange var result = _extractor.Extract("오늘"); // Act var sql = result.ToSqlCondition("\"time\"", _kstClock); // Assert // KST 2026-04-23 00:00 = UTC 2026-04-22 15:00 // KST 2026-04-24 00:00 = UTC 2026-04-23 15:00 Assert.Contains("\"time\" >=", sql); Assert.Contains("AND \"time\" <", sql); Assert.Contains("2026-04-22 15:00:00+00", sql); Assert.Contains("2026-04-23 15:00:00+00", sql); } [Fact] public void ToSqlCondition_FromOnly() { // Arrange var result = _extractor.Extract("오늘 오후 2시 이후"); // Act var sql = result.ToSqlCondition("\"time\"", _kstClock); // Assert // KST 2026-04-23 14:00 = UTC 2026-04-23 05:00 Assert.Contains("\"time\" >=", sql); Assert.DoesNotContain("AND", sql); Assert.Contains("2026-04-23 05:00:00+00", sql); } [Fact] public void ToSqlCondition_ToOnly() { // Arrange var result = _extractor.Extract("2026-05-05 이전"); // Act var sql = result.ToSqlCondition("\"time\"", _kstClock); // Assert // KST 2026-05-05 00:00 = UTC 2026-05-04 15:00 Assert.Contains("\"time\" <", sql); Assert.DoesNotContain("AND", sql); Assert.Contains("2026-05-04 15:00:00+00", sql); } #endregion #region ParseKstDateTime Tests (via Extract) // ParseKstDateTime은 internal이므로 Extract를 통해 간접 테스트합니다. [Theory] [InlineData("오늘", "2026-04-23")] [InlineData("어제", "2026-04-22")] [InlineData("4월 3일", "2026-04-03")] public void Extract_KoreanDate_ParsesCorrectly(string input, string expectedDate) { // Arrange & Act var result = _extractor.Extract(input); // Assert Assert.NotNull(result.KstFrom); Assert.Equal(expectedDate, result.KstFrom!.Value.ToString("yyyy-MM-dd")); } [Fact] public void Extract_WithTime_PreservesTime() { // Arrange & Act var result = _extractor.Extract("오늘 14:30 이후"); // Assert Assert.NotNull(result.KstFrom); Assert.Equal(14, result.KstFrom!.Value.Hour); Assert.Equal(30, result.KstFrom.Value.Minute); } [Fact] public void Extract_WithAmPm_ConvertsTo24Hour() { // Arrange & Act var result = _extractor.Extract("오후 3시 이후"); // Assert Assert.NotNull(result.KstFrom); Assert.Equal(15, result.KstFrom!.Value.Hour); } [Fact] public void Extract_IsoFormat_ParsesCorrectly() { // Arrange & Act var result = _extractor.Extract("2026-03-01 14:00 이후"); // Assert Assert.NotNull(result.KstFrom); Assert.Equal(2026, result.KstFrom!.Value.Year); Assert.Equal(3, result.KstFrom.Value.Month); Assert.Equal(1, result.KstFrom.Value.Day); Assert.Equal(14, result.KstFrom.Value.Hour); } #endregion }