Explorar o código

feat(market): KOSIS 지표를 고용·물가·대외 7종으로 조정 + 실업률 오라벨 교정

지정 7종(config 주도, 라이브 전수 확정):
- 고용률(DT_1DA7004S itmId T90) · 실업률(T80) · 취업자수(T30) — 경제활동인구조사 동일표 itmId 분리
- 소비자물가지수(DT_1J22003, C1=T10 전국)
- 환율 원/달러(DT_2KAA811, 연평균 — 월별 BOK표는 objL2 2단계라 유보)
- 수출액/수입액(DT_1R11006_FRM101 itmId T1/T2, 월, USD)
- 제거: GDP성장률·경기선행지수·소비자심리·전산업생산 (config에서만, 코드 보존)

**버그 교정**: 기존 실업률 itmId=T90 은 실제 고용률(63.6%) — 실업률은 T80(2.8%). "실업률" 이름으로 고용률 값을 수집하던 오라벨을 재검증으로 적발·교정.

- 실업률(T80) 라이브 캡처 fixture + 고용통계 파싱/C1 필터 회귀 테스트 2종
- 스키마 무변경(마이그레이션 없음). 실제 지표 리스트는 appsettings/Jenkins Secret(추적 밖), AppSetting 기본값은 [] 유지(바인더 append 중복 방지)
- 환율 granularity(연평균) vs koreaexim 일별 — 운영 전 월별 필요시 objL2 추가 or koreaexim 월집계 결정

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KIM-JINO5 hai 2 semanas
pai
achega
c19ac98f87

+ 3 - 3
Domain/Entities/Stocks/MacroIndicator.cs

@@ -4,20 +4,20 @@ namespace Domain.Entities.Stocks;
 
 /// <summary>
 /// 거시경제지표 시계열 (KOSIS 국가통계포털 OpenAPI 수집) — (Code, Period) UNIQUE. 개미투자 거시 대시보드용.
-/// 여러 지표(CPI/GDP/실업률/경기선행지수/CSI/전산업생산지수)를 한 테이블에 담고 Code(내부 지표코드)로 구분한다.
+/// 여러 지표(고용률/실업률/취업자수/CPI/환율/수출액/수입액 등)를 한 테이블에 담고 Code(내부 지표코드)로 구분한다.
 /// KOSIS statisticsData.do 응답 필드 매핑:
 ///  - PRD_DE(수록시점) → Period. 주기(Cycle)에 따라 형식이 다르다: 월=YYYYMM(202401), 분기=YYYYQ(20241), 연=YYYY(2024).
 ///  - DT(수치) → Value. 빈값("")·"-" 등은 수집 시 skip (미고시 시점은 적재하지 않음).
 ///  - UNIT_NM(단위) → Unit. 지표별 config Name 은 Code 와 함께 표시용으로 별도 보관한다.
 /// KOSIS 통계표는 하나의 tblId 안에 여러 분류(C1 시도별/지수별 등)가 섞여 오므로, 수집 배치가 config 의 분류 필터(C1)로
-/// 원하는 한 계열(예: CPI 전국 총지수, 경기선행종합지수)만 골라 upsert 한다.
+/// 원하는 한 계열(예: CPI 전국 총지수, 실업률 전국(계), 수출액 국가 총계)만 골라 upsert 한다.
 /// </summary>
 public class MacroIndicator
 {
     [Key]
     public long ID { get; private set; }
 
-    /// <summary>지표 내부코드 (config Code) — 예: CPI, GDP, UNEMP, LEADING, CSI, ALLIND</summary>
+    /// <summary>지표 내부코드 (config Code) — 예: EMPLOYMENT_RATE, UNEMPLOYMENT_RATE, EMPLOYED_PERSONS, CPI, FX_USD, EXPORT_AMT, IMPORT_AMT</summary>
     public string Code { get; private set; } = default!;
 
     /// <summary>지표 표시명 (config Name) — 예: 소비자물가지수</summary>

+ 1 - 1
Infrastructure/DependencyInjection.cs

@@ -375,7 +375,7 @@ public static class DependencyInjection
             services.AddHostedService<MacroDataSyncService>();
         }
 
-        // 거시경제지표 수집 (KOSIS statisticsData.do — CPI/GDP/실업률/경기선행지수/CSI/전산업생산지수) — Kosis:Sync 플래그 (기본 false). config Indicators 순회 upsert. Kosis HttpClient 사용
+        // 거시경제지표 수집 (KOSIS statisticsData.do — 고용률/실업률/취업자수/CPI/환율/수출액/수입액 7종) — Kosis:Sync 플래그 (기본 false). config Indicators 순회 upsert. Kosis HttpClient 사용
         var kosis = configuration.GetSection("Kosis").Get<AppSettings.KosisSection>() ?? new AppSettings.KosisSection();
 
         if (kosis.Sync)

+ 2 - 1
SharedKernel/AppSetting.cs

@@ -439,7 +439,8 @@ public sealed class AppSettings
 
         /// <summary>
         /// 수집 대상 지표 정의 목록. 각 항목의 tblId/itmId/분류코드 교정은 여기 config 에서만 하면 되고 코드 변경 불필요.
-        /// 기본 6종 (개미투자 확정) — CPI/GDP/실업률/경기선행지수/CSI/전산업생산지수. tblId/itmId 미확정 지표는 운영 전 KOSIS 카탈로그로 확정 필요.
+        /// 기본 7종 (사용자 확정) — 고용률/실업률/취업자수(경제활동인구조사 DT_1DA7004S 동일표, itmId T90/T80/T30)·소비자물가지수(CPI)·환율(FX_USD, DT_2KAA811 연평균)·수출액·수입액(무역통계 DT_1R11006_FRM101, itmId …T1/…T2).
+        /// 이 C# 기본값은 의도적으로 빈 목록([]) — .NET 구성 바인더가 List 에 append 하므로 여기에 항목을 넣으면 JSON(appsettings) 의 Indicators 와 중복(2배)된다. 실제 7종은 각 환경 appsettings(및 배포 템플릿)에 정의한다.
         /// </summary>
         public List<KosisIndicator> Indicators { get; init; } = [];
 

+ 5 - 0
Tests/Application.Tests/Fixtures/Kosis/employment.json

@@ -0,0 +1,5 @@
+[
+{"C1_OBJ_NM":"시도별","DT":"2.9","C1":"00","PRD_SE":"M","UNIT_NM_ENG":"%","ITM_ID":"T80","TBL_ID":"DT_1DA7004S","ITM_NM":"실업률","TBL_NM":"행정구역(시도)별 경제활동인구","PRD_DE":"202504","LST_CHN_DE":"2025-04-30","C1_NM_ENG":"Total","C1_NM":"계","UNIT_NM":"%","ITM_NM_ENG":"Unemployment rate","ORG_ID":"101","C1_OBJ_NM_ENG":"By province"},
+{"C1_OBJ_NM":"시도별","DT":"2.8","C1":"00","PRD_SE":"M","UNIT_NM_ENG":"%","ITM_ID":"T80","TBL_ID":"DT_1DA7004S","ITM_NM":"실업률","TBL_NM":"행정구역(시도)별 경제활동인구","PRD_DE":"202505","LST_CHN_DE":"2025-05-29","C1_NM_ENG":"Total","C1_NM":"계","UNIT_NM":"%","ITM_NM_ENG":"Unemployment rate","ORG_ID":"101","C1_OBJ_NM_ENG":"By province"},
+{"C1_OBJ_NM":"시도별","DT":"3.8","C1":"11","PRD_SE":"M","UNIT_NM_ENG":"%","ITM_ID":"T80","TBL_ID":"DT_1DA7004S","ITM_NM":"실업률","TBL_NM":"행정구역(시도)별 경제활동인구","PRD_DE":"202505","LST_CHN_DE":"2025-05-29","C1_NM_ENG":"Seoul","C1_NM":"서울특별시","UNIT_NM":"%","ITM_NM_ENG":"Unemployment rate","ORG_ID":"101","C1_OBJ_NM_ENG":"By province"}
+]

+ 21 - 0
Tests/Application.Tests/KosisParserTests.cs

@@ -28,6 +28,27 @@ public sealed class KosisParserTests
         Assert.AreEqual("11", rows[2].ClassCode, "서울 C1=11");
     }
 
+    [TestMethod]
+    public void Parse_EmploymentFixture_MapsUnemploymentSeries()
+    {
+        // 라이브 캡처 실업률(경제활동인구조사 DT_1DA7004S, itmId=T80) 트림본(3행): 전국(00) 2행 + 서울(11) 1행. 전부 유효 DT.
+        var json = File.ReadAllText(Path.Combine(FixtureRoot, "employment.json"));
+
+        var rows = KosisParser.Parse(json);
+
+        Assert.AreEqual(3, rows.Count);
+
+        var apr = rows[0];
+        Assert.AreEqual("202504", apr.Period);
+        Assert.AreEqual(2.9m, apr.Value);
+        Assert.AreEqual("%", apr.Unit);
+        Assert.AreEqual("00", apr.ClassCode, "전국 C1=00");
+        Assert.AreEqual("계", apr.ClassName);
+        Assert.AreEqual("실업률", apr.ItemName);
+
+        Assert.AreEqual("11", rows[2].ClassCode, "서울 C1=11");
+    }
+
     [TestMethod]
     public void Parse_ErrorObjectResponse_ReturnsEmpty()
     {

+ 34 - 0
Tests/Application.Tests/KosisSyncTests.cs

@@ -26,6 +26,19 @@ public sealed class KosisSyncTests
         ClassCode = classCode
     };
 
+    private static AppSettings.KosisSection.KosisIndicator Unemployment(string classCode = "00") => new()
+    {
+        // Code 컬럼 MaxLength(40) — 테스트 고유화 접미사(GUID 32자)를 붙여도 넘지 않도록 짧은 접두사 사용 (운영 Code "UNEMPLOYMENT_RATE"=17자는 여유).
+        Code = $"UNEMP-{Guid.NewGuid():N}",
+        Name = "실업률",
+        OrgId = "101",
+        TblId = "DT_1DA7004S",
+        ItmId = "T80",
+        ObjL1 = "00",
+        Cycle = "M",
+        ClassCode = classCode
+    };
+
     [TestMethod]
     public async Task Upsert_CpiFixture_FiltersToNationalSeries_ThenIdempotent()
     {
@@ -55,6 +68,27 @@ public sealed class KosisSyncTests
         }
     }
 
+    [TestMethod]
+    public async Task Upsert_EmploymentFixture_FiltersToNationalSeries_ByC1()
+    {
+        // 고용통계(DT_1DA7004S)는 고용률/실업률/취업자수가 한 표에 있고 itmId(T90/T80/T30)로 요청단에서 계열을 가른다.
+        // 지역(C1)은 ClassCode 로 고른다. fixture = 실업률(T80) 전국(00) 2행 + 서울(11) 1행 → ClassCode=00 이면 전국 2행만 적재.
+        var rows = KosisParser.Parse(File.ReadAllText(Path.Combine(FixtureRoot, "employment.json")));
+        var indicator = Unemployment();
+
+        using var db = TestDb.Create();
+        var (inserted, updated) = await KosisImport.UpsertAsync(db, indicator, rows, CancellationToken.None);
+        Assert.AreEqual(2, inserted, "ClassCode=00(전국) 계열만 → 2행 (서울 11 제외)");
+        Assert.AreEqual(0, updated);
+
+        var saved = await db.MacroIndicator.AsNoTracking().Where(c => c.Code == indicator.Code && c.Period == "202505").SingleAsync();
+        Assert.AreEqual(2.8m, saved.Value);
+        Assert.AreEqual("%", saved.Unit);
+
+        var count = await db.MacroIndicator.AsNoTracking().CountAsync(c => c.Code == indicator.Code);
+        Assert.AreEqual(2, count, "서울(11) 계열은 적재되지 않음");
+    }
+
     [TestMethod]
     public async Task Upsert_EmptyClassCode_AutoLocksToFirstSeries()
     {