korean-stats-mcp 1.5.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +221 -173
  2. package/dist/api/client.d.ts +1 -1
  3. package/dist/api/client.js +1 -1
  4. package/dist/cache/index.d.ts +2 -0
  5. package/dist/cache/index.d.ts.map +1 -1
  6. package/dist/cache/index.js +24 -4
  7. package/dist/cache/index.js.map +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +4 -4
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/data/districtFileMap.d.ts +27 -3
  12. package/dist/data/districtFileMap.d.ts.map +1 -1
  13. package/dist/data/districtFileMap.js +70 -3
  14. package/dist/data/districtFileMap.js.map +1 -1
  15. package/dist/data/quickStatsParams.d.ts +24 -1
  16. package/dist/data/quickStatsParams.d.ts.map +1 -1
  17. package/dist/data/quickStatsParams.js +70 -22
  18. package/dist/data/quickStatsParams.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/server-http.js +68 -12
  22. package/dist/server-http.js.map +1 -1
  23. package/dist/server.d.ts +4 -1
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +24 -4
  26. package/dist/server.js.map +1 -1
  27. package/dist/tools/analyzeTimeSeries.d.ts.map +1 -1
  28. package/dist/tools/analyzeTimeSeries.js +11 -10
  29. package/dist/tools/analyzeTimeSeries.js.map +1 -1
  30. package/dist/tools/chains.d.ts +4 -0
  31. package/dist/tools/chains.d.ts.map +1 -1
  32. package/dist/tools/chains.js +63 -42
  33. package/dist/tools/chains.js.map +1 -1
  34. package/dist/tools/compareStatistics.d.ts.map +1 -1
  35. package/dist/tools/compareStatistics.js +21 -4
  36. package/dist/tools/compareStatistics.js.map +1 -1
  37. package/dist/tools/explainStatistic.d.ts +43 -0
  38. package/dist/tools/explainStatistic.d.ts.map +1 -0
  39. package/dist/tools/explainStatistic.js +156 -0
  40. package/dist/tools/explainStatistic.js.map +1 -0
  41. package/dist/tools/fetchKosisExcel.d.ts +3 -0
  42. package/dist/tools/fetchKosisExcel.d.ts.map +1 -1
  43. package/dist/tools/fetchKosisExcel.js +33 -12
  44. package/dist/tools/fetchKosisExcel.js.map +1 -1
  45. package/dist/tools/getStatisticsData.js +2 -2
  46. package/dist/tools/getStatisticsList.js +1 -1
  47. package/dist/tools/getStatisticsList.js.map +1 -1
  48. package/dist/tools/index.d.ts +2 -0
  49. package/dist/tools/index.d.ts.map +1 -1
  50. package/dist/tools/index.js +2 -0
  51. package/dist/tools/index.js.map +1 -1
  52. package/dist/tools/quickRank.d.ts +62 -0
  53. package/dist/tools/quickRank.d.ts.map +1 -0
  54. package/dist/tools/quickRank.js +395 -0
  55. package/dist/tools/quickRank.js.map +1 -0
  56. package/dist/tools/quickStats.d.ts +4 -0
  57. package/dist/tools/quickStats.d.ts.map +1 -1
  58. package/dist/tools/quickStats.js +266 -159
  59. package/dist/tools/quickStats.js.map +1 -1
  60. package/dist/tools/quickTrend.d.ts +1 -1
  61. package/dist/tools/quickTrend.d.ts.map +1 -1
  62. package/dist/tools/quickTrend.js +79 -43
  63. package/dist/tools/quickTrend.js.map +1 -1
  64. package/dist/tools/searchStatistics.d.ts.map +1 -1
  65. package/dist/tools/searchStatistics.js +14 -11
  66. package/dist/tools/searchStatistics.js.map +1 -1
  67. package/dist/utils/concurrency.d.ts +11 -0
  68. package/dist/utils/concurrency.d.ts.map +1 -0
  69. package/dist/utils/concurrency.js +23 -0
  70. package/dist/utils/concurrency.js.map +1 -0
  71. package/dist/utils/dataFormatter.d.ts +7 -0
  72. package/dist/utils/dataFormatter.d.ts.map +1 -1
  73. package/dist/utils/dataFormatter.js +15 -0
  74. package/dist/utils/dataFormatter.js.map +1 -1
  75. package/dist/utils/districtKosisCodes.d.ts +7 -6
  76. package/dist/utils/districtKosisCodes.d.ts.map +1 -1
  77. package/dist/utils/districtKosisCodes.js +92 -42
  78. package/dist/utils/districtKosisCodes.js.map +1 -1
  79. package/dist/utils/queryParser.d.ts.map +1 -1
  80. package/dist/utils/queryParser.js +4 -2
  81. package/dist/utils/queryParser.js.map +1 -1
  82. package/dist/utils/regions.d.ts +11 -0
  83. package/dist/utils/regions.d.ts.map +1 -1
  84. package/dist/utils/regions.js +39 -1
  85. package/dist/utils/regions.js.map +1 -1
  86. package/package.json +22 -8
  87. package/dist/tools/getRecommendedStats.d.ts +0 -41
  88. package/dist/tools/getRecommendedStats.d.ts.map +0 -1
  89. package/dist/tools/getRecommendedStats.js +0 -251
  90. package/dist/tools/getRecommendedStats.js.map +0 -1
package/README.md CHANGED
@@ -1,130 +1,236 @@
1
1
  # Korean Stats MCP
2
2
 
3
- **KOSIS 통계청 OpenAPI를 12개 도구로.** 인구, 경제, 고용, 주거, 사회, 환경 등 91개 키워드 + 17개 시도 + 자치구·시군 230+ 자동 라우팅을 AI 어시스턴트에서 바로 사용.
3
+ **국가데이터처 KOSIS, 이제 사이트에 들어가지 않습니다.**
4
+ AI 어시스턴트에게 한국어로 물어보면 국가데이터처 공식 수치가 출처와 함께 바로 나옵니다.
4
5
 
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
7
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
7
8
  [![KOSIS](https://img.shields.io/badge/KOSIS-OpenAPI-green)](https://kosis.kr/openapi/)
8
9
 
9
- > 통계청 KOSIS OpenAPI 기반 MCP 서버. Claude Desktop, Cursor, Windsurf, Claude.ai 등에서 바로 사용 가능.
10
+ > 국가데이터처 KOSIS OpenAPI 기반 MCP 서버. Claude Desktop·Cursor·Claude.ai 등에서 바로 사용. 설치 없이 웹 커넥터로도 가능.
10
11
 
11
12
  ---
12
13
 
13
- ## v1.4 자연어 약어·오타·공백 정규화 + 체인 도구
14
+ ## 30초 만에 겪어보기
14
15
 
15
- **LLM이 통계청 수치를 학습 시점으로 답하는 문제를 끝낸다.** 질문마다 KOSIS 공식 DB를 직접 조회.
16
+ > 채팅창에 이렇게 칩니다 (Claude.ai 커넥터 등록 [아래 설치법](#설치-3가지-방법) 참고)
16
17
 
17
18
  ```
18
- "한국 인구가 명이야?"
19
- "GDP 추세 보여줘"
20
- "전국 17개 시도 인구·출산율·GRDP 비교"
21
- "성남시 핵심 지표 한장 브리핑"
22
- "민선 4기 출산율 추이"
19
+ 나: 광진구 고용률 알려줘
20
+
21
+ AI: 2025년 하반기 광진구의 고용률은 61.6%입니다.
22
+ 📊 출처: 고용률 (KOSIS DT_1ES3A03_A01S)
23
23
  ```
24
24
 
25
- 번의 호출로 KOSIS API 실시간 조회 + 자연어 응답. 별도 SQL·API 호출 불필요.
25
+ KOSIS 사이트에 들어가 통계표를 찾고 분류 코드를 고르고 자치구 행을 스크롤할 필요가 없습니다.
26
+ **자치구 이름과 궁금한 것만 한국어로 던지면 됩니다.**
27
+
28
+ ---
29
+
30
+ ## 공무원 업무, 이렇게 바뀝니다
31
+
32
+ 국가데이터처 통계는 [KOSIS](https://kosis.kr)에 다 있습니다. 문제는 **꺼내 쓰는 비용**입니다.
33
+ 통계표 ID를 모르면 검색에서 헤매고, 찾아도 분류·항목·주기 코드를 조립해야 하고, 자치구 단위는 표 안에서 행을 뒤져야 합니다. 그래서 보고서 마감 직전마다 KOSIS와 씨름합니다.
26
34
 
27
- **ChatGPT·Claude가 추정한 통계 수치를 그대로 믿지 마세요.** 정책 보고서, 시정 연설, 민원 답변, 연구 자료 등 수치 신뢰가 필요한 모든 곳에 필수.
35
+ 도구는 과정을 **자연어 줄**로 줄입니다. 실제 업무 장면으로 봅니다.
28
36
 
29
37
  ---
30
38
 
31
- ## 자연어 줄이면
39
+ ### 📋 시정연설·신년사·업무보고 30분이 5초로
32
40
 
33
- **그냥 자연어로 물어보세요.** AI가 키워드·지역·연도·주기·기간을 알아서 추출합니다.
41
+ > **상황** 시장 신년사 원고에 "우리 시 현황" 한 단락을 넣어야 한다. 인구·출산율·고용·지역경제 수치를 KOSIS에서 하나씩 베껴오는 데 매번 30분~1시간.
34
42
 
35
- ### 약어·오타·공백 변형 자동 정규화 (v1.4 신규)
43
+ ```
44
+ 나: 성남시 신년사에 넣을 핵심 통계 한 줄로 정리해줘
36
45
 
37
- | 입력 | 매칭 결과 |
38
- |------|----------|
39
- | `GDP` / `gdp` / `G D P` | 국내총생산 |
40
- | `출산률` / `출산율` / `합계출산` | 합계출산율 |
41
- | `고용율` / `취업률` | 고용률 |
42
- | `실엄률` / `실업률` / `청년실업` | 실업률 |
43
- | `노인` / `노년` / `65세 이상` | 65세 이상 고령인구 |
44
- | `집값` / `주택값` | 주택매매가격지수 |
45
- | `아파트값` / `전셋값` | 아파트·전세 가격지수 |
46
- | `연봉` / `월소득` / `봉급` | 상용근로자 월평균 임금 |
47
- | `population` / `fertility` / `inflation` | 영문 별칭 (인구·출산율·물가) |
46
+ AI: 성남시의 주민등록 인구 ○○만 명, 합계출산율 ○.○○명,
47
+ 65세 이상 고령인구 ○○만 명, 실업률 ○.○%, 지역내총생산 ○○조 원
48
+ (○○○○년 기준)입니다.
49
+ ```
50
+
51
+ `chain_region_brief`의 **연설용 모드**가 핵심 지표를 자동으로 추려 한 문장으로 만듭니다. 13개 지표 전체 브리핑도 한 번에 가능합니다.
48
52
 
49
- → 100개 이상의 줄임말·오타·공백 변형을 정식 키워드로 자동 매핑. 사용자가 정식 용어를 몰라도 동작.
53
+ ---
50
54
 
51
- ### 시도·자치구·시군 자동 라우팅
55
+ ### 🏛 지방의회 답변·행정사무감사 대비 — "옆 동네보다 낮은 거 아니냐"
56
+
57
+ > **상황** — 의원이 "우리 구 실업률이 인접 구보다 높은 것 아니냐"고 질의할 예정. 인접 자치구를 일일이 조회해 비교표를 만들어야 한다.
52
58
 
53
59
  ```
54
- "한국 인구" → 51,117,378명 (2025)
55
- "서울 인구" → 9,299,548명
56
- "제주 인구" → 664,792명
60
+ 나: 강남구, 서초구, 송파구 실업률이랑 고용률 비교해줘
61
+
62
+ AI: 📊 3개 지역 × 2개 지표 비교
63
+ • 실업률: 최고 ○○구(○.○%), 최저 ○○구(○.○%)
64
+ • 고용률: 최고 ○○구(○○.○%), 최저 ○○구(○○.○%)
65
+ [지역별 순위 매트릭스 + 기준시점 포함]
57
66
  ```
58
67
 
59
- 자치구·시군 230+개도 인식합니다. KOSIS OpenAPI는 광역시도 단위까지만 지원하므로, 자치구 입력 **소속 광역시도로 자동 fallback** + 자치구 단위 정밀 조회 경로(`fetch_kosis_excel`) 안내가 함께 나옵니다. 동명 자치구(`중구`, `남구` 등)는 광역시 컨텍스트가 같이 있으면 정확히 disambiguate.
68
+ `chain_compare_regions`는 **최대 17개 지역 × 8개 지표**를 번에 매트릭스로 만들고 지표별 순위까지 매깁니다. "전국 17개 시도 출산율 순위"도 줄입니다.
69
+
70
+ ---
71
+
72
+ ### 📑 정책보고서·연구용역 현황 분석 — 10년 추세를 한 번에
73
+
74
+ > **상황** — 저출산 대응 5개년 계획 보고서. "최근 10년 추세" 챕터에 들어갈 시계열 데이터가 필요하다.
75
+
76
+ ```
77
+ 나: 저출산 영역 최근 10년 추세 정리해줘
78
+
79
+ AI: 📑 저출산 영역 10년 추세
80
+ • 합계출산율: 지속 하락 (20○○→20○○년, -○○%)
81
+ • 출생아수: 지속 하락 (-○○%)
82
+ • 혼인건수: 지속 하락 (-○○%)
83
+ • 평균초혼연령: 상승 (+○.○세)
84
+ [연도별 데이터포인트 + 변화율 전체]
85
+ ```
86
+
87
+ `chain_policy_indicator`는 **7개 정책 영역**(저출산·고령화·주거·일자리·치안·보건·경제)을 영역별 3~4개 지표 묶음으로 시계열 분석합니다. 평균 변화율·최고/최저점·추세 분류가 함께 나옵니다.
88
+
89
+ ---
90
+
91
+ ### 🗣 민원 응대·보도자료 — 묻는 즉시 공식 수치
60
92
 
61
- ### 시계열 추세 + 자연어 기간 추출
93
+ > **상황** "우리 동네 미세먼지가 요즘 어떠냐"는 민원 전화. 또는 보도자료에 들어갈 수치를 30분 뒤 회신해야 한다.
62
94
 
63
95
  ```
64
- "최근 10년 출산율 추이"
65
- "민선 4기 인구 변화"
66
- "임기 4년차 GRDP"
67
- "작년 대비 실업률"
68
- "역대 출산율"
96
+ 나: 충남 미세먼지 수치
97
+ AI: 20○○년 충남의 PM2.5 농도는 ○○㎍/㎥입니다. 📊 출처: KOSIS
98
+
99
+ 나: 부산 인구 최근 10년 변화는?
100
+ AI: 부산의 인구 10년 추세: 지속적인 하락 추세. ... (변화율·최고/최저점 포함)
69
101
  ```
70
102
 
71
- "지난 N년", "민선 N기", "임기 N년차", "작년 대비", "역대" 같은 표현이 자동으로 `yearCount`로 환산됩니다. 응답에는 평균 변화율, 최고/최저점, 변동성, 추세 분류(상승·하락·안정·변동)가 함께 나옵니다.
103
+ 단일 수치는 `quick_stats`, 추세는 `quick_trend`. **모든 응답에 통계표 출처가 붙어** 그대로 인용할 있습니다.
104
+
105
+ ---
106
+
107
+ ### 🎯 자치구·시군 단위까지 — 광역 평균에 묻히지 않습니다
72
108
 
73
- ### 체인 도구 번에 다지표·다지역·정책영역
109
+ > **상황**"광진구" 고용률이 필요한데 검색하면 늘 "서울특별시" 평균만 나온다.
74
110
 
75
111
  ```
76
- "성남시 통계 한장 보고"
77
- "전국 17개 시도 인구·출산율·GRDP 비교"
78
- "저출산 영역 10추세"
112
+ 나: 광진구 고용률, 광진구 65세 이상 인구
113
+
114
+ AI: 2025하반기 광진구의 고용률은 61.6%입니다.
115
+ 2024년 광진구의 65세 이상 고령인구는 ○○,○○○명입니다.
79
116
  ```
80
117
 
81
- 체인 도구가 여러 `quick_stats`/`quick_trend` 호출을 자동으로 묶어줍니다:
118
+ 전국 **230개 이상의 자치구·시군**을 KOSIS 자치구 단위 통계표로 직접 조회합니다. 전국 226개 시군구가 동일 구조로 수록된 **KOSIS 표준 통계표(자치구 코드 라우팅)를 우선** 쓰고, 표준표에 없는 분야만 자치구 통계연보(`.xlsx`)로 보완합니다. `중구`·`남구`처럼 여러 시에 있는 이름도 "부산 중구"처럼 광역시를 같이 말하면 정확히 구분합니다.
82
119
 
83
- - **`chain_region_brief`** — 단일 지역의 13개 핵심 지표(인구·고용·경제·주거·사회·환경) 한장 브리핑. `format='speech'`면 한 줄 요약(취임사·신년사용)
84
- - **`chain_compare_regions`** — N개 지역 × M개 지표 매트릭스 + 자동 순위 (전국 17개 동시 비교 가능)
85
- - **`chain_policy_indicator`** 7개 정책 영역(저출산·고령화·주거·일자리·치안·보건·경제) 묶음 10년 시계열
120
+ ---
121
+
122
+ ### 🛡 ChatGPT가 찍어준 통계, 그대로 보고서에 넣지 마세요
123
+
124
+ 일반 AI는 통계 수치를 **학습 시점 기준으로 기억**합니다. "서울 인구"를 물으면 몇 년 전 값을 자신 있게 답합니다. 그 수치가 보고서·연설문·국정감사 자료에 들어가면 사고입니다.
86
125
 
87
- ### 장래추계 데이터 안내
126
+ 커넥터를 켜면 AI는 **질문할 때마다 KOSIS 공식 DB를 실시간 조회**하고, 답변에 통계표 ID(출처)를 함께 표기합니다. 추정이 아니라 인용입니다.
88
127
 
89
- 노령화지수 같이 KOSIS DB가 **장래추계 데이터를 포함**하는 통계는 응답에 `isProjection: true` + "추계" 명시 안내가 자동 부착됩니다. LLM이 미래 추계를 실측처럼 인용하는 위험 방지.
128
+ > 장래추계가 포함된 통계는 "이 수치는 실측이 아닌 국가데이터처 추계"라는 안내가, 인구동향(출생·사망·혼인·이혼) 최근 시점에는 "잠정치일 수 있음" 안내가 자동으로 붙습니다. 추계·잠정치를 확정 실측처럼 인용하는 실수를 막습니다.
90
129
 
91
130
  ---
92
131
 
93
- ## 만들었나
132
+ ## 무엇을 물어볼 수 있나
133
+
134
+ ### 통계 키워드 — 92개 + 자연어 별칭 100개 이상
135
+
136
+ | 분야 | 예시 키워드 |
137
+ |------|------------|
138
+ | 인구·출산·고령 | 인구, 출산율, 출생아수, 사망률, 기대수명, 고령인구, 노령화지수 |
139
+ | 혼인·이혼 | 혼인건수, 이혼율, 초혼연령, 평균초혼연령 |
140
+ | 고용·소득 | 실업률, 고용률, 취업자수, 경제활동인구, 월평균임금 |
141
+ | 경제 | GDP, 경제성장률, 물가(소비자물가지수), GRDP(지역내총생산) |
142
+ | 무역 | 수출, 수입, 무역수지 |
143
+ | 주거 | 주택매매가격, 아파트가격, 전세가격 |
144
+ | 환경·교통·사회 | 미세먼지(PM2.5/PM10), 자동차등록, 교통사고, 범죄율, 의사수, 외래관광객 |
94
145
 
95
- 대한민국 정부 통계는 [KOSIS](https://kosis.kr)에 모여 있지만, 공무원·연구자·기자가 매번 사이트를 뒤지거나 OpenAPI 파라미터를 조립하는 비용이 너무 큽니다. 그리고 LLM은 통계 수치를 학습 시점으로 환각하기 일쑤입니다.
146
+ **정식 용어를 몰라도 됩니다.** `집값`→주택매매가격, `노인`→고령인구, `월소득`→월평균임금처럼 줄임말·구어체를 자동 변환합니다. `출산률`·`고용율` 같은 률/율 오타, `G D P` 같은 공백, `population`·`gdp` 같은 영문도 인식합니다.
96
147
 
97
- 프로젝트는 KOSIS의 핵심 통계를 **자연어 줄로 호출 가능한 12개 MCP 도구**로 감싸서, AI 어시스턴트나 스크립트가 KOSIS 공식 DB를 직접 조회하도록 만듭니다.
148
+ > 정의가 **다른** 지표는 조용히 바꿔치지 않습니다 `청년실업률`(15~29세)·`연봉`(연 단위)·`가계소득`처럼 비슷해 보여도 다른 통계인 질문에는 오답 대신 "어느 통계를 봐야 하는지" 안내가 나갑니다. 지역명도 마찬가지 — 인식 못 한 지역명에 전국값을 대신 내놓지 않습니다.
149
+
150
+ ### 지역 — 17개 시도 + 자치구·시군 230개 이상
151
+
152
+ 전국 광역시도 17개(풀네임·약칭 모두)와 자치구·시군 230여 곳. `"민선 8기 출산율 추이"`, `"임기 4년차 GRDP"`, `"작년 대비 실업률"`, `"역대 인구"` 같은 한국 행정 어법의 기간 표현도 자동으로 분석 연수로 환산합니다.
153
+
154
+ ---
155
+
156
+ ## 14개 도구
157
+
158
+ 대부분의 질문은 **`quick_stats`·`quick_trend`·`quick_rank`·체인 도구 3종**이면 끝납니다. 나머지는 정밀 조회용입니다.
159
+
160
+ | 구분 | 도구 | 하는 일 |
161
+ |------|------|---------|
162
+ | **자연어 즉답** ⭐ | `quick_stats` | 자연어 한 줄 → KOSIS 수치 즉답 |
163
+ | | `quick_trend` | 시계열 추세 + 변화율 + 최고/최저점 (자연어 기간 인식) |
164
+ | | `quick_rank` 🆕 | "우리 지역 전국 몇 위?" — 17개 시도 또는 시군구 전수 대비 순위·백분위·평균 격차·순위 변동. 동일 표·동일 시점 단일 조회로 비교가능성 보장 |
165
+ | **출처·각주** 🆕 | `explain_statistic` | 통계 공식 정의·작성목적·조사주기·용어해설 + 보고서 인용 각주 문구 생성 |
166
+ | **체인** ⛓ | `chain_region_brief` | 한 지역 13개 지표 종합 브리핑 (연설용 한 줄 모드 포함) |
167
+ | | `chain_compare_regions` | N개 지역 × M개 지표 매트릭스 + 순위 (최대 17×8) |
168
+ | | `chain_policy_indicator` | 7개 정책 영역 묶음 10년 시계열 |
169
+ | **검색·탐색** | `search_statistics` | KOSIS 통계표 키워드 검색 |
170
+ | | `get_statistics_list` | 주제별·기관별 트리 탐색 + 분야별 추천 |
171
+ | | `get_table_info` | 통계표 메타데이터(분류·항목·주기) |
172
+ | **정밀 데이터** | `get_statistics_data` | 특정 통계표 데이터 조회 (지역명·항목명 자동 매칭) |
173
+ | | `compare_statistics` | 시점별·항목별 정밀 비교 |
174
+ | | `analyze_time_series` | 상세 시계열 (CAGR·표준편차·추세선) |
175
+ | **파일 통계표** | `fetch_kosis_excel` | KOSIS 파일통계표(`.xlsx`) 다운로드·파싱 — 자치구 통계연보 등 OpenAPI 미지원 표 커버 |
98
176
 
99
177
  ---
100
178
 
101
- ## 설치 사용법
179
+ ## 설치 (3가지 방법)
102
180
 
103
- ### 방법 1: Claude.ai 웹 커넥터 (설치 없이 바로) ⭐ 가장 쉬움
181
+ ### 방법 1 Claude.ai 웹 커넥터 (설치 없음) ⭐ 가장 쉬움
104
182
 
105
- Claude Pro/Max/Team/Enterprise 요금제에서 동작.
183
+ Claude Pro/Max/Team/Enterprise 요금제에서 동작합니다.
106
184
 
107
- 1. [claude.ai](https://claude.ai) 로그인 → 좌측 사이드바 본인 이름 → **설정** → **커넥터**
108
- 2. **커스텀 커넥터 추가** 클릭
109
- 3. 입력:
110
- - **이름**: `korean-stats` (원하는 이름)
111
- - **URL**: `https://korean-stats-mcp.fly.dev/mcp`
112
- 4. **추가** → 추가된 커넥터 **구성** → 모든 도구를 **"항상 사용"**으로 설정
113
- 5. 채팅창에 `"한국 출산율 추세 보여줘"` 같이 자연어로 질문하면 끝
185
+ 1. [claude.ai](https://claude.ai) 로그인 → 좌측 본인 이름 → **설정** → **커넥터**
186
+ 2. **커스텀 커넥터 추가**
187
+ 3. 입력 — 이름: `korean-stats` / URL: `https://korean-stats-mcp.fly.dev/mcp`
188
+ 4. **추가** 추가된 커넥터 **구성** → 모든 도구를 **"항상 사용"**으로
189
+ 5. 채팅창에 `"광진구 고용률 알려줘"` 처럼 한국어로 질문하면 끝
114
190
 
115
- ### 방법 2: AI 데스크톱 앱 (설치 없음)
191
+ ### 방법 2 AI 데스크톱 앱 (설치 없음)
116
192
 
117
- Claude Desktop / Cursor / Windsurf 설정 파일에 추가.
193
+ Claude Desktop / Cursor / Windsurf 원격 MCP 서버를 등록합니다.
118
194
 
119
- **설정 파일 위치:**
195
+ #### 원클릭 자동 설치 ⭐
120
196
 
121
- | | Windows | macOS |
122
- |------|---------|-----|
123
- | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` | `~/Library/Application Support/Claude/claude_desktop_config.json` |
124
- | Cursor | 프로젝트 `.cursor/mcp.json` | 프로젝트 `.cursor/mcp.json` |
125
- | Windsurf | 프로젝트 `.windsurf/mcp.json` | 프로젝트 `.windsurf/mcp.json` |
197
+ 설치 스크립트가 클라이언트 설정 파일을 자동으로 찾아 `korean-stats` 항목을 등록합니다. 기존 설정은 백업(`*.bak.*`) 후 보존되고, 다른 MCP 서버 항목은 그대로 둡니다.
198
+
199
+ **macOS / Linux** (`jq` 또는 `python3` 필요)
200
+
201
+ ```bash
202
+ curl -fsSL https://raw.githubusercontent.com/chrisryugj/korean-stats-mcp/main/install.sh | bash
203
+ ```
126
204
 
127
- **설정 내용:**
205
+ **Windows (PowerShell)**
206
+
207
+ ```powershell
208
+ irm https://raw.githubusercontent.com/chrisryugj/korean-stats-mcp/main/install.ps1 | iex
209
+ ```
210
+
211
+ 기본값은 세 클라이언트 모두 등록(`all`)입니다. 특정 클라이언트만 설치하려면 `--client`(`claude`·`cursor`·`windsurf`·`all`) 옵션을 씁니다.
212
+
213
+ ```bash
214
+ # macOS / Linux — Cursor만
215
+ curl -fsSL https://raw.githubusercontent.com/chrisryugj/korean-stats-mcp/main/install.sh | bash -s -- --client cursor
216
+ ```
217
+
218
+ ```powershell
219
+ # Windows — Cursor만
220
+ & ([scriptblock]::Create((irm https://raw.githubusercontent.com/chrisryugj/korean-stats-mcp/main/install.ps1))) -Client cursor
221
+ ```
222
+
223
+ 스크립트는 등록 전 원격 서버 헬스 체크를 수행합니다. 설치 후 해당 앱을 재시작하세요.
224
+
225
+ #### 수동 등록
226
+
227
+ 설정 파일을 직접 편집해도 됩니다.
228
+
229
+ | 앱 | 설정 파일 위치 |
230
+ |----|---------------|
231
+ | Claude Desktop | Windows `%APPDATA%\Claude\claude_desktop_config.json` · macOS `~/Library/Application Support/Claude/claude_desktop_config.json` |
232
+ | Cursor | 프로젝트 `.cursor/mcp.json` |
233
+ | Windsurf | 프로젝트 `.windsurf/mcp.json` |
128
234
 
129
235
  ```json
130
236
  {
@@ -137,11 +243,11 @@ Claude Desktop / Cursor / Windsurf 설정 파일에 추가.
137
243
  }
138
244
  ```
139
245
 
140
- 저장 후 재시작.
246
+ 저장 후 앱을 재시작합니다.
141
247
 
142
- ### 방법 3: 로컬 설치 (오프라인 가능)
248
+ ### 방법 3 로컬 설치 (오프라인 가능)
143
249
 
144
- **사전 준비:** [Node.js](https://nodejs.org) 20 이상.
250
+ **준비물**: [Node.js](https://nodejs.org) 20 이상 · [KOSIS OpenAPI 키](https://kosis.kr/openapi/) (무료 발급)
145
251
 
146
252
  ```bash
147
253
  git clone https://github.com/chrisryugj/korean-stats-mcp.git
@@ -150,152 +256,94 @@ pnpm install
150
256
  pnpm run build
151
257
  ```
152
258
 
153
- AI 앱 설정:
259
+ AI 앱 설정에 발급받은 키를 넣습니다.
154
260
 
155
261
  ```json
156
262
  {
157
263
  "mcpServers": {
158
264
  "korean-stats": {
159
265
  "command": "node",
160
- "args": ["/absolute/path/korean-stats-mcp/dist/index.js"]
266
+ "args": ["/절대경로/korean-stats-mcp/dist/index.js"],
267
+ "env": { "KOSIS_API_KEY": "발급받은_키" }
161
268
  }
162
269
  }
163
270
  }
164
271
  ```
165
272
 
166
- **자동 설치 스크립트:**
167
-
168
- ```bash
169
- curl -fsSL https://raw.githubusercontent.com/chrisryugj/korean-stats-mcp/main/install.sh | bash
170
- ```
171
-
172
- ---
173
-
174
- ## 도구 구조 (12개)
175
-
176
- | 구분 | 도구 | 설명 |
177
- |------|------|------|
178
- | **빠른 자연어** ⭐ | `quick_stats` | 자연어 한 줄 → KOSIS 수치 즉답. 91 키워드 + 100+ 별칭/오타 정규화. |
179
- | | `quick_trend` | 시계열 추세 + 평균 변화율 + 최고/최저점 + 자연어 기간 추출 ("민선 4기" 등). |
180
- | **체인** ⛓ | `chain_region_brief` | 단일 지역 13지표 한장 브리핑. `format='speech'`로 연설용 한 줄. |
181
- | | `chain_compare_regions` | N지역 × M지표 매트릭스 + 자동 순위 (전국 17개 동시 가능). |
182
- | | `chain_policy_indicator` | 7개 정책 영역(저출산·고령화·주거·일자리·치안·보건·경제) 10년 시계열. |
183
- | **검색·탐색** | `search_statistics` | KOSIS 90만+ 통계표 키워드 검색 → orgId/tableId 메타 획득. |
184
- | | `get_statistics_list` | 주제별·기관별 트리 탐색 + 9개 분야 추천 카드(`recommendedTopic` 옵션). |
185
- | | `get_table_info` | 통계표 메타데이터 (분류·항목·주기). 경량 응답. |
186
- | **데이터** | `get_statistics_data` | 특정 통계표 데이터 조회. regionName/itemName 자동 매칭. |
187
- | | `compare_statistics` | 시점별·항목별 정밀 비교. |
188
- | | `analyze_time_series` | 상세 시계열 (CAGR·표준편차·추세선). |
189
- | **특수** | `fetch_kosis_excel` | KOSIS 파일통계표(.xlsx) 다운로드 + 파싱 ([kordoc](https://github.com/chrisryugj/kordoc) 엔진). **자치구 기본통계** 등 OpenAPI 미지원 표 커버. |
273
+ > 로컬에서 직접 실행할 때는 프로젝트 루트에 `.env` 파일을 만들어 `KOSIS_API_KEY=...`를 넣어도 됩니다. (`.env.example` 참고)
190
274
 
191
275
  ---
192
276
 
193
- ## 지원 키워드 (91개 정식 + 100+ 자연어 별칭)
194
-
195
- ### 인구·출산·사망·고령화
196
- 인구, 총인구, 출산율, 합계출산율, 출생아수, 조출생률, 사망자수, 조사망률, 사망률, 자연증가(율), 기대수명, 평균수명, 고령인구, 노인인구, 65세이상인구, 노령화지수, 고령화지수
197
-
198
- ### 혼인·이혼
199
- 혼인건수, 혼인율, 조혼인율, 이혼건수, 이혼율, 조이혼율, 초혼연령, 평균초혼연령, 남성·여성초혼연령
277
+ ## 정확성과 신뢰
200
278
 
201
- ### 고용·소득
202
- 실업률, 고용률, 취업자수, 실업자수, 경제활동인구, 비경제활동인구, 임금, 월평균임금, 월급, 평균임금
203
-
204
- ### 경제
205
- GDP, 국내총생산, 경제성장률, GDP성장률, 물가, 소비자물가, 소비자물가지수, GRDP, 지역내총생산
206
-
207
- ### 무역
208
- 수출, 수출액, 수입, 수입액, 무역수지
209
-
210
- ### 주거
211
- 주택가격, 주택매매가격, 아파트가격, 아파트매매가격, 전세가격, 주택전세, 전세, 아파트전세
212
-
213
- ### 환경·교통·사회
214
- 미세먼지(PM2.5), PM10, 대기오염, 초미세먼지, 자동차, 자동차등록, 교통사고, 사고건수, 범죄, 범죄율, 범죄발생, 의사, 의사수, 의료인력, 외래관광객, 입국자, 관광객
215
-
216
- ### 자연어 별칭 (v1.4 신규)
217
- 출산·출생·노인·청년실업·연봉·집값·아파트값·전셋값·의료진·차량·관광객수 등 + 영문 (`population`, `fertility`, `aging`, `inflation`, `gdp` 등) + 률/율 오타 (`출산률`, `고용율`, `실엄률`, `이혼률` 등).
279
+ - **공식 출처** — 모든 수치는 국가데이터처 KOSIS OpenAPI를 실시간 조회합니다. 응답에 통계표 ID가 표기되어 그대로 인용·검증할 수 있습니다.
280
+ - **추계 데이터 구분** 장래추계가 포함된 통계는 "추계" 안내가 자동으로 붙습니다.
281
+ - **자치구 데이터 무결성** — 자치구 단위 데이터가 KOSIS에 없으면 임의로 광역시도 값을 자치구 값인 척 답하지 않고, "광역시도 데이터로 대체했다"고 명시합니다.
282
+ - **캐시** — 동일 질의는 6시간 캐싱하여 빠르게 응답하되, 통계 갱신 주기를 해치지 않습니다.
218
283
 
219
284
  ---
220
285
 
221
- ## 지역 라우팅
286
+ ## 원격 엔드포인트
222
287
 
223
- - **17개 광역시도** 서울, 부산, 대구, 인천, 광주, 대전, 울산, 세종, 경기, 강원, 충북, 충남, 전북, 전남, 경북, 경남, 제주 (풀네임·약칭 모두 인식)
224
- - **자치구·시군 230+** — 자동으로 소속 광역시도로 fallback + 자치구 단위 정밀 조회 경로 안내
225
- - **동명 자치구 disambiguate** — `중구`, `남구`, `동구` 등은 광역시 컨텍스트가 같이 있으면 정확히 매칭
288
+ - 엔드포인트: `https://korean-stats-mcp.fly.dev/mcp` (14개 도구 전체 동작)
289
+ - 헬스체크: `https://korean-stats-mcp.fly.dev/health`
226
290
 
227
291
  ---
228
292
 
229
293
  ## 변경 이력
230
294
 
231
295
  <details>
232
- <summary>v1.4자연어 정규화 + 체인 도구 + 통폐합</summary>
233
-
234
- **자연어 약어/오타/공백 정규화 (법령 MCP의 `LAW_ALIAS_ENTRIES` 패턴 차용)**
235
- - `KEYWORD_ALIASES` 100+ 별칭으로 확장 (출생·노인·연봉·집값·청년실업·영문 별칭 등)
236
- - `BASIC_TYPO_MAP` 신설 — 률/율 받침 오타 자동 교정 (`출산률→출산율`, `고용율→고용률`)
237
- - `normalizeKeywordKey` — 공백·대소문자·중점/하이픈 정규화 후 매칭
238
- - `extractKeyword` 단일 정규화 매칭으로 재작성. "G D P", "65세 이상", "경기 노인 인구" 등 자동 인식
239
-
240
- **체인 도구 3종**
241
- - `chain_region_brief` — 13지표 한장 브리핑, `format='speech'` 옵션 추가
242
- - `chain_compare_regions` — N지역×M지표 매트릭스, **전국 17개 동시 비교** (max 10→17)
243
- - `chain_policy_indicator` — 7개 정책 영역 10년 시계열
244
-
245
- **P0 fixes**
246
- - 자치구 region 파라미터 fallback 미동작 버그 — `quickStats`에서 `input.region`이 자치구일 때 광역시도 변환 분기를 skip하던 가드 제거
247
- - `chain_region_brief.fallbackNote` — 자치구→광역시도 변환 노트 우선 노출
248
- - 노령화지수 등 장래추계 데이터 — `isProjection: true` 메타 + "추계" 명시 안내
296
+ <summary>v1.7자치구 데이터 소스 우선순위 재정립 + HTTP 서버 안정화</summary>
249
297
 
250
- **자연어 기간 추출**
251
- - `quick_trend` 키워드에서 "지난 N년", "민선 N기", "임기 N년차", "작년 대비", "역대" 자동 `yearCount`
252
-
253
- **통폐합**
254
- - 도구 13개 **12개**. `get_recommended_statistics`를 `get_statistics_list`의 `recommendedTopic` 옵션으로 흡수
298
+ - 자치구 조회 우선순위 전환 — KOSIS **표준 OpenAPI(자치구 코드 라우팅)를 1순위**로, 통계연보(`.xlsx`)는 표준표에 없는 분야의 보조 경로로. 표준표는 전국 226개 시군구가 동일 구조라 일관성·다지역 비교가능성이 보장됨
299
+ - 통계연보 `.xlsx`의 `file_sn`을 분야명 매칭으로 **자치구별 동적 도출** 자치구마다 다른 통계연보 분야 순서 때문에 엉뚱한 분야 파일을 읽던 문제 차단
300
+ - 자치구 데이터 미수록 시 광역값을 자치구 값인 척 답하지 않도록 **응답 격하 강화** — "○○구 단위 미수록, 참고로 상위 지역 □□는 X" 형태로 첫 문장부터 명시
301
+ - `chain_compare_regions` **소스 혼합 감지** — 같은 지표가 지역별로 다른 KOSIS 통계표에서 조회되면 비교가능성 경고 자동 부착
302
+ - HTTP 서버 메모리 누수 차단 (v1.7.1) rate-limit IP 버킷 주기적 정리, 매 요청 생성되는 MCP Server 인스턴스 명시적 종료, SIGTERM graceful shutdown (korean-law-mcp 안정화 패턴 적용)
255
303
 
256
304
  </details>
257
305
 
258
306
  <details>
259
- <summary>v1.3P0 자치구 라우팅 일소 + 체인 도구 도입</summary>
307
+ <summary>v1.6 — 자치구 고용·인구동태 정밀 라우팅 확장</summary>
260
308
 
261
- - `DISTRICT_TO_PROVINCE` 33 200+ (경기·강원·충북·충남·전북·전남·경북·경남 전체 시군 + 부산 영도·동래·인천 남동 등)
262
- - `AMBIGUOUS_DISTRICTS` + 광역시도 컨텍스트로 "광주 동구" / "부산 강서구" 정확 매칭
263
- - `extractDistrictName` 광역시 약칭 가드 "대구" 같은 광역시 약칭이 자치구로 오인되는 버그 fix
264
- - 체인 도구 3종 신설 (region_brief, compare_regions, policy_indicator)
309
+ - 자치구 단위 OpenAPI 라우팅을 14개 분야로 확장 고용(`DT_1ES3A03_A01S`)·실업(`DT_1ES3A01S`)·인구동태(`INH_*` 사망·혼인·이혼)
310
+ - 고용·실업 통계표의 반기(`prdSe='S'`) 주기를 응답의 `PRD_SE` 필드 기준으로 라벨링 "2025년 하반기"처럼 정확 표기
311
+ - `getDistrictKscdCodeFor` `UP_ITM_ID` 없는 메타 대응("서울 광진구" 결합형 / "수원시" 단일형), 동명 시군 ambiguous 처리
312
+ - 자치구 통계연보(`.xlsx`) value 추출 실패 OpenAPI 라우팅으로 자동 fall-through
313
+ - 배포를 Fly.io 컨테이너로 전환 — 자치구 `.xlsx` 파싱(kordoc) 엔진 포함
265
314
 
266
315
  </details>
267
316
 
268
317
  <details>
269
- <summary>v1.2자치구 라우팅 + 부분매칭 버그 픽스</summary>
318
+ <summary>v1.4자연어 정규화 + 체인 도구</summary>
270
319
 
271
- - `quick_trend` keyword 자연어 처리 (extract* 헬퍼 공유)
272
- - `extractProvinceName` 단어 경계 매칭으로 "해운대구"의 "대구" 부분매칭 버그 차단
273
- - 자치구 광역시도 fallback + 자치구 단위 정밀 조회 경로(`fetch_kosis_excel`) 안내
320
+ - 약어·오타·공백 변형 자동 정규화 (`KEYWORD_ALIASES` 100개 이상, 률/율 오타 교정)
321
+ - 체인 도구 3종 — `chain_region_brief`(연설용 모드 포함)·`chain_compare_regions`(전국 17개 동시)·`chain_policy_indicator`
322
+ - `quick_trend` 자연어 기간 추출 "민선 N기", "임기 N년차", "작년 대비", "역대"
323
+ - 도구 13개 → 12개 통폐합
274
324
 
275
325
  </details>
276
326
 
277
- ---
327
+ <details>
328
+ <summary>v1.2 ~ v1.3 — 자치구 라우팅 도입</summary>
278
329
 
279
- ## 주요 특징
330
+ - `DISTRICT_TO_PROVINCE` 자치구·시군 매핑 200개 이상으로 확장
331
+ - 동명 자치구를 광역시도 컨텍스트로 구분 (`AMBIGUOUS_DISTRICTS`)
332
+ - "해운대구"의 "대구" 부분매칭 등 부분매칭 버그 차단
333
+ - 자치구 정밀 조회 경로(`fetch_kosis_excel`) 도입
280
334
 
281
- - **91개 키워드 + 100+ 자연어 별칭** — 줄임말·률/율 오타·공백 변형 자동 정규화
282
- - **17개 시도 + 자치구·시군 230+** — 자치구는 광역시도로 자동 fallback + 정밀 조회 경로 안내
283
- - **체인 도구 3종** — 단일 지역 13지표 한장 브리핑, N지역×M지표 매트릭스(전국 17개 동시), 7개 정책 영역 시계열
284
- - **자연어 기간 추출** — "민선 4기", "임기 4년차", "작년 대비", "역대" 자동 환산
285
- - **장래추계 안내** — 노령화지수 등 추계 데이터는 `isProjection: true` + "추계" 명시 안내
286
- - **파일통계표 파싱** — KOSIS OpenAPI 미지원 표(.xlsx)는 [kordoc](https://github.com/chrisryugj/kordoc) 엔진으로 다운로드·파싱·Markdown 변환
287
- - **캐시** — LRU 기반, 통계 데이터 6시간 TTL
288
- - **원격 엔드포인트** — 설치 없이 `https://korean-stats-mcp.fly.dev/mcp`로 바로 사용
335
+ </details>
289
336
 
290
337
  ---
291
338
 
292
- ## 원격 엔드포인트
339
+ ## 라이선스
293
340
 
294
- - **`https://korean-stats-mcp.fly.dev/mcp`** — Fly.io Singapore 리전, stateless HTTP, 12개 도구 전체 동작
295
- - 헬스체크: `https://korean-stats-mcp.fly.dev/health`
341
+ [MIT](./LICENSE)
296
342
 
297
343
  ---
298
344
 
299
- ## 라이선스
345
+ ## 참고한 프로젝트
300
346
 
301
- [MIT](./LICENSE)
347
+ - **[Dayoooun/korea-stats-mcp](https://github.com/Dayoooun/korea-stats-mcp)** — 이 프로젝트의 포크 시작점. 원본에 깊은 감사를 표합니다. 라이선스는 원본과 동일한 MIT.
348
+ - **[kordoc](https://github.com/chrisryugj/kordoc)** — KOSIS 파일통계표(`.xlsx`)를 다운로드·파싱하는 엔진. `fetch_kosis_excel` 도구가 이 엔진을 사용합니다.
349
+ - **[korean-law-mcp](https://github.com/chrisryugj/korean-law-mcp)** — HTTP 서버 안정화 패턴(rate-limit 버킷 정리, MCP 인스턴스 명시적 종료, SIGTERM graceful shutdown)을 v1.7.1에 적용했습니다.
@@ -17,7 +17,7 @@ export declare class KosisClient {
17
17
  /**
18
18
  * API 요청 실행 (timeout 15s + 3회 재시도 + 지수 백오프)
19
19
  *
20
- * - Fly Singapore → KOSIS Korea cold path 일시 abort 대응
20
+ * - Fly 해외 리전(nrt 등) → KOSIS Korea cold path 일시 abort 대응
21
21
  * - KOSIS 응답 에러(err/errMsg 필드)는 영구 실패 → 즉시 throw, retry 안 함
22
22
  * - HTTP 4xx도 영구 실패 → 즉시 throw
23
23
  * - 네트워크 오류·타임아웃·5xx만 retry (800ms / 1600ms 백오프)
@@ -25,7 +25,7 @@ export class KosisClient {
25
25
  /**
26
26
  * API 요청 실행 (timeout 15s + 3회 재시도 + 지수 백오프)
27
27
  *
28
- * - Fly Singapore → KOSIS Korea cold path 일시 abort 대응
28
+ * - Fly 해외 리전(nrt 등) → KOSIS Korea cold path 일시 abort 대응
29
29
  * - KOSIS 응답 에러(err/errMsg 필드)는 영구 실패 → 즉시 throw, retry 안 함
30
30
  * - HTTP 4xx도 영구 실패 → 즉시 throw
31
31
  * - 네트워크 오류·타임아웃·5xx만 retry (800ms / 1600ms 백오프)
@@ -9,9 +9,11 @@ declare const TTL: {
9
9
  readonly SEARCH_RESULTS: number;
10
10
  readonly EXPLANATION: number;
11
11
  readonly TABLE_META: number;
12
+ readonly EMPTY_RESULT: 60;
12
13
  };
13
14
  declare class CacheManager {
14
15
  private cache;
16
+ private inflight;
15
17
  constructor();
16
18
  /**
17
19
  * 캐시 키 생성
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,YAAY,CAAC;AAInC,QAAA,MAAM,GAAG;;;;;;CAMC,CAAC;AAEX,cAAM,YAAY;IAChB,OAAO,CAAC,KAAK,CAAY;;IAWzB;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACG,UAAU,CAAC,CAAC,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,CAAC,CAAC;IAgBb;;OAEG;IACG,iBAAiB,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,iBAAiB,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,gBAAgB,CAAC,CAAC,EACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,cAAc,CAAC,CAAC,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,YAAY,CAAC,CAAC,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACH,QAAQ;IAIR;;OAEG;IACH,KAAK;CAGN;AAKD,wBAAgB,eAAe,IAAI,YAAY,CAK9C;AAED,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,SAAS,MAAM,YAAY,CAAC;AAInC,QAAA,MAAM,GAAG;;;;;;;CAQC,CAAC;AAEX,cAAM,YAAY;IAChB,OAAO,CAAC,KAAK,CAAY;IAGzB,OAAO,CAAC,QAAQ,CAAuC;;IAWvD;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACG,UAAU,CAAC,CAAC,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,CAAC,CAAC;IAmCb;;OAEG;IACG,iBAAiB,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,iBAAiB,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,gBAAgB,CAAC,CAAC,EACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,cAAc,CAAC,CAAC,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACG,YAAY,CAAC,CAAC,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAIb;;OAEG;IACH,QAAQ;IAIR;;OAEG;IACH,KAAK;CAGN;AAKD,wBAAgB,eAAe,IAAI,YAAY,CAK9C;AAED,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC"}
@@ -11,9 +11,14 @@ const TTL = {
11
11
  SEARCH_RESULTS: 1 * 60 * 60, // 검색: 1시간
12
12
  EXPLANATION: 7 * 24 * 60 * 60, // 설명: 7일
13
13
  TABLE_META: 24 * 60 * 60, // 테이블 메타: 24시간
14
+ // 빈 결과: KOSIS 일시 장애로 0건이 왔을 때 6시간 고착되지 않도록 짧게
15
+ EMPTY_RESULT: 60,
14
16
  };
15
17
  class CacheManager {
16
18
  cache;
19
+ // 동일 키 동시 요청 dedup — chain 도구가 같은 (지표×지역)을 병렬 호출할 때
20
+ // 캐시 미스 stampede로 KOSIS 중복 호출되는 것을 방지 (promise 공유)
21
+ inflight = new Map();
17
22
  constructor() {
18
23
  this.cache = new NodeCache({
19
24
  stdTTL: config.cache.ttlHours * 60 * 60,
@@ -42,10 +47,25 @@ class CacheManager {
42
47
  if (cached !== undefined) {
43
48
  return cached;
44
49
  }
45
- // API 호출 캐시 저장
46
- const data = await fetcher();
47
- this.cache.set(key, data, ttl ?? config.cache.ttlHours * 60 * 60);
48
- return data;
50
+ // 동일 in-flight 요청이 있으면 그 promise 공유 (stampede 방지)
51
+ const existing = this.inflight.get(key);
52
+ if (existing) {
53
+ return existing;
54
+ }
55
+ const promise = (async () => {
56
+ try {
57
+ const data = await fetcher();
58
+ // 빈 배열은 일시 장애일 수 있음 — 짧은 TTL로만 캐시 (장기 고착 방지)
59
+ const isEmpty = Array.isArray(data) && data.length === 0;
60
+ this.cache.set(key, data, isEmpty ? TTL.EMPTY_RESULT : (ttl ?? config.cache.ttlHours * 60 * 60));
61
+ return data;
62
+ }
63
+ finally {
64
+ this.inflight.delete(key);
65
+ }
66
+ })();
67
+ this.inflight.set(key, promise);
68
+ return promise;
49
69
  }
50
70
  /**
51
71
  * 통계 목록 캐시