@things-factory/integration-data-go-kr 10.0.0-beta.108

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 (113) hide show
  1. package/README.md +94 -0
  2. package/ROADMAP.md +282 -0
  3. package/dist-server/engine/connector/data-go-kr-client.d.ts +27 -0
  4. package/dist-server/engine/connector/data-go-kr-client.js +138 -0
  5. package/dist-server/engine/connector/data-go-kr-client.js.map +1 -0
  6. package/dist-server/engine/connector/data-go-kr-connector.d.ts +29 -0
  7. package/dist-server/engine/connector/data-go-kr-connector.js +53 -0
  8. package/dist-server/engine/connector/data-go-kr-connector.js.map +1 -0
  9. package/dist-server/engine/connector/index.d.ts +1 -0
  10. package/dist-server/engine/connector/index.js +4 -0
  11. package/dist-server/engine/connector/index.js.map +1 -0
  12. package/dist-server/engine/index.d.ts +2 -0
  13. package/dist-server/engine/index.js +5 -0
  14. package/dist-server/engine/index.js.map +1 -0
  15. package/dist-server/engine/spec/building-ledger/hub.d.ts +12 -0
  16. package/dist-server/engine/spec/building-ledger/hub.js +138 -0
  17. package/dist-server/engine/spec/building-ledger/hub.js.map +1 -0
  18. package/dist-server/engine/spec/building-ledger/index.d.ts +7 -0
  19. package/dist-server/engine/spec/building-ledger/index.js +12 -0
  20. package/dist-server/engine/spec/building-ledger/index.js.map +1 -0
  21. package/dist-server/engine/spec/index.d.ts +17 -0
  22. package/dist-server/engine/spec/index.js +59 -0
  23. package/dist-server/engine/spec/index.js.map +1 -0
  24. package/dist-server/engine/spec/types.d.ts +79 -0
  25. package/dist-server/engine/spec/types.js +30 -0
  26. package/dist-server/engine/spec/types.js.map +1 -0
  27. package/dist-server/engine/spec/weather/asos.d.ts +2 -0
  28. package/dist-server/engine/spec/weather/asos.js +33 -0
  29. package/dist-server/engine/spec/weather/asos.js.map +1 -0
  30. package/dist-server/engine/spec/weather/aws.d.ts +2 -0
  31. package/dist-server/engine/spec/weather/aws.js +28 -0
  32. package/dist-server/engine/spec/weather/aws.js.map +1 -0
  33. package/dist-server/engine/spec/weather/index.d.ts +14 -0
  34. package/dist-server/engine/spec/weather/index.js +23 -0
  35. package/dist-server/engine/spec/weather/index.js.map +1 -0
  36. package/dist-server/engine/spec/weather/mid-term-forecast.d.ts +2 -0
  37. package/dist-server/engine/spec/weather/mid-term-forecast.js +56 -0
  38. package/dist-server/engine/spec/weather/mid-term-forecast.js.map +1 -0
  39. package/dist-server/engine/spec/weather/short-term-forecast.d.ts +2 -0
  40. package/dist-server/engine/spec/weather/short-term-forecast.js +71 -0
  41. package/dist-server/engine/spec/weather/short-term-forecast.js.map +1 -0
  42. package/dist-server/engine/spec/weather/warning.d.ts +2 -0
  43. package/dist-server/engine/spec/weather/warning.js +34 -0
  44. package/dist-server/engine/spec/weather/warning.js.map +1 -0
  45. package/dist-server/engine/task/building-ledger/general.d.ts +30 -0
  46. package/dist-server/engine/task/building-ledger/general.js +123 -0
  47. package/dist-server/engine/task/building-ledger/general.js.map +1 -0
  48. package/dist-server/engine/task/building-ledger/index.d.ts +13 -0
  49. package/dist-server/engine/task/building-ledger/index.js +16 -0
  50. package/dist-server/engine/task/building-ledger/index.js.map +1 -0
  51. package/dist-server/engine/task/building-ledger/summary.d.ts +95 -0
  52. package/dist-server/engine/task/building-ledger/summary.js +174 -0
  53. package/dist-server/engine/task/building-ledger/summary.js.map +1 -0
  54. package/dist-server/engine/task/call.d.ts +40 -0
  55. package/dist-server/engine/task/call.js +144 -0
  56. package/dist-server/engine/task/call.js.map +1 -0
  57. package/dist-server/engine/task/index.d.ts +20 -0
  58. package/dist-server/engine/task/index.js +23 -0
  59. package/dist-server/engine/task/index.js.map +1 -0
  60. package/dist-server/engine/task/weather/grid-converter.d.ts +24 -0
  61. package/dist-server/engine/task/weather/grid-converter.js +69 -0
  62. package/dist-server/engine/task/weather/grid-converter.js.map +1 -0
  63. package/dist-server/engine/task/weather/index.d.ts +13 -0
  64. package/dist-server/engine/task/weather/index.js +18 -0
  65. package/dist-server/engine/task/weather/index.js.map +1 -0
  66. package/dist-server/engine/task/weather/short-term-forecast.d.ts +38 -0
  67. package/dist-server/engine/task/weather/short-term-forecast.js +99 -0
  68. package/dist-server/engine/task/weather/short-term-forecast.js.map +1 -0
  69. package/dist-server/engine/types.d.ts +63 -0
  70. package/dist-server/engine/types.js +9 -0
  71. package/dist-server/engine/types.js.map +1 -0
  72. package/dist-server/index.d.ts +6 -0
  73. package/dist-server/index.js +13 -0
  74. package/dist-server/index.js.map +1 -0
  75. package/dist-server/tsconfig.tsbuildinfo +1 -0
  76. package/helps/integration/connector/data-go-kr-connector.md +38 -0
  77. package/helps/integration/services/building-ledger.md +110 -0
  78. package/helps/integration/services/weather.md +82 -0
  79. package/helps/integration/task/building-ledger.md +135 -0
  80. package/helps/integration/task/data-go-kr-call.md +114 -0
  81. package/helps/integration/task/weather-short-term-forecast.md +96 -0
  82. package/package.json +28 -0
  83. package/server/engine/connector/data-go-kr-client.ts +157 -0
  84. package/server/engine/connector/data-go-kr-connector.ts +72 -0
  85. package/server/engine/connector/index.ts +1 -0
  86. package/server/engine/index.ts +2 -0
  87. package/server/engine/spec/building-ledger/hub.ts +143 -0
  88. package/server/engine/spec/building-ledger/index.ts +7 -0
  89. package/server/engine/spec/index.ts +62 -0
  90. package/server/engine/spec/types.ts +84 -0
  91. package/server/engine/spec/weather/asos.ts +32 -0
  92. package/server/engine/spec/weather/aws.ts +27 -0
  93. package/server/engine/spec/weather/index.ts +14 -0
  94. package/server/engine/spec/weather/mid-term-forecast.ts +56 -0
  95. package/server/engine/spec/weather/short-term-forecast.ts +73 -0
  96. package/server/engine/spec/weather/warning.ts +33 -0
  97. package/server/engine/task/building-ledger/general.ts +141 -0
  98. package/server/engine/task/building-ledger/index.ts +13 -0
  99. package/server/engine/task/building-ledger/summary.ts +277 -0
  100. package/server/engine/task/call.ts +177 -0
  101. package/server/engine/task/index.ts +20 -0
  102. package/server/engine/task/weather/grid-converter.ts +79 -0
  103. package/server/engine/task/weather/index.ts +14 -0
  104. package/server/engine/task/weather/short-term-forecast.ts +110 -0
  105. package/server/engine/types.ts +64 -0
  106. package/server/index.ts +7 -0
  107. package/things-factory.config.js +1 -0
  108. package/translations/en.json +18 -0
  109. package/translations/ja.json +18 -0
  110. package/translations/ko.json +18 -0
  111. package/translations/ms.json +18 -0
  112. package/translations/zh.json +18 -0
  113. package/tsconfig.json +10 -0
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @things-factory/integration-data-go-kr
2
+
3
+ 한국 공공데이터포털 **[data.go.kr](https://www.data.go.kr)** 의 Open API 를 Things-Factory 통합 엔진에서 호출하기 위한 커넥터 + 태스크 묶음.
4
+
5
+ ## 설계 철학
6
+
7
+ 공공데이터포털에는 수천 개의 API 가 있지만 대부분 **같은 봉투 규약**(`response.header.resultCode` + `response.body.items.item`) 을 따른다. 그래서:
8
+
9
+ - **한 개의 커넥터** (`data-go-kr-connector`) 가 ServiceKey 를 관리
10
+ - **한 개의 범용 태스크** (`data-go-kr-call`) 가 모든 오퍼레이션을 호출
11
+ - **선언적 스펙 레지스트리** 가 파라미터 검증·문서·(장기) UI 도움말의 단일 원천
12
+ - **자주 쓰이는 도메인 전용 태스크** 만 별도 등록 (예: `data-go-kr-building-ledger`)
13
+
14
+ 이렇게 하면 TaskRegistry 가 폭발하지 않고, 새 서비스는 스펙 파일 한 개만 추가해서 즉시 사용 가능하다.
15
+
16
+ ## 제공 기능 (Phase 0)
17
+
18
+ ### Connector
19
+ - `data-go-kr-connector` — ServiceKey 기반 공용 클라이언트
20
+
21
+ ### Tasks
22
+ - `data-go-kr-call` — 범용 오퍼레이션 호출 (스펙 기반 자동 검증)
23
+ - `data-go-kr-building-ledger` — 건축물대장 표제부 조회 (순수 fetch)
24
+ - `data-go-kr-weather-short-term-forecast` — 기상청 단기예보 조회 (위경도→격자 자동 변환)
25
+
26
+ ### 서비스 스펙
27
+ - 건축물대장 도메인
28
+ - `1613000/BldRgstHubService` — 국토교통부 건축물대장 허브 서비스 (8 오퍼레이션)
29
+ - 기상청 도메인
30
+ - `1360000/VilageFcstInfoService_2.0` — 단기예보 (3 오퍼레이션)
31
+ - `1360000/MidFcstInfoService` — 중기예보 (4 오퍼레이션)
32
+ - `1360000/WthrWrnInfoService` — 기상특보 (2 오퍼레이션)
33
+ - `1360000/AsosHourlyInfoService` — 종관기상관측 시간자료 (1 오퍼레이션)
34
+ - `1360000/AwsHrInfoService` — 방재기상관측 시간자료 (1 오퍼레이션)
35
+
36
+ ## 빠른 시작
37
+
38
+ ```
39
+ 1. data.go.kr 가입 + 서비스 활용신청 + ServiceKey 복사
40
+ 2. Things-Factory 통합 UI → Connection 생성 (type=data-go-kr-connector)
41
+ 3. Scenario → Step 추가 (data-go-kr-call 또는 data-go-kr-building-ledger)
42
+ 4. 실행 후 로그 파일에서 결과 확인
43
+ ```
44
+
45
+ 자세한 가이드는 [helps/integration/connector/data-go-kr-connector.md](./helps/integration/connector/data-go-kr-connector.md) 참조.
46
+
47
+ ## 확장 — 새 공공 API 추가
48
+
49
+ **확장성 우선 원칙: 도메인별 폴더 + 한 파일 = 한 서비스/태스크.**
50
+
51
+ ```
52
+ server/engine/
53
+ ├── spec/ # 서비스 스펙 (도메인 = 폴더)
54
+ │ ├── types.ts # OperationSpec, ServiceSpec 정의
55
+ │ ├── index.ts # SpecRegistry (자동 수집·등록)
56
+ │ ├── building-ledger/
57
+ │ │ ├── index.ts
58
+ │ │ └── hub.ts # BldRgstHubService
59
+ │ └── weather/
60
+ │ ├── index.ts
61
+ │ ├── short-term-forecast.ts # 단기예보
62
+ │ ├── mid-term-forecast.ts # 중기예보
63
+ │ ├── warning.ts # 기상특보
64
+ │ ├── asos.ts # 종관기상관측
65
+ │ └── aws.ts # 방재기상관측
66
+ │
67
+ └── task/ # 태스크 (도메인 = 폴더)
68
+ ├── index.ts # 전체 배럴
69
+ ├── call.ts # data-go-kr-call (범용)
70
+ ├── building-ledger/
71
+ │ ├── index.ts
72
+ │ └── general.ts # data-go-kr-building-ledger
73
+ └── weather/
74
+ ├── index.ts
75
+ ├── short-term-forecast.ts # data-go-kr-weather-short-term-forecast
76
+ └── grid-converter.ts # 위경도 ↔ KMA 격자 헬퍼 (export only)
77
+ ```
78
+
79
+ **추가 우선순위**:
80
+ 1. **공통 경로로 가능한지 먼저 검토** — `spec/<domain>/<service>.ts` 만 등록하면 `data-go-kr-call` 로 즉시 호출 가능
81
+ 2. **공통 경로로 못 푸는 예외만 전용 태스크** — 입력 폼·헬퍼·도메인 가공이 필요한 경우 `task/<domain>/<variant>.ts` 추가
82
+
83
+ **새 도메인 추가**: `spec/<domain>/` 폴더 + `task/<domain>/` 폴더를 짝으로 만들고, 각 폴더의 `index.ts` 가 내부 파일을 import 하도록 작성. 최상위 `spec/index.ts` 는 도메인 폴더 import 한 줄만 추가하면 자동 등록.
84
+
85
+ 자세한 단계별 절차는 [ROADMAP.md 의 "기여 가이드"](./ROADMAP.md#기여-가이드--새-서비스-추가-워크플로우) 참조.
86
+
87
+ ## 로드맵
88
+
89
+ 전체 중장기 계획은 [ROADMAP.md](./ROADMAP.md) 참조.
90
+
91
+ ## 레퍼런스
92
+
93
+ - [공공데이터포털](https://www.data.go.kr)
94
+ - [Things-Factory integration-base](https://github.com/hatiolab/things-factory/tree/main/packages/integration-base)
package/ROADMAP.md ADDED
@@ -0,0 +1,282 @@
1
+ # @things-factory/integration-data-go-kr — ROADMAP
2
+
3
+ 본 문서는 본 모듈의 **단기·중기·장기 계획** 을 추적한다. 각 Phase 는 상호 독립적으로 진행 가능하며, 위로 갈수록 진입 장벽이 낮고 아래로 갈수록 integration-base 업스트림 변경 등 의존이 생긴다.
4
+
5
+ ---
6
+
7
+ ## Phase 0 — 초기 구조 (✅ Done)
8
+
9
+ | 항목 | 상태 | 비고 |
10
+ | --- | --- | --- |
11
+ | 패키지 스켈레톤 (`@things-factory/integration-data-go-kr`) | ✅ | integration-weather/integration-openai 패턴 준수 |
12
+ | `DataGoKrConnector` — ServiceKey 관리 + Encoding/Decoding 자동 정규화 | ✅ | Connection 의 `endpoint` 필드를 API 오리진으로 사용 (모의 서버 라우팅 가능) |
13
+ | `DataGoKrClient` — 범용 HTTP + User-Agent/Accept + URL 인코딩 이슈 방지 | ✅ | 단건/다건 자동 정규화(`getItems`) |
14
+ | `OperationSpec` / `ServiceSpec` 타입 + `SpecRegistry` | ✅ | 도움말 메타 포함 (description, example, responseNotes, externalDocsUrl) |
15
+ | 건축물대장 허브 서비스 (`1613000/BldRgstHubService`) 8 오퍼레이션 스펙 | ✅ | PNU 5요소 공통 파라미터 |
16
+ | 기상청 5개 서비스 스펙 + 단기예보 전용 태스크 + KMA 격자 변환 헬퍼 | ✅ | spec/weather/, task/weather/ 분할 (확장성 우선) |
17
+ | 범용 태스크 `data-go-kr-call` | ✅ | 스펙 기반 validation + zero-pad + default + enum |
18
+ | 도메인 전용 태스크 `data-go-kr-building-ledger` | ✅ | 표제부 조회 (순수 fetch; 집계·KPI 추출은 소비자 스텝에서 처리) |
19
+ | 5개 언어 번역 (ko/en/ja/ms/zh) | ✅ | label 기반 |
20
+ | 도움말 마크다운 (커넥터/태스크/서비스 카탈로그) | ✅ | |
21
+ | dssp/dkpi 연동 | ⏳ Phase 1 | 기존 `@dssp/integration-seumter` 제거 후 본 모듈로 교체 |
22
+
23
+ ---
24
+
25
+ ## Phase 1 — 운영 안정성 (단기, ~2주)
26
+
27
+ > 실사용 중 빈번한 장애 패턴을 클라이언트 레벨에서 흡수.
28
+
29
+ ### 1.1 자동 재시도 with exponential backoff
30
+
31
+ data.go.kr 은 500 "Unexpected errors" 가 간헐적으로 발생 (피크 타임대). 현재 1회 실패 = 시나리오 실패인데 운영에서는 **일시 장애 자동 복구** 가 필수.
32
+
33
+ - `DataGoKrClient` 에 `retry` 옵션 추가 (기본 3회, 2초→4초→8초 backoff)
34
+ - 5xx 또는 네트워크 오류에만 재시도, 4xx (키/파라미터 문제) 는 즉시 실패
35
+ - Connection 레벨에서 `maxRetries`, `retryBackoffBaseMs` 파라미터 노출
36
+
37
+ ### 1.2 레이트리밋/쿼터 백프레셔
38
+
39
+ - 일일 10,000건 제약을 클라이언트가 의식하도록 메트릭 수집 (선택적 Prometheus counter)
40
+ - 제한 근접 시 로그 경고
41
+ - Connection 파라미터로 `requestsPerMinute` 소프트 제한 (선택)
42
+
43
+ ### 1.3 결과 캐싱 layer (optional)
44
+
45
+ - 건축물대장처럼 **자주 변경되지 않는 데이터** 는 PNU 단위로 TTL 캐시 (기본 24시간)
46
+ - `@things-factory/cache-service` 활용
47
+ - Connection 파라미터로 `cacheTtlSec` 노출, `0` 이면 캐싱 OFF
48
+
49
+ ### 1.4 dssp/dkpi 이관 완료
50
+
51
+ - `@dssp/integration-seumter` 삭제
52
+ - `dkpi/package.json` 에 `@things-factory/integration-data-go-kr` 추가
53
+ - `dkpi/server/index.ts` import 교체
54
+ - 기존 Connection row 의 `type` 값 업데이트 SQL:
55
+ ```sql
56
+ UPDATE connection SET type = 'data-go-kr-connector' WHERE type = 'seumter-connector';
57
+ ```
58
+ - 기존 Scenario step 의 `task` 값 업데이트:
59
+ ```sql
60
+ UPDATE step SET task = 'data-go-kr-building-ledger' WHERE task = 'seumter-general-building';
61
+ ```
62
+ - 파라미터 키(`sigunguCd`, `bjdongCd` 등) 는 그대로 호환.
63
+
64
+ ---
65
+
66
+ ## Phase 2 — 스펙 기반 자동화 (중기, ~4주)
67
+
68
+ > 스펙이 진실의 원천이므로, 문서·UI·검증 모든 산출물이 스펙에서 자동 파생되도록.
69
+
70
+ ### 2.1 서비스 카탈로그 markdown 자동 생성
71
+
72
+ - `scripts/generate-service-help.ts` 작성
73
+ - `ServiceSpec` → `helps/integration/services/<serviceId>.md` 로 변환
74
+ - CI 에서 `npm run docs:services` → 수동으로 쓴 markdown 과 diff 검증
75
+ - 장기적으로는 모든 서비스 카탈로그가 spec 에서 자동 생성 (현재는 building-ledger 만 수작업)
76
+
77
+ ### 2.2 스펙 기반 타입 생성 (선택)
78
+
79
+ - 각 오퍼레이션의 응답 레코드 타입(`BrTitleItem` 등) 을 spec 으로부터 TypeScript 인터페이스로 자동 생성
80
+ - 현재는 `types.ts` 에 수작업. 서비스 수가 늘면 필요.
81
+
82
+ ### 2.3 OpenAPI/Swagger 호환 export
83
+
84
+ - `SpecRegistry` 를 OpenAPI 3.0 스펙으로 export
85
+ - Swagger UI 로 로컬 문서 서빙 (개발자 탐색용)
86
+
87
+ ---
88
+
89
+ ## Phase 3 — 동적 parameterSpec (중기, ~6주)
90
+
91
+ > 현재는 범용 태스크의 `params` 가 freeform JSON. operation 선택 시 **자동 생성된 폼** 으로 바뀌게 하려면 integration-base 업스트림 작업 필요.
92
+
93
+ ### 3.1 integration-base 업스트림 제안
94
+
95
+ - `TaskHandler.parameterSpec` 이 **정적 배열 | 동적 함수** 양쪽 수용하도록 타입 확장
96
+ ```ts
97
+ type DynamicParameterSpec = (context: { step: Step; connection: Connection }) => PropertySpec[]
98
+ parameterSpec?: PropertySpec[] | DynamicParameterSpec
99
+ ```
100
+ - UI(`integration-ui`) 가 `operationRef` 변경 시 `parameterSpec` 를 재조회하도록 훅 추가
101
+ - PR → things-factory 모노레포 주변 검토
102
+
103
+ ### 3.2 본 모듈에서 동적 parameterSpec 활용
104
+
105
+ - `data-go-kr-call.ts` 의 `parameterSpec` 을 함수형으로 전환
106
+ - `operationRef` 가 선택되면 해당 스펙의 `params` 로 UI 폼 자동 생성 (label, description 툴팁, enum → select, required 등)
107
+ - 도움말이 **각 필드별 툴팁** 으로 렌더 → markdown 을 안 열어도 됨
108
+
109
+ ---
110
+
111
+ ## Phase 4 — 서비스 라인업 확장 (중기, ~8주)
112
+
113
+ > 실제 프로젝트 수요를 확인하며 스펙을 하나씩 추가. 각각은 스펙 파일 1개 + 선택적으로 도메인 태스크 1개.
114
+
115
+ ### 4.1 집합건축물 정밀 조회
116
+
117
+ - `data-go-kr-building-ledger-apt` 전용 태스크 추가
118
+ - `getBrRecapTitleInfo` + `getBrExposPubuseAreaInfo` 조합으로 아파트 단지 정확한 연면적·세대수 추출
119
+ - 현재 `building-ledger` 는 일반건축물 표제부 기반
120
+
121
+ ### 4.2 부동산 실거래가 (`1613000/RTMSOBJSvc`)
122
+
123
+ - 아파트매매/전월세, 연립다세대, 단독다가구, 토지 거래 데이터
124
+ - 스펙: `server/engine/spec/rtms-trade.ts`
125
+ - 활용: KPI "시장가격 대비 비용성과" 지표 자동화
126
+
127
+ ### 4.3 주소/공간 (`1613000/JusoService` 또는 VWorld)
128
+
129
+ - 도로명주소 ↔ PNU 변환 (체이닝 핵심)
130
+ - `data-go-kr-address-to-pnu` 전용 태스크 → 주소 문자열 입력 → PNU 자동 반환
131
+ - dkpi 프로젝트 등록 시 UX 혁신 (사용자가 PNU 수동 입력 불필요)
132
+
133
+ ### 4.4 국토부 공간정보 / SGIS / 통계청
134
+
135
+ - 필요 시점에 추가
136
+
137
+ ### 4.5 용도별 전용 도메인 태스크
138
+
139
+ 도메인 가공이 필요할 때만 추가. 기준: **"시나리오에서 후처리로 3줄 이상 쓸 값" = 전용 태스크 후보**.
140
+
141
+ ---
142
+
143
+ ## Phase 5 — 엔터프라이즈 운영 (장기)
144
+
145
+ ### 5.1 멀티 ServiceKey 페일오버
146
+
147
+ - Connection 에 백업 ServiceKey 배열 지원
148
+ - 주 키가 실패/쿼터 초과 시 대체 키로 자동 전환
149
+
150
+ ### 5.2 비동기 배치 모드
151
+
152
+ - Phase 1 의 재시도·캐싱과 별개로, 수백~수천 건 일괄 조회 시 워커 풀 + 레이트리밋 제어
153
+ - dkpi 의 "프로젝트 전체 리프레시" 같은 시나리오용
154
+
155
+ ### 5.3 스펙 라이선싱 모델 공유
156
+
157
+ - 본 패키지의 `ServiceSpec` 포맷이 유용하다면, 다른 공공 포털(일본 e-Gov 등) 과 공유 가능한 메타 포맷으로 표준화
158
+ - `@things-factory/integration-public-data-core` 를 만들고 `integration-data-go-kr`, `integration-data-go-jp` 등을 파생
159
+
160
+ ---
161
+
162
+ ## 기여 가이드 — 새 서비스 추가 워크플로우
163
+
164
+ ### 원칙
165
+
166
+ - **1순위: 공통 경로 (`data-go-kr-call` + SpecRegistry) 로 최대한 커버**
167
+ - **2순위: 공통 경로로 못 푸는 예외만 전용 태스크로**
168
+
169
+ 대부분의 공공데이터포털 API 는 동일한 봉투 규약(`response.header.resultCode` + `response.body.items.item`) 을 따르므로 1순위로 처리된다. 그러나 아래 경우에는 2순위(전용 태스크) 가 필요하다:
170
+ - 입력 폼이 명시되어야 운영이 편한 자주 쓰는 오퍼레이션
171
+ - 응답 구조가 비표준 (다른 봉투, 배열 아닌 중첩 구조 등)
172
+ - 오퍼레이션 호출 전후에 도메인 가공 (파생 계산, 추가 호출 조합 등) 이 필요
173
+ - 여러 오퍼레이션을 묶은 매크로 호출
174
+
175
+ ### 디렉토리 컨벤션 — 확장성 우선 (도메인=폴더, 한 파일=한 서비스/태스크)
176
+
177
+ ```
178
+ server/engine/
179
+ ├── spec/ # 서비스 스펙
180
+ │ ├── types.ts # OperationSpec, ServiceSpec 정의
181
+ │ ├── index.ts # SpecRegistry (도메인 자동 수집·등록)
182
+ │ ├── building-ledger/
183
+ │ │ ├── index.ts
184
+ │ │ └── hub.ts # BldRgstHubService
185
+ │ └── weather/
186
+ │ ├── index.ts
187
+ │ ├── short-term-forecast.ts # 단기예보
188
+ │ ├── mid-term-forecast.ts
189
+ │ ├── warning.ts
190
+ │ ├── asos.ts
191
+ │ └── aws.ts
192
+ │
193
+ └── task/
194
+ ├── index.ts # 전체 배럴
195
+ ├── call.ts # data-go-kr-call (범용)
196
+ ├── building-ledger/
197
+ │ ├── index.ts
198
+ │ └── general.ts # data-go-kr-building-ledger
199
+ └── weather/
200
+ ├── index.ts
201
+ ├── short-term-forecast.ts # data-go-kr-weather-short-term-forecast
202
+ └── grid-converter.ts # 위경도 ↔ 격자 헬퍼 (export)
203
+ ```
204
+
205
+ 새 서비스/태스크 추가 = **해당 도메인 폴더 안에 파일 한 개 + 폴더 index 한 줄.** 다른 도메인을 건드리지 않는다.
206
+
207
+ ### 단계별 절차
208
+
209
+ **1. 데이터셋 선정**
210
+ - [data.go.kr](https://www.data.go.kr) 에서 상품 확인
211
+ - 앱에 구독 추가, ServiceKey 는 기존 Connection 재사용 가능
212
+
213
+ **2. 스펙 파일 작성** — `server/engine/spec/<domain>/<service-slug>.ts`. 새 도메인이면 폴더 + `index.ts` 도 만들고 `spec/index.ts` 에 도메인 import 한 줄 추가
214
+ ```ts
215
+ import { ServiceSpec } from './types'
216
+
217
+ export const MyService: ServiceSpec = {
218
+ serviceId: '1234567/MyService',
219
+ label: '...',
220
+ description: '...',
221
+ homepage: 'https://www.data.go.kr/data/<dataset-id>/openapi.do',
222
+ operations: {
223
+ getFoo: {
224
+ operationId: 'getFoo',
225
+ label: '...',
226
+ description: '...',
227
+ params: [
228
+ { name: 'param1', required: true, length: 5, description: '...' }
229
+ ]
230
+ }
231
+ }
232
+ }
233
+ ```
234
+
235
+ **3. 레지스트리 등록** — `spec/index.ts` 의 `registerService(MyService)` 에 추가
236
+
237
+ **4. 범용 경로로 호출 검증** — `data-go-kr-call` + `operationRef: "<serviceId>.getFoo"` 으로 시나리오 돌려봄. 여기서 끝나면 이상적.
238
+
239
+ **5. 필요 시 전용 태스크 작성** — `task/<domain>/<variant>.ts`
240
+ ```ts
241
+ import { TaskRegistry, ConnectionManager, Context, evaluateTemplate } from '@things-factory/integration-base'
242
+ import { DataGoKrConnectionInstance } from '../../connector/data-go-kr-connector'
243
+
244
+ async function MyTask(step, context: Context) {
245
+ const instance: DataGoKrConnectionInstance = await ConnectionManager.getConnectionInstanceByName(context.domain, step.connection)
246
+ // ... 전용 로직
247
+ return { data: { ... } }
248
+ }
249
+
250
+ MyTask.parameterSpec = [ /* 명시적 입력 폼 */ ]
251
+ MyTask.help = 'integration/task/<my-task-name>'
252
+
253
+ TaskRegistry.registerTaskHandler('data-go-kr-<my-task-name>', MyTask)
254
+ ```
255
+
256
+ **6. 태스크 배럴에 import 추가**
257
+ - `task/<domain>/index.ts` 에 `import './<variant>'`
258
+ - (신규 도메인 폴더인 경우) `task/index.ts` 에 `import './<domain>'`
259
+
260
+ **7. 도움말 작성**
261
+ - 서비스 카탈로그: `helps/integration/services/<domain>.md` (수동 — Phase 2 에서 자동 생성 예정)
262
+ - 전용 태스크 생성 시: `helps/integration/task/<my-task-name>.md`
263
+
264
+ **8. 테스트 시나리오**
265
+ - 범용 태스크로 최소 1회 정상 응답 확인
266
+ - 전용 태스크가 있다면 전용 태스크로도 호출 확인
267
+
268
+ ---
269
+
270
+ ## 현재 우선순위
271
+
272
+ | 우선순위 | 항목 | 담당 |
273
+ | --- | --- | --- |
274
+ | P0 | Phase 1.4 (dssp/dkpi 이관) | 진행 중 |
275
+ | P1 | Phase 1.1 (자동 재시도) | 다음 |
276
+ | P2 | Phase 3.1 (integration-base 동적 parameterSpec 제안 PR) | 중기 |
277
+ | P3 | Phase 4.3 (주소→PNU 자동화) | dkpi UX 개선 시점 |
278
+
279
+ ---
280
+
281
+ **최종 수정**: 2026-04-25
282
+ **담당**: @hatiolab/dssp 팀
@@ -0,0 +1,27 @@
1
+ import { DataGoKrResponse } from '../types';
2
+ export interface DataGoKrClientOptions {
3
+ serviceKey: string;
4
+ /** 오리진 오버라이드 (모의 서버/사설 프록시 용도). 기본 `https://apis.data.go.kr`. */
5
+ origin?: string;
6
+ }
7
+ export declare class DataGoKrClient {
8
+ private readonly serviceKey;
9
+ private readonly origin;
10
+ constructor(options: DataGoKrClientOptions);
11
+ /**
12
+ * 범용 GET 호출.
13
+ *
14
+ * @param servicePath `<agency>/<service>/<operation>` (예: `1613000/BldRgstHubService/getBrTitleInfo`)
15
+ * @param params 쿼리 파라미터 (serviceKey / _type 은 자동 추가)
16
+ */
17
+ get<T>(servicePath: string, params: Record<string, any>): Promise<DataGoKrResponse<T>>;
18
+ /**
19
+ * 단건 또는 복수 items 를 배열로 정규화해서 반환한다.
20
+ *
21
+ * data.go.kr 공통 특성으로, body.items 가 아래 세 가지 형태로 올 수 있다:
22
+ * - `""` (빈 문자열, 결과 없음)
23
+ * - `{ item: { ... } }` (단건)
24
+ * - `{ item: [ ... ] }` (복수)
25
+ */
26
+ getItems<T>(servicePath: string, params: Record<string, any>): Promise<T[]>;
27
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataGoKrClient = void 0;
4
+ /**
5
+ * 공공데이터포털(data.go.kr) 공용 HTTP 클라이언트.
6
+ *
7
+ * 모든 data.go.kr Open API 는 아래 공통 규약을 따른다:
8
+ * - URL: `https://apis.data.go.kr/<agency>/<service>/<operation>`
9
+ * - 인증: `serviceKey` 쿼리 파라미터 (Encoding/Decoding 두 형태 모두 수용)
10
+ * - 포맷: `_type=json` 없으면 XML
11
+ * - 에러: `<OpenAPI_ServiceResponse>` XML 또는 `response.header.resultCode !== '00'`
12
+ *
13
+ * 본 클라이언트는 서비스·오퍼레이션에 독립적으로 동작하며, 경로를 파라미터로 받는다.
14
+ * 서비스별 도메인 로직(파라미터 검증, 응답 매핑) 은 task 레이어에서 처리.
15
+ */
16
+ const DEFAULT_ORIGIN = 'https://apis.data.go.kr';
17
+ class DataGoKrClient {
18
+ constructor(options) {
19
+ if (!options.serviceKey) {
20
+ throw new Error('DataGoKrClient requires serviceKey');
21
+ }
22
+ /* Encoding 키(%2B 포함)를 붙여넣어도 Decoding 키처럼 동작하도록 정규화.
23
+ base64 키는 `%` 를 포함하지 않으므로 raw Decoding 키는 영향 없음. */
24
+ this.serviceKey = normalizeServiceKey(options.serviceKey.trim());
25
+ this.origin = resolveOrigin(options.origin);
26
+ }
27
+ /**
28
+ * 범용 GET 호출.
29
+ *
30
+ * @param servicePath `<agency>/<service>/<operation>` (예: `1613000/BldRgstHubService/getBrTitleInfo`)
31
+ * @param params 쿼리 파라미터 (serviceKey / _type 은 자동 추가)
32
+ */
33
+ async get(servicePath, params) {
34
+ const path = servicePath.startsWith('/') ? servicePath : `/${servicePath}`;
35
+ const url = new URL(`${this.origin}${path}`);
36
+ url.searchParams.set('serviceKey', this.serviceKey);
37
+ url.searchParams.set('_type', 'json');
38
+ for (const [k, v] of Object.entries(params)) {
39
+ if (v === undefined || v === null || v === '')
40
+ continue;
41
+ url.searchParams.set(k, String(v));
42
+ }
43
+ const response = await fetch(url.toString(), {
44
+ method: 'GET',
45
+ headers: {
46
+ 'User-Agent': '@things-factory/integration-data-go-kr',
47
+ Accept: 'application/json'
48
+ }
49
+ });
50
+ const raw = await response.text();
51
+ const nonSensitiveParams = Object.entries(params)
52
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
53
+ .map(([k, v]) => `${k}=${v}`)
54
+ .join(' ');
55
+ if (!response.ok) {
56
+ throw new Error(`data.go.kr ${servicePath} failed (${response.status}): ${raw.slice(0, 300)} — params: ${nonSensitiveParams}`);
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(raw);
61
+ }
62
+ catch (err) {
63
+ if (raw.includes('<OpenAPI_ServiceResponse') || raw.includes('<returnReasonCode>')) {
64
+ throw new Error(`data.go.kr ${servicePath}: ${parseOpenApiError(raw)} — params: ${nonSensitiveParams}`);
65
+ }
66
+ throw new Error(`data.go.kr ${servicePath}: non-JSON response: ${raw.slice(0, 300)} — params: ${nonSensitiveParams}`);
67
+ }
68
+ const header = parsed?.response?.header;
69
+ if (!header) {
70
+ throw new Error(`data.go.kr ${servicePath}: response missing header — body: ${raw.slice(0, 300)}`);
71
+ }
72
+ if (header.resultCode !== '00') {
73
+ throw new Error(`data.go.kr ${servicePath}: resultCode=${header.resultCode} msg=${header.resultMsg} — params: ${nonSensitiveParams}`);
74
+ }
75
+ return parsed;
76
+ }
77
+ /**
78
+ * 단건 또는 복수 items 를 배열로 정규화해서 반환한다.
79
+ *
80
+ * data.go.kr 공통 특성으로, body.items 가 아래 세 가지 형태로 올 수 있다:
81
+ * - `""` (빈 문자열, 결과 없음)
82
+ * - `{ item: { ... } }` (단건)
83
+ * - `{ item: [ ... ] }` (복수)
84
+ */
85
+ async getItems(servicePath, params) {
86
+ const response = await this.get(servicePath, params);
87
+ const items = response.response?.body?.items;
88
+ if (!items)
89
+ return [];
90
+ const arr = items.item;
91
+ if (!arr)
92
+ return [];
93
+ return Array.isArray(arr) ? arr : [arr];
94
+ }
95
+ }
96
+ exports.DataGoKrClient = DataGoKrClient;
97
+ /**
98
+ * Connection.endpoint 값을 검증하여 유효한 origin URL 로 정규화한다.
99
+ *
100
+ * - 비어있거나 공백만: 기본 오리진 사용
101
+ * - 유효한 절대 URL: trailing slash 제거 후 사용
102
+ * - 유효하지 않은 값: 친절한 에러 throw (단순 오타·누락 방지)
103
+ */
104
+ function resolveOrigin(value) {
105
+ if (!value || !value.trim())
106
+ return DEFAULT_ORIGIN;
107
+ const trimmed = value.trim();
108
+ try {
109
+ const parsed = new URL(trimmed);
110
+ if (!parsed.protocol.startsWith('http')) {
111
+ throw new Error('protocol must be http or https');
112
+ }
113
+ return trimmed.replace(/\/$/, '');
114
+ }
115
+ catch (err) {
116
+ throw new Error(`Invalid Connection.endpoint value '${value}': must be a full URL (예: https://apis.data.go.kr) ` +
117
+ `or empty (기본값 ${DEFAULT_ORIGIN} 사용). Reason: ${err.message}`);
118
+ }
119
+ }
120
+ /** Encoding 키(%2B) 를 붙여넣어도 Decoding 키로 정규화. */
121
+ function normalizeServiceKey(key) {
122
+ if (!key.includes('%'))
123
+ return key;
124
+ try {
125
+ return decodeURIComponent(key);
126
+ }
127
+ catch {
128
+ return key;
129
+ }
130
+ }
131
+ /** data.go.kr 공용 OpenAPI 에러 XML 에서 원인 추출. */
132
+ function parseOpenApiError(xml) {
133
+ const code = /<returnReasonCode>([^<]+)<\/returnReasonCode>/.exec(xml)?.[1];
134
+ const msg = /<errMsg>([^<]+)<\/errMsg>/.exec(xml)?.[1];
135
+ const auth = /<returnAuthMsg>([^<]+)<\/returnAuthMsg>/.exec(xml)?.[1];
136
+ return `code=${code} msg=${msg} auth=${auth}`;
137
+ }
138
+ //# sourceMappingURL=data-go-kr-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-go-kr-client.js","sourceRoot":"","sources":["../../../server/engine/connector/data-go-kr-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { DataGoKrResponse } from '../types'\n\n/**\n * 공공데이터포털(data.go.kr) 공용 HTTP 클라이언트.\n *\n * 모든 data.go.kr Open API 는 아래 공통 규약을 따른다:\n * - URL: `https://apis.data.go.kr/<agency>/<service>/<operation>`\n * - 인증: `serviceKey` 쿼리 파라미터 (Encoding/Decoding 두 형태 모두 수용)\n * - 포맷: `_type=json` 없으면 XML\n * - 에러: `<OpenAPI_ServiceResponse>` XML 또는 `response.header.resultCode !== '00'`\n *\n * 본 클라이언트는 서비스·오퍼레이션에 독립적으로 동작하며, 경로를 파라미터로 받는다.\n * 서비스별 도메인 로직(파라미터 검증, 응답 매핑) 은 task 레이어에서 처리.\n */\n\nconst DEFAULT_ORIGIN = 'https://apis.data.go.kr'\n\nexport interface DataGoKrClientOptions {\n serviceKey: string\n /** 오리진 오버라이드 (모의 서버/사설 프록시 용도). 기본 `https://apis.data.go.kr`. */\n origin?: string\n}\n\nexport class DataGoKrClient {\n private readonly serviceKey: string\n private readonly origin: string\n\n constructor(options: DataGoKrClientOptions) {\n if (!options.serviceKey) {\n throw new Error('DataGoKrClient requires serviceKey')\n }\n /* Encoding 키(%2B 포함)를 붙여넣어도 Decoding 키처럼 동작하도록 정규화.\n base64 키는 `%` 를 포함하지 않으므로 raw Decoding 키는 영향 없음. */\n this.serviceKey = normalizeServiceKey(options.serviceKey.trim())\n this.origin = resolveOrigin(options.origin)\n }\n\n /**\n * 범용 GET 호출.\n *\n * @param servicePath `<agency>/<service>/<operation>` (예: `1613000/BldRgstHubService/getBrTitleInfo`)\n * @param params 쿼리 파라미터 (serviceKey / _type 은 자동 추가)\n */\n async get<T>(servicePath: string, params: Record<string, any>): Promise<DataGoKrResponse<T>> {\n const path = servicePath.startsWith('/') ? servicePath : `/${servicePath}`\n const url = new URL(`${this.origin}${path}`)\n url.searchParams.set('serviceKey', this.serviceKey)\n url.searchParams.set('_type', 'json')\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null || v === '') continue\n url.searchParams.set(k, String(v))\n }\n\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'User-Agent': '@things-factory/integration-data-go-kr',\n Accept: 'application/json'\n }\n })\n const raw = await response.text()\n\n const nonSensitiveParams = Object.entries(params)\n .filter(([, v]) => v !== undefined && v !== null && v !== '')\n .map(([k, v]) => `${k}=${v}`)\n .join(' ')\n\n if (!response.ok) {\n throw new Error(\n `data.go.kr ${servicePath} failed (${response.status}): ${raw.slice(0, 300)} — params: ${nonSensitiveParams}`\n )\n }\n\n let parsed: DataGoKrResponse<T>\n try {\n parsed = JSON.parse(raw) as DataGoKrResponse<T>\n } catch (err) {\n if (raw.includes('<OpenAPI_ServiceResponse') || raw.includes('<returnReasonCode>')) {\n throw new Error(`data.go.kr ${servicePath}: ${parseOpenApiError(raw)} — params: ${nonSensitiveParams}`)\n }\n throw new Error(\n `data.go.kr ${servicePath}: non-JSON response: ${raw.slice(0, 300)} — params: ${nonSensitiveParams}`\n )\n }\n\n const header = parsed?.response?.header\n if (!header) {\n throw new Error(`data.go.kr ${servicePath}: response missing header — body: ${raw.slice(0, 300)}`)\n }\n if (header.resultCode !== '00') {\n throw new Error(\n `data.go.kr ${servicePath}: resultCode=${header.resultCode} msg=${header.resultMsg} — params: ${nonSensitiveParams}`\n )\n }\n\n return parsed\n }\n\n /**\n * 단건 또는 복수 items 를 배열로 정규화해서 반환한다.\n *\n * data.go.kr 공통 특성으로, body.items 가 아래 세 가지 형태로 올 수 있다:\n * - `\"\"` (빈 문자열, 결과 없음)\n * - `{ item: { ... } }` (단건)\n * - `{ item: [ ... ] }` (복수)\n */\n async getItems<T>(servicePath: string, params: Record<string, any>): Promise<T[]> {\n const response = await this.get<T>(servicePath, params)\n const items = response.response?.body?.items\n if (!items) return []\n const arr = (items as any).item\n if (!arr) return []\n return Array.isArray(arr) ? arr : [arr]\n }\n}\n\n/**\n * Connection.endpoint 값을 검증하여 유효한 origin URL 로 정규화한다.\n *\n * - 비어있거나 공백만: 기본 오리진 사용\n * - 유효한 절대 URL: trailing slash 제거 후 사용\n * - 유효하지 않은 값: 친절한 에러 throw (단순 오타·누락 방지)\n */\nfunction resolveOrigin(value: string | undefined): string {\n if (!value || !value.trim()) return DEFAULT_ORIGIN\n const trimmed = value.trim()\n try {\n const parsed = new URL(trimmed)\n if (!parsed.protocol.startsWith('http')) {\n throw new Error('protocol must be http or https')\n }\n return trimmed.replace(/\\/$/, '')\n } catch (err) {\n throw new Error(\n `Invalid Connection.endpoint value '${value}': must be a full URL (예: https://apis.data.go.kr) ` +\n `or empty (기본값 ${DEFAULT_ORIGIN} 사용). Reason: ${(err as Error).message}`\n )\n }\n}\n\n/** Encoding 키(%2B) 를 붙여넣어도 Decoding 키로 정규화. */\nfunction normalizeServiceKey(key: string): string {\n if (!key.includes('%')) return key\n try {\n return decodeURIComponent(key)\n } catch {\n return key\n }\n}\n\n/** data.go.kr 공용 OpenAPI 에러 XML 에서 원인 추출. */\nfunction parseOpenApiError(xml: string): string {\n const code = /<returnReasonCode>([^<]+)<\\/returnReasonCode>/.exec(xml)?.[1]\n const msg = /<errMsg>([^<]+)<\\/errMsg>/.exec(xml)?.[1]\n const auth = /<returnAuthMsg>([^<]+)<\\/returnAuthMsg>/.exec(xml)?.[1]\n return `code=${code} msg=${msg} auth=${auth}`\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import { Connector } from '@things-factory/integration-base';
2
+ import { DataGoKrClient } from './data-go-kr-client';
3
+ /**
4
+ * 공공데이터포털(data.go.kr) Open API 커넥터.
5
+ *
6
+ * 하나의 ServiceKey 로 data.go.kr 산하 **모든 오퍼레이션** 을 호출할 수 있으므로,
7
+ * 사용자는 Connection 을 보통 한 개만 만들고 (앱·법인별로 1개) 여러 태스크에서 재사용한다.
8
+ *
9
+ * Connection 의 `endpoint` 필드를 API 오리진(host) 으로 사용한다. 비워두면 기본값
10
+ * `https://apis.data.go.kr` 가 사용되며, 모의 서버/사설 프록시로 라우팅하려면
11
+ * 해당 오리진을 endpoint 에 입력한다.
12
+ */
13
+ export interface DataGoKrConnectionInstance {
14
+ client: DataGoKrClient;
15
+ }
16
+ export declare class DataGoKrConnector implements Connector {
17
+ ready(connectionConfigs: any): Promise<void>;
18
+ connect(connection: any): Promise<void>;
19
+ disconnect(connection: any): Promise<void>;
20
+ get parameterSpec(): {
21
+ type: string;
22
+ name: string;
23
+ label: string;
24
+ useDomainAttribute: boolean;
25
+ }[];
26
+ get taskPrefixes(): string[];
27
+ get help(): string;
28
+ get description(): string;
29
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataGoKrConnector = void 0;
4
+ const integration_base_1 = require("@things-factory/integration-base");
5
+ const data_go_kr_client_1 = require("./data-go-kr-client");
6
+ class DataGoKrConnector {
7
+ async ready(connectionConfigs) {
8
+ await Promise.all(connectionConfigs.map(this.connect.bind(this)));
9
+ integration_base_1.ConnectionManager.logger.info('data-go-kr-connector connections are ready');
10
+ }
11
+ async connect(connection) {
12
+ const { endpoint, params } = connection;
13
+ try {
14
+ const client = new data_go_kr_client_1.DataGoKrClient({
15
+ serviceKey: params.serviceKey,
16
+ origin: endpoint
17
+ });
18
+ const instance = { client };
19
+ integration_base_1.ConnectionManager.addConnectionInstance(connection, instance);
20
+ integration_base_1.ConnectionManager.logger.info(`data-go-kr-connector connection(${connection.name}) is connected (endpoint: ${endpoint || '(default)'})`);
21
+ }
22
+ catch (ex) {
23
+ integration_base_1.ConnectionManager.logger.error(`data-go-kr-connector connection(${connection.name}) failed`, ex);
24
+ throw ex;
25
+ }
26
+ }
27
+ async disconnect(connection) {
28
+ integration_base_1.ConnectionManager.removeConnectionInstance(connection);
29
+ integration_base_1.ConnectionManager.logger.info(`data-go-kr-connector connection(${connection.name}) is disconnected`);
30
+ }
31
+ get parameterSpec() {
32
+ return [
33
+ {
34
+ type: 'secret',
35
+ name: 'serviceKey',
36
+ label: 'label.service-key',
37
+ useDomainAttribute: true
38
+ }
39
+ ];
40
+ }
41
+ get taskPrefixes() {
42
+ return ['data-go-kr'];
43
+ }
44
+ get help() {
45
+ return 'integration/connector/data-go-kr-connector';
46
+ }
47
+ get description() {
48
+ return 'Korean Public Data Portal (data.go.kr) Connector';
49
+ }
50
+ }
51
+ exports.DataGoKrConnector = DataGoKrConnector;
52
+ integration_base_1.ConnectionManager.registerConnector('data-go-kr-connector', new DataGoKrConnector());
53
+ //# sourceMappingURL=data-go-kr-connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-go-kr-connector.js","sourceRoot":"","sources":["../../../server/engine/connector/data-go-kr-connector.ts"],"names":[],"mappings":";;;AAAA,uEAA+E;AAE/E,2DAAoD;AAgBpD,MAAa,iBAAiB;IAC5B,KAAK,CAAC,KAAK,CAAC,iBAAiB;QAC3B,MAAM,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjE,oCAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAA;IAC7E,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAU;QACtB,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,UAAU,CAAA;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,kCAAc,CAAC;gBAChC,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,MAAM,EAAE,QAAQ;aACjB,CAAC,CAAA;YACF,MAAM,QAAQ,GAA+B,EAAE,MAAM,EAAE,CAAA;YACvD,oCAAiB,CAAC,qBAAqB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;YAC7D,oCAAiB,CAAC,MAAM,CAAC,IAAI,CAC3B,mCAAmC,UAAU,CAAC,IAAI,6BAA6B,QAAQ,IAAI,WAAW,GAAG,CAC1G,CAAA;QACH,CAAC;QAAC,OAAO,EAAE,EAAE,CAAC;YACZ,oCAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,mCAAmC,UAAU,CAAC,IAAI,UAAU,EAAE,EAAE,CAAC,CAAA;YAChG,MAAM,EAAE,CAAA;QACV,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAAU;QACzB,oCAAiB,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAA;QACtD,oCAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,mCAAmC,UAAU,CAAC,IAAI,mBAAmB,CAAC,CAAA;IACtG,CAAC;IAED,IAAI,aAAa;QACf,OAAO;YACL;gBACE,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,YAAY;gBAClB,KAAK,EAAE,mBAAmB;gBAC1B,kBAAkB,EAAE,IAAI;aACzB;SACF,CAAA;IACH,CAAC;IAED,IAAI,YAAY;QACd,OAAO,CAAC,YAAY,CAAC,CAAA;IACvB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,4CAA4C,CAAA;IACrD,CAAC;IAED,IAAI,WAAW;QACb,OAAO,kDAAkD,CAAA;IAC3D,CAAC;CACF;AAnDD,8CAmDC;AAED,oCAAiB,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,IAAI,iBAAiB,EAAE,CAAC,CAAA","sourcesContent":["import { ConnectionManager, Connector } from '@things-factory/integration-base'\n\nimport { DataGoKrClient } from './data-go-kr-client'\n\n/**\n * 공공데이터포털(data.go.kr) Open API 커넥터.\n *\n * 하나의 ServiceKey 로 data.go.kr 산하 **모든 오퍼레이션** 을 호출할 수 있으므로,\n * 사용자는 Connection 을 보통 한 개만 만들고 (앱·법인별로 1개) 여러 태스크에서 재사용한다.\n *\n * Connection 의 `endpoint` 필드를 API 오리진(host) 으로 사용한다. 비워두면 기본값\n * `https://apis.data.go.kr` 가 사용되며, 모의 서버/사설 프록시로 라우팅하려면\n * 해당 오리진을 endpoint 에 입력한다.\n */\nexport interface DataGoKrConnectionInstance {\n client: DataGoKrClient\n}\n\nexport class DataGoKrConnector implements Connector {\n async ready(connectionConfigs) {\n await Promise.all(connectionConfigs.map(this.connect.bind(this)))\n ConnectionManager.logger.info('data-go-kr-connector connections are ready')\n }\n\n async connect(connection) {\n const { endpoint, params } = connection\n try {\n const client = new DataGoKrClient({\n serviceKey: params.serviceKey,\n origin: endpoint\n })\n const instance: DataGoKrConnectionInstance = { client }\n ConnectionManager.addConnectionInstance(connection, instance)\n ConnectionManager.logger.info(\n `data-go-kr-connector connection(${connection.name}) is connected (endpoint: ${endpoint || '(default)'})`\n )\n } catch (ex) {\n ConnectionManager.logger.error(`data-go-kr-connector connection(${connection.name}) failed`, ex)\n throw ex\n }\n }\n\n async disconnect(connection) {\n ConnectionManager.removeConnectionInstance(connection)\n ConnectionManager.logger.info(`data-go-kr-connector connection(${connection.name}) is disconnected`)\n }\n\n get parameterSpec() {\n return [\n {\n type: 'secret',\n name: 'serviceKey',\n label: 'label.service-key',\n useDomainAttribute: true\n }\n ]\n }\n\n get taskPrefixes() {\n return ['data-go-kr']\n }\n\n get help() {\n return 'integration/connector/data-go-kr-connector'\n }\n\n get description() {\n return 'Korean Public Data Portal (data.go.kr) Connector'\n }\n}\n\nConnectionManager.registerConnector('data-go-kr-connector', new DataGoKrConnector())\n"]}
@@ -0,0 +1 @@
1
+ import './data-go-kr-connector';