fix: preserve search input and add choseong lookup

This commit is contained in:
2026-07-21 11:23:42 +09:00
parent 5ccf8b8ffd
commit b6208c0971
11 changed files with 801 additions and 64 deletions

View File

@@ -0,0 +1,56 @@
using System.Text;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.Core.Tests;
public sealed class HangulSearchMatcherTests
{
[Theory]
[InlineData("삼성전자", "ㅅㅅㅈㅈ")]
[InlineData("홍길동 위원", "ㅎㄱㄷ ㅇㅇ")]
[InlineData("까치산", "ㄲㅊㅅ")]
[InlineData("삼성전자(NXT)", "ㅅㅅㅈㅈ")]
[InlineData("삼성전자", "ᄉᄉᄌᄌ")]
public void IsMatch_MatchesCompatibilityOrLeadingInitialConsonants(
string candidate,
string query)
{
Assert.True(HangulSearchMatcher.IsMatch(candidate, query));
Assert.True(HangulSearchMatcher.IsInitialConsonantQuery(query));
}
[Fact]
public void IsMatch_NormalizesDecomposedHangulAndKeepsOrdinarySubstringSearch()
{
var decomposed = "삼성전자".Normalize(NormalizationForm.FormD);
Assert.True(HangulSearchMatcher.IsMatch(decomposed, "ㅅㅅㅈㅈ"));
Assert.True(HangulSearchMatcher.IsMatch("김전문 연구원", "전문"));
Assert.True(HangulSearchMatcher.IsMatch("Alpha Analyst", "analyst"));
}
[Theory]
[InlineData("삼성전자", "ㅅ성")]
[InlineData("삼성전자", "ㅏ")]
public void IsMatch_DoesNotTreatMixedOrNonInitialJamoAsFuzzyInput(
string candidate,
string query)
{
Assert.False(HangulSearchMatcher.IsMatch(candidate, query));
Assert.False(HangulSearchMatcher.IsInitialConsonantQuery(query));
}
[Fact]
public void IsMatch_RecognizesAValidDoubleInitialWithoutCreatingAFalseMatch()
{
Assert.True(HangulSearchMatcher.IsInitialConsonantQuery("ㄲㅅ"));
Assert.False(HangulSearchMatcher.IsMatch("삼성전자", "ㄲㅅ"));
}
[Fact]
public void IsMatch_BlankQueryRetainsExistingInitialListBehavior()
{
Assert.True(HangulSearchMatcher.IsMatch("아무 이름", " "));
Assert.False(HangulSearchMatcher.IsInitialConsonantQuery(" "));
}
}

View File

@@ -72,6 +72,46 @@ public sealed class LegacyExpertSelectionServiceTests
Assert.Equal(2, executor.Calls[0].Spec.Parameters[1].Value);
}
[Fact]
public async Task SearchAsync_MatchesHangulInitialConsonantsAfterBoundedCandidateRead()
{
var executor = new RecordingExecutor(_ => SearchTable(
("박전문", "0003"),
("김투자", "0002"),
("김전문", "0001"),
("강지민", "0004")));
var service = new LegacyExpertSelectionService(executor);
var result = await service.SearchAsync(" ㄱㅈㅁ ", maximumResults: 10);
Assert.Equal("ㄱㅈㅁ", result.Query);
Assert.False(result.IsTruncated);
Assert.Equal(
["강지민", "김전문"],
result.Items.Select(item => item.Identity.ExpertName));
var call = Assert.Single(executor.Calls);
Assert.Equal(string.Empty, call.Spec.Parameters[0].Value);
Assert.Equal(LegacyExpertSelectionService.MaximumResults + 1,
call.Spec.Parameters[1].Value);
Assert.DoesNotContain("ㄱㅈㅁ", call.Spec.Sql, StringComparison.Ordinal);
}
[Fact]
public async Task SearchAsync_AppliesRequestedLimitAfterInitialConsonantFiltering()
{
var executor = new RecordingExecutor(_ => SearchTable(
("김전문", "0001"),
("강지민", "0002"),
("고정민", "0003")));
var result = await new LegacyExpertSelectionService(executor)
.SearchAsync("ᄀᄌᄆ", maximumResults: 1);
Assert.Single(result.Items);
Assert.Equal("강지민", result.Items[0].Identity.ExpertName);
Assert.True(result.IsTruncated);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]