mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Compare commits
4 commits
main
...
feature/#2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6e428d923 | ||
|
|
be46d689d1 | ||
|
|
a826b118c0 | ||
|
|
ad76da95a5 |
12 changed files with 890 additions and 18 deletions
5
.changeset/donation-place-search.md
Normal file
5
.changeset/donation-place-search.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"donation-place-search": minor
|
||||
---
|
||||
|
||||
Add a donation place recommendation skill and package for Korean location/category-based donation recipient lookup.
|
||||
|
|
@ -40,6 +40,7 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
|
|||
| 개별공시지가 조회 | `gongsijiga-search` | realtyprice.kr 공개 API에서 지번 단위 개별공시지가(원/㎡) 다년도 추이·전년 대비 변동률 조회 | 불필요 | [개별공시지가 조회 가이드](docs/features/gongsijiga-search.md) |
|
||||
| LH 청약 공고문 조회 | `lh-notice-search` | 한국토지주택공사(LH) 임대/분양/주거복지(신혼희망타운)/토지/상가 공고를 지역·상태·공고유형·키워드로 조회하고 마감 여부를 KST 기준으로 표시 | 불필요 | [LH 청약 공고문 조회 가이드](docs/features/lh-notice-search.md) |
|
||||
| 법원 경매 부동산 매각공고 조회 | `court-auction-notice-search` | 대법원경매정보(courtauction.go.kr) 부동산 매각공고를 매각기일·법원·기일/기간 입찰 조건으로 검색해 사건번호·용도·주소·감정평가액·최저매각가격을 펼치고, 사건번호로 직접 사건정보·물건내역·매각기일이력을 조회 | 불필요 | [법원 경매 부동산 매각공고 조회 가이드](docs/features/court-auction-notice-search.md) |
|
||||
| 기부처 조회 | `donation-place-search` | 지역·관심 분야 기준 기부처 후보와 공식 페이지/1365 확인용 검색 링크 안내 (기부·결제 자동화 제외) | 불필요 | [기부처 조회 가이드](docs/features/donation-place-search.md) |
|
||||
| 장학금 검색 및 조회 | `korean-scholarship-search` | 한국장학재단·전국 대학교·재단·기업 장학 공고를 검색해 금액·자격·지원구간·링크를 정리하고 KST 기준 현재 날짜 마감 상태와 조건별 필터링까지 제공 | 불필요 | [장학금 검색 및 조회 가이드](docs/features/korean-scholarship-search.md) |
|
||||
| 생활쓰레기 배출정보 조회 | `household-waste-info` | 시군구 기준 생활쓰레기·음식물·재활용 배출요일·시간·장소·관리부서 확인 | 불필요 | [생활쓰레기 배출정보 조회 가이드](docs/features/household-waste-info.md) |
|
||||
| 학교 급식 식단 조회 | `k-schoollunch-menu` | 교육청·학교명으로 NEIS 학교 검색·급식 식단 조회 | 불필요 | [학교 급식 식단 조회 가이드](docs/features/k-schoollunch-menu.md) |
|
||||
|
|
@ -136,6 +137,7 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
|
|||
- [생활쓰레기 배출정보 조회 가이드](docs/features/household-waste-info.md)
|
||||
- [학교 급식 식단 조회 가이드](docs/features/k-schoollunch-menu.md)
|
||||
- [도서관 도서 조회 가이드](docs/features/library-book-search.md)
|
||||
- [기부처 조회 가이드](docs/features/donation-place-search.md)
|
||||
- [의약품 안전 체크 가이드](docs/features/mfds-drug-safety.md)
|
||||
- [식품 안전 체크 가이드](docs/features/mfds-food-safety.md)
|
||||
- [한국 주식 정보 조회 가이드](docs/features/korean-stock-search.md)
|
||||
|
|
|
|||
34
docs/features/donation-place-search.md
Normal file
34
docs/features/donation-place-search.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# 기부처 조회 가이드
|
||||
|
||||
`donation-place-search`는 사용자가 제공한 지역과 관심 분야를 기준으로 한국 기부처 후보를 추천하는 조회형 스킬이다.
|
||||
|
||||
- 자동 후원 신청, 결제, 개인정보 입력은 하지 않는다.
|
||||
- 1365 기부포털 공식 진입점(`https://www.1365.go.kr/dntn/main.do`)과 각 단체 공식 홈페이지에서 최신 등록 상태, 모금 기간, 기부금영수증 가능 여부를 확인하도록 안내한다.
|
||||
- 공개 페이지와 로컬 후보 랭킹만 사용하므로 `k-skill-proxy`나 API key가 필요 없다.
|
||||
|
||||
## 사용 예
|
||||
|
||||
```js
|
||||
const {
|
||||
recommendDonationPlaces,
|
||||
formatDonationRecommendationReport
|
||||
} = require("donation-place-search");
|
||||
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(formatDonationRecommendationReport(result));
|
||||
```
|
||||
|
||||
## 입력
|
||||
|
||||
- `location`: `서울 마포구`, `부산 해운대구`, `제주`, `온라인` 같은 위치 힌트
|
||||
- `category`: `아동`, `동물보호`, `환경`, `재난`, `장애`, `노인`, `의료`, `생계`, `해외구호`
|
||||
- `limit`: 기본 5, 최대 20
|
||||
|
||||
## 검증 표면
|
||||
|
||||
`nanumkorea.go.kr`는 1365 자원봉사/기부 통합 안내를 반환하므로, 스킬은 `www.1365.go.kr/dntn/main.do`를 최신 공식 확인 진입점의 기준으로 사용한다. 1365 페이지가 headless HTTP에서 느리거나 빈 응답을 줄 수 있어 화면 스크래핑 대신 best-effort 확인 보조 링크와 후보 공식 홈페이지를 함께 제시하며, 후보별 등록 검증이 이미 완료됐다고 표현하지 않는다.
|
||||
|
|
@ -92,6 +92,7 @@ npx --yes skills add <owner/repo> \
|
|||
--skill k-schoollunch-menu \
|
||||
--skill korean-character-count \
|
||||
--skill court-auction-notice-search \
|
||||
--skill donation-place-search \
|
||||
--skill k-skill-cleaner
|
||||
```
|
||||
|
||||
|
|
@ -277,7 +278,7 @@ npm run ci
|
|||
### Node 패키지
|
||||
|
||||
```bash
|
||||
npm install -g kordoc pdfjs-dist kbo-game kbl-results kleague-results lck-analytics toss-securities hipass-receipt k-lotto coupang-product-search used-car-price-search cheap-gas-nearby public-restroom-nearby korean-law-mcp market-kurly-search daiso bunjang-cli court-auction-notice-search gongsijiga-search
|
||||
npm install -g kordoc pdfjs-dist kbo-game kbl-results kleague-results lck-analytics toss-securities hipass-receipt k-lotto coupang-product-search used-car-price-search cheap-gas-nearby public-restroom-nearby korean-law-mcp market-kurly-search daiso bunjang-cli court-auction-notice-search gongsijiga-search donation-place-search
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
```
|
||||
|
||||
|
|
|
|||
139
donation-place-search/SKILL.md
Normal file
139
donation-place-search/SKILL.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
name: donation-place-search
|
||||
description: Use when the user asks where to donate, 기부처 조회, or donation place recommendations by Korean location and category. Recommend recipients with best-effort 1365 verification-assist links and never execute donations.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: utility
|
||||
locale: ko-KR
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# 기부처 조회 / Donation Place Search
|
||||
|
||||
## What this skill does
|
||||
|
||||
사용자가 “어디에 기부하면 좋을지”, “서울 아동 기부처”, “동물보호 기부처 추천”처럼 묻는 경우 **장소와 카테고리 기준으로 기부처 후보를 추천**한다.
|
||||
|
||||
- 기부를 대신 실행하지 않는다.
|
||||
- 결제, 개인정보 입력, 자동 후원 신청은 하지 않는다.
|
||||
- 추천은 의사결정 보조이며, 최종 기부 전 공식 페이지와 1365 기부포털에서 최신 등록·모금기간·기부금영수증 가능 여부를 확인한다.
|
||||
- 위치는 사용자가 제공한 행정구역/동네/랜드마크 텍스트만 사용한다. 자동 위치 추적을 하지 않는다.
|
||||
|
||||
## When to use
|
||||
|
||||
- “기부처 조회해줘”
|
||||
- “서울 마포구에서 동물보호 기부할 만한 곳 추천해줘”
|
||||
- “부산 노인 복지 기부처 알려줘”
|
||||
- “아동/재난 분야 기부처 비교해줘”
|
||||
- “어디에 기부하는 곳이 좋을지 장소와 카테고리별로 추천해줘”
|
||||
|
||||
## Inputs
|
||||
|
||||
- `location`: 선택. 예: `서울 마포구`, `부산 해운대구`, `제주`, `온라인`
|
||||
- `category`: 선택. 예: `아동`, `동물보호`, `환경`, `재난 구호`, `장애`, `노인`, `생계`, `의료`, `해외구호`
|
||||
- `limit`: 선택. 기본 5개
|
||||
|
||||
위치나 카테고리가 없으면 보수적으로 `전국`·`일반/종합` 후보와 1365 공식 확인 보조 링크를 제공한다. 비대화형 자동화에서는 임의로 좁히지 말고 “입력 없음”을 명시한다.
|
||||
|
||||
## Public access path discovered
|
||||
|
||||
### Primary official verification surface
|
||||
|
||||
- Legacy `https://www.nanumkorea.go.kr/` currently returns a notice that 1365 기부포털 has moved/integrated into 1365 자원봉사.
|
||||
- The notice links to `https://www.1365.go.kr/dntn/main.do`.
|
||||
- The skill therefore uses `https://www.1365.go.kr/dntn/main.do` as the official public verification entry point.
|
||||
|
||||
### Search-link strategy
|
||||
|
||||
1365 pages can be slow or unavailable to headless HTTP clients, so the package does not depend on brittle screen scraping. It builds a best-effort official-entry/search-assist link with the user’s location/category keywords, then ranks a curated fallback list locally. The package does not assert that 1365 has already verified each returned candidate:
|
||||
|
||||
```js
|
||||
const { recommendDonationPlaces } = require("donation-place-search");
|
||||
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(result.items);
|
||||
console.log(result.officialSearchUrl);
|
||||
```
|
||||
|
||||
The returned `officialSearchUrl` is a best-effort verification assist: open it as an official 1365 entry point, then confirm current registration and campaign status before giving the final answer.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Extract `location`, `category`, and optional `limit` from the user request.
|
||||
2. Run the helper:
|
||||
|
||||
```bash
|
||||
node - <<'NODE'
|
||||
const {
|
||||
recommendDonationPlaces,
|
||||
formatDonationRecommendationReport
|
||||
} = require("donation-place-search");
|
||||
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(formatDonationRecommendationReport(result));
|
||||
NODE
|
||||
```
|
||||
|
||||
3. Open or cite the returned best-effort 1365 verification-assist URL for latest verification when fresh browsing is available.
|
||||
4. Summarize 3–5 candidates, including:
|
||||
- 기부처명
|
||||
- 분야/카테고리
|
||||
- 지역 일치 여부 또는 전국 단위 여부
|
||||
- 왜 맞는지 한 줄
|
||||
- 공식 홈페이지
|
||||
- 1365 확인 보조 링크
|
||||
5. Add a caution that campaign status, donation receipt eligibility, and designated-use options must be checked on official pages before donating.
|
||||
|
||||
## Output fields
|
||||
|
||||
The npm helper returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"location": { "raw": "서울 마포구", "province": "서울", "district": "마포구" },
|
||||
"category": "animals",
|
||||
"items": [
|
||||
{
|
||||
"name": "동물권행동 카라",
|
||||
"categories": ["animals"],
|
||||
"coverage": "local",
|
||||
"homepageUrl": "https://www.ekara.org/",
|
||||
"officialSearchUrl": "https://www.1365.go.kr/dntn/main.do?...",
|
||||
"match": { "category": true, "local": true, "nationwide": false }
|
||||
}
|
||||
],
|
||||
"officialSearchUrl": "https://www.1365.go.kr/dntn/main.do?...",
|
||||
"meta": { "source": "curated-fallback-plus-1365-search-assist" }
|
||||
}
|
||||
```
|
||||
|
||||
## Done when
|
||||
|
||||
- 장소/카테고리 조건을 반영해 후보를 3–5개 이내로 정리했다.
|
||||
- 각 후보마다 공식 홈페이지 또는 1365 확인 보조 링크를 제공했다.
|
||||
- 최종 기부 전 등록 상태, 모금 기간, 기부금영수증 가능 여부를 확인하라고 안내했다.
|
||||
- 자동 결제/후원 신청을 시도하지 않았다.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- 1365 사이트가 느리거나 headless HTTP에서 timeout/empty page를 반환할 수 있다. 이 경우 확인 보조 URL과 후보 홈페이지를 제공하고 “최신 상태는 직접 확인 필요”라고 명시한다.
|
||||
- 위치 문자열이 행정구역으로 파싱되지 않으면 전국 후보 위주로 제안한다.
|
||||
- 지역·카테고리 모두 정확히 맞는 후보가 없으면 전국 단위 후보를 fallback으로 보여준다.
|
||||
- 특정 단체의 모금 캠페인, 지정기부 가능 여부, 기부금영수증 처리는 수시로 바뀌므로 package 내 curated 설명만으로 확정하지 않는다.
|
||||
- 로그인, 결제, CAPTCHA, 후원 신청서 제출은 자동화하지 않는다.
|
||||
|
||||
## Notes
|
||||
|
||||
- 이 스킬은 read-only 추천/조회 스킬이다.
|
||||
- 기부는 금전 의사결정이므로 최신 공식 근거를 우선한다.
|
||||
- 공개 표면만 사용하므로 `k-skill-proxy`와 API key가 필요 없다.
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -642,6 +642,10 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/donation-place-search": {
|
||||
"resolved": "packages/donation-place-search",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"dev": true,
|
||||
|
|
@ -1754,6 +1758,13 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/donation-place-search": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/gongsijiga-search": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py coupang-product-search/scripts/coupang_partners_mcp.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace court-auction-notice-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace court-auction-notice-search --dry-run && npm pack --workspace donation-place-search --dry-run && npm pack --workspace gongsijiga-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run",
|
||||
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
|
||||
"version-packages": "changeset version",
|
||||
"release:npm": "changeset publish"
|
||||
|
|
|
|||
40
packages/donation-place-search/README.md
Normal file
40
packages/donation-place-search/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# donation-place-search
|
||||
|
||||
Recommend Korean donation recipients by location and donation category.
|
||||
|
||||
The package combines:
|
||||
|
||||
- a public 1365 Give Korea (`www.1365.go.kr`) best-effort search-assist link builder for latest official verification;
|
||||
- deterministic category/location ranking over a small curated fallback set of well-known donation recipients;
|
||||
- Korean report formatting with cautions to verify current registration, campaign period, and donation receipt handling before donating.
|
||||
|
||||
No proxy and no API key are required. This package does **not** execute donations or submit personal/payment data.
|
||||
|
||||
```js
|
||||
const {
|
||||
recommendDonationPlaces,
|
||||
formatDonationRecommendationReport
|
||||
} = require("donation-place-search");
|
||||
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(formatDonationRecommendationReport(result));
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
- `recommendDonationPlaces(options)`
|
||||
- `formatDonationRecommendationReport(result)`
|
||||
- `build1365DonationSearchUrl(options)`
|
||||
- `normalizeCategory(input)`
|
||||
- `parseLocationQuery(location)`
|
||||
- `CATEGORIES`
|
||||
- `DONATION_PLACES`
|
||||
|
||||
## Notes
|
||||
|
||||
Donation campaigns and registration status change frequently. Always treat returned 1365 URLs as best-effort verification assists: open the 1365 official entry/search page and the recipient's official homepage before recommending a final donation decision.
|
||||
33
packages/donation-place-search/package.json
Normal file
33
packages/donation-place-search/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "donation-place-search",
|
||||
"version": "0.1.0",
|
||||
"description": "Recommend Korean donation recipients by location and category with best-effort 1365 Give Korea search-assist links and curated fallback data",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/NomaDamas/k-skill.git"
|
||||
},
|
||||
"keywords": [
|
||||
"k-skill",
|
||||
"korea",
|
||||
"donation",
|
||||
"charity",
|
||||
"1365",
|
||||
"nanum"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "node --check src/index.js && node --check test/index.test.js",
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
413
packages/donation-place-search/src/index.js
Normal file
413
packages/donation-place-search/src/index.js
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
const OFFICIAL_1365_DONATION_URL = "https://www.1365.go.kr/dntn/main.do";
|
||||
|
||||
const CATEGORIES = Object.freeze({
|
||||
general: {
|
||||
label: "일반/종합",
|
||||
keywords: ["일반", "종합", "기부", "나눔", "모금"]
|
||||
},
|
||||
children: {
|
||||
label: "아동·청소년",
|
||||
keywords: ["아동", "어린이", "청소년", "보육", "결식", "교육"]
|
||||
},
|
||||
elderly: {
|
||||
label: "노인",
|
||||
keywords: ["노인", "어르신", "독거", "요양"]
|
||||
},
|
||||
disability: {
|
||||
label: "장애",
|
||||
keywords: ["장애", "장애인", "발달장애", "이동권"]
|
||||
},
|
||||
animals: {
|
||||
label: "동물보호",
|
||||
keywords: ["동물", "동물보호", "유기동물", "반려동물"]
|
||||
},
|
||||
environment: {
|
||||
label: "환경",
|
||||
keywords: ["환경", "기후", "생태", "숲", "해양"]
|
||||
},
|
||||
disaster: {
|
||||
label: "재난·구호",
|
||||
keywords: ["재난", "구호", "긴급", "재해", "복구"]
|
||||
},
|
||||
health: {
|
||||
label: "보건·의료",
|
||||
keywords: ["의료", "보건", "환자", "치료", "질병"]
|
||||
},
|
||||
poverty: {
|
||||
label: "생계·주거",
|
||||
keywords: ["생계", "주거", "저소득", "취약계층", "노숙"]
|
||||
},
|
||||
international: {
|
||||
label: "해외구호",
|
||||
keywords: ["해외", "국제", "난민", "개발협력"]
|
||||
}
|
||||
});
|
||||
|
||||
const PROVINCE_ALIASES = Object.freeze([
|
||||
["서울", /서울|서울특별시|서울시/],
|
||||
["부산", /부산|부산광역시|부산시/],
|
||||
["대구", /대구|대구광역시|대구시/],
|
||||
["인천", /인천|인천광역시|인천시/],
|
||||
["광주", /광주|광주광역시|광주시/],
|
||||
["대전", /대전|대전광역시|대전시/],
|
||||
["울산", /울산|울산광역시|울산시/],
|
||||
["세종", /세종|세종특별자치시|세종시/],
|
||||
["경기", /경기|경기도/],
|
||||
["강원", /강원|강원도|강원특별자치도/],
|
||||
["충북", /충북|충청북도/],
|
||||
["충남", /충남|충청남도/],
|
||||
["전북", /전북|전라북도|전북특별자치도/],
|
||||
["전남", /전남|전라남도/],
|
||||
["경북", /경북|경상북도/],
|
||||
["경남", /경남|경상남도/],
|
||||
["제주", /제주|제주도|제주특별자치도/]
|
||||
]);
|
||||
|
||||
const DONATION_PLACES = Object.freeze([
|
||||
{
|
||||
id: "kara",
|
||||
name: "동물권행동 카라",
|
||||
categories: ["animals"],
|
||||
coverage: "local",
|
||||
locations: ["서울", "마포구"],
|
||||
description: "동물권 교육, 유기동물 구조·입양, 동물복지 캠페인을 하는 비영리단체입니다.",
|
||||
homepageUrl: "https://www.ekara.org/",
|
||||
verification: "공식 홈페이지의 후원/결산 공시와 1365 기부포털 등록 여부를 함께 확인하세요."
|
||||
},
|
||||
|
||||
{
|
||||
id: "animal-freedom",
|
||||
name: "동물자유연대",
|
||||
categories: ["animals"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국", "경기", "남양주"],
|
||||
description: "반려동물 복지, 구조동물 보호, 동물학대 대응과 정책 캠페인을 진행합니다.",
|
||||
homepageUrl: "https://www.animals.or.kr/",
|
||||
verification: "구조·보호 캠페인별 후원 목적과 기부금영수증 안내를 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "korean-cat-protection",
|
||||
name: "한국고양이보호협회",
|
||||
categories: ["animals"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "길고양이 보호, 치료지원, 입양·캠페인 활동에 초점을 둔 동물보호 단체입니다.",
|
||||
homepageUrl: "https://www.catcare.or.kr/",
|
||||
verification: "치료지원·입양 캠페인의 현재 모금 상태를 공식 공지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "kfem",
|
||||
name: "환경운동연합",
|
||||
categories: ["environment"],
|
||||
coverage: "local",
|
||||
locations: ["서울", "종로구"],
|
||||
description: "기후위기, 생태보전, 생활환경 이슈를 다루는 환경 시민단체입니다.",
|
||||
homepageUrl: "https://kfem.or.kr/",
|
||||
verification: "지역 조직과 캠페인별 모금 목적을 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "beautiful-store",
|
||||
name: "아름다운가게",
|
||||
categories: ["poverty", "environment", "general"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "물품 기부와 재사용 판매 수익으로 국내외 공익활동을 지원합니다.",
|
||||
homepageUrl: "https://www.beautifulstore.org/",
|
||||
verification: "방문 전 가까운 매장의 접수 가능 물품과 운영시간을 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "goodwill",
|
||||
name: "굿윌스토어",
|
||||
categories: ["disability", "poverty", "general"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "물품 기부를 장애인 일자리와 직업훈련으로 연결하는 기부처입니다.",
|
||||
homepageUrl: "https://www.goodwillstore.org/",
|
||||
verification: "가까운 지점의 물품 기증 기준과 방문수거 가능 여부를 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "childfund",
|
||||
name: "초록우산",
|
||||
categories: ["children", "poverty", "disaster"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "아동 복지, 결연, 긴급지원, 인재양성 사업을 운영하는 아동복지 전문기관입니다.",
|
||||
homepageUrl: "https://www.childfund.or.kr/",
|
||||
verification: "캠페인별 후원금 사용처와 연차보고서를 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "korean-red-cross",
|
||||
name: "대한적십자사",
|
||||
categories: ["disaster", "health", "poverty", "international"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "재난구호, 취약계층 지원, 헌혈·보건, 국제구호 사업을 수행합니다.",
|
||||
homepageUrl: "https://www.redcross.or.kr/",
|
||||
verification: "긴급모금은 모금 기간과 목적이 자주 바뀌므로 공식 공지에서 최신 상태를 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "community-chest",
|
||||
name: "사회복지공동모금회 사랑의열매",
|
||||
categories: ["general", "poverty", "children", "elderly", "disability"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "지역 공동모금과 배분사업을 운영하는 대표 법정 모금기관입니다.",
|
||||
homepageUrl: "https://chest.or.kr/",
|
||||
verification: "지역지회·캠페인별 배분 분야와 공시자료를 확인하세요."
|
||||
},
|
||||
|
||||
{
|
||||
id: "miral",
|
||||
name: "밀알복지재단",
|
||||
categories: ["disability", "children", "poverty", "international"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "장애인, 아동, 에너지 취약계층, 해외구호 사업을 운영하는 복지재단입니다.",
|
||||
homepageUrl: "https://www.miral.org/",
|
||||
verification: "사업별 지정후원 가능 여부와 공시자료를 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "okfoundation",
|
||||
name: "노인의료나눔재단",
|
||||
categories: ["elderly", "health", "poverty"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국"],
|
||||
description: "취약계층 어르신 의료비와 건강 지원 사업에 초점을 둔 재단입니다.",
|
||||
homepageUrl: "https://www.ok6595.or.kr/",
|
||||
verification: "현재 지원사업과 후원금 사용처를 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "babsang",
|
||||
name: "밥상공동체복지재단 연탄은행",
|
||||
categories: ["poverty", "elderly"],
|
||||
coverage: "nationwide",
|
||||
locations: ["전국", "강원", "원주"],
|
||||
description: "에너지 취약계층 연탄·난방 지원과 지역 복지사업을 운영합니다.",
|
||||
homepageUrl: "https://www.babsang.or.kr/",
|
||||
verification: "계절성 캠페인이 많으므로 현재 모금 주제와 물품/봉사 필요 여부를 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "greenpeace-korea",
|
||||
name: "그린피스 서울사무소",
|
||||
categories: ["environment", "international"],
|
||||
coverage: "nationwide",
|
||||
locations: ["서울", "전국"],
|
||||
description: "기후·해양·생물다양성 관련 국제 환경 캠페인을 진행합니다.",
|
||||
homepageUrl: "https://www.greenpeace.org/korea/",
|
||||
verification: "캠페인 성격과 기부금 영수증 처리 주체를 공식 페이지에서 확인하세요."
|
||||
},
|
||||
{
|
||||
id: "snuh-children",
|
||||
name: "서울대학교어린이병원 후원회",
|
||||
categories: ["children", "health"],
|
||||
coverage: "local",
|
||||
locations: ["서울", "종로구"],
|
||||
description: "중증·희귀질환 아동 치료와 병원 내 환아 지원에 초점을 둔 후원처입니다.",
|
||||
homepageUrl: "https://www.snuh.org/child/",
|
||||
verification: "병원 후원 경로와 지정기부 가능 범위를 공식 병원 페이지에서 확인하세요."
|
||||
}
|
||||
]);
|
||||
|
||||
function normalizeCategoryToken(value) {
|
||||
return String(value || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
||||
}
|
||||
|
||||
function normalizeCategory(input) {
|
||||
if (Array.isArray(input)) {
|
||||
return input.map(normalizeCategory).filter((value, index, values) => values.indexOf(value) === index);
|
||||
}
|
||||
|
||||
const query = normalizeCategoryToken(input);
|
||||
if (!query) {
|
||||
return "general";
|
||||
}
|
||||
|
||||
const categoryEntries = Object.entries(CATEGORIES);
|
||||
for (const [key, category] of categoryEntries) {
|
||||
if (query === key.toLowerCase()) {
|
||||
return key;
|
||||
}
|
||||
if (key !== "general" && category.keywords.some((keyword) => query.includes(normalizeCategoryToken(keyword)))) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, category] of categoryEntries) {
|
||||
if (category.keywords.some((keyword) => query.includes(normalizeCategoryToken(keyword)))) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return "general";
|
||||
}
|
||||
|
||||
function parseLocationQuery(location) {
|
||||
const raw = String(location || "").trim();
|
||||
let province = null;
|
||||
for (const [normalized, pattern] of PROVINCE_ALIASES) {
|
||||
if (pattern.test(raw)) {
|
||||
province = normalized;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const districtMatches = [...raw.matchAll(/([가-힣A-Za-z0-9]+(?:구|군|시))/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((value) => !/^(서울|부산|대구|인천|광주|대전|울산|세종)시?$/.test(value));
|
||||
const district = districtMatches[0] || null;
|
||||
|
||||
return { raw, province, district };
|
||||
}
|
||||
|
||||
function normalizeCategoriesForSearch(input) {
|
||||
const normalized = normalizeCategory(input);
|
||||
if (!Array.isArray(normalized)) {
|
||||
return [normalized];
|
||||
}
|
||||
return normalized.length ? normalized : ["general"];
|
||||
}
|
||||
|
||||
function build1365DonationSearchUrl(options = {}) {
|
||||
if (Object.prototype.hasOwnProperty.call(options, "baseUrl")) {
|
||||
throw new Error("baseUrl is not supported for 1365 donation search-assist links.");
|
||||
}
|
||||
|
||||
const url = new URL(OFFICIAL_1365_DONATION_URL);
|
||||
const [category] = normalizeCategoriesForSearch(options.category);
|
||||
const parts = [options.keyword, options.location].map((value) => String(value || "").trim()).filter(Boolean);
|
||||
url.searchParams.set("query", parts.join(" ") || CATEGORIES[category].label);
|
||||
url.searchParams.set("category", category);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function buildCandidateSearchKeyword(place, keyword) {
|
||||
const baseKeyword = String(keyword || "").trim();
|
||||
if (!baseKeyword || baseKeyword.includes(place.name)) {
|
||||
return place.name;
|
||||
}
|
||||
return `${place.name} ${baseKeyword}`;
|
||||
}
|
||||
|
||||
function selectCandidateSearchCategory(place, categories) {
|
||||
return categories.find((category) => place.categories.includes(category)) || categories[0];
|
||||
}
|
||||
|
||||
function scoreDonationPlace(place, categories, location) {
|
||||
const categoryMatch = categories.some((category) => place.categories.includes(category));
|
||||
const provinceMatch = !!location.province && place.locations.includes(location.province);
|
||||
const districtMatch = !!location.district && place.locations.includes(location.district);
|
||||
const nationwide = place.coverage === "nationwide" || place.locations.includes("전국");
|
||||
const localMatch = districtMatch || provinceMatch;
|
||||
|
||||
let score = 0;
|
||||
if (categoryMatch) score += 60;
|
||||
if (districtMatch) score += 35;
|
||||
else if (provinceMatch) score += 25;
|
||||
else if (nationwide) score += 10;
|
||||
if (place.coverage === "local" && localMatch) score += 5;
|
||||
|
||||
return {
|
||||
score,
|
||||
category: categoryMatch,
|
||||
local: localMatch,
|
||||
nationwide
|
||||
};
|
||||
}
|
||||
|
||||
function recommendDonationPlaces(options = {}) {
|
||||
const limit = normalizeLimit(options.limit);
|
||||
const location = parseLocationQuery(options.location || "");
|
||||
const categories = normalizeCategoriesForSearch(options.category);
|
||||
const keyword = String(options.keyword || categories.map((category) => CATEGORIES[category].label).join(" ")).trim();
|
||||
|
||||
const ranked = DONATION_PLACES
|
||||
.map((place) => ({ place, match: scoreDonationPlace(place, categories, location) }))
|
||||
.filter(({ match }) => match.category)
|
||||
.sort((a, b) => b.match.score - a.match.score || a.place.name.localeCompare(b.place.name, "ko"));
|
||||
|
||||
const items = ranked.slice(0, limit).map(({ place, match }) => ({
|
||||
...place,
|
||||
match,
|
||||
officialSearchUrl: build1365DonationSearchUrl({
|
||||
location: location.raw,
|
||||
category: selectCandidateSearchCategory(place, categories),
|
||||
keyword: buildCandidateSearchKeyword(place, keyword)
|
||||
})
|
||||
}));
|
||||
|
||||
const notes = [
|
||||
"추천 목록은 기부 실행 전 공식 페이지와 1365 기부포털에서 등록·모금기간·기부금영수증 가능 여부를 재확인해야 합니다."
|
||||
];
|
||||
if (items.length > 0 && !items.some((item) => item.match.local)) {
|
||||
notes.push("정확한 지역 일치 기부처를 찾지 못해 전국 단위 기부처를 우선 제안했습니다.");
|
||||
}
|
||||
if (items.length === 0) {
|
||||
notes.push("조건에 맞는 기본 후보가 없어 1365 기부포털 확인 보조 링크로 최신 등록 기부처를 직접 확인해야 합니다.");
|
||||
}
|
||||
|
||||
return {
|
||||
location,
|
||||
category: Array.isArray(options.category) ? categories : categories[0],
|
||||
items,
|
||||
officialSearchUrl: build1365DonationSearchUrl({ location: location.raw, category: categories[0], keyword }),
|
||||
meta: {
|
||||
totalCandidates: ranked.length,
|
||||
limit,
|
||||
source: "curated-fallback-plus-1365-search-assist",
|
||||
notes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLimit(value) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return 5;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new Error("limit must be an integer between 1 and 20.");
|
||||
}
|
||||
if (parsed < 1 || parsed > 20) {
|
||||
throw new Error("limit must be between 1 and 20.");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function formatDonationRecommendationReport(result) {
|
||||
const categoryLabels = (Array.isArray(result.category) ? result.category : [result.category])
|
||||
.map((category) => CATEGORIES[category]?.label || category)
|
||||
.join(", ");
|
||||
const where = result.location.raw || "지역 미지정";
|
||||
const lines = [`## 기부처 추천 (${where} / ${categoryLabels})`, ""];
|
||||
|
||||
if (result.items.length === 0) {
|
||||
lines.push("조건에 맞는 기본 후보를 찾지 못했습니다.");
|
||||
} else {
|
||||
result.items.forEach((item, index) => {
|
||||
const locality = item.match.local ? "지역 일치" : item.coverage === "nationwide" ? "전국" : "참고";
|
||||
lines.push(`${index + 1}. ${item.name} — ${item.description}`);
|
||||
lines.push(` - 분야: ${item.categories.map((category) => CATEGORIES[category]?.label || category).join(", ")} / 범위: ${locality}`);
|
||||
lines.push(` - 공식 페이지: ${item.homepageUrl}`);
|
||||
lines.push(` - 1365 확인 보조 링크: ${item.officialSearchUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("확인 메모:");
|
||||
for (const note of result.meta.notes) {
|
||||
lines.push(`- ${note}`);
|
||||
}
|
||||
lines.push(`- 1365 링크는 검색 보조용입니다. 최신 모금 상태는 1365 공식 페이지에서 직접 다시 확인하세요: ${result.officialSearchUrl}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CATEGORIES,
|
||||
DONATION_PLACES,
|
||||
OFFICIAL_1365_DONATION_URL,
|
||||
build1365DonationSearchUrl,
|
||||
formatDonationRecommendationReport,
|
||||
normalizeCategory,
|
||||
parseLocationQuery,
|
||||
recommendDonationPlaces
|
||||
};
|
||||
169
packages/donation-place-search/test/index.test.js
Normal file
169
packages/donation-place-search/test/index.test.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {
|
||||
CATEGORIES,
|
||||
build1365DonationSearchUrl,
|
||||
normalizeCategory,
|
||||
parseLocationQuery,
|
||||
recommendDonationPlaces,
|
||||
formatDonationRecommendationReport
|
||||
} = require("../src/index");
|
||||
|
||||
test("normalizeCategory maps Korean aliases to canonical donation categories", () => {
|
||||
assert.equal(normalizeCategory("아동"), "children");
|
||||
assert.equal(normalizeCategory("동물보호"), "animals");
|
||||
assert.equal(normalizeCategory("재난 구호"), "disaster");
|
||||
assert.equal(normalizeCategory("환경"), "environment");
|
||||
assert.equal(normalizeCategory("모르는분야"), "general");
|
||||
assert.ok(CATEGORIES.children.keywords.includes("아동"));
|
||||
});
|
||||
|
||||
test("normalizeCategory prioritizes specific categories in natural donation phrases", () => {
|
||||
assert.equal(normalizeCategory("동물 기부"), "animals");
|
||||
assert.equal(normalizeCategory("아동 기부"), "children");
|
||||
assert.equal(normalizeCategory("환경 모금"), "environment");
|
||||
assert.equal(normalizeCategory("장애인 나눔"), "disability");
|
||||
});
|
||||
|
||||
test("parseLocationQuery extracts Korean province and district hints conservatively", () => {
|
||||
assert.deepEqual(parseLocationQuery("서울시 마포구 공덕동"), {
|
||||
raw: "서울시 마포구 공덕동",
|
||||
province: "서울",
|
||||
district: "마포구"
|
||||
});
|
||||
assert.deepEqual(parseLocationQuery("부산 해운대구"), {
|
||||
raw: "부산 해운대구",
|
||||
province: "부산",
|
||||
district: "해운대구"
|
||||
});
|
||||
assert.deepEqual(parseLocationQuery("온라인"), {
|
||||
raw: "온라인",
|
||||
province: null,
|
||||
district: null
|
||||
});
|
||||
});
|
||||
|
||||
test("build1365DonationSearchUrl creates a public 1365 search-assist link without proxy auth", () => {
|
||||
const url = new URL(build1365DonationSearchUrl({
|
||||
location: "서울 마포구",
|
||||
category: "animals",
|
||||
keyword: "유기동물"
|
||||
}));
|
||||
|
||||
assert.equal(url.origin, "https://www.1365.go.kr");
|
||||
assert.equal(url.pathname, "/dntn/main.do");
|
||||
assert.equal(url.searchParams.get("query"), "유기동물 서울 마포구");
|
||||
assert.equal(url.searchParams.get("category"), "animals");
|
||||
});
|
||||
|
||||
test("build1365DonationSearchUrl treats an empty category list like the default category", () => {
|
||||
const url = new URL(build1365DonationSearchUrl({
|
||||
location: "서울 마포구",
|
||||
category: [],
|
||||
keyword: "기부처"
|
||||
}));
|
||||
|
||||
assert.equal(url.searchParams.get("category"), "general");
|
||||
assert.equal(url.searchParams.get("query"), "기부처 서울 마포구");
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces ranks local category matches before broad national fallback", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
assert.equal(result.category, "animals");
|
||||
assert.equal(result.location.province, "서울");
|
||||
assert.equal(result.items.length, 3);
|
||||
assert.equal(result.items[0].name, "동물권행동 카라");
|
||||
assert.equal(result.items[0].match.local, true);
|
||||
assert.equal(result.items[0].match.category, true);
|
||||
assert.ok(result.items[0].officialSearchUrl.includes("1365.go.kr"));
|
||||
assert.ok(result.items.some((item) => item.coverage === "nationwide"));
|
||||
assert.ok(result.items.every((item) => item.categories.includes("animals")));
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces emits candidate-specific 1365 search-assist links", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
limit: 3
|
||||
});
|
||||
|
||||
const itemUrls = result.items.map((item) => new URL(item.officialSearchUrl));
|
||||
const itemQueries = itemUrls.map((url) => url.searchParams.get("query"));
|
||||
|
||||
assert.equal(new Set(result.items.map((item) => item.officialSearchUrl)).size, result.items.length);
|
||||
result.items.forEach((item, index) => {
|
||||
assert.equal(itemUrls[index].origin, "https://www.1365.go.kr");
|
||||
assert.equal(itemUrls[index].searchParams.get("category"), "animals");
|
||||
assert.match(itemQueries[index], new RegExp(item.name));
|
||||
assert.match(itemQueries[index], /서울 마포구/);
|
||||
});
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces treats an empty category list like the optional default", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: [],
|
||||
limit: 2
|
||||
});
|
||||
|
||||
assert.deepEqual(result.category, ["general"]);
|
||||
assert.equal(result.items.length, 2);
|
||||
assert.ok(result.items.every((item) => item.match.category));
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces supports multiple category filters and explains no exact local hit", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "제주 서귀포시",
|
||||
category: ["장애", "노인"],
|
||||
limit: 4
|
||||
});
|
||||
|
||||
assert.deepEqual(result.category, ["disability", "elderly"]);
|
||||
assert.equal(result.items.length, 4);
|
||||
assert.ok(result.items.every((item) => item.match.category));
|
||||
assert.ok(result.meta.notes.some((note) => note.includes("정확한 지역 일치")));
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces uses each matched candidate category in multi-category item links", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "제주 서귀포시",
|
||||
category: ["장애", "노인"],
|
||||
limit: 4
|
||||
});
|
||||
|
||||
assert.deepEqual(result.category, ["disability", "elderly"]);
|
||||
result.items.forEach((item) => {
|
||||
const url = new URL(item.officialSearchUrl);
|
||||
const urlCategory = url.searchParams.get("category");
|
||||
assert.ok(item.categories.includes(urlCategory), `${item.name} URL category ${urlCategory} must match candidate categories`);
|
||||
});
|
||||
});
|
||||
|
||||
test("build1365DonationSearchUrl does not allow overriding the official 1365 endpoint", () => {
|
||||
assert.throws(
|
||||
() => build1365DonationSearchUrl({ baseUrl: "https://example.com/dntn/main.do" }),
|
||||
/baseUrl is not supported/
|
||||
);
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces rejects malformed non-integer limits", () => {
|
||||
assert.throws(() => recommendDonationPlaces({ limit: "2abc" }), /limit must be an integer/);
|
||||
assert.throws(() => recommendDonationPlaces({ limit: "1.9" }), /limit must be an integer/);
|
||||
});
|
||||
|
||||
test("formatDonationRecommendationReport creates a concise Korean report with verification cautions", () => {
|
||||
const result = recommendDonationPlaces({ location: "서울", category: "아동", limit: 2 });
|
||||
const report = formatDonationRecommendationReport(result);
|
||||
|
||||
assert.match(report, /기부처 추천/);
|
||||
assert.match(report, /서울/);
|
||||
assert.match(report, /아동/);
|
||||
assert.match(report, /공식 페이지/);
|
||||
assert.match(report, /1365/);
|
||||
});
|
||||
|
|
@ -671,8 +671,6 @@ test("ktx-booking helper python regression tests pass", () => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("repository docs advertise the geeknews-search skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -1295,17 +1293,48 @@ test("coupang-product-search docs drop non-allowlisted coupang-mcp-fallback and
|
|||
|
||||
test("root pack:dry-run script covers all publishable workspaces", () => {
|
||||
const packageJson = readJson("package.json");
|
||||
const packScript = packageJson.scripts["pack:dry-run"];
|
||||
const publishableWorkspaces = fs
|
||||
.readdirSync(path.join(repoRoot, "packages"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join("packages", entry.name, "package.json"))
|
||||
.filter((packagePath) => fs.existsSync(path.join(repoRoot, packagePath)))
|
||||
.map((packagePath) => readJson(packagePath))
|
||||
.filter((workspacePackage) => workspacePackage.private !== true)
|
||||
.map((workspacePackage) => workspacePackage.name);
|
||||
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace k-lotto/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace daiso-product-search/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace market-kurly-search/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace blue-ribbon-nearby/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace kakao-bar-nearby/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace public-restroom-nearby/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace court-auction-notice-search/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace kbl-results/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace kleague-results/);
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace lck-analytics/);
|
||||
assert.ok(publishableWorkspaces.includes("donation-place-search"));
|
||||
for (const workspaceName of publishableWorkspaces) {
|
||||
assert.match(packScript, new RegExp(`workspace ${escapeRegex(workspaceName)}(?:\\s|$)`));
|
||||
}
|
||||
});
|
||||
|
||||
test("README main capability table advertises the donation-place-search skill", () => {
|
||||
const readme = read("README.md");
|
||||
const tableSection = findSection(readme, "## 어떤 걸 할 수 있나");
|
||||
|
||||
assert.match(tableSection, /기부처 조회/);
|
||||
assert.match(tableSection, /`donation-place-search`/);
|
||||
assert.match(tableSection, /docs\/features\/donation-place-search\.md/);
|
||||
});
|
||||
|
||||
test("donation-place-search install docs include the skill and npm helper", () => {
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
||||
assert.match(install, /--skill donation-place-search/);
|
||||
assert.match(install, /npm install -g .*donation-place-search/);
|
||||
});
|
||||
|
||||
test("donation-place-search docs describe 1365 links as best-effort verification assists", () => {
|
||||
const skill = read(path.join("donation-place-search", "SKILL.md"));
|
||||
const packageReadme = read(path.join("packages", "donation-place-search", "README.md"));
|
||||
const featureDoc = read(path.join("docs", "features", "donation-place-search.md"));
|
||||
const packageJson = readJson(path.join("packages", "donation-place-search", "package.json"));
|
||||
|
||||
for (const doc of [skill, packageReadme, featureDoc, packageJson.description]) {
|
||||
assert.match(doc, /best-effort|보조|assist/i);
|
||||
assert.doesNotMatch(doc, /candidate-verified|후보별 검증 완료/);
|
||||
}
|
||||
});
|
||||
|
||||
test("repository docs advertise the kbl-results skill across the documented surfaces", () => {
|
||||
|
|
@ -1475,8 +1504,6 @@ test("blue-ribbon-nearby package README stays aligned with the location-first an
|
|||
assert.match(packageReadme, /searchNearbyByLocationQuery/);
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("repository docs advertise the kakao-bar-nearby skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -2557,7 +2584,6 @@ test("repository docs advertise the han-river-water-level skill and rollout-pend
|
|||
assert.match(roadmap, /한강 수위 정보 조회 스킬 출시/);
|
||||
});
|
||||
|
||||
|
||||
test("repository docs advertise the MFDS drug and food safety skills", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -3555,7 +3581,6 @@ test("corporate-registration-consulting skill covers court registry workflow, ta
|
|||
assert.match(sources, /law\.go\.kr/);
|
||||
});
|
||||
|
||||
|
||||
test("iros-registry-automation skill documents safe IROS registry certificate automation and upstream credit", () => {
|
||||
const skillPath = path.join(repoRoot, "iros-registry-automation", "SKILL.md");
|
||||
const featureDocPath = path.join(repoRoot, "docs", "features", "iros-registry-automation.md");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue