@things-factory/integration-juso 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.
- package/README.md +63 -0
- package/dist-server/engine/connector/index.d.ts +1 -0
- package/dist-server/engine/connector/index.js +4 -0
- package/dist-server/engine/connector/index.js.map +1 -0
- package/dist-server/engine/connector/juso-client.d.ts +20 -0
- package/dist-server/engine/connector/juso-client.js +74 -0
- package/dist-server/engine/connector/juso-client.js.map +1 -0
- package/dist-server/engine/connector/juso-connector.d.ts +26 -0
- package/dist-server/engine/connector/juso-connector.js +50 -0
- package/dist-server/engine/connector/juso-connector.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +5 -0
- package/dist-server/engine/index.js.map +1 -0
- package/dist-server/engine/task/address/index.d.ts +7 -0
- package/dist-server/engine/task/address/index.js +10 -0
- package/dist-server/engine/task/address/index.js.map +1 -0
- package/dist-server/engine/task/address/resolve.d.ts +54 -0
- package/dist-server/engine/task/address/resolve.js +111 -0
- package/dist-server/engine/task/address/resolve.js.map +1 -0
- package/dist-server/engine/task/index.d.ts +6 -0
- package/dist-server/engine/task/index.js +9 -0
- package/dist-server/engine/task/index.js.map +1 -0
- package/dist-server/engine/types.d.ts +86 -0
- package/dist-server/engine/types.js +10 -0
- package/dist-server/engine/types.js.map +1 -0
- package/dist-server/index.d.ts +4 -0
- package/dist-server/index.js +11 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/helps/integration/connector/juso-connector.md +28 -0
- package/helps/integration/task/juso-resolve-address.md +110 -0
- package/package.json +28 -0
- package/server/engine/connector/index.ts +1 -0
- package/server/engine/connector/juso-client.ts +98 -0
- package/server/engine/connector/juso-connector.ts +66 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/task/address/index.ts +7 -0
- package/server/engine/task/address/resolve.ts +133 -0
- package/server/engine/task/index.ts +6 -0
- package/server/engine/types.ts +86 -0
- package/server/index.ts +5 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +5 -0
- package/translations/ja.json +5 -0
- package/translations/ko.json +5 -0
- package/translations/ms.json +5 -0
- package/translations/zh.json +5 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# juso Connector
|
|
2
|
+
|
|
3
|
+
행정안전부 **도로명주소 안내시스템 (juso.go.kr)** Open API 커넥터.
|
|
4
|
+
|
|
5
|
+
- **운영주체**: 행정안전부
|
|
6
|
+
- **API 호스트**: `https://business.juso.go.kr`
|
|
7
|
+
- **인증**: 발급받은 `confmKey` (승인키) 를 쿼리 파라미터로 전달
|
|
8
|
+
- **비용**: 무료 (활용신청 후 승인)
|
|
9
|
+
|
|
10
|
+
## Connection 필드
|
|
11
|
+
|
|
12
|
+
| 필드 | 필수 | 설명 |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `name` | O | Connection 이름 |
|
|
15
|
+
| `endpoint` | △ | API 오리진. 비우면 기본 `https://business.juso.go.kr`. 모의 서버 라우팅 시에만 입력 |
|
|
16
|
+
| `confmKey` (params) | O | juso.go.kr 활용신청 후 발급된 승인키 (도메인 시크릿 권장) |
|
|
17
|
+
|
|
18
|
+
## 관련 태스크
|
|
19
|
+
|
|
20
|
+
- [`juso-resolve-address`](../task/juso-resolve-address.md) — 주소 → PNU + admCd + 우편번호 통합 변환
|
|
21
|
+
|
|
22
|
+
## 활용신청 (요약)
|
|
23
|
+
|
|
24
|
+
1. [https://business.juso.go.kr](https://business.juso.go.kr) 접속 → 회원가입
|
|
25
|
+
2. **OPEN API → 신청** → 사용 목적·서비스 정보 입력 (자동 승인 ~ 1영업일)
|
|
26
|
+
3. 마이페이지 → **승인키 (confmKey)** 복사 → Connection 의 `confmKey` 에 붙여넣기
|
|
27
|
+
|
|
28
|
+
> juso.go.kr 의 승인키는 data.go.kr ServiceKey 와 **별개의 키 시스템** 입니다. 같은 사용자도 서비스별로 따로 신청해야 합니다.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# juso-resolve-address
|
|
2
|
+
|
|
3
|
+
도로명/지번 주소 문자열을 받아 한국 부동산·행정 API 에서 공통으로 쓰이는 표준 식별자 (PNU 5요소 + 법정동코드 + 우편번호) 로 변환하는 태스크.
|
|
4
|
+
|
|
5
|
+
## 입력
|
|
6
|
+
|
|
7
|
+
| 이름 | 필수 | 설명 | 예시 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| `query` | O | 도로명 또는 지번 주소 | `서울특별시 강남구 테헤란로 223` 또는 `서울특별시 강남구 역삼동 823` |
|
|
10
|
+
| `pickFirst` | △ | 다중 매칭 시 처리. `true`(기본) 첫 결과 자동 선택, `false` 다중 매칭 시 에러 | `true` |
|
|
11
|
+
|
|
12
|
+
### 데이터 바인딩
|
|
13
|
+
|
|
14
|
+
`query` 등 모든 필드는 **JS 템플릿 리터럴 `${...}`** 표현식 지원 (Things-Factory `evaluateTemplate` 표준):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
query: ${data.project.address}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`{{...}}` 가 아닌 `${...}` 입니다. 임의 JS 표현식도 가능 (`${data.foo + '-' + variables.suffix}` 등).
|
|
21
|
+
|
|
22
|
+
## 출력 (`data`)
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
{
|
|
26
|
+
"address": {
|
|
27
|
+
"road": "서울특별시 강남구 테헤란로 223",
|
|
28
|
+
"jibun": "서울특별시 강남구 역삼동 823",
|
|
29
|
+
"buildingName": "한국타이어빌딩",
|
|
30
|
+
"siNm": "서울특별시",
|
|
31
|
+
"sggNm": "강남구",
|
|
32
|
+
"emdNm": "역삼동"
|
|
33
|
+
},
|
|
34
|
+
"pnu": {
|
|
35
|
+
"sigunguCd": "11680",
|
|
36
|
+
"bjdongCd": "10300",
|
|
37
|
+
"platGbCd": "0", // '1' = 산 지번
|
|
38
|
+
"bun": "823",
|
|
39
|
+
"ji": "0"
|
|
40
|
+
},
|
|
41
|
+
"admCd": "1168010300", // 법정동코드 10자리
|
|
42
|
+
"bdMgtSn": "1168010300100823000000001",
|
|
43
|
+
"rnMgtSn": "116803122023",
|
|
44
|
+
"zipNo": "06168",
|
|
45
|
+
"raw": { /* juso.go.kr 원본 응답 */ }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> `coord` 필드는 본 태스크에서 제공하지 않습니다 (juso 응답이 WGS84 좌표를 직접 주지 않음). 위경도가 필요하면 별도 VWorld geocoder task 와 체이닝하거나, 동일 출력 형태의 `vworld-resolve-address` 태스크가 추가되면 그걸 사용.
|
|
50
|
+
|
|
51
|
+
## 매핑 규칙
|
|
52
|
+
|
|
53
|
+
PNU 5요소는 juso 응답에서 다음과 같이 도출됩니다:
|
|
54
|
+
|
|
55
|
+
| PNU 필드 | juso 응답 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `sigunguCd` | `admCd[0..4]` |
|
|
58
|
+
| `bjdongCd` | `admCd[5..9]` |
|
|
59
|
+
| `platGbCd` | `mtYn === '1' ? '1' : '0'` |
|
|
60
|
+
| `bun` | `lnbrMnnm` |
|
|
61
|
+
| `ji` | `lnbrSlno` |
|
|
62
|
+
|
|
63
|
+
## 시나리오 예시 — 건축물대장 조회 체인
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
"name": "resolve",
|
|
69
|
+
"task": "juso-resolve-address",
|
|
70
|
+
"connection": "juso",
|
|
71
|
+
"params": { "query": "${data.project.address}" }
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "fetch-ledger",
|
|
75
|
+
"task": "data-go-kr-building-ledger",
|
|
76
|
+
"connection": "data-go-kr",
|
|
77
|
+
"params": {
|
|
78
|
+
"sigunguCd": "${data.pnu.sigunguCd}",
|
|
79
|
+
"bjdongCd": "${data.pnu.bjdongCd}",
|
|
80
|
+
"platGbCd": "${data.pnu.platGbCd}",
|
|
81
|
+
"bun": "${data.pnu.bun}",
|
|
82
|
+
"ji": "${data.pnu.ji}"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
이 패턴은 dkpi 의 KPI 산정 파이프라인에 그대로 적용됩니다. **현장 등록 1회** 시점에 resolve → 결과 PNU 를 Project 엔티티에 저장하면 이후 KPI 호출은 저장된 PNU 만 사용 (resolve 반복 호출 불필요).
|
|
89
|
+
|
|
90
|
+
## 트러블슈팅
|
|
91
|
+
|
|
92
|
+
### `no match for '<query>'`
|
|
93
|
+
|
|
94
|
+
- 도로명/지번 표기에 띄어쓰기 또는 도시명 누락이 있는 경우. 더 구체적으로 입력.
|
|
95
|
+
- 신축 단지 등 신규 주소가 아직 도로명주소 DB 에 등록되지 않은 경우 — 사용승인 후 며칠 지연될 수 있음.
|
|
96
|
+
|
|
97
|
+
### 매칭 다중 발생
|
|
98
|
+
|
|
99
|
+
- 같은 동에 같은 도로명·번지가 여러 동(棟) 일 때 (예: 아파트 단지). 동/호 정보까지 query 에 포함하거나 `pickFirst=true` 로 자동 선택.
|
|
100
|
+
|
|
101
|
+
### 좌표가 필요한 경우
|
|
102
|
+
|
|
103
|
+
본 태스크는 좌표를 반환하지 않습니다. 위경도가 필요하면:
|
|
104
|
+
1. VWorld geocoder task 와 체이닝 (별도 패키지 추가 시)
|
|
105
|
+
2. juso `entX/entY` (TM 좌표계) 를 WGS84 로 변환하는 후속 task 추가
|
|
106
|
+
|
|
107
|
+
## 관련
|
|
108
|
+
|
|
109
|
+
- [`juso-connector`](../connector/juso-connector.md) — Connection 설정
|
|
110
|
+
- [`data-go-kr-building-ledger`](../../../../integration-data-go-kr/helps/integration/task/building-ledger.md) — PNU → 건축물대장 표제부 (체이닝 후속 step)
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@things-factory/integration-juso",
|
|
3
|
+
"version": "10.0.0-beta.108",
|
|
4
|
+
"main": "dist-server/index.js",
|
|
5
|
+
"things-factory": true,
|
|
6
|
+
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
7
|
+
"description": "Korean address standardization (juso.go.kr) connector for Things-Factory integration engine. Resolves road-name/jibun addresses into PNU 5-element + admCd + coordinates.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"@things-factory:registry": "https://registry.npmjs.org"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/hatiolab/things-factory.git",
|
|
16
|
+
"directory": "packages/integration-juso"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc --p tsconfig.json",
|
|
20
|
+
"build:server": "npm run clean:server && tsc",
|
|
21
|
+
"clean:server": "rm -rf dist-server",
|
|
22
|
+
"clean": "npm run clean:server"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@things-factory/integration-base": "^10.0.0-beta.108"
|
|
26
|
+
},
|
|
27
|
+
"gitHead": "7de47925ebd3f39dc232d4dbfa43f9687f3603c4"
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './juso-connector'
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { JusoItem, JusoResponse } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* juso.go.kr (도로명주소 안내시스템) Open API 호출 클라이언트.
|
|
5
|
+
*
|
|
6
|
+
* - 엔드포인트: `https://business.juso.go.kr/addrlink/addrLinkApi.do`
|
|
7
|
+
* - 인증: `confmKey` 쿼리 파라미터 (활용신청 후 발급)
|
|
8
|
+
* - 응답: JSON (기본은 XML 이라 `resultType=json` 필수)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ORIGIN = 'https://business.juso.go.kr'
|
|
12
|
+
const SEARCH_PATH = '/addrlink/addrLinkApi.do'
|
|
13
|
+
|
|
14
|
+
export interface JusoClientOptions {
|
|
15
|
+
confmKey: string
|
|
16
|
+
origin?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JusoSearchOptions {
|
|
20
|
+
/** 검색어 (도로명주소 또는 지번주소). */
|
|
21
|
+
keyword: string
|
|
22
|
+
/** 페이지당 결과수 (기본 10). */
|
|
23
|
+
countPerPage?: number
|
|
24
|
+
/** 현재 페이지 (기본 1). */
|
|
25
|
+
currentPage?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class JusoClient {
|
|
29
|
+
private readonly confmKey: string
|
|
30
|
+
private readonly origin: string
|
|
31
|
+
|
|
32
|
+
constructor(options: JusoClientOptions) {
|
|
33
|
+
if (!options.confmKey) {
|
|
34
|
+
throw new Error('JusoClient requires confmKey')
|
|
35
|
+
}
|
|
36
|
+
this.confmKey = options.confmKey.trim()
|
|
37
|
+
this.origin = resolveOrigin(options.origin)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 도로명/지번 주소 검색. 응답의 juso 배열을 반환 (없으면 빈 배열). */
|
|
41
|
+
async search(options: JusoSearchOptions): Promise<JusoItem[]> {
|
|
42
|
+
const url = new URL(`${this.origin}${SEARCH_PATH}`)
|
|
43
|
+
url.searchParams.set('confmKey', this.confmKey)
|
|
44
|
+
url.searchParams.set('resultType', 'json')
|
|
45
|
+
url.searchParams.set('keyword', options.keyword)
|
|
46
|
+
url.searchParams.set('currentPage', String(options.currentPage ?? 1))
|
|
47
|
+
url.searchParams.set('countPerPage', String(options.countPerPage ?? 10))
|
|
48
|
+
|
|
49
|
+
const response = await fetch(url.toString(), {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers: {
|
|
52
|
+
'User-Agent': '@things-factory/integration-juso',
|
|
53
|
+
Accept: 'application/json'
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
const raw = await response.text()
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`juso.go.kr search failed (${response.status}): ${raw.slice(0, 300)}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let parsed: JusoResponse
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw) as JusoResponse
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(`juso.go.kr search returned non-JSON: ${raw.slice(0, 300)}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const common = parsed?.results?.common
|
|
70
|
+
if (!common) {
|
|
71
|
+
throw new Error('juso.go.kr response missing results.common')
|
|
72
|
+
}
|
|
73
|
+
if (common.errorCode !== '0') {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`juso.go.kr error: code=${common.errorCode} msg=${common.errorMessage} keyword='${options.keyword}'`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parsed.results.juso || []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveOrigin(value: string | undefined): string {
|
|
84
|
+
if (!value || !value.trim()) return DEFAULT_ORIGIN
|
|
85
|
+
const trimmed = value.trim()
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(trimmed)
|
|
88
|
+
if (!parsed.protocol.startsWith('http')) {
|
|
89
|
+
throw new Error('protocol must be http or https')
|
|
90
|
+
}
|
|
91
|
+
return trimmed.replace(/\/$/, '')
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid Connection.endpoint value '${value}': must be a full URL (예: https://business.juso.go.kr) ` +
|
|
95
|
+
`or empty (기본값 ${DEFAULT_ORIGIN} 사용). Reason: ${(err as Error).message}`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ConnectionManager, Connector } from '@things-factory/integration-base'
|
|
2
|
+
|
|
3
|
+
import { JusoClient } from './juso-client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* juso.go.kr (행정안전부 도로명주소 안내시스템) Open API 커넥터.
|
|
7
|
+
*
|
|
8
|
+
* - 활용신청: https://business.juso.go.kr/addrlink/openApi/apiReqstRule.do
|
|
9
|
+
* - confmKey (승인키) 발급 후 본 커넥터 params 에 입력.
|
|
10
|
+
* - Connection 의 `endpoint` 필드는 API 오리진 오버라이드용 (비우면 기본 `https://business.juso.go.kr`).
|
|
11
|
+
*/
|
|
12
|
+
export interface JusoConnectionInstance {
|
|
13
|
+
client: JusoClient
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class JusoConnector implements Connector {
|
|
17
|
+
async ready(connectionConfigs) {
|
|
18
|
+
await Promise.all(connectionConfigs.map(this.connect.bind(this)))
|
|
19
|
+
ConnectionManager.logger.info('juso-connector connections are ready')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async connect(connection) {
|
|
23
|
+
const { endpoint, params } = connection
|
|
24
|
+
try {
|
|
25
|
+
const client = new JusoClient({ confmKey: params.confmKey, origin: endpoint })
|
|
26
|
+
const instance: JusoConnectionInstance = { client }
|
|
27
|
+
ConnectionManager.addConnectionInstance(connection, instance)
|
|
28
|
+
ConnectionManager.logger.info(
|
|
29
|
+
`juso-connector connection(${connection.name}) is connected (endpoint: ${endpoint || '(default)'})`
|
|
30
|
+
)
|
|
31
|
+
} catch (ex) {
|
|
32
|
+
ConnectionManager.logger.error(`juso-connector connection(${connection.name}) failed`, ex)
|
|
33
|
+
throw ex
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async disconnect(connection) {
|
|
38
|
+
ConnectionManager.removeConnectionInstance(connection)
|
|
39
|
+
ConnectionManager.logger.info(`juso-connector connection(${connection.name}) is disconnected`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get parameterSpec() {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
type: 'secret',
|
|
46
|
+
name: 'confmKey',
|
|
47
|
+
label: 'label.confm-key',
|
|
48
|
+
useDomainAttribute: true
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get taskPrefixes() {
|
|
54
|
+
return ['juso']
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get help() {
|
|
58
|
+
return 'integration/connector/juso-connector'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get description() {
|
|
62
|
+
return '도로명주소 안내시스템 (juso.go.kr) Connector'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ConnectionManager.registerConnector('juso-connector', new JusoConnector())
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ConnectionManager, Context, TaskRegistry, evaluateTemplate } from '@things-factory/integration-base'
|
|
2
|
+
|
|
3
|
+
import { JusoConnectionInstance } from '../../connector/juso-connector'
|
|
4
|
+
import { JusoItem, ResolvedAddress } from '../../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 도로명/지번 주소 → 표준 식별자 (PNU 5요소 + 법정동코드 + 우편번호) 변환 태스크.
|
|
8
|
+
*
|
|
9
|
+
* 다른 백엔드 (VWorld 등) 가 이 task 와 **같은 출력 형태(`ResolvedAddress`)** 를 반환하면
|
|
10
|
+
* 소비자 시나리오는 백엔드 교체 시 매핑 변경이 필요 없도록 설계됨.
|
|
11
|
+
*
|
|
12
|
+
* 입력:
|
|
13
|
+
* - `query` : 검색할 주소 문자열 (도로명 또는 지번 모두 가능)
|
|
14
|
+
* - `pickFirst` : 다수 매칭 시 첫 결과 선택 (기본 true). false 면 결과가 둘 이상일 때 에러.
|
|
15
|
+
*
|
|
16
|
+
* 출력 (`data`):
|
|
17
|
+
* - `ResolvedAddress` (단건). 매칭 0건이면 에러.
|
|
18
|
+
*/
|
|
19
|
+
async function JusoResolveAddressTask(step, context: Context): Promise<{ data: ResolvedAddress }> {
|
|
20
|
+
const { connection: connectionName, params } = step
|
|
21
|
+
const { domain, user, data, variables, lng, logger } = context
|
|
22
|
+
|
|
23
|
+
const instance: JusoConnectionInstance = await ConnectionManager.getConnectionInstanceByName(
|
|
24
|
+
domain,
|
|
25
|
+
connectionName
|
|
26
|
+
)
|
|
27
|
+
if (!instance?.client) {
|
|
28
|
+
throw new Error(`juso connection '${connectionName}' is not established.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const scope = { domain, user, lng, data, variables, console }
|
|
32
|
+
const evalString = (value: string | undefined) => {
|
|
33
|
+
if (value === undefined || value === null || value === '') return undefined
|
|
34
|
+
try {
|
|
35
|
+
return evaluateTemplate(String(value), scope)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(`juso-resolve-address: failed to evaluate parameter: ${(err as Error).message}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const query = evalString(params.query)
|
|
42
|
+
if (!query) {
|
|
43
|
+
throw new Error('juso-resolve-address: query is required (address string).')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pickFirst = params.pickFirst === undefined ? true : Boolean(params.pickFirst)
|
|
47
|
+
|
|
48
|
+
logger?.info?.(`[juso] search keyword='${query}'`)
|
|
49
|
+
|
|
50
|
+
const items = await instance.client.search({ keyword: query, countPerPage: pickFirst ? 1 : 10 })
|
|
51
|
+
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
throw new Error(`juso-resolve-address: no match for '${query}'. 도로명/지번 표기가 정확한지 확인.`)
|
|
54
|
+
}
|
|
55
|
+
if (!pickFirst && items.length > 1) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`juso-resolve-address: '${query}' 매칭 ${items.length}건. pickFirst=true 로 강제 선택 또는 query 더 구체적으로 입력.`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resolved = mapToResolvedAddress(items[0])
|
|
62
|
+
|
|
63
|
+
logger?.info?.(
|
|
64
|
+
`[juso] resolved → admCd=${resolved.admCd} pnu=${resolved.pnu.sigunguCd}-${resolved.pnu.bjdongCd}-${resolved.pnu.platGbCd}-${resolved.pnu.bun}-${resolved.pnu.ji}`
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return { data: resolved }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* juso 단일 결과 → 표준 ResolvedAddress.
|
|
72
|
+
*
|
|
73
|
+
* 매핑 규칙:
|
|
74
|
+
* - sigunguCd = admCd[0..4], bjdongCd = admCd[5..9]
|
|
75
|
+
* - platGbCd = mtYn === '1' ? '1' : '0' (산 지번 여부)
|
|
76
|
+
* - bun = lnbrMnnm, ji = lnbrSlno
|
|
77
|
+
* - 좌표는 juso 응답에 없으므로 undefined
|
|
78
|
+
*/
|
|
79
|
+
export function mapToResolvedAddress(item: JusoItem): ResolvedAddress {
|
|
80
|
+
const admCd = (item.admCd || '').padEnd(10, '0').slice(0, 10)
|
|
81
|
+
const sigunguCd = admCd.slice(0, 5)
|
|
82
|
+
const bjdongCd = admCd.slice(5, 10)
|
|
83
|
+
const platGbCd = item.mtYn === '1' ? '1' : '0'
|
|
84
|
+
const bun = String(item.lnbrMnnm ?? '').replace(/^0+/, '') || '0'
|
|
85
|
+
const ji = String(item.lnbrSlno ?? '').replace(/^0+/, '') || '0'
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
address: {
|
|
89
|
+
road: item.roadAddrPart1 || item.roadAddr,
|
|
90
|
+
jibun: item.jibunAddr,
|
|
91
|
+
buildingName: item.bdNm || undefined,
|
|
92
|
+
siNm: item.siNm,
|
|
93
|
+
sggNm: item.sggNm,
|
|
94
|
+
emdNm: item.emdNm
|
|
95
|
+
},
|
|
96
|
+
pnu: { sigunguCd, bjdongCd, platGbCd, bun, ji },
|
|
97
|
+
admCd,
|
|
98
|
+
bdMgtSn: item.bdMgtSn,
|
|
99
|
+
rnMgtSn: item.rnMgtSn,
|
|
100
|
+
zipNo: item.zipNo,
|
|
101
|
+
raw: item
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
JusoResolveAddressTask.parameterSpec = [
|
|
106
|
+
{
|
|
107
|
+
type: 'string',
|
|
108
|
+
name: 'query',
|
|
109
|
+
label: 'label.address-query',
|
|
110
|
+
placeholder: '예) 서울특별시 강남구 테헤란로 223',
|
|
111
|
+
description:
|
|
112
|
+
'검색할 주소 문자열 (도로명/지번). 도메인 EnvVar `Step::<scenario>::<step>::query` 로 ' +
|
|
113
|
+
'프로젝트별 주소 override 가능. 시나리오 변수도 사용 가능 (예: `${variables.address}`).',
|
|
114
|
+
useDomainAttribute: true
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'select',
|
|
118
|
+
name: 'pickFirst',
|
|
119
|
+
label: 'label.pick-first',
|
|
120
|
+
property: {
|
|
121
|
+
options: [
|
|
122
|
+
{ value: 'true', display: '첫 결과 자동 선택' },
|
|
123
|
+
{ value: 'false', display: '다중 매칭 시 에러' }
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
JusoResolveAddressTask.help = 'integration/task/juso-resolve-address'
|
|
130
|
+
|
|
131
|
+
TaskRegistry.registerTaskHandler('juso-resolve-address', JusoResolveAddressTask)
|
|
132
|
+
|
|
133
|
+
export default JusoResolveAddressTask
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* juso.go.kr (도로명주소 안내시스템) Open API 응답 타입.
|
|
3
|
+
*
|
|
4
|
+
* 본 패키지는 행정안전부 도로명주소조회 API 의 결과를 받아 우리 표준 결과 형태로
|
|
5
|
+
* 매핑한다. 다른 주소 resolver (예: VWorld) 와 호환되는 통일된 출력 형태를 사용해
|
|
6
|
+
* 소비자가 백엔드를 바꿔 끼울 수 있도록 한다.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** juso.go.kr 도로명주소 단일 항목 (응답의 results.juso 배열 원소). */
|
|
10
|
+
export interface JusoItem {
|
|
11
|
+
roadAddr: string
|
|
12
|
+
roadAddrPart1: string
|
|
13
|
+
roadAddrPart2?: string
|
|
14
|
+
jibunAddr: string
|
|
15
|
+
engAddr?: string
|
|
16
|
+
zipNo?: string
|
|
17
|
+
admCd: string /* 법정동코드 10자리 = sigunguCd 5 + bjdongCd 5 */
|
|
18
|
+
rnMgtSn?: string
|
|
19
|
+
bdMgtSn?: string /* 건물관리번호 25자리 (PNU 정보 인코딩됨) */
|
|
20
|
+
bdNm?: string /* 건물명 */
|
|
21
|
+
bdKdcd?: string
|
|
22
|
+
siNm?: string
|
|
23
|
+
sggNm?: string
|
|
24
|
+
emdNm?: string
|
|
25
|
+
liNm?: string
|
|
26
|
+
rn?: string
|
|
27
|
+
udrtYn?: string
|
|
28
|
+
buldMnnm?: string /* 도로명 건물 본번 */
|
|
29
|
+
buldSlno?: string
|
|
30
|
+
mtYn?: string /* 산 여부 ('1' 산, '0' 일반) */
|
|
31
|
+
lnbrMnnm: string /* 지번 본번 */
|
|
32
|
+
lnbrSlno: string /* 지번 부번 */
|
|
33
|
+
emdNo?: string
|
|
34
|
+
[key: string]: any
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** juso.go.kr 응답 봉투. */
|
|
38
|
+
export interface JusoResponse {
|
|
39
|
+
results: {
|
|
40
|
+
common: {
|
|
41
|
+
errorMessage: string
|
|
42
|
+
countPerPage: string
|
|
43
|
+
totalCount: string
|
|
44
|
+
errorCode: string
|
|
45
|
+
currentPage: string
|
|
46
|
+
}
|
|
47
|
+
juso: JusoItem[] | null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 주소 resolver 의 통일 출력 형태.
|
|
53
|
+
*
|
|
54
|
+
* 다른 백엔드 (juso, VWorld 등) 가 동일 형태로 반환해 소비자가 자유롭게 교체 가능.
|
|
55
|
+
* coord 는 백엔드에 따라 누락 가능 (juso 는 별도 변환 없이는 WGS84 좌표를 못 줌).
|
|
56
|
+
*/
|
|
57
|
+
export interface ResolvedAddress {
|
|
58
|
+
address: {
|
|
59
|
+
road?: string
|
|
60
|
+
jibun?: string
|
|
61
|
+
buildingName?: string
|
|
62
|
+
siNm?: string
|
|
63
|
+
sggNm?: string
|
|
64
|
+
emdNm?: string
|
|
65
|
+
}
|
|
66
|
+
/** 건축물대장 등 부동산 API 호출용 PNU 5요소. */
|
|
67
|
+
pnu: {
|
|
68
|
+
sigunguCd: string
|
|
69
|
+
bjdongCd: string
|
|
70
|
+
platGbCd: string
|
|
71
|
+
bun: string
|
|
72
|
+
ji: string
|
|
73
|
+
}
|
|
74
|
+
/** 법정동코드 10자리. */
|
|
75
|
+
admCd: string
|
|
76
|
+
/** 건물관리번호 25자리. */
|
|
77
|
+
bdMgtSn?: string
|
|
78
|
+
/** 도로명관리번호. */
|
|
79
|
+
rnMgtSn?: string
|
|
80
|
+
/** 우편번호 5자리. */
|
|
81
|
+
zipNo?: string
|
|
82
|
+
/** WGS84 위경도 (백엔드가 줄 때만). */
|
|
83
|
+
coord?: { latitude: number; longitude: number }
|
|
84
|
+
/** 백엔드 원본 응답 (디버깅·확장용). */
|
|
85
|
+
raw?: unknown
|
|
86
|
+
}
|
package/server/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {}
|