Compare commits

..

2 commits

Author SHA1 Message Date
Jeffrey (Dongkyu) Kim
f08fc576ef Prevent optional standings lookups from over-fetching the KBL API
The new kbl-results summary helper exposes includeStandings=false, so the
regression suite now proves that path stays schedule-only and never calls
the standings endpoint when the caller opts out.

Constraint: The KBL package should preserve the caller's no-standings contract
Rejected: Rely on manual inspection of the helper options | a targeted test is cheaper and safer
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep includeStandings=false side-effect free unless the public API contract changes explicitly
Tested: npm test --workspace kbl-results; npm run lint --workspace kbl-results
Not-tested: Full-repo CI before stacking this commit onto the rebased branch
2026-04-18 01:16:07 +09:00
Jeffrey (Dongkyu) Kim
a7aeec072e Add official KBL results support so basketball queries use live league data
Issue #129 needs a read-only skill and reusable package for KBL schedules, results, and standings. The implementation follows the existing sports package pattern and uses the league's live JSON APIs after verifying they respond successfully in real requests.

Constraint: Must use official KBL JSON surfaces before considering scraping
Constraint: Packaging changes must pass npm run ci and include docs plus Changesets updates
Rejected: Browser scraping first | official api.kbl.or.kr endpoints are live and simpler to maintain
Rejected: Reuse KBO/K League package shapes verbatim | KBL payload and team/status fields differ materially
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep seasonGrade=1 as the default KBL path unless future docs/tests explicitly widen to D-League flows
Tested: npm run ci; npm run lint --workspace kbl-results; npm test --workspace kbl-results; live getKBLSummary("2026-04-01", { team: "KCC", includeStandings: true })
Not-tested: Historical standings snapshots for past seasons via alternative KBL endpoints
2026-04-18 01:16:07 +09:00
481 changed files with 4094 additions and 70896 deletions

View file

@ -0,0 +1,5 @@
---
"public-restroom-nearby": minor
---
Add the first official public-restroom nearby lookup package and skill/docs set.

View file

@ -0,0 +1,5 @@
---
"kbl-results": minor
---
Add a reusable KBL results and standings package backed by the official JSON APIs.

View file

@ -1,13 +0,0 @@
{
"name": "k-skill",
"owner": {
"name": "NomaDamas"
},
"plugins": [
{
"name": "k-skill",
"source": "./",
"description": "한국인을 위한 90+ Agent Skill 번들 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화"
}
]
}

View file

@ -1,110 +0,0 @@
{
"name": "k-skill",
"description": "한국인을 위한 90+ Agent Skill 모음 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화",
"version": "1.0.0",
"author": {
"name": "NomaDamas"
},
"homepage": "https://github.com/NomaDamas/k-skill",
"repository": "https://github.com/NomaDamas/k-skill",
"license": "MIT",
"skills": [
"./biz-health-check",
"./bunjang-search",
"./catchtable-sniper",
"./cheap-gas-nearby",
"./corporate-registration-consulting",
"./coupang-product-search",
"./court-auction-notice-search",
"./daangn-cars-search",
"./daangn-jobs-search",
"./daangn-realty-search",
"./daangn-used-goods-search",
"./daishin-report-search",
"./daiso-product-search",
"./danawa-price-search",
"./delivery-tracking",
"./donation-place-search",
"./emergency-room-beds",
"./express-bus-booking",
"./fine-dust-location",
"./flight-ticket-search",
"./foresttrip-vacancy",
"./fsc-corporate-info",
"./g2b-sanctioned-supplier",
"./gangnamunni-clinic-search",
"./geeknews-search",
"./gongsijiga-search",
"./han-river-water-level",
"./hipass-receipt",
"./hola-poke-yeoksam",
"./household-waste-info",
"./hwp",
"./intercity-bus-booking",
"./iros-registry-automation",
"./jobkorea-talent-search",
"./joseon-sillok-search",
"./k-dart",
"./k-schoollunch-menu",
"./k-skill-cleaner",
"./k-skill-setup",
"./kakao-bar-nearby",
"./kakao-map",
"./kakaotalk-mac",
"./kbl-results",
"./kbo-results",
"./kleague-results",
"./korea-weather",
"./korean-character-count",
"./korean-cinema-search",
"./korean-humanizer",
"./korean-jangbu-for",
"./korean-law-search",
"./korean-marathon-schedule",
"./korean-middle-korean",
"./korean-patent-search",
"./korean-privacy-terms",
"./korean-scholarship-search",
"./korean-slang-writing",
"./korean-spell-check",
"./korean-stock-search",
"./korean-transit-route",
"./kosis-stats",
"./kstartup-search",
"./ktx-booking",
"./lck-analytics",
"./lh-notice-search",
"./library-book-search",
"./local-election-candidate-search",
"./localdata-business-status",
"./lotto-results",
"./market-kurly-search",
"./mfds-drug-safety",
"./mfds-food-safety",
"./myrealtrip-search",
"./national-pension-workplace",
"./naver-blog-research",
"./naver-news-search",
"./naver-shopping-search",
"./nts-business-registration",
"./nts-tax-delinquency",
"./ohou-today-deal",
"./olive-young-search",
"./parking-lot-search",
"./public-restroom-nearby",
"./real-estate-search",
"./rhwp-advanced",
"./rhwp-edit",
"./saramin-talent-search",
"./seoul-bike",
"./seoul-density",
"./seoul-subway-arrival",
"./sh-notice-search",
"./srt-booking",
"./subway-lost-property",
"./ticket-availability",
"./toss-securities",
"./used-car-price-search",
"./zipcode-search"
]
}

View file

@ -1,38 +0,0 @@
.git
.github
.gitignore
.DS_Store
.omx
.sisyphus
.venv
.env
.env.*
*.dec
*.plaintext
__pycache__
**/__pycache__
**/node_modules
**/dist
**/.next
**/.cache
docs
tests
test
**/test
**/tests
**/*.test.js
**/*.test.py
**/*.spec.js
scripts/build-manus-bundle.js
*.md
LICENSE
CONTRIBUTING.md
CHANGELOG.md
**/CHANGELOG.md
**/README.md
**/*.test.js
.changeset
.claude
.agents
.cursor
.kiro

View file

@ -10,9 +10,9 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

View file

@ -1,147 +0,0 @@
name: Deploy k-skill-proxy to Cloud Run
# Live: https://k-skill-proxy.nomadamas.org
# GCP project: k-skill-proxy, region: asia-northeast1
# Auth: Workload Identity Federation. Setup: docs/deploy-k-skill-proxy.md
on:
push:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
id-token: write
concurrency:
group: deploy-k-skill-proxy
cancel-in-progress: false
env:
GCP_PROJECT_ID: k-skill-proxy
GCP_REGION: asia-northeast1
AR_REPO: k-skill
SERVICE_NAME: k-skill-proxy
IMAGE_NAME: k-skill-proxy
jobs:
deploy:
name: Build and deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Authenticate to Google Cloud (Workload Identity Federation)
id: auth
uses: google-github-actions/auth@v3
with:
project_id: ${{ env.GCP_PROJECT_ID }}
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }}
token_format: access_token
- name: Set up gcloud CLI
uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ env.GCP_PROJECT_ID }}
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev --quiet
- name: Resolve image URI
id: image
run: |
IMAGE_URI="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${AR_REPO}/${IMAGE_NAME}:${GITHUB_SHA}"
echo "uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT"
echo "Image: ${IMAGE_URI}"
- name: Build container image
run: |
docker build \
--tag "${{ steps.image.outputs.uri }}" \
--file packages/k-skill-proxy/Dockerfile \
.
- name: Push image to Artifact Registry
run: docker push "${{ steps.image.outputs.uri }}"
- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.GCP_REGION }}
image: ${{ steps.image.outputs.uri }}
secrets: |-
AIR_KOREA_OPEN_API_KEY=AIR_KOREA_OPEN_API_KEY:latest
KMA_OPEN_API_KEY=KMA_OPEN_API_KEY:latest
SEOUL_OPEN_API_KEY=SEOUL_OPEN_API_KEY:latest
HRFCO_OPEN_API_KEY=HRFCO_OPEN_API_KEY:latest
OPINET_API_KEY=OPINET_API_KEY:latest
DATA_GO_KR_API_KEY=DATA_GO_KR_API_KEY:latest
KEDU_INFO_KEY=KEDU_INFO_KEY:latest
DATA4LIBRARY_AUTH_KEY=DATA4LIBRARY_AUTH_KEY:latest
FOODSAFETYKOREA_API_KEY=FOODSAFETYKOREA_API_KEY:latest
KAKAO_REST_API_KEY=KAKAO_REST_API_KEY:latest
KRX_API_KEY=KRX_API_KEY:latest
KOSIS_API_KEY=KOSIS_API_KEY:latest
NAVER_SEARCH_CLIENT_ID=NAVER_SEARCH_CLIENT_ID:latest
NAVER_SEARCH_CLIENT_SECRET=NAVER_SEARCH_CLIENT_SECRET:latest
LAW_OC=LAW_OC:latest
env_vars: |-
KSKILL_PROXY_HOST=0.0.0.0
KSKILL_PROXY_NAME=k-skill-proxy
KSKILL_PROXY_CACHE_TTL_MS=300000
KSKILL_PROXY_RATE_LIMIT_WINDOW_MS=60000
KSKILL_PROXY_RATE_LIMIT_MAX=60
flags: >-
--platform=managed
--allow-unauthenticated
--cpu=1
--memory=512Mi
--min-instances=0
--max-instances=3
--concurrency=80
--timeout=60
--execution-environment=gen2
--cpu-boost
- name: Smoke test /health on the new revision
env:
SERVICE_URL: ${{ steps.deploy.outputs.url }}
run: |
set -euo pipefail
echo "Service URL: ${SERVICE_URL}"
for attempt in 1 2 3 4 5; do
if curl -fsS --max-time 15 "${SERVICE_URL}/health" >/tmp/health.json; then
break
fi
echo "Health probe attempt ${attempt} failed, retrying in 5s..."
sleep 5
done
python3 -c "
import json, sys
data = json.load(open('/tmp/health.json'))
if not data.get('ok'):
print('Health response is not ok:', data)
sys.exit(1)
missing = [k for k, v in data.get('upstreams', {}).items() if k.endswith('Configured') and v is not True]
if missing:
print('Upstreams not configured:', missing)
sys.exit(1)
print('Health OK. All upstreams configured.')
"
- name: Smoke test custom domain (k-skill-proxy.nomadamas.org)
run: |
set -euo pipefail
if curl -fsS --max-time 15 https://k-skill-proxy.nomadamas.org/health >/tmp/prod-health.json; then
python3 -c "
import json
data = json.load(open('/tmp/prod-health.json'))
print('Prod /health ok:', data.get('ok'))
"
else
echo "::warning::Custom domain /health probe failed; revision may need traffic split or DNS warm-up."
fi

View file

@ -1,91 +0,0 @@
name: Publish Manus bundle
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "*/SKILL.md"
- "*/scripts/**"
- "*/references/**"
- "*/templates/**"
- "scripts/build-manus-bundle.js"
- "scripts/validate-skills.sh"
- ".github/workflows/manus-bundle.yml"
permissions:
contents: write
concurrency:
group: manus-bundle-latest
cancel-in-progress: true
jobs:
publish:
runs-on: ubuntu-latest
env:
RELEASE_TAG: manus-bundle-latest
RELEASE_TITLE: "Manus bundle (rolling)"
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
- run: npm ci
- name: Build .skill bundle
run: npm run build:manus-bundle
- name: Verify build output
run: |
set -euo pipefail
test -f dist/manus/k-skill-manus-all.zip
test -f dist/manus/INDEX.md
count=$(ls dist/manus/*.skill | wc -l)
if [ "$count" -lt 1 ]; then
echo "no .skill files produced" >&2
exit 1
fi
echo "built $count .skill files"
- name: Publish rolling release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
notes=$(cat <<EOF
Auto-built Manus.ai-compatible \`.skill\` bundle for every skill in k-skill.
- **\`k-skill-manus-all.zip\`** — combined download containing every \`.skill\` file plus \`INDEX.md\`. Unzip, then drag-drop individual \`.skill\` files into Manus.
- **\`INDEX.md\`** — human-readable listing of every bundled skill.
Manus accepts one skill per upload (\`.skill\`/\`.zip\`/folder). See [\`docs/install-manus.md\`](https://github.com/${GITHUB_REPOSITORY}/blob/main/docs/install-manus.md) for the full flow.
This is a rolling pre-release that is overwritten on every push to \`main\`. Built from commit \`${GITHUB_SHA}\`.
EOF
)
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$RELEASE_TITLE" \
--notes "$notes" \
--prerelease
else
gh release create "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$RELEASE_TITLE" \
--notes "$notes" \
--prerelease \
--target "$GITHUB_SHA"
fi
gh release upload "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--clobber \
dist/manus/k-skill-manus-all.zip \
dist/manus/INDEX.md

View file

@ -24,11 +24,11 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
@ -37,41 +37,6 @@ jobs:
- run: npm ci
- run: npm run ci
- name: Preflight verify npm auth
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
echo "::group::npm whoami"
NPM_USER=$(npm whoami 2>&1) || {
echo "::error::npm whoami failed NPM_TOKEN is invalid or expired. Rotate the token and update the repository secret."
exit 1
}
echo "Authenticated as: ${NPM_USER}"
echo "::endgroup::"
- name: Preflight list unpublished packages (diagnostic)
run: |
set -euo pipefail
echo "Packages that will be published:"
FOUND=0
for pkg_json in packages/*/package.json; do
PRIVATE=$(node -e "console.log(require('./${pkg_json}').private || false)")
[ "$PRIVATE" = "true" ] && continue
PKG=$(node -e "console.log(require('./${pkg_json}').name)")
LOCAL_VER=$(node -e "console.log(require('./${pkg_json}').version)")
REMOTE_VER=$(npm view "${PKG}" version 2>/dev/null || echo "")
if [ "${LOCAL_VER}" != "${REMOTE_VER}" ]; then
echo " → ${PKG}@${LOCAL_VER} (npm: ${REMOTE_VER:-not yet published})"
FOUND=1
fi
done
if [ "$FOUND" -eq 0 ]; then
echo " (none all versions are already on npm)"
fi
- name: Create npm release PR or publish changed packages
uses: changesets/action@v1
with:

View file

@ -16,37 +16,20 @@ permissions:
id-token: write
jobs:
detect_python_packages:
runs-on: ubuntu-latest
outputs:
has_python_packages: ${{ steps.detect.outputs.has_python_packages }}
steps:
- uses: actions/checkout@v5
- id: detect
shell: bash
run: |
if find python-packages -mindepth 2 -maxdepth 2 -name pyproject.toml -print -quit | grep -q .; then
echo "has_python_packages=true" >> "$GITHUB_OUTPUT"
else
echo "has_python_packages=false" >> "$GITHUB_OUTPUT"
fi
scaffold-only:
needs: detect_python_packages
if: ${{ needs.detect_python_packages.outputs.has_python_packages != 'true' }}
if: ${{ hashFiles('python-packages/**/pyproject.toml') == '' }}
runs-on: ubuntu-latest
steps:
- run: echo "No Python package exists yet. release-please remains scaffold-only."
release:
needs: detect_python_packages
if: ${{ needs.detect_python_packages.outputs.has_python_packages == 'true' }}
if: ${{ hashFiles('python-packages/**/pyproject.toml') != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- id: release
uses: googleapis/release-please-action@v5
uses: googleapis/release-please-action@v4
with:
config-file: .github/release-please/python-config.json
manifest-file: .github/release-please/python-manifest.json

6
.gitignore vendored
View file

@ -7,9 +7,3 @@ node_modules/
*.plaintext
.venv/
__pycache__/
dist/
.sisyphus/
.omo/
.gjc/
.agents/

View file

@ -20,7 +20,6 @@ These rules are repo-specific and apply to everything under this directory.
## Testing anti-patterns
- **Never write tests that assert `.changeset/*.md` files exist.** Changesets are consumed (deleted) by `changeset version` during the release flow. Any test guarding changeset file presence will break CI on the version-bump commit and block the release pipeline.
- **Never write tests that pin a workspace package's `version` field** (in `package.json` or `package-lock.json`). `changeset version` bumps these on every release, so any hardcoded version assertion will fail the next release commit and block the npm publish pipeline. Stable invariants like `name`, `license`, `engines.node`, or workspace link metadata are fine to assert; the `version` is not.
## Development skill install rules
@ -29,17 +28,9 @@ These rules are repo-specific and apply to everything under this directory.
- Respect existing home-directory indirection such as symlinks when syncing `~/.agents/skills`.
- Do **not** create repo-local `.claude` or `.agents` directories for skill installation unless the user explicitly asks for a repository-local test fixture.
## Crawling/search skill authoring
- For any k-skill that crawls or searches a website, the expected output is a site-dependent recipe packaged into that skill.
- Before fixing that recipe, use an insane-search-style, site-agnostic discovery pass: identify public entry points, observe browser-visible data flows when needed, prefer stable public/data endpoints over brittle screen scraping, and classify login/CAPTCHA/empty/blocked responses as explicit failure modes.
- Record the discovered site-dependent access path, fallback order, inputs/outputs, and failure modes in `SKILL.md` and any helper package code. See `docs/adding-a-skill.md` for the canonical checklist.
- Do not add crawling dependencies by default; first prefer existing runtime capabilities, public endpoints, or narrow allowlisted proxy routes.
## Free API proxy policy
- The built-in `k-skill-proxy` is for **free APIs only**.
- **k-skill-proxy inclusion rule**: A skill should be served through `k-skill-proxy` **only when the upstream requires an API key** (e.g., data.go.kr, KRX, Naver Search Open API, NEIS, Data4Library). Fully public endpoints that work without any authentication (e.g., realtyprice.kr) should be called directly from the user's machine, not routed through the proxy.
- Default posture: public read-only endpoint, **no proxy auth by default**.
- Keep free-API proxy surfaces narrow, allowlisted, cache-backed, and rate-limited.
- If abuse or operational issues appear later, add stricter controls then instead of preemptively requiring auth.
@ -47,12 +38,10 @@ These rules are repo-specific and apply to everything under this directory.
## Proxy server development
- 개발 repo (`dev` 브랜치)에서 proxy 코드를 수정하고, main에 merge하면 프로덕션에 반영된다.
- 프로덕션 배포 대상은 **Google Cloud Run** (`asia-northeast1`, GCP project `k-skill-proxy`)이며, 커스텀 도메인 `k-skill-proxy.nomadamas.org`로 노출된다.
- `main` 브랜치에 merge되면 `.github/workflows/deploy-k-skill-proxy.yml`이 Workload Identity Federation으로 GCP 인증 → Artifact Registry로 image build/push → Cloud Run 재배포 → `/health` smoke test까지 자동으로 수행한다.
- 따라서 **dev에서 route를 추가/수정한 뒤 main에 merge되기 전까지는 프로덕션 proxy에 반영되지 않는다.**
- 프로덕션 배포본은 `~/.local/share/k-skill-proxy`에 main 브랜치 단독 clone으로 존재한다.
- cron job (`0 * * * *`)이 매시 정각에 `~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh`를 실행해 origin/main fetch → fast-forward pull → package-lock 변경 시 npm ci → pm2 restart 순서로 자동 배포한다.
- 로그: `/tmp/k-skill-proxy-update.log`
- proxy 서버 코드: `packages/k-skill-proxy/src/server.js`
- 컨테이너 이미지 빌드 정의: `packages/k-skill-proxy/Dockerfile`
- proxy 서버 테스트: `packages/k-skill-proxy/test/server.test.js`
- 로컬 테스트: `node packages/k-skill-proxy/src/server.js` (환경변수는 `~/.config/k-skill/secrets.env` 등에서 직접 export해서 띄운다)
- 프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run runtime에 주입된다.
- **운영 관련 모든 절차는 [`docs/deploy-k-skill-proxy.md`](docs/deploy-k-skill-proxy.md)에 정리되어 있다.** 새 maintainer 인계를 위한 1회성 GCP/WIF 셋업, GitHub repository secrets 등록, upstream API 키 회전(rotation), 자동 배포 상태/로그/이미지 태그 확인, Cloud Run 트래픽 롤백, GitHub Actions 장애 시 로컬에서 동일한 배포를 수동으로 돌리는 비상 명령까지 전부 거기서 본다. proxy 운영 관련 어떤 질문이 들어와도 먼저 그 문서를 확인한다.
- proxy 환경변수(API key 등)는 `~/.config/k-skill/secrets.env`에 넣고, `scripts/run-k-skill-proxy.sh`가 source한다.
- **dev에서 route를 추가/수정한 뒤 main에 merge되기 전까지는 프로덕션 proxy에 반영되지 않는다.** 로컬 테스트는 `node packages/k-skill-proxy/src/server.js`로 직접 실행한다.

View file

@ -3,20 +3,11 @@
## Testing anti-patterns
- **Never write tests that assert `.changeset/*.md` files exist.** Changesets are consumed (deleted) by `changeset version` during the release flow. Any test guarding changeset file presence will break CI on the version-bump commit and block the release pipeline.
- **Never write tests that pin a workspace package's `version` field** (in `package.json` or `package-lock.json`). `changeset version` bumps these on every release, so any hardcoded version assertion will fail the next release commit and block the npm publish pipeline. Stable invariants like `name`, `license`, `engines.node`, or workspace link metadata are fine to assert; the `version` is not.
## Crawling/search skill authoring
- 크롤링/검색 k-skill의 목표는 최종적으로 대상 사이트에 맞는 site-dependent 접근 방법을 스킬에 패키징하는 것이다.
- 다만 방법을 고정하기 전에 `insane-search`식 site-agnostic discovery를 먼저 수행한다: 공개 입구, 브라우저에서 보이는 데이터 흐름, RSS/sitemap/정적 JSON/모바일 페이지, 차단·빈 응답·로그인벽 실패 모드를 확인한다.
- 발견한 검색 URL, 필수 입력값, 결과 해석 규칙, fallback 순서, 실패 모드는 `SKILL.md`와 helper 코드에 명확히 남긴다. 자세한 체크리스트는 `docs/adding-a-skill.md`를 따른다.
- 새 크롤링 dependency는 기본값으로 추가하지 말고 기존 기능, 공개 endpoint, 좁은 proxy route로 해결 가능한지 먼저 확인한다.
## Proxy server development
- 개발 repo: 이 디렉토리, `dev` 브랜치
- 프로덕션 배포 대상: **Google Cloud Run** (project `k-skill-proxy`, region `asia-northeast1`, custom domain `k-skill-proxy.nomadamas.org`)
- `main` 브랜치에 merge되면 `.github/workflows/deploy-k-skill-proxy.yml`이 자동으로 Cloud Run 재배포를 수행한다. 인증은 Workload Identity Federation, 이미지 빌드 정의는 `packages/k-skill-proxy/Dockerfile`, 시크릿은 GCP Secret Manager에서 주입된다. WIF/Secret Manager 셋업은 `docs/deploy-k-skill-proxy.md` 참고.
- 개발 repo: `/Users/jeffrey/Projects/k-skill` (이 디렉토리, `dev` 브랜치)
- 프로덕션 배포본: `~/.local/share/k-skill-proxy` (main 브랜치 단독 clone)
- **cron job** 이 매시 정각에 `origin/main` fetch → fast-forward pull → pm2 restart 실행
- 따라서 proxy route 변경은 **main에 merge되어야 프로덕션에 반영**된다. dev에서 코드를 바꿔도 프로덕션 proxy에는 영향 없음.
- 로컬 테스트는 `node packages/k-skill-proxy/src/server.js` 로 직접 실행하거나 `node --test packages/k-skill-proxy/test/server.test.js` 로 확인.
- **Proxy 편입 규칙**: k-skill-proxy에 route를 추가하려면 upstream이 API 키를 필요로 해야 한다. 공개 엔드포인트(키 불필요)는 skill 코드에서 직접 호출하고 프록시를 거치지 않는다.

View file

@ -1,76 +0,0 @@
# 기여 가이드
외부 기여자는 이 문서를 기준으로 이슈, PR, 스킬, 패키지, 프록시 변경을 준비해 주세요. 이 레포의 세부 운영 규칙은 `AGENTS.md``CLAUDE.md`에도 있으며, 충돌할 때는 더 구체적인 최신 지침을 우선합니다.
## 소통 언어
- PR 코멘트, 이슈, 리뷰 등 모든 소통은 한국어로 진행합니다.
- 외부 문서나 로그를 인용해야 할 때는 원문을 함께 둘 수 있지만, 결정 사항과 요청 사항은 한국어로 요약해 주세요.
## 브랜치와 PR 대상
- 기능/수정 브랜치는 가능한 한 `feature/<issue-number>` 또는 `feature/#<issue-number>`처럼 추적 가능한 이름을 사용합니다.
- PR의 대상 브랜치는 반드시 `dev` 브랜치여야 합니다.
- `main` 브랜치로 PR을 만들 수 있는 사람은 `@vkehfdl1`뿐입니다. 그 외 기여자는 `main` 대상 PR을 만들지 않습니다.
- 프록시 서버 변경도 개발 레포의 `dev` 브랜치에서 작업하고, `main`에 머지된 뒤에만 프로덕션에 반영됩니다.
## 스킬 추가 또는 변경
스킬을 추가하거나 변경할 때는 관련 기능 문서와 `README.md`의 표를 포함해 코드와 문서를 함께 갱신합니다.
- 관련 기능 문서(`docs/features/<skill-name>.md`)를 추가하거나 업데이트합니다.
- `README.md`의 "어떤 걸 할 수 있나" 표에 스킬 이름, 설명, 사용자 로그인 필요 여부, 문서 링크를 업데이트합니다.
- 설치 흐름이 바뀌면 `docs/install.md`, `docs/setup.md`, `docs/security-and-secrets.md` 등 관련 문서도 함께 맞춥니다.
- 출처나 공식 표면이 바뀌면 `docs/sources.md`에 반영합니다.
- 스킬 개발/테스트 시에는 현재 스킬 디렉터리를 먼저 홈 디렉터리 전역 스킬 위치에 동기화합니다.
- Claude Code: `~/.claude/skills/<skill-name>`
- agents 호환 런타임: `~/.agents/skills/<skill-name>`
- `~/.agents/skills`가 symlink 등으로 우회되어 있으면 기존 indirection을 존중합니다.
- 사용자가 명시적으로 요청하지 않는 한 레포 내부에 `.claude` 또는 `.agents` 설치 테스트 디렉터리를 만들지 않습니다.
## npm 패키지와 릴리스
- Node 패키지는 `packages/*` 아래 npm workspaces로 관리합니다.
- npm 패키지를 수정할 때는 Changesets를 조사하고, 자동 CD가 올바르게 트리거되도록 `.changeset/*.md` 변경이 필요한지 신중히 판단합니다.
- 패키지 릴리스 목적의 버전 변경은 `package.json`만 직접 수정하지 말고 Changesets 흐름을 사용합니다.
- npm publish는 GitHub Actions가 생성하는 **Version Packages** PR이 `main`에 머지된 뒤 자동으로 수행되는 것을 전제로 합니다.
- Changeset 파일의 존재 여부를 테스트로 검증하지 않는다. Changesets는 `changeset version` 단계에서 소비되어 삭제될 수 있으므로, 그런 테스트는 버전 bump 커밋의 CI를 막습니다.
- `package.json``package-lock.json``version` 필드를 테스트에서 고정하지 않는다. Changesets 릴리스 흐름에서 매번 바뀔 수 있으므로, 테스트는 `name`, `license`, `engines.node`, workspace link metadata처럼 안정적인 invariant를 검증합니다.
- 현재 구현이 registry token 기반인 경우에도 신규 또는 재설계 흐름은 trusted publishing/OIDC를 우선합니다. 기존 token 기반 경로를 고칠 때는 현재 구현 예외와 목표 원칙을 PR 설명에 분리해 적습니다.
## Python 패키지와 PyPI
- Python 패키지는 `python-packages/*` 아래에 둡니다.
- Python 릴리스는 release-please 기반입니다.
- 실제 Python 패키지가 생기기 전까지 Python release workflow는 scaffold-only로 유지합니다.
- PyPI publish는 release-please가 구체적인 패키지 경로에 대해 `release_created=true`를 보고할 때만 실행되도록 설계합니다.
- PyPI도 가능하면 trusted publishing/OIDC를 우선합니다.
## API와 k-skill-proxy 정책
- `k-skill-proxy`는 무료 API 전용입니다.
- 신규 proxy route는 upstream이 API key를 요구하는 무료 API인 경우에만 `k-skill-proxy` 경유를 검토합니다. 기존 승인 예외를 넓히려면 근거와 운영 경계를 문서화합니다.
- 인증 없이 동작하는 공개 read-only endpoint는 기본적으로 사용자 머신에서 직접 호출하고, 불필요하게 프록시 운영 표면을 넓히지 않습니다.
- 유료 API, 사용자별 과금 API, 개인 계정 권한이 필요한 API는 `k-skill-proxy`를 타지 않도록 설계합니다.
- 기본 자세는 공개 read-only endpoint, proxy auth 없음입니다.
- 프록시 표면은 좁게 유지하고 allowlist, cache, rate limit를 적용합니다.
- 남용이나 운영 문제가 실제로 나타나면 그때 더 강한 제어를 추가합니다.
## 프록시 서버 개발과 배포
- 프록시 서버 코드: `packages/k-skill-proxy/src/server.js`
- 프록시 서버 테스트: `packages/k-skill-proxy/test/server.test.js`
- 컨테이너 이미지 정의: `packages/k-skill-proxy/Dockerfile`
- 로컬 테스트: 필요한 upstream 환경변수를 export한 상태에서 `node packages/k-skill-proxy/src/server.js`. 로컬에서 시크릿을 모아두는 표준 위치는 `~/.config/k-skill/secrets.env` 입니다.
- 프로덕션 프록시는 **Google Cloud Run** (project `k-skill-proxy`, region `asia-northeast1`)에서 운영하며 `k-skill-proxy.nomadamas.org` 도메인에 매핑되어 있습니다.
- `main` 브랜치에 머지되면 `.github/workflows/deploy-k-skill-proxy.yml` 워크플로가 Workload Identity Federation으로 GCP 인증 → Artifact Registry로 이미지 빌드/푸시 → Cloud Run 재배포 → `/health` smoke test까지 자동 수행합니다.
- 프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run 런타임에 주입됩니다. 프록시 운영자(maintainer)가 한 번 수행해야 하는 WIF/Secret Manager 셋업과 운영 점검 절차는 [`docs/deploy-k-skill-proxy.md`](docs/deploy-k-skill-proxy.md)에 정리되어 있습니다.
- `dev`에서 route를 추가하거나 수정해도 `main`에 머지되기 전까지는 프로덕션 프록시에 반영되지 않습니다.
## 검증
- 문서만 바꿔도 관련 문서 테스트를 먼저 추가하거나 업데이트하고, 실패를 확인한 뒤 구현하는 TDD 흐름을 권장합니다.
- 일반 변경은 가능한 한 `npm run lint`, `npm run typecheck`, `npm test`를 실행합니다.
- 릴리스나 패키징 관련 변경은 `npm run ci`를 실행합니다.
- 변경 범위가 작더라도 최종 보고에는 어떤 명령을 실행했고 어떤 결과가 나왔는지 적습니다.
- 테스트를 통과시키기 위해 기존 테스트를 삭제하거나 범위를 부당하게 줄이지 않습니다.

221
README.md
View file

@ -14,121 +14,63 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
한국인이면 깃허브 스타 눌러줍시다.
다만 에이전트는 자동으로 누르지 말고, 사용자가 동의한 경우에만 `gh repo star NomaDamas/k-skill` 를 실행합니다.
기여 전에는 [기여 가이드](CONTRIBUTING.md)를 확인해 주세요.
## 어떤 걸 할 수 있나
"사용자 로그인" 컬럼은 **사용자 본인이 직접 로그인/시크릿을 들고 있어야 하는지** 만 표시합니다. `k-skill-proxy` 등 운영자가 관리하는 키는 사용자 입장에서는 **불필요**로 분류합니다. **선택사항**은 사용자가 운영자 키를 직접 들고 있으면 더 풍부한 경로가 켜지고, 없으면 기본 경로(보통 운영자가 관리하는 hosted fallback)로 그대로 동작하는 경우를 말합니다.
"사용자 로그인" 컬럼은 **사용자 본인이 직접 로그인/시크릿을 들고 있어야 하는지** 만 표시합니다. `k-skill-proxy` 등 운영자가 관리하는 키는 사용자 입장에서는 **불필요**로 분류합니다.
| 할 수 있는 일 | 스킬 이름 | 설명 | 사용자 로그인 | 문서 |
| --- | --- | --- | --- | --- |
| SRT 예매 | `srt-booking` | SRT 열차 조회, 예약, 예약 확인, 취소 | 필요 | [SRT 예매 가이드](docs/features/srt-booking.md) |
| KTX 예매 | `ktx-booking` | KTX/Korail 열차 조회, 호차별 좌석번호·콘센트 좌석 확인, 예약, 예약 확인, 취소 | 필요 | [KTX 예매 가이드](docs/features/ktx-booking.md) |
| 고속버스 예매 | `express-bus-booking` | KOBUS 고속버스 배차·좌석·요금 조회와 결제 직전 handoff(결제는 수동) | 불필요 | [고속버스 예매 가이드](docs/features/express-bus-booking.md) |
| 시외버스 예매 | `intercity-bus-booking` | 티머니 시외버스 배차·좌석·요금 조회와 결제 직전 handoff(결제는 수동) | 불필요 | [시외버스 예매 가이드](docs/features/intercity-bus-booking.md) |
| 자연휴양림 빈 객실 조회 | `foresttrip-vacancy` | 공식 숲나들e 자연휴양림 예약 가능 객실 조회 자동화 (예약/결제 제외) | 필요 | [자연휴양림 빈 객실 조회 가이드](docs/features/foresttrip-vacancy.md) |
| 카카오톡 Mac 아카이브 검색 | `kakaotalk-mac` | `katok`으로 macOS 카카오톡 로컬 아카이브를 동기화하고 keyword/BM25/semantic 검색 | 불필요(로컬 앱/권한 필요) | [카카오톡 Mac 아카이브 검색](docs/features/kakaotalk-mac.md) |
| 서울 지하철 도착정보 조회 | `seoul-subway-arrival` | 서울 지하철 역 기준 실시간 도착 예정 열차 확인 | 불필요 | [서울 지하철 도착정보 가이드](docs/features/seoul-subway-arrival.md) |
| 서울 실시간 혼잡도 조회 | `seoul-density` | 서울 주요 121개 핫스팟의 실시간 혼잡도 단계와 추정 인구 조회 | 불필요 | [서울 실시간 혼잡도 가이드](docs/features/seoul-density.md) |
| 서울 따릉이 실시간 대여소 조회 | `seoul-bike` | 현재 좌표 주변 또는 대여소 이름 기준 따릉이 대여 가능 자전거와 빈 거치대 조회 | 불필요 | [서울 따릉이 실시간 대여소 가이드](docs/features/seoul-bike.md) |
| 한국 대중교통 길찾기 | `korean-transit-route` | ODsay LIVE API + Kakao geocoding 기반 출발지→도착지 지하철+버스+도보 경로 및 환승 정보 조회 | 필요 | [한국 대중교통 길찾기 가이드](docs/features/korean-transit-route.md) |
| 카카오맵 장소·자동차 길찾기 | `kakao-map` | Kakao Local 키워드/카테고리/좌표↔주소 변환 + Kakao Mobility 자동차 길찾기(거리·소요시간·통행료·예상 택시요금) | 불필요 | [카카오맵 가이드](docs/features/kakao-map.md) |
| 지하철 분실물 조회 | `subway-lost-property` | 지하철 역/물품명 기준 공식 LOST112 분실물 검색 조건과 유실물센터 진입점 안내 | 불필요 | [지하철 분실물 조회 가이드](docs/features/subway-lost-property.md) |
| 긱뉴스 조회 | `geeknews-search` | GeekNews 공개 RSS/Atom 피드 기반 최신 글 목록, 검색, 상세 확인 | 불필요 | [긱뉴스 조회 가이드](docs/features/geeknews-search.md) |
| 한국 날씨 조회 | `korea-weather` | 기상청 단기예보 기반 한국 날씨 조회 | 불필요 | [한국 날씨 조회 가이드](docs/features/korea-weather.md) |
| 사용자 위치 미세먼지 조회 | `fine-dust-location` | 현재 위치 또는 지역 기준 PM10/PM2.5 미세먼지 조회 | 불필요 | [사용자 위치 미세먼지 조회 가이드](docs/features/fine-dust-location.md) |
| 한강 수위 정보 조회 | `han-river-water-level` | 한강 관측소 기준 현재 수위·유량·기준수위 확인 | 불필요 | [한강 수위 정보 가이드](docs/features/han-river-water-level.md) |
| 한국 법령 검색 | `korean-law-search` | 한국 법령/조문/판례/유권해석 검색 | 불필요 | [한국 법령 검색 가이드](docs/features/korean-law-search.md) |
| 등기부등본 자동화 | `iros-registry-automation` | 인터넷등기소(IROS)에서 법인/부동산 등기부등본 장바구니, 수동 결제 후 열람·저장 흐름을 보조 | 필요(수동 로그인·결제/TouchEn) | [등기부등본 자동화 가이드](docs/features/iros-registry-automation.md) |
| 법인등기 신청 컨설팅 | `corporate-registration-consulting` | 일반 영리 주식회사 발기설립 기준으로 법인명·이사·주소 등 사용자 결정사항을 받아 표준 정관, 설립등기 첨부서류, 등록면허세·과밀억제권역 중과 체크, rhwp 기반 HWP 양식 순차 작성 흐름을 참고용으로 안내 | 불필요 | [법인등기 신청 컨설팅 가이드](docs/features/corporate-registration-consulting.md) |
| 사업자등록정보 확인 | `nts-business-registration` | 국세청 사업자등록번호 상태조회와 사업자등록정보 진위확인(공공데이터포털 API, 프록시 경유) | 불필요 | [사업자등록정보 확인 가이드](docs/features/nts-business-registration.md) |
| 사업자 실사 종합 | `biz-health-check` | 사업자등록번호를 중심으로, 상호·지역을 함께 주면 국세청 상태·국민연금·체납 명단·금융위 법인개요·부정당제재·인허가 영업상태를 교차 조회한 실사 리포트(점수·판정 없이 사실만) | 불필요 | [사업자 실사 종합 가이드](docs/features/biz-health-check.md) |
| 국민연금 가입 사업장 조회 | `national-pension-workplace` | 사업장명으로 국민연금 가입자수·당월 고지금액·월별 추이 조회(공공데이터포털 API, 프록시 경유) | 불필요 | [국민연금 가입 사업장 조회 가이드](docs/features/national-pension-workplace.md) |
| 국세 체납 명단공개 검색 | `nts-tax-delinquency` | 상호·법인명으로 국세청 고액·상습체납자 명단공개 대조(무인증 공개 검색) | 불필요 | [국세 체납 명단공개 검색 가이드](docs/features/nts-tax-delinquency.md) |
| 금융위 기업기본정보 조회 | `fsc-corporate-info` | 법인명으로 대표자·설립일·업종 등 법인 개요 조회와 사업자번호 교차검증(공공데이터포털 API, 프록시 경유) | 불필요 | [금융위 기업기본정보 조회 가이드](docs/features/fsc-corporate-info.md) |
| 부정당제재업체 조회 | `g2b-sanctioned-supplier` | 사업자번호로 나라장터 부정당제재(조회시점 유효 제재) 조회(공공데이터포털 API, 프록시 경유) | 불필요 | [부정당제재업체 조회 가이드](docs/features/g2b-sanctioned-supplier.md) |
| 인허가 영업상태 조회 | `localdata-business-status` | 상호+시군구로 동네 사업장(208업종)의 영업/휴업/폐업·업력·주소 조회(LOCALDATA 무인증) | 불필요 | [인허가 영업상태 조회 가이드](docs/features/localdata-business-status.md) |
| 창업진흥원 K-Startup 조회 | `kstartup-search` | 창업진흥원 K-Startup 통합공고 사업·지원사업 공고·창업 콘텐츠·통계보고서 조회 (공공데이터포털 15125364, 프록시 경유) | 불필요 | [창업진흥원 K-Startup 조회 가이드](docs/features/kstartup-search.md) |
| 지방선거 후보자 조회 | `local-election-candidate-search` | 중앙선거관리위원회 선거통계시스템 공개 통합검색으로 지방선거 후보자 이력·선거종류·정당·지역·득표 정보를 이름 기준으로 조회 | 불필요 | [지방선거 후보자 조회 가이드](docs/features/local-election-candidate-search.md) |
| 한국 사업자 장부 자동화 | `korean-jangbu-for` | `kimlawtech/korean-jangbu-for` 기반 카드·은행·영수증·세금계산서 입력 → 표준 거래내역·계정과목·세무사 전달 CSV·경영 리포트 생성 thin wrapper | 선택사항(CODEF BYOK 자동 수집 시 필요) | [한국 사업자 장부 자동화 가이드](docs/features/korean-jangbu-for.md) |
| 한국 개인정보처리방침·이용약관 자동 생성 | `korean-privacy-terms` | Next.js 프로젝트에 개인정보보호법·약관규제법·전자상거래법 기반 개인정보처리방침/이용약관/쿠키 배너/동의 모달을 생성하는 `kimlawtech/korean-privacy-terms` (Apache-2.0) thin wrapper | 불필요 | [한국 개인정보처리방침·이용약관 자동 생성 가이드](docs/features/korean-privacy-terms.md) |
| 한국 부동산 실거래가 조회 | `real-estate-search` | 아파트/오피스텔/빌라/단독주택 실거래가·전월세·지역코드 조회 | 불필요 | [한국 부동산 실거래가 조회 가이드](docs/features/real-estate-search.md) |
| 개별공시지가 조회 | `gongsijiga-search` | realtyprice.kr 공개 API에서 지번 단위 개별공시지가(원/㎡) 다년도 추이·전년 대비 변동률 조회 | 불필요 | [개별공시지가 조회 가이드](docs/features/gongsijiga-search.md) |
| SH 청약·주택 공고문 조회 | `sh-notice-search` | 서울주택도시개발공사(SH) 공개 공고/공지 게시판을 직접 조회해 키워드·공고 종류별 목록, 상세 본문, 첨부 미리보기 메타데이터 확인 | 불필요 | [SH 청약·주택 공고문 조회 가이드](docs/features/sh-notice-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) |
| 도서관 도서 조회 | `library-book-search` | 도서관 정보나루 기반 도서 검색, 상세, 소장 도서관, 도서관별 소장 여부 조회 | 불필요 | [도서관 도서 조회 가이드](docs/features/library-book-search.md) |
| 의약품 안전 체크 | `mfds-drug-safety` | 식약처 e약은요·안전상비의약품 정보를 인터뷰-first 흐름으로 프록시 조회 | 불필요 | [의약품 안전 체크 가이드](docs/features/mfds-drug-safety.md) |
| 식품 안전 체크 | `mfds-food-safety` | 식약처 부적합 식품·식품안전나라 회수 정보를 인터뷰-first 흐름으로 프록시 조회 | 불필요 | [식품 안전 체크 가이드](docs/features/mfds-food-safety.md) |
| 한국 주식 정보 조회 | `korean-stock-search` | KRX 상장 종목 검색, 기본정보, 일별 시세 조회 | 불필요 | [한국 주식 정보 조회 가이드](docs/features/korean-stock-search.md) |
| 금감원 DART 전자공시 조회 | `k-dart` | 공시검색, 기업개황, 재무제표, 배당, 증자/감자, 감사의견, 주요사항보고서 등 14개 endpoint | 필요 | [금감원 DART 전자공시 조회 가이드](docs/features/k-dart.md) |
| 잡코리아 인재검색 | `jobkorea-talent-search` | 잡코리아 기업회원 로그인 세션에서 마스킹 후보 정보를 읽고 유료 열람 전 shortlist 작성 | 필요 | [잡코리아 인재검색 가이드](docs/features/jobkorea-talent-search.md) |
| 사람인 인재풀 검색 | `saramin-talent-search` | 사람인 기업회원 인재풀에서 마스킹 후보 정보를 읽고 유료 열람 전 shortlist 작성 | 필요 | [사람인 인재풀 검색 가이드](docs/features/saramin-talent-search.md) |
| 대신증권 리포트 조회 | `daishin-report-search` | GitHub Pages에 공개된 대신증권 리포트 HTML 미러에서 최신 리포트 목록, 원문, 설명 페이지, Rating/Target 표를 조회 | 불필요 | [대신증권 리포트 조회 가이드](docs/features/daishin-report-search.md) |
| 국가데이터처 KOSIS 통계 조회 | `kosis-stats` | 국가데이터처가 운영하는 KOSIS(국가통계포털) Open API로 통계표 검색·메타·데이터·대용량 자료 조회 (조회 전용) | 일반 조회 불필요 (`bigdata`/`--direct` 필요) | [국가데이터처 KOSIS 통계 조회 가이드](docs/features/kosis-stats.md) |
| 조선왕조실록 검색 | `joseon-sillok-search` | 조선왕조실록 키워드 검색과 왕별/연도별 필터, 기사 발췌 조회 | 불필요 | [조선왕조실록 검색 가이드](docs/features/joseon-sillok-search.md) |
| 한국 특허 정보 검색 | `korean-patent-search` | 한국 특허/실용신안 키워드 검색 및 출원번호 상세 조회 | 필요 | [한국 특허 정보 검색 가이드](docs/features/korean-patent-search.md) |
| 근처 가장 싼 주유소 찾기 | `cheap-gas-nearby` | 현재 위치 기준 근처 최저가 주유소 조회 | 불필요 | [근처 가장 싼 주유소 찾기 가이드](docs/features/cheap-gas-nearby.md) |
| 근처 공중화장실 찾기 | `public-restroom-nearby` | 현재 위치 기준 근처 공중화장실/개방화장실 조회 | 불필요 | [근처 공중화장실 찾기 가이드](docs/features/public-restroom-nearby.md) |
| 근처 공영주차장 찾기 | `parking-lot-search` | 현재 위치 기준 근처 공영주차장 위치·요금·운영시간 조회 | 불필요 | [근처 공영주차장 찾기 가이드](docs/features/parking-lot-search.md) |
| 근처 응급실 병상 상태 확인 | `emergency-room-beds` | 현재 위치 기준 가까운 응급실 운영·입원실/병상 운영 플래그와 갱신시각 조회 (정확한 잔여 병상 수/가동률은 공개 E-Gen nearby 목록에 없음) | 불필요 | [근처 응급실 병상 상태 확인 가이드](docs/features/emergency-room-beds.md) |
| 한국 마라톤 일정 조회 | `korean-marathon-schedule` | 고러닝 공개 페이지와 대한철인3종협회 일정에서 마라톤·철인3종 대회 일정, 장소, 신청 마감일, 종목 조회 | 불필요 | [한국 마라톤 일정 조회 가이드](docs/features/korean-marathon-schedule.md) |
| KBO 경기 결과 조회 | `kbo-results` | 날짜별 KBO 경기 일정, 결과, 팀별 필터링 | 불필요 | [KBO 결과 가이드](docs/features/kbo-results.md) |
| KBL 경기 결과 조회 | `kbl-results` | 날짜별 KBL 경기 일정, 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [KBL 경기 결과 가이드](docs/features/kbl-results.md) |
| K리그 경기 결과 조회 | `kleague-results` | 날짜별 K리그1/K리그2 경기 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [K리그 결과 가이드](docs/features/kleague-results.md) |
| LCK 경기 분석 | `lck-analytics` | LCK 경기 결과, 현재 순위, live turning point, 밴픽, 패치 메타, 팀 파워 레이팅 | 불필요 | [LCK 경기 분석 가이드](docs/features/lck-analytics.md) |
| 토스증권 조회 | `toss-securities` | 토스증권 공식 Open API(OAuth2) 우선, tossctl fallback으로 계좌·보유주식·시세·주문조회 등 조회 전용 | 필요 | [토스증권 조회 가이드](docs/features/toss-securities.md) |
| 하이패스 영수증 발급 | `hipass-receipt` | 하이패스 사용내역 조회 및 영수증 출력 payload 준비 | 필요 | [하이패스 영수증 발급 가이드](docs/features/hipass-receipt.md) |
| 캐치테이블 예약 스나이핑 | `catchtable-sniper` | 로그인된 캐치테이블 Chrome 세션으로 빈자리 감시, 오픈런, 자동 예약 시도 | 필요 | [캐치테이블 예약 스나이핑 가이드](docs/features/catchtable-sniper.md) |
| 공연 일정·잔여석 조회 | `ticket-availability` | YES24·인터파크 공연의 회차별 일정과 등급별 잔여석 수를 단일 HTTP 호출로 조회 (조회 전용, 예매·결제 없음) | 불필요 | [공연 일정·잔여석 조회 가이드](docs/features/ticket-availability.md) |
| 로또 당첨 확인 | `lotto-results` | 로또 최신 회차, 특정 회차, 번호 대조 | 불필요 | [로또 결과 가이드](docs/features/lotto-results.md) |
| HWP 문서 조회/변환 | `hwp` | `.hwp/.hwpx` → Markdown/JSON 변환, 문서 비교, 양식 필드 추출, Markdown→HWPX 역변환 (kordoc 기반 read-only) | 불필요 | [HWP 문서 처리 가이드](docs/features/hwp.md) |
| HWP 문서 편집 | `rhwp-edit` | `.hwp` 본문 텍스트 삽입/삭제, 표 생성, 셀 수정, replace-all (`k-skill-rhwp` CLI + `@rhwp/core` WASM, HWP 5.x round-trip) | 불필요 | [HWP 문서 편집 가이드](docs/features/rhwp-edit.md) |
| HWP 레이아웃·IR 디버깅 | `rhwp-advanced` | 업스트림 `rhwp` Rust CLI(`export-svg --debug-overlay`, `dump`, `dump-pages`, `ir-diff`, `thumbnail`, `convert`)로 HWP 레이아웃 진단·IR 덤프·버전 비교·썸네일 추출·배포용 문서 잠금 해제 | 불필요 | [HWP 레이아웃·IR 디버깅 가이드](docs/features/rhwp-advanced.md) |
| 근처 술집 조회 | `kakao-bar-nearby` | 현재 위치 기준 영업 상태·메뉴·좌석·전화번호가 포함된 근처 술집 조회 | 불필요 | [근처 술집 조회 가이드](docs/features/kakao-bar-nearby.md) |
| 우편번호 검색 | `zipcode-search` | 주소 키워드로 우편번호 + 공식 영문주소 조회 | 불필요 | [우편번호 검색 가이드](docs/features/zipcode-search.md) |
| 다이소 상품 조회 | `daiso-product-search` | 다이소 매장별 상품 픽업 가능 여부 확인 (정확한 매장별 재고 수량은 다이소몰 보안 정책으로 2026-05-05 부터 차단됨) | 불필요 | [다이소 상품 조회 가이드](docs/features/daiso-product-search.md) |
| 강남언니 병원 조회 | `gangnamunni-clinic-search` | 강남언니 공개 검색 페이지에서 성형외과·피부과 병원 후보, 평점, 리뷰 수, 지원 언어, 공개 링크 조회 | 불필요 | [강남언니 병원 조회 가이드](docs/features/gangnamunni-clinic-search.md) |
| 마켓컬리 상품 조회 | `market-kurly-search` | 마켓컬리 상품 검색, 현재 가격, 할인 여부, 품절 여부 조회 | 불필요 | [마켓컬리 상품 조회 가이드](docs/features/market-kurly-search.md) |
| 올리브영 검색 | `olive-young-search` | 올리브영 매장·상품·재고 조회 | 불필요 | [올리브영 검색 가이드](docs/features/olive-young-search.md) |
| 영화관 검색 | `korean-cinema-search` | CGV·메가박스·롯데시네마 영화관, 상영작, 시간표, 잔여석 조회 | 불필요 | [영화관 검색 가이드](docs/features/korean-cinema-search.md) |
| 올라포케 역삼 포케 | `hola-poke-yeoksam` | 올라포케 역삼점 메뉴, 매장 정보, 이벤트 참여 흐름 안내 | 불필요 | [올라포케 역삼 포케 가이드](docs/features/hola-poke-yeoksam.md) |
| 마이리얼트립 MCP 검색 | `myrealtrip-search` | 공식 MCP 서버로 항공권, 숙소, 투어·티켓·액티비티 검색과 상세·옵션 확인 | 불필요 | [마이리얼트립 MCP 검색 가이드](docs/features/myrealtrip-search.md) |
| 항공권 가격 조회 | `flight-ticket-search` | `fast-flights` 기반 Google Flights 공개 검색으로 항공권 후보, 예약 검색 링크, 날짜/월/연도별 최저가·평균가 비교 (조회 전용, 예매·결제 없음) | 불필요 | [항공권 가격 조회 가이드](docs/features/flight-ticket-search.md) |
| 택배 배송조회 | `delivery-tracking` | CJ대한통운·우체국 송장 번호로 배송 상태 조회 | 불필요 | [택배 배송조회 가이드](docs/features/delivery-tracking.md) |
| 쿠팡 상품 검색 | `coupang-product-search` | 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 비교, 베스트, 골드박스 특가 조회 | 선택사항 (운영 키 있으면 로컬 HMAC 경로, 없으면 hosted fallback) | [쿠팡 상품 검색 가이드](docs/features/coupang-product-search.md) |
| 오늘의집 오늘의딜 조회 | `ohou-today-deal` | 오늘의집 공개 오늘의딜 특가 상품의 할인율·가격·리뷰·링크 조회 | 불필요 | [오늘의집 오늘의딜 조회 가이드](docs/features/ohou-today-deal.md) |
| 번개장터 검색 | `bunjang-search` | 번개장터 검색, 상세조회, 선택적 찜/채팅, AI TOON export | 불필요 | [번개장터 검색 가이드](docs/features/bunjang-search.md) |
| 당근 중고거래 검색 | `daangn-used-goods-search` | 당근 중고거래 공개 웹 데이터 표면으로 키워드·지역 기반 매물 검색과 상세 조회 | 불필요 | [당근 중고거래 검색 가이드](docs/features/daangn-used-goods-search.md) |
| 당근부동산 검색 | `daangn-realty-search` | 당근부동산 공개 웹 데이터 표면으로 지역 기반 부동산 매물 검색과 상세 확인 | 불필요 | [당근부동산 검색 가이드](docs/features/daangn-realty-search.md) |
| 당근알바 검색 | `daangn-jobs-search` | 당근알바 공개 웹 데이터 표면으로 키워드·지역 기반 알바 공고 검색과 상세 조회 | 불필요 | [당근알바 검색 가이드](docs/features/daangn-jobs-search.md) |
| 당근중고차 검색 | `daangn-cars-search` | 당근중고차 공개 웹 데이터 표면으로 지역·가격 조건 기반 차량 검색과 상세 조회 | 불필요 | [당근중고차 검색 가이드](docs/features/daangn-cars-search.md) |
| 중고차 가격 조회 | `used-car-price-search` | 중고차 인수가/월 렌트료 비교 조회 | 불필요 | [중고차 가격 조회 가이드](docs/features/used-car-price-search.md) |
| 한국어 맞춤법 검사 | `korean-spell-check` | 한국어 텍스트 맞춤법/문법 검사 및 교정안 정리 | 불필요 | [한국어 맞춤법 검사 가이드](docs/features/korean-spell-check.md) |
| 네이버 블로그 리서치 | `naver-blog-research` | 네이버 블로그 검색, 원문 읽기, 이미지 다운로드, 한국어 콘텐츠 교차 검증 | 불필요 | [네이버 블로그 리서치 가이드](docs/features/naver-blog-research.md) |
| 네이버 쇼핑 가격비교 | `naver-shopping-search` | 네이버 검색 Open API 우선, 공개 BFF JSON fallback으로 상품 후보·현재 노출가·판매처 링크 비교 | 불필요 | [네이버 쇼핑 가격비교 가이드](docs/features/naver-shopping-search.md) |
| 다나와 최저가 비교 | `danawa-price-search` | 다나와 공개 검색/가격비교 표면으로 상품 후보·쇼핑몰별 가격·배송비 포함 실구매가·카드 할인가·무이자 할부 비교 | 불필요 | [다나와 최저가 비교 가이드](docs/features/danawa-price-search.md) |
| 네이버 뉴스 검색 | `naver-news-search` | 네이버 검색 Open API 뉴스 검색으로 기사 제목·요약·발행시각·원문/네이버 링크를 정리 | 불필요 | [네이버 뉴스 검색 가이드](docs/features/naver-news-search.md) |
| 한국어 글자 수 세기 | `korean-character-count` | 한국어 텍스트의 글자 수·줄 수·UTF-8/NEIS byte 수를 결정론적으로 계산 | 불필요 | [한국어 글자 수 세기 가이드](docs/features/korean-character-count.md) |
| 한국어 유행어 글쓰기 | `korean-slang-writing` | 나무위키 유행어 기반 큐레이션 시드로 한국 유행어 후보 조회, 무드/문맥/safety 필터 및 나무위키 best-effort 요약으로 한국어 글을 유행어 느낌으로 작성 | 불필요 | [한국어 유행어 글쓰기 가이드](docs/features/korean-slang-writing.md) |
| 한국어 AI 윤문 | `korean-humanizer` | AI가 쓴 티 나는 한국어 글을 번역체·AI 상투어·과장된 의의·줄표/이모지 등 흔적을 심각도(S1/S2/S3)로 분류해 의미는 보존하며 사람 글로 윤문, 목표 글자수도 맞춤 | 불필요 | [한국어 AI 윤문 가이드](docs/features/korean-humanizer.md) |
| 한국 중세 국어풍 변환 | `korean-middle-korean` | 한국어 입력문을 중세국어풍 조사·어미·Hanja 힌트·성조점이 섞인 창작용 문체로 결정론적 변환 | 불필요 | [한국 중세 국어풍 변환 가이드](docs/features/korean-middle-korean.md) |
| K-스킬 클리너 | `k-skill-cleaner` | 인터뷰와 코딩 에이전트별 트리거 횟수 통계를 합쳐 불필요한 K-스킬 삭제 후보를 추천 | 불필요 | [K-스킬 클리너 가이드](docs/features/k-skill-cleaner.md) |
| 할 수 있는 일 | 설명 | 사용자 로그인 | 문서 |
| --- | --- | --- | --- |
| SRT 예매 | SRT 열차 조회, 예약, 예약 확인, 취소 | 필요 | [SRT 예매 가이드](docs/features/srt-booking.md) |
| KTX 예매 | KTX/Korail 열차 조회, 예약, 예약 확인, 취소 | 필요 | [KTX 예매 가이드](docs/features/ktx-booking.md) |
| 카카오톡 Mac CLI | macOS에서 카카오톡 대화 조회, 검색, 메시지 전송 | 불필요 | [카카오톡 Mac CLI 가이드](docs/features/kakaotalk-mac.md) |
| 서울 지하철 도착정보 조회 | 서울 지하철 역 기준 실시간 도착 예정 열차 확인 | 불필요 | [서울 지하철 도착정보 가이드](docs/features/seoul-subway-arrival.md) |
| 지하철 분실물 조회 | 지하철 역/물품명 기준 공식 LOST112 분실물 검색 조건과 유실물센터 진입점 안내 | 불필요 | [지하철 분실물 조회 가이드](docs/features/subway-lost-property.md) |
| 긱뉴스 조회 | GeekNews 공개 RSS/Atom 피드 기반 최신 글 목록, 검색, 상세 확인 | 불필요 | [긱뉴스 조회 가이드](docs/features/geeknews-search.md) |
| 한국 날씨 조회 | 기상청 단기예보 기반 한국 날씨 조회 | 불필요 | [한국 날씨 조회 가이드](docs/features/korea-weather.md) |
| 사용자 위치 미세먼지 조회 | 현재 위치 또는 지역 기준 PM10/PM2.5 미세먼지 조회 | 불필요 | [사용자 위치 미세먼지 조회 가이드](docs/features/fine-dust-location.md) |
| 한강 수위 정보 조회 | 한강 관측소 기준 현재 수위·유량·기준수위 확인 | 불필요 | [한강 수위 정보 가이드](docs/features/han-river-water-level.md) |
| 한국 법령 검색 | 한국 법령/조문/판례/유권해석 검색 | 불필요 | [한국 법령 검색 가이드](docs/features/korean-law-search.md) |
| 한국 부동산 실거래가 조회 | 아파트/오피스텔/빌라/단독주택 실거래가·전월세·지역코드 조회 | 불필요 | [한국 부동산 실거래가 조회 가이드](docs/features/real-estate-search.md) |
| 장학금 검색 및 조회 | 한국장학재단·전국 대학교·재단·기업 장학 공고를 검색해 금액·자격·지원구간·링크를 정리하고 KST 기준 현재 날짜 마감 상태와 조건별 필터링까지 제공 | 불필요 | [장학금 검색 및 조회 가이드](docs/features/korean-scholarship-search.md) |
| 생활쓰레기 배출정보 조회 | 시군구 기준 생활쓰레기·음식물·재활용 배출요일·시간·장소·관리부서 확인 | 불필요 | [생활쓰레기 배출정보 조회 가이드](docs/features/household-waste-info.md) |
| 학교 급식 식단 조회 | 교육청·학교명으로 NEIS 학교 검색·급식 식단 조회 | 불필요 | [학교 급식 식단 조회 가이드](docs/features/k-schoollunch-menu.md) |
| 의약품 안전 체크 | 식약처 e약은요·안전상비의약품 정보를 인터뷰-first 흐름으로 프록시 조회 | 불필요 | [의약품 안전 체크 가이드](docs/features/mfds-drug-safety.md) |
| 식품 안전 체크 | 식약처 부적합 식품·식품안전나라 회수 정보를 인터뷰-first 흐름으로 프록시 조회 | 불필요 | [식품 안전 체크 가이드](docs/features/mfds-food-safety.md) |
| 한국 주식 정보 조회 | KRX 상장 종목 검색, 기본정보, 일별 시세 조회 | 불필요 | [한국 주식 정보 조회 가이드](docs/features/korean-stock-search.md) |
| 조선왕조실록 검색 | 조선왕조실록 키워드 검색과 왕별/연도별 필터, 기사 발췌 조회 | 불필요 | [조선왕조실록 검색 가이드](docs/features/joseon-sillok-search.md) |
| 한국 특허 정보 검색 | 한국 특허/실용신안 키워드 검색 및 출원번호 상세 조회 | 필요 | [한국 특허 정보 검색 가이드](docs/features/korean-patent-search.md) |
| 근처 가장 싼 주유소 찾기 | 현재 위치 기준 근처 최저가 주유소 조회 | 불필요 | [근처 가장 싼 주유소 찾기 가이드](docs/features/cheap-gas-nearby.md) |
| 근처 공중화장실 찾기 | 현재 위치 기준 근처 공중화장실/개방화장실 조회 | 불필요 | [근처 공중화장실 찾기 가이드](docs/features/public-restroom-nearby.md) |
| KBO 경기 결과 조회 | 날짜별 KBO 경기 일정, 결과, 팀별 필터링 | 불필요 | [KBO 결과 가이드](docs/features/kbo-results.md) |
| KBL 경기 결과 조회 | 날짜별 KBL 경기 일정, 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [KBL 경기 결과 가이드](docs/features/kbl-results.md) |
| K리그 경기 결과 조회 | 날짜별 K리그1/K리그2 경기 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [K리그 결과 가이드](docs/features/kleague-results.md) |
| LCK 경기 분석 | LCK 경기 결과, 현재 순위, live turning point, 밴픽, 패치 메타, 팀 파워 레이팅 | 불필요 | [LCK 경기 분석 가이드](docs/features/lck-analytics.md) |
| 토스증권 조회 | 토스증권 계좌 요약, 포트폴리오, 시세, 주문내역, 관심종목 조회 | 필요 | [토스증권 조회 가이드](docs/features/toss-securities.md) |
| 하이패스 영수증 발급 | 하이패스 사용내역 조회 및 영수증 출력 payload 준비 | 필요 | [하이패스 영수증 발급 가이드](docs/features/hipass-receipt.md) |
| 로또 당첨 확인 | 로또 최신 회차, 특정 회차, 번호 대조 | 불필요 | [로또 결과 가이드](docs/features/lotto-results.md) |
| HWP 문서 처리 | `.hwp/.hwpx` → Markdown/JSON 변환, 문서 비교, 양식 필드 추출, Markdown→HWPX 역변환 | 불필요 | [HWP 문서 처리 가이드](docs/features/hwp.md) |
| ~~근처 블루리본 맛집~~ ⚠️ 지원 중단 | ~~현재 위치 기준 근처 블루리본 선정 맛집 조회~~ | ~~불필요~~ | ~~[근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md)~~ |
| 근처 술집 조회 | 현재 위치 기준 영업 상태·메뉴·좌석·전화번호가 포함된 근처 술집 조회 | 불필요 | [근처 술집 조회 가이드](docs/features/kakao-bar-nearby.md) |
| 우편번호 검색 | 주소 키워드로 우편번호 + 공식 영문주소 조회 | 불필요 | [우편번호 검색 가이드](docs/features/zipcode-search.md) |
| 다이소 상품 조회 | 다이소 매장별 상품 재고 확인 | 불필요 | [다이소 상품 조회 가이드](docs/features/daiso-product-search.md) |
| 마켓컬리 상품 조회 | 마켓컬리 상품 검색, 현재 가격, 할인 여부, 품절 여부 조회 | 불필요 | [마켓컬리 상품 조회 가이드](docs/features/market-kurly-search.md) |
| 올리브영 검색 | 올리브영 매장·상품·재고 조회 | 불필요 | [올리브영 검색 가이드](docs/features/olive-young-search.md) |
| 올라포케 역삼 포케 | 올라포케 역삼점 메뉴, 매장 정보, 이벤트 참여 흐름 안내 | 불필요 | [올라포케 역삼 포케 가이드](docs/features/hola-poke-yeoksam.md) |
| 택배 배송조회 | CJ대한통운·우체국 송장 번호로 배송 상태 조회 | 불필요 | [택배 배송조회 가이드](docs/features/delivery-tracking.md) |
| 쿠팡 상품 검색 | 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 비교, 베스트, 골드박스 특가 조회 | 불필요 | [쿠팡 상품 검색 가이드](docs/features/coupang-product-search.md) |
| 번개장터 검색 | 번개장터 검색, 상세조회, 선택적 찜/채팅, AI TOON export | 불필요 | [번개장터 검색 가이드](docs/features/bunjang-search.md) |
| 중고차 가격 조회 | 중고차 인수가/월 렌트료 비교 조회 | 불필요 | [중고차 가격 조회 가이드](docs/features/used-car-price-search.md) |
| 한국어 맞춤법 검사 | 한국어 텍스트 맞춤법/문법 검사 및 교정안 정리 | 불필요 | [한국어 맞춤법 검사 가이드](docs/features/korean-spell-check.md) |
| 네이버 블로그 리서치 | 네이버 블로그 검색, 원문 읽기, 이미지 다운로드, 한국어 콘텐츠 교차 검증 | 불필요 | [네이버 블로그 리서치 가이드](docs/features/naver-blog-research.md) |
| 한국어 글자 수 세기 | 한국어 텍스트의 글자 수·줄 수·UTF-8/NEIS byte 수를 결정론적으로 계산 | 불필요 | [한국어 글자 수 세기 가이드](docs/features/korean-character-count.md) |
## Claude Code 플러그인으로 설치
[Claude Code](https://claude.com/claude-code)에서는 마켓플레이스로 전체 스킬을 한 번에 설치할 수 있습니다.
```
/plugin marketplace add NomaDamas/k-skill
/plugin install k-skill@k-skill
```
설치하면 스킬이 `/k-skill:<스킬 이름>` 네임스페이스로 호출됩니다 (예: `/k-skill:lotto-results`). 개별 디렉토리를 직접 복사하는 수동 설치나 다른 에이전트 설치는 [설치 방법](docs/install.md)을 참고하세요.
> ## ⚠️ 근처 블루리본 맛집 스킬 — 지원 중단
>
> **블루리본 측이 `www.bluer.co.kr` 에 자동화 접근 전면 차단을 적용해 스킬이 더 이상 동작하지 않습니다.**
>
> - 브라우저·`curl`·Playwright·TLS impersonation 등 가능한 우회를 모두 검증했지만 nginx 단에서 403이 반환되며, 같은 가구 공인 IP로도 특정 장비만 차단되는 상황이 관측되었습니다.
> - 유료 회원권 보유자도 접근이 막히는 사례가 확인되었습니다. 복구 여부와 일정은 블루리본 측 정책에 전적으로 달려 있어 이 레포에서 대응할 수 있는 범위를 벗어났습니다.
> - 해당 스킬 디렉토리(`blue-ribbon-nearby/`)와 관련 프록시 라우트는 히스토리 보존을 위해 당분간 남겨두지만, **새 프로젝트에서는 해당 스킬을 사용하지 마세요.** 차단이 해제되는 날이 오면 이 안내를 제거하고 재검증하겠습니다.
## 처음 시작하는 순서
@ -143,8 +85,6 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
| 문서 | 설명 |
| --- | --- |
| [설치 방법](docs/install.md) | 패키지 설치, 선택 설치, 로컬 테스트 방법 |
| [기여 가이드](CONTRIBUTING.md) | 외부 기여자를 위한 소통, PR 대상 브랜치, 스킬 문서, Changesets, 프록시 정책 |
| [Manus.ai 에서 가져오기](docs/install-manus.md) | Manus.ai 에서 개별 스킬 폴더 URL 가져오기 또는 `npm run build:manus-bundle` 로 빌드한 `.skill` 파일을 드래그-드롭으로 업로드하는 방법 |
| [공통 설정 가이드](docs/setup.md) | credential resolution order, 기본 secrets 파일 준비 |
| [보안/시크릿 정책](docs/security-and-secrets.md) | 인증 정보 저장 원칙, 금지 패턴, 표준 환경변수 이름 |
| [k-skill 프록시 서버 가이드](docs/features/k-skill-proxy.md) | 무료 API를 프록시 서버로 바로 호출하는 방법 |
@ -156,96 +96,47 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
- [SRT 예매](docs/features/srt-booking.md)
- [KTX 예매](docs/features/ktx-booking.md)
- [고속버스 예매](docs/features/express-bus-booking.md)
- [시외버스 예매](docs/features/intercity-bus-booking.md)
- [자연휴양림 빈 객실 조회](docs/features/foresttrip-vacancy.md)
- [카카오톡 Mac 아카이브 검색](docs/features/kakaotalk-mac.md)
- [카카오톡 Mac CLI](docs/features/kakaotalk-mac.md)
- [서울 지하철 도착정보 조회](docs/features/seoul-subway-arrival.md)
- [서울 실시간 혼잡도 조회](docs/features/seoul-density.md)
- [한국 대중교통 길찾기 가이드](docs/features/korean-transit-route.md)
- [카카오맵 가이드](docs/features/kakao-map.md)
- [지하철 분실물 조회 가이드](docs/features/subway-lost-property.md)
- [긱뉴스 조회 가이드](docs/features/geeknews-search.md)
- [한국 날씨 조회 가이드](docs/features/korea-weather.md)
- [사용자 위치 미세먼지 조회](docs/features/fine-dust-location.md)
- [한강 수위 정보 가이드](docs/features/han-river-water-level.md)
- [한국 법령 검색 가이드](docs/features/korean-law-search.md)
- [한국 개인정보처리방침·이용약관 자동 생성 가이드](docs/features/korean-privacy-terms.md)
- [사업자등록정보 확인 가이드](docs/features/nts-business-registration.md)
- [사업자 실사 종합 가이드](docs/features/biz-health-check.md)
- [국민연금 가입 사업장 조회 가이드](docs/features/national-pension-workplace.md)
- [국세 체납 명단공개 검색 가이드](docs/features/nts-tax-delinquency.md)
- [금융위 기업기본정보 조회 가이드](docs/features/fsc-corporate-info.md)
- [부정당제재업체 조회 가이드](docs/features/g2b-sanctioned-supplier.md)
- [인허가 영업상태 조회 가이드](docs/features/localdata-business-status.md)
- [창업진흥원 K-Startup 조회 가이드](docs/features/kstartup-search.md)
- [지방선거 후보자 조회 가이드](docs/features/local-election-candidate-search.md)
- [한국 사업자 장부 자동화 가이드](docs/features/korean-jangbu-for.md)
- [한국 부동산 실거래가 조회 가이드](docs/features/real-estate-search.md)
- [개별공시지가 조회 가이드](docs/features/gongsijiga-search.md)
- [LH 청약 공고문 조회 가이드](docs/features/lh-notice-search.md)
- [SH 청약·주택 공고문 조회 가이드](docs/features/sh-notice-search.md)
- [법원 경매 부동산 매각공고 조회 가이드](docs/features/court-auction-notice-search.md)
- [장학금 검색 및 조회 가이드](docs/features/korean-scholarship-search.md)
- [생활쓰레기 배출정보 조회 가이드](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)
- [국가데이터처 KOSIS 통계 조회 가이드](docs/features/kosis-stats.md)
- [조선왕조실록 검색 가이드](docs/features/joseon-sillok-search.md)
- [한국 특허 정보 검색 가이드](docs/features/korean-patent-search.md)
- [근처 가장 싼 주유소 찾기 가이드](docs/features/cheap-gas-nearby.md)
- [근처 공중화장실 찾기 가이드](docs/features/public-restroom-nearby.md)
- [근처 공영주차장 찾기 가이드](docs/features/parking-lot-search.md)
- [근처 응급실 병상 상태 확인 가이드](docs/features/emergency-room-beds.md)
- [한국 마라톤 일정 조회 가이드](docs/features/korean-marathon-schedule.md)
- [KBO 경기 결과 조회](docs/features/kbo-results.md)
- [KBL 경기 결과 가이드](docs/features/kbl-results.md)
- [K리그 경기 결과 조회](docs/features/kleague-results.md)
- [LCK 경기 분석 가이드](docs/features/lck-analytics.md)
- [토스증권 조회 가이드](docs/features/toss-securities.md)
- [대신증권 리포트 조회 가이드](docs/features/daishin-report-search.md)
- [하이패스 영수증 발급 가이드](docs/features/hipass-receipt.md)
- [캐치테이블 예약 스나이핑 가이드](docs/features/catchtable-sniper.md)
- [공연 일정·잔여석 조회 가이드](docs/features/ticket-availability.md)
- [로또 당첨 확인](docs/features/lotto-results.md)
- [등기부등본 자동화 가이드](docs/features/iros-registry-automation.md)
- [법인등기 신청 컨설팅](docs/features/corporate-registration-consulting.md)
- [HWP 문서 조회/변환](docs/features/hwp.md)
- [HWP 문서 편집](docs/features/rhwp-edit.md)
- [HWP 레이아웃·IR 디버깅](docs/features/rhwp-advanced.md)
- [HWP 문서 처리](docs/features/hwp.md)
- [근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md)
- [근처 술집 조회 가이드](docs/features/kakao-bar-nearby.md)
- [우편번호 검색](docs/features/zipcode-search.md)
- [다이소 상품 조회](docs/features/daiso-product-search.md)
- [강남언니 병원 조회 가이드](docs/features/gangnamunni-clinic-search.md)
- [마켓컬리 상품 조회 가이드](docs/features/market-kurly-search.md)
- [올리브영 검색 가이드](docs/features/olive-young-search.md)
- [영화관 검색 가이드](docs/features/korean-cinema-search.md)
- [올라포케 역삼 포케 가이드](docs/features/hola-poke-yeoksam.md)
- [마이리얼트립 MCP 검색 가이드](docs/features/myrealtrip-search.md)
- [항공권 가격 조회 가이드](docs/features/flight-ticket-search.md)
- [택배 배송조회](docs/features/delivery-tracking.md)
- [쿠팡 상품 검색](docs/features/coupang-product-search.md)
- [오늘의집 오늘의딜 조회](docs/features/ohou-today-deal.md)
- [번개장터 검색 가이드](docs/features/bunjang-search.md)
- [당근 중고거래 검색 가이드](docs/features/daangn-used-goods-search.md)
- [당근부동산 검색 가이드](docs/features/daangn-realty-search.md)
- [당근알바 검색 가이드](docs/features/daangn-jobs-search.md)
- [당근중고차 검색 가이드](docs/features/daangn-cars-search.md)
- [중고차 가격 조회 가이드](docs/features/used-car-price-search.md)
- [한국어 맞춤법 검사 가이드](docs/features/korean-spell-check.md)
- [네이버 블로그 리서치 가이드](docs/features/naver-blog-research.md)
- [네이버 쇼핑 가격비교 가이드](docs/features/naver-shopping-search.md)
- [다나와 최저가 비교 가이드](docs/features/danawa-price-search.md)
- [네이버 뉴스 검색 가이드](docs/features/naver-news-search.md)
- [한국어 글자 수 세기 가이드](docs/features/korean-character-count.md)
- [한국어 유행어 글쓰기 가이드](docs/features/korean-slang-writing.md)
- [한국어 AI 윤문 가이드](docs/features/korean-humanizer.md)
- [한국 중세 국어풍 변환 가이드](docs/features/korean-middle-korean.md)
- [K-스킬 클리너 가이드](docs/features/k-skill-cleaner.md)
- [릴리스/배포 가이드](docs/releasing.md)
설치 기본 흐름은 "전체 스킬 설치 → `k-skill-setup` 실행 → 개별 기능 사용" 입니다.

View file

@ -1,79 +0,0 @@
---
name: biz-health-check
description: 사업자등록번호 하나로 "이 사업자, 실제 문제 없나"를 확인한다 — 국세청 사업자등록 상태·국민연금 가입 사업장·국세 체납 명단·금융위 법인개요·조달청 부정당제재·지방행정 인허가 영업상태를 무료 공공 데이터로 교차 조회해 사실만 병렬하는 실사 리포트(점수·등급·위험 판정 없음).
license: MIT
metadata:
category: business
locale: ko-KR
phase: v1
---
# 사업자 실사 복합 조회 (biz-health-check)
## What this skill does
사업자등록번호(+상호/지역)를 입력하면 무료 공공 데이터 6종을 한 번에 교차 조회해 실사 리포트 한 장을 만든다. 같은 레포의 단품 스킬 helper를 그대로 재사용한다(단일 진실원천).
| 섹션 | 데이터 | 단품 스킬 | 경로 |
|---|---|---|---|
| 국세청 상태 | 계속/휴업/폐업·과세유형 | `nts-business-registration` | proxy |
| 국민연금 | 가입자수·당월 고지금액·월별 | `national-pension-workplace` | proxy |
| 체납 명단 | 고액·상습체납자 명단공개 대조 | `nts-tax-delinquency` | 직접(무인증) |
| 금융위 | 대표자·설립일·업종 법인개요 | `fsc-corporate-info` | proxy |
| 부정당제재 | 조회시점 유효 제재 | `g2b-sanctioned-supplier` | proxy |
| 인허가 영업상태 | 동네 사업장(208업종) 영업/폐업·업력 | `localdata-business-status` | 직접(무인증) |
공시 유무는 기존 `k-dart` 스킬을 함께 쓰면 된다.
## Design principles
- **점수·등급·"위험" 같은 해석 라벨을 산출하지 않는다.** 각 항목의 사실 + 출처 + 조회시각만 병렬한다. 판단은 사용자 몫이다.
- 한 항목 조회가 실패해도 전체를 막지 않고 그 항목만 정직하게 강등한다(`unavailable` + 사유).
- 단품 helper를 찾지 못하면(개별 설치 등) 해당 섹션만 건너뛰고 나머지를 진행한다.
## When to use
- "이 사업자(거래처/의뢰인) 실제 문제 없는지 한 번에 확인해줘"
- "○○○-○○-○○○○○ 살아있는 회사야? 직원은 좀 있고, 체납·입찰 제재 이력은 없어?"
## Prerequisites
- 인터넷 연결, `python3`
- 같은 레포의 단품 스킬 6종(이 복합이 helper를 재사용)
- proxy 섹션을 켜려면 hosted/self-host `k-skill-proxy` 접근 가능
## Credential requirements
- 사용자 측 필수 시크릿 없음.
- proxy 섹션(국세청 상태·국민연금·금융위·부정당)은 운영 서버의 `DATA_GO_KR_API_KEY`로 동작한다. 활용신청 항목은 각 단품 스킬 문서를 따른다.
- 무인증 섹션(체납·인허가)은 키 없이 사용자 머신에서 직접 동작한다.
## Inputs
- `b_no`: 사업자등록번호 10자리(하이픈 허용) — 상태조회·부정당제재에 필요
- `--name`: 상호·법인명 — 국민연금·금융위·체납·인허가에 필요
- `--region`: 시군구 — 인허가(동네 사업장) 조회에 필요 (예: `제주제주시`)
- `--industry`: 인허가 업종(여러 번 지정 가능). 생략 시 음식점·카페·숙박
## CLI examples
```bash
python3 biz-health-check/scripts/biz_health_check.py 124-81-00998 --name "삼성전자"
# 동네 사업장까지 포함
python3 biz-health-check/scripts/biz_health_check.py --name "호텔샬롬" --region 제주제주시 --industry 숙박업
```
## Output
- `sections`: 6개 섹션 각각의 `data`(단품 응답 원문) 또는 `status: unavailable` + `note`
- 입력에 따라 일부 섹션은 생략된다(예: `--name` 없으면 국민연금/금융위/체납 생략).
## Failure modes
- 섹션별 강등은 리포트에 그대로 남는다(전체 실패가 아니다).
- proxy 섹션이 `503/502`면 운영 서버 키·활용신청 문제 — 각 단품 스킬 문서 참고.
## Official surfaces
- 각 단품 스킬 문서(`docs/features/<skill>.md`)의 공식 출처를 따른다.

View file

@ -1,161 +0,0 @@
"""Business due-diligence composite — runs the sibling k-skill providers at once.
사업자등록번호(+상호/지역) 하나로 "이 사업자, 실제 문제 없나" 무료 공공 데이터로
교차 조회해 실사 리포트 장을 만든다. 점수·등급·"위험" 라벨을 만들지 않고,
항목의 사실 + 출처 + 조회시각만 병렬한다. 판단은 사용자 몫이다.
복합 스킬은 같은 레포의 단품 스킬 helper들을 그대로 재사용한다(단일 진실원천):
- nts-business-registration 상태조회 (k-skill-proxy)
- national-pension-workplace 국민연금 사업장 (k-skill-proxy)
- fsc-corporate-info 금융위 법인개요 (k-skill-proxy)
- g2b-sanctioned-supplier 부정당제재 (k-skill-proxy)
- nts-tax-delinquency 체납 명단 (무인증 직접)
- localdata-business-status 인허가 영업상태 (무인증 직접, --region 필요)
단품 helper를 찾지 못하면 해당 항목만 정직하게 강등하고 나머지는 계속 진행한다.
"""
from __future__ import annotations
import argparse
import datetime as dt
import importlib.util
import json
import pathlib
import re
import sys
from typing import Any, Callable
KST = dt.timezone(dt.timedelta(hours=9))
_REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
# (섹션 키, 사람이 읽는 라벨, 단품 스킬 디렉토리, helper 파일명)
_SIBLINGS = {
"nts_status": ("국세청 사업자등록 상태", "nts-business-registration", "nts_business_registration.py"),
"national_pension": ("국민연금 가입 사업장", "national-pension-workplace", "national_pension_workplace.py"),
"fsc_corp": ("금융위 기업기본정보", "fsc-corporate-info", "fsc_corporate_info.py"),
"g2b_sanction": ("조달청 부정당제재", "g2b-sanctioned-supplier", "g2b_sanctioned_supplier.py"),
"tax_delinquency": ("국세 체납 명단공개", "nts-tax-delinquency", "nts_tax_delinquency.py"),
"localdata": ("지방행정 인허가 영업상태", "localdata-business-status", "localdata_business_status.py"),
}
def _now_iso() -> str:
return dt.datetime.now(KST).isoformat(timespec="seconds")
def _normalize_b_no(value: Any) -> str:
normalized = re.sub(r"\D", "", str(value or ""))
if not re.fullmatch(r"\d{10}", normalized):
raise ValueError("사업자등록번호는 숫자 10자리여야 합니다 (하이픈 허용).")
return normalized
def _unavailable(module_key: str, note: str) -> dict:
label, skill_dir, _ = _SIBLINGS[module_key]
return {"provider": label, "skill": skill_dir, "status": "unavailable",
"looked_up_at": _now_iso(), "data": None, "note": note}
def _load(module_key: str) -> Any | None:
"""단품 스킬 helper를 레포 레이아웃 기준 파일 경로로 로드. 없으면 None."""
_, skill_dir, filename = _SIBLINGS[module_key]
path = _REPO_ROOT / skill_dir / "scripts" / filename
if not path.exists():
return None
spec = importlib.util.spec_from_file_location(f"_bhc_{module_key}", path)
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _section(module_key: str, caller: Callable[[Any], dict]) -> dict:
"""단품 helper 하나를 호출해 섹션 결과로 감싼다. 어떤 오류든 강등."""
label, skill_dir, _ = _SIBLINGS[module_key]
base = {"provider": label, "skill": skill_dir, "looked_up_at": _now_iso()}
try:
module = _load(module_key)
except Exception as err:
return {**base, "status": "unavailable", "data": None,
"note": f"단품 스킬 '{skill_dir}' helper import 실패({type(err).__name__}: {err})."}
if module is None:
return {**base, "status": "unavailable", "data": None,
"note": f"단품 스킬 '{skill_dir}' helper를 찾지 못해 건너뜀 (개별 설치 시 함께 두세요)."}
try:
data = caller(module)
status = "unavailable" if isinstance(data, dict) and (data.get("status") == "unavailable" or data.get("error")) else "ok"
return {**base, "status": status, "data": data}
except Exception as err: # 경계 계약: 한 항목 실패가 전체를 막지 않는다
return {**base, "status": "unavailable", "data": None, "note": f"조회 실패({type(err).__name__}: {err})."}
def run(b_no: str | None, name: str | None = None, region: str | None = None,
industries: list[str] | None = None, *, base_url: str | None = None) -> dict:
no = _normalize_b_no(b_no) if b_no else None
name = (name or "").strip() or None
sections: dict[str, dict] = {}
if no:
sections["nts_status"] = _section(
"nts_status", lambda m: m.query_status([no], base_url=base_url))
else:
sections["nts_status"] = _unavailable("nts_status", "사업자등록번호가 없어 상태조회 생략.")
sections["national_pension"] = _section(
"national_pension",
lambda m: m.query_workplace(name, no, base_url=base_url)) if name else \
_unavailable("national_pension", "상호(--name)가 없어 국민연금 조회 생략.")
sections["fsc_corp"] = _section(
"fsc_corp",
lambda m: m.query_corp_outline(name, no, base_url=base_url)) if name else \
_unavailable("fsc_corp", "법인명(--name)이 없어 금융위 조회 생략.")
sections["g2b_sanction"] = _section(
"g2b_sanction", lambda m: m.query_sanctions(no, base_url=base_url)) if no else \
_unavailable("g2b_sanction", "사업자등록번호가 없어 부정당제재 조회 생략.")
sections["tax_delinquency"] = _section(
"tax_delinquency", lambda m: m.lookup(name)) if name else \
_unavailable("tax_delinquency", "상호(--name)가 없어 체납 명단 조회 생략.")
if name and region:
sections["localdata"] = _section(
"localdata", lambda m: m.lookup(name, region, industries))
else:
sections["localdata"] = _unavailable("localdata", "동네 사업장 인허가 조회는 상호(--name)와 지역(--region)이 함께 필요.")
return {
"query": {"b_no": no, "name": name, "region": region, "industries": industries},
"generated_at": _now_iso(),
"disclaimer": ("무료 공공 데이터의 사실만 병렬한 실사 리포트다. 점수·등급·위험 판정은 "
"하지 않으며, 동일성·해석은 사용자가 판단한다."),
"sections": sections,
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="사업자 실사 복합 조회 (단품 k-skill 6종 묶음)")
parser.add_argument("b_no", nargs="?", default=None, help="사업자등록번호 10자리(하이픈 허용)")
parser.add_argument("--name", help="상호·법인명 — 국민연금/금융위/체납/인허가 조회에 필요")
parser.add_argument("--region", help="시군구 (동네 사업장 인허가 조회용 — 예: 제주제주시)")
parser.add_argument("--industry", action="append", dest="industries", help="인허가 업종(여러 번 지정 가능)")
parser.add_argument("--proxy-base-url")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
report = run(args.b_no, args.name, args.region, args.industries, base_url=args.proxy_base_url)
except ValueError as err:
print(json.dumps({"error": str(err)}, ensure_ascii=False, indent=2), file=sys.stderr)
return 1
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,284 +0,0 @@
---
name: catchtable-sniper
description: Monitor Catchtable for open reservation slots and attempt booking using a logged-in Chrome session.
license: MIT
metadata:
category: lifestyle
subcategory: food
locale: ko-KR
phase: v1
requires:
- Chrome MCP
- Logged-in Catchtable Chrome session
---
# catchtable-sniper
## 📋 기본 정보
- **스킬명**: catchtable-sniper
- **라이선스**: MIT
- **단계**: v1
- **카테고리**: lifestyle / food
- **로케일**: ko-KR
- **요구사항**: Chrome MCP, 캐치테이블 로그인된 Chrome 세션
---
## 🎯 주요 기능
캐치테이블에서 원하는 식당의 빈자리(취소 슬롯)를 30초 간격으로 감시하다가 발견하는 즉시 자동 예약합니다.
멀티 타겟 동시 감시, 예약 오픈런 모드, 인원 유연 매칭, Dry-run 알림 전용 모드를 지원합니다.
---
## ✅ 적합한 사용 사례
- `"온지음 5월 토요일 저녁 2인 빈자리 나오면 예약해줘"`
- `"온지음, 밍글스, 라연 중 5월 주말 2인 아무데나 먼저 뜨는 거 잡아줘"` ← 멀티 타겟
- `"라연 5월 예약 오픈이 4월 30일 오전 10시야, 그때 맞춰서 잡아줘"` ← 오픈런 모드
- `"스시야마 이번달 안에 2인 — 못 잡으면 4인 있으면 알려줘"` ← 인원 유연
- `"밍글스 빈자리 뜨면 예약은 내가 할게 알림만 줘"` ← Dry-run 모드
- `"https://app.catchtable.co.kr/ct/shop/mingles 토요일 4명 자동예약"`
---
## ❌ 부적합한 사용 사례
- 로그인 자동화 (카카오/네이버 로그인은 직접 해야 함)
- 선결제 식당의 결제 정보 자동 입력 (결제 단계는 사람이 직접)
- 캐치테이블 외 플랫폼 예약 (네이버 예약, 식신 등)
- 30초 미만 폴링 간격 (서버 부하 방지)
---
## 🔧 기술 요구사항
- **Chrome MCP** 연결 필수
- 캐치테이블(`app.catchtable.co.kr`)에 **로그인된 Chrome 세션** 필요
- 별도 API 키, 패키지 설치 불필요
---
## 🔐 인증 처리
이 스킬은 이미 Chrome에 로그인된 세션을 그대로 사용합니다.
로그인 정보를 스킬에 전달하지 않습니다.
로그인 안 된 경우:
```
"캐치테이블에 로그인되어 있지 않습니다.
Chrome에서 캐치테이블에 카카오/네이버 로그인 후 다시 실행해주세요."
```
→ 스킬 중단. 로그인 자동화 없음.
---
## 🗂️ 입력 파싱
사용자 입력에서 다음을 추출한다:
| 항목 | 예시 | 필수 여부 |
|------|------|----------|
| 식당명 또는 URL | `"온지음"` / `app.catchtable.co.kr/ct/shop/onjium` | 필수 (복수 가능) |
| 날짜 | `"5월 3일"`, `"이번 주 토요일"`, `"5월 주말 전체"` | 필수 |
| 인원 | `"2명"`, `"4인"` | 필수 |
| 시간대 | `"저녁"`, `"19시 이후"` | 선택 (없으면 전체) |
| 모드 | `"알림만"`, `"dry-run"` | 선택 (없으면 자동예약) |
| 인원 유연 | `"2인 없으면 4인도 괜찮아"` | 선택 |
| 오픈 시간 | `"4월 30일 오전 10시 오픈"` | 선택 (오픈런 모드) |
| 폴링 간격 | `"30초마다"` | 선택 (기본: 30초) |
**멀티 타겟 감지**: 식당명이 쉼표/슬래시로 구분되거나 "중 아무데나", "먼저 뜨는 거" 표현이 있으면 멀티 타겟 모드로 전환.
---
## 📊 실행 플로우
### STEP 1 — 브라우저 준비 및 로그인 확인
Chrome MCP로 캐치테이블 접속:
```
navigate: https://app.catchtable.co.kr
```
MY 탭에서 로그인 상태 확인. 미로그인 시 중단.
---
### STEP 2 — 모드 분기
```
입력 파싱 완료
├─ 오픈 시간 명시됨 → STEP 2-A (오픈런 모드)
└─ 오픈 시간 없음 → STEP 2-B (취소 스나이핑 모드)
```
#### STEP 2-A: 오픈런 모드
예약 오픈 시간까지 대기:
```
[10:00:00 오픈 예정] 현재 09:58:42 — 77초 후 오픈
[10:00:00] ✅ 오픈 시각 도달 — 즉시 예약 시도
```
오픈 시각 정각에 날짜 선택 → 슬롯 클릭 → 예약 폼 진입.
슬롯이 이미 마감이면 → 취소 스나이핑 모드(STEP 2-B)로 자동 전환.
#### STEP 2-B: 취소 스나이핑 모드 (폴링 루프)
```
while 빈자리 없음:
{폴링 간격}초 대기
페이지 새로고침 또는 날짜 재클릭
슬롯 파싱
빈자리 발견 → STEP 3
```
---
### STEP 3 — 멀티 타겟 처리
**단일 타겟**: 해당 식당 슬롯 확인.
**멀티 타겟**: 지정된 식당들을 순차 순회하며 슬롯 확인.
```
[14:23:15] 온지음 5/3 확인 중... 없음
[14:23:17] 밍글스 5/3 확인 중... 없음
[14:23:19] 라연 5/3 확인 중... 없음 (30초 후 재시도)
[14:23:49] ✅ 밍글스 5/3 19:30 빈자리 발견! — 예약 시작
```
한 곳에서 슬롯 발견 시 나머지 감시 즉시 중단 → 발견된 식당 예약 진행.
---
### STEP 4 — 인원 유연 매칭
지정 인원(예: 2인) 슬롯이 없을 경우:
```
if 인원_유연 == True:
대안_인원(예: 4인) 슬롯 확인
발견 시:
"2인 슬롯은 없지만 4인 슬롯(19:00)이 있습니다.
4인으로 예약할까요? (예/아니오)"
→ 사용자 확인 후 진행
```
---
### STEP 5 — 예약 진행 (모드 분기)
**Dry-run 모드** (`"알림만"` / `"dry-run"` 입력 시):
```
✅ 빈자리 발견! 예약은 진행하지 않습니다.
식당: 밍글스
날짜: 5월 3일(토)
시간: 19:30
인원: 2명
→ 지금 바로 예약하시겠습니까? (예/아니오)
```
→ 예약 여부는 사람이 결정.
**자동예약 모드** (기본):
빈 슬롯 버튼 즉시 클릭 → 예약 폼 진입.
폼 자동 입력:
- 인원수: 지정한 인원 선택
- 방문 목적: "식사" (기본값)
- 주의사항 동의: 전체 동의 체크
- 예약자 정보: 앱 저장 정보 자동 사용
**선결제 식당인 경우**:
```
"빈자리를 발견했습니다! 결제가 필요합니다.
결제 금액: {금액}원
지금 결제를 진행할까요? (예/아니오)"
```
→ 결제 정보 자동 입력 없음. 사용자 확인 후 결제 진행.
**무료 예약**: "예약하기" 최종 확인 버튼 클릭.
---
### STEP 6 — 완료 확인
```
🎉 예약 완료!
식당: {식당명}
날짜: {날짜}
시간: {시간}
인원: {인원}명
모드: {자동예약 / Dry-run}
예약번호: {예약번호}
캐치테이블 앱 > MY > 예약내역에서 확인 가능합니다.
```
---
## 💡 중간 상태 출력 형식
```
[14:23:15] 밍글스 5/3 저녁 슬롯 확인 중... 빈자리 없음 (30초 후 재시도)
[14:23:45] 온지음 5/3 저녁 슬롯 확인 중... 빈자리 없음
[14:24:15] ✅ 밍글스 5/3 19:30 (2인) 빈자리 발견! — 예약 시작
```
---
## ⚙️ 설정값
| 항목 | 기본값 | 범위 |
|------|--------|------|
| 폴링 간격 | 30초 | 30초 이상 |
| 최대 감시 시간 | 2시간 | — |
| 멀티 타겟 최대 수 | 5개 | — |
2시간 초과 시:
```
"2시간 동안 빈자리가 없었습니다. 계속 시도할까요? (예/아니오)"
```
---
## 🚨 에러 핸들링
| 상황 | 대응 |
|------|------|
| 식당 페이지 404 | "식당을 찾을 수 없습니다. 이름을 다시 확인해주세요." |
| 예약 오픈 전 | 오픈 일정 안내 후 오픈런 모드로 전환 제안 |
| 슬롯 클릭 후 이미 마감 | 즉시 재폴링 재개 |
| 네트워크 오류 | 10초 후 재시도, 3회 연속 실패 시 사용자 알림 |
| 멀티 타겟 중 일부 404 | 해당 식당 제외, 나머지 계속 감시 |
| 2시간 초과 | "계속 시도할까요?" 확인 후 연장 또는 종료 |
---
## ✨ 완료 기준
다음 중 하나:
- 예약 완료 화면 확인 + 예약번호 수집
- Dry-run 모드에서 빈자리 발견 및 사용자 알림 완료
- 사용자가 명시적으로 중단 요청
---
## 사용 예시
```
"온지음 5월 10일 저녁 2인 빈자리 나오면 예약해줘"
"온지음, 밍글스, 라연 5월 토요일 저녁 2인 중 아무데나 먼저 뜨는 거 잡아줘"
"라연 5월 예약이 4월 30일 오전 10시 오픈이야, 그때 맞춰 2인 잡아줘"
"스시야마 이번달 2인 — 없으면 4인도 괜찮아, dry-run으로"
"https://app.catchtable.co.kr/ct/shop/mingles 토요일 4명 자동예약"
```
---
## ⚠️ 주의사항
- Chrome에 캐치테이블 로그인 세션이 있어야 동작합니다.
- 선결제 식당의 결제 정보는 직접 입력해야 합니다.
- 폴링 간격은 최소 30초를 유지합니다 (서버 부하 방지).
- 캐치테이블 이용약관을 준수하는 범위에서 사용하세요.

View file

@ -1,128 +0,0 @@
---
name: corporate-registration-consulting
description: 법인등기소/인터넷등기소 상업등기 신청을 처음 하는 사용자를 위해 일반 영리 주식회사 발기설립 절차, 정관·첨부서류 실제 HWP 양식 작성, 등록면허세·과밀억제권역 중과 체크, rhwp 기반 순차 검토 흐름을 참고용으로 안내한다.
license: MIT
metadata:
category: legal-documents
locale: ko-KR
---
# 법인등기 신청 컨설팅
## 가장 중요한 면책
이 스킬은 **참고용** 절차 안내와 문서 초안 자동화 도구다. **법률 자문, 세무 자문, 법무사 업무 대행이 아니다.**
등기소 보정명령·각하, 세금 산정, 정관 유효성, 업종별 인허가 여부는 사건별로 달라질 수 있으므로 제출 전에는 관할 등기소, 위택스/지방자치단체 세무부서, 세무사, 법무사, 변호사 확인을 권한다.
## When to use
- “주식회사 법인 설립등기 처음 하는데 전체 절차 알려줘”
- “법인명, 이사, 주소를 넣어 정관과 첨부서류 초안을 만들어줘”
- “등록면허세, 과밀억제권역 중과, 소프트웨어 업종 감면/중과 제외 가능성을 체크해줘”
- “등기 신청서류를 HWP로 만들어야 해서 rhwp-edit/k-skill-rhwp로 채울 수 있게 준비해줘”
## 운영 원칙
1. **사용자 결정 사항만 묻고 나머지는 에이전트가 처리한다.** 법인명, 본점 주소, 목적, 자본금, 1주의 금액, 발기인/주주, 이사/감사, 공고방법, 결산기, 주금납입 은행, 제출 방식처럼 사용자가 결정해야 하는 값만 확인한다.
2. **쉬운 말로 설명한다.** “발기인=처음 회사를 세우는 사람”, “정관=회사 기본 규칙”, “등록면허세=등기 전에 내는 지방세”처럼 어려운 말을 풀어쓴다.
3. **최신 확인이 필요한 법령·세율은 공식 출처를 다시 확인한다.** 법령은 국가법령정보센터(law.go.kr), 신청 절차는 인터넷등기소(iros.go.kr) 또는 온라인법인설립시스템(startbiz.go.kr), 지방세는 위택스(wetax.go.kr)·관할 지자체를 우선한다.
4. **정관은 최대한 저장된 표준정관을 그대로 따른다.** 불필요한 창작 문구를 만들지 말고 `templates/attachment-hwp/standard-articles-startup-moj.hwp`, `templates/attachment-hwp/articles-of-incorporation.hwp`의 구조와 표현을 우선 유지한다. 목적/업태·종목, 상호, 본점, 주식 수, 임원, 결산기처럼 사용자 회사에 맞게 바꿔야 하는 부분만 고치고, 애매한 부분은 에이전트가 법령·양식·기존 템플릿을 더 확인해 가능한 초안을 제시한다.
5. **일반 영리 주식회사 발기설립을 기본값으로 빠르게 진행한다.** 모집설립은 일반적이지 않으므로 기본 플로우에서는 제외하고, 사용자가 별도로 요청하지 않는 한 저장된 발기설립 양식과 첨부서류 양식을 채우는 데 집중한다.
6. **이미 저장해 둔 HWP 양식을 우선 활용해 완성한다.** 에이전트가 매번 공식 양식을 새로 찾게 하지 말고, 이 스킬에 저장된 `templates/official/form-65-1-stock-company-incorporation-promoter.hwp``templates/attachment-hwp/*.hwp` 사본을 레포 밖 작업 디렉터리에 복사해 채운다. 최신 양식 확인은 제출 전 대조 안내로만 두고, 실제 초안 작성의 기본 경로는 저장된 양식 채우기다. 발기설립 신청서는 `scripts/fill_official_hwp.py``templates/official/form-65-1-fill-map.json`으로 작성하고, 정관·주식발행사항동의서·주식인수증·발기인회의사록·주주명부·조사보고서·취임승낙서·이사회의사록·인감신고서·위임장 등은 `templates/attachment-hwp/`의 저장된 HWP 양식을 채운다.
7. **사용자가 서류 작성을 요청하면 기본 산출물은 실제 HWP 파일이다.** 단순 절차 설명만 요청한 경우가 아니면 Markdown만 반환하지 말고, 레포 밖 비공개 작업 디렉터리에 공식 신청서와 첨부서류 HWP 사본을 만들고, `rhwp-edit`/`k-skill-rhwp`로 그 사본의 자리표시자와 표 셀에 사용자 값을 입력한다. Markdown은 검토용 요약·체크리스트·정관 대조본으로만 보조 제공한다.
8. **HWP 편집은 기존 rhwp 계열 스킬을 적극적으로 재사용한다.** 문서 생성/편집은 [`rhwp-edit`](../rhwp-edit/SKILL.md)의 `k-skill-rhwp`, HWP/HWPX 조회·필드 추출은 [`hwp`](../hwp/SKILL.md), 레이아웃 디버깅은 [`rhwp-advanced`](../rhwp-advanced/SKILL.md)를 사용한다. 본문 자리표시자는 `replace-all`, 공식 신청서와 표 기반 첨부서류는 `set-cell-text`, 구조 확인은 `info`/`list-paragraphs`, 생성 후 확인은 `info`와 가능하면 `render` 또는 `kordoc` 변환으로 검증한다.
9. **양식의 어느 부분을 고칠지 문서마다 명시한다.** 특히 정관은 앞부분 제2조 목적/사업 내용에 실제 수행할 업태와 종목을 빠짐없이 채우고, 맨 마지막 부칙 아래 작성일자·발기인 성명·서명/기명날인란을 제출일·발기인별 실제 인감 날인 기준으로 확인한다. 각 첨부서류는 상단 법인명/본점, 중간 결의·인수·취임 내용, 하단 날짜·성명·날인란을 순서대로 확인한다.
10. **dummy 값을 지양하고 필요한 개인정보를 직접 받아 로컬 제출본에 채운다.** 빠른 초안을 위해 기본값을 제안하되, 제출용 HWP에는 `홍길동`, `서울특별시 ...`, `000000-0000000` 같은 dummy를 남기지 않는다. 사용자가 실제 이름·주소·생년월일·주식 수·인감 관련 표시 등 필요한 정보를 입력하면 에이전트가 그 값을 레포 밖 사본에 반영한다. 모르는 항목만 자리표시자로 남기고, 남은 자리표시자 목록을 사용자에게 알려준다.
11. **간인·법인인감 준비를 반드시 안내한다.** 정관처럼 여러 장으로 된 문서, 의사록/결정서, 위임장 등 원본성이 중요한 문서는 제출 전 각 장 사이에 간인이 필요한지 관할 등기소 요구를 확인하고 간인하도록 안내한다. 법인인감은 인감신고서와 함께 사용할 실제 도장을 미리 제작·준비해야 하며, 발기인/임원 개인 인감 또는 서명 요구와 구분해 설명한다.
12. **사람만 할 수 있는 최종 행위를 대신하지 않는다.** 에이전트는 초안·체크리스트·자리표시자 치환까지만 돕고, 인터넷등기소/위택스 로그인, 전자서명, 세금 납부, 등기 제출, 사용자 사칭, 최종 법률 판단, 최종 세무 판단은 수행하지 않는다.
13. **개인정보는 최소화하되 제출용 작성에는 실제 값을 받는다.** 초안 단계에서는 가능한 한 `{{OFFICER_NAME}}`, `{{OFFICER_ADDRESS}}` 같은 자리표시자를 쓰고, 실제 생성 직전에 필요한 필드만 받는다. 주민등록번호 원문, 신분증 이미지, 인감증명서 스캔본은 꼭 필요한 경우가 아니면 요구하지 않으며, 요약·로그·테스트·PR에는 이름/주소/생년월일 등 개인정보를 마스킹한다. 채워진 산출물은 로컬에만 두고 레포에 커밋하지 말라고 안내한다.
## 먼저 묻는 최소 정보
아래 값을 한 번에 표로 받는다. 모르는 항목은 기본안을 제안하고 사용자가 승인하게 한다.
| 항목 | 쉬운 설명 | 기본 제안 |
| --- | --- | --- |
| 법인명/상호 | 회사 이름. 같은 특별시·광역시·시·군 안의 같은 업종·같은 상호는 문제가 될 수 있어 인터넷등기소 상호검색을 먼저 한다. | `주식회사 {{COMPANY_NAME}}` |
| 본점 주소 | 회사 주소. 과밀억제권역/대도시 중과 판단의 핵심이다. | 임대차계약서 주소 그대로 |
| 사업 목적/업태·종목 | 등기부와 정관 제2조 앞부분에 들어가는 실제 수행 사업. 너무 넓거나 불명확하면 보정될 수 있고, 업태·종목은 사업자등록·세금 검토에도 이어진다. | 소프트웨어 개발 및 공급업, 정보통신업, 전자상거래업 등 실제 할 목적만 |
| 자본금·1주의 금액 | 초기 자금과 주식 한 장 가격. 등록면허세 계산에도 쓰인다. | 자본금 1,000,000원 / 1주 100원 또는 500원 |
| 발기인/주주 실제 정보 | 회사를 세우고 주식을 인수하는 사람. 제출용에는 성명, 주소, 생년월일/주민등록번호 필요 여부, 인수 주식 수, 날인 방식이 필요하다. | 대표 1인 설립 가능 |
| 이사/대표이사/감사 실제 정보 | 등기 이사와 대표자. 제출용 취임승낙서에는 성명, 주소, 생년월일, 취임일, 개인 인감/서명 방식이 필요하다. 소규모 회사는 감사 생략 가능 여부를 검토한다. | 1인 이사 회사 기본안 |
| 공고방법 | 회사 공고를 어디에 낼지. | 회사 홈페이지, 없으면 일간신문 |
| 결산기 | 회계연도 종료일. | 매년 12월 31일 |
| 주금납입 증빙 | 자본금이 입금됐다는 은행 잔고증명/거래내역. | 대표/발기인 명의 계좌 잔고증명 |
| 제출 방식 | 인터넷등기소 전자신청/방문/법무사 위임. | 전자신청 가능성 우선 확인 |
## 전체 절차 체크리스트
1. **상호·본점·목적 결정**: 인터넷등기소 상호검색으로 같은 관할 내 충돌 가능성을 확인한다.
2. **과밀억제권역/대도시 세금 체크**: 본점 주소가 수도권 과밀억제권역 등 중과 대상인지 먼저 본다. 대도시 법인 설립은 등록면허세가 중과될 수 있다.
3. **저장된 HWP 양식 준비**: 먼저 `templates/official/form-65-1-stock-company-incorporation-promoter.hwp``templates/attachment-hwp/`의 필요한 양식을 레포 밖 작업 디렉터리에 복사한다. 에이전트가 새 양식을 찾는 흐름이 아니라, 저장된 양식 사본을 채워 서류 초안을 완성하는 흐름을 기본으로 한다.
4. **정관 및 첨부서면 HWP 작성**: `templates/attachment-hwp/articles-of-incorporation.hwp`, `founder-meeting-minutes.hwp`, `share-subscription.hwp`, `share-issuance-consent.hwp`, `inspection-report.hwp`, `officer-acceptance-director-ceo.hwp / officer-acceptance-auditor.hwp` 등 공개 배포 HWP 양식을 레포 밖 작업 디렉터리에 복사해 자리표시자를 채운다. 각 HWP는 한 장 한 장 열람/구조 확인 후 상단·본문·하단·날인란을 순차 점검하며, replace-all은 shortcut일 뿐 최종 검토를 대체하지 않는다.
5. **발기인 결정서·주식인수·주금납입**: 발기인이 주식을 인수하고 자본금을 입금한 뒤 잔고증명서 또는 주금납입보관증명에 준하는 증빙을 준비한다.
6. **임원 취임승낙서·인감·주민등록/주소 증빙 준비**: 이사·대표이사·감사가 취임을 승낙했다는 서류와 인감 관련 서류를 준비한다. **등기이사는 개인 인감증명서 또는 본인서명사실확인서, 주민등록초본/등본 등 주소·신원 확인 발급서류가 필요하다는 점을 사용자에게 반드시 안내한다.** 법인인감 도장을 미리 준비하고 인감신고서 날인란과 위임장/의사록 날인 요구를 확인한다. 주민등록번호·신분증·인감증명 같은 민감정보는 원문을 대화나 로그에 남기지 말고 제출 직전 로컬 문서에만 반영한다.
7. **조사보고서/이사회·발기인 의사록 작성**: 현물출자 등 특수 사정이 없더라도 설립 경과를 확인하는 문서를 준비한다. 1인 회사면 결정을 단순화한다.
8. **등록면허세 신고·납부 준비**: 위택스 또는 관할 지자체 납부 화면에 넣을 금액·근거·체크리스트를 정리한다. 사용자가 실제 신고와 세금 납부를 직접 수행하고 영수필확인서를 확보한다.
9. **등기신청서 작성·첨부서류 묶기**: `templates/incorporation-document-pack.md`의 순서대로 신청서, 정관, 취임승낙서, 조사보고서, 주금납입 증빙, 인감신고서, 세금 영수증을 점검한다. 정관 등 여러 장 문서는 간인 필요 여부를 확인하고, 실제 간인·날인은 사용자가 원본에 수행하도록 안내한다.
10. **인터넷등기소/관할 등기소 제출 준비**: 전자신청이면 인증서·전자서명·스캔본 품질 체크리스트를 만들고, 방문이면 원본/사본과 도장 확인 목록을 만든다. 사용자가 실제 로그인, 전자서명, 등기 제출 절차를 직접 완료한다.
11. **보정 대응**: 등기소가 보정명령을 내리면 문구·첨부서류·세금 계산을 수정한다. 보정 사유를 쉬운 말로 풀고 다음 조치만 제시한다.
12. **등기 완료 후 후속 작업**: 법인등기사항증명서, 법인인감증명서, 사업자등록, 4대보험, 은행 법인계좌, 통신판매업/소프트웨어사업자 신고 등 후속 일정을 안내한다.
## 반드시 작성할 문서와 저장된 양식 경로
아래 표를 기준으로 법원등기소/인터넷등기소 기준 설립등기 필요 문서명과 실제 양식 경로를 대조한다. 사용자가 서류 작성을 요청하면 이 경로의 원본을 레포 밖 작업 디렉터리에 복사해 채우고, 원본 파일은 수정하지 않는다. 저장 양식이 없는 영수필확인서, 등기이사 인감증명서/본인서명사실확인서, 주민등록초본/등본은 사용자가 직접 발급해야 하는 필수 첨부서류로 체크리스트에 남긴다.
| 필요 문서 | 저장된 양식 경로 | 고쳐야 하는 부분 | 제출 전 확인 |
| --- | --- | --- | --- |
| 주식회사설립등기신청서(발기설립) | `corporate-registration-consulting/templates/official/form-65-1-stock-company-incorporation-promoter.hwp` | 상호, 본점, 등기 목적, 자본금, 발행주식, 임원, 첨부서류 목록, 신청인/대리인, 날짜 | 첨부서류 통수와 실제 묶음 일치 |
| 정관 | `corporate-registration-consulting/templates/attachment-hwp/standard-articles-startup-moj.hwp` 또는 `corporate-registration-consulting/templates/attachment-hwp/articles-of-incorporation.hwp` | 앞부분 제1조 상호, **제2조 목적/사업 내용의 업태·종목**, 본점, 주식 수/1주의 금액, 임원 규정, 결산기, 부칙 | 맨 마지막 작성일자, 발기인 성명, 서명/기명날인, 여러 장이면 간인 |
| 발기인이 정한 주식발행사항 등 증명정보(상법 제291조 사항) | `corporate-registration-consulting/templates/attachment-hwp/share-issuance-consent.hwp` | 발행주식 수, 1주의 금액, 납입기일, 발기인 동의 내용, 날짜·날인란 | 정관·신청서의 주식 수와 일치 |
| 주식인수증 | `corporate-registration-consulting/templates/attachment-hwp/share-subscription.hwp` | 인수인별 주식 수와 인수가액, 인수일, 성명·주소·날인란 | 인수 주식 수 합계가 설립 시 발행주식 수와 일치 |
| 발기인회의사록 | `corporate-registration-consulting/templates/attachment-hwp/founder-meeting-minutes.hwp` | 회의 일시·장소, 의안, 발기인, 결의 내용, 날짜, 날인란 | 1인/복수 발기인 구조와 맞는지, 여러 장이면 간인 |
| 발기인총회 기간단축 동의서 | `corporate-registration-consulting/templates/attachment-hwp/founder-meeting-period-shortening-consent.hwp` | 동의자, 기간단축 대상 절차, 날짜, 날인란 | 실제 절차상 필요한 경우에만 포함 |
| 주주명부 | `corporate-registration-consulting/templates/attachment-hwp/shareholder-register.hwp` | 주주 성명/주소, 주식 수, 1주 금액, 총액 | 정관·주식인수증·신청서의 주식 수와 일치 |
| 조사보고서 | `corporate-registration-consulting/templates/attachment-hwp/inspection-report.hwp` | 조사자, 조사 대상, 주금납입 확인, 변태설립사항 유무, 날짜, 날인란 | 근거 확인 가능한 표현으로 작성 |
| 이사/대표이사 취임승낙서 | `corporate-registration-consulting/templates/attachment-hwp/officer-acceptance-director-ceo.hwp` | 임원 성명, 주소, 생년월일, 직책, 취임일, 날짜, 서명/개인 인감 날인란 | 등기 임원 명단과 일치 |
| 감사 취임승낙서 | `corporate-registration-consulting/templates/attachment-hwp/officer-acceptance-auditor.hwp` | 감사 성명, 주소, 생년월일, 취임일, 날짜, 서명/개인 인감 날인란 | 감사 선임 시에만 포함 |
| 이사회의사록 | `corporate-registration-consulting/templates/attachment-hwp/board-minutes.hwp` | 대표이사 선임 등 결의, 일시·장소, 출석 이사, 날인란 | 이사회가 있는 구조에서만 사용 |
| 인감신고서 | `corporate-registration-consulting/templates/attachment-hwp/corporate-seal-report.hwp` | 상호, 본점, 대표자, 법인인감 날인란, 개인 인감/서명 관련 칸 | 실제 법인인감 도장 준비 및 날인 위치 |
| 위임장 | `corporate-registration-consulting/templates/attachment-hwp/power-of-attorney.hwp` | 위임인, 수임인, 위임 범위, 날짜, 날인란 | 대리 제출 시 원본 날인·간인 요구 확인 |
| 등록면허세 영수필확인서 | 저장 양식 없음. 위택스/지자체 납부 결과물 첨부 | 납부번호, 납부자, 세액, 관할 | **필수 발급/첨부.** 최종 신고·납부 결과 기준 |
| 등기신청수수료 영수필확인서 | 저장 양식 없음. 인터넷등기소/등기소 수수료 납부 결과물 첨부 | 납부번호, 납부자, 수수료, 신청 사건 | **필수 발급/첨부.** 등록면허세 영수필확인서와 별도 문서로 관리 |
| 등기이사 개인 인감증명서 또는 본인서명사실확인서 | 저장 양식 없음. 주민센터/정부24 등에서 임원 본인이 발급 | 등기이사별 성명, 발급일, 인감/서명 확인 정보 | **등기이사는 무조건 준비해야 하는 발급서류로 안내.** 주민등록번호·인감 정보는 로컬 제출본에만 보관 |
| 등기이사 주민등록초본/등본 등 주소·주민등록번호 확인 증빙 | 저장 양식 없음. 주민센터/정부24 등에서 임원 본인이 발급 | 등기이사별 성명, 주소, 주민등록번호/생년월일 확인 정보 | **등기이사는 무조건 준비해야 하는 발급서류로 안내.** 발급일·주민등록번호 표시 범위는 관할 요구 확인 |
| 주금납입/잔고증명 | 저장 양식 없음. 은행 증명서 또는 거래내역 첨부 | 계좌명의, 납입금액, 기준일 | 자본금·주식인수 금액과 일치 |
### 조건부 추가 서류 체크
아래는 일반 영리 주식회사 발기설립에서도 사실관계에 따라 추가될 수 있다. 해당 여부를 사용자에게 물어보고, 해당하면 체크리스트와 첨부서류 목록에 추가한다.
| 조건 | 추가 확인/서류 | 안내 방식 |
| --- | --- | --- |
| 명의개서대리인을 둔 경우 | 명의개서대리인 계약 또는 선임을 증명하는 정보 | 정관·주식 관련 문서와 일치 여부 확인 |
| 현물출자, 재산인수, 설립비용 등 변태설립사항이 있는 경우 | 검사인/공증인 조사보고, 감정인 감정, 관련 재판서류 등 상업등기규칙 제129조상 첨부정보 | 표준 1인/현금출자 설립보다 복잡하므로 사실관계를 먼저 정리하고 필요한 양식을 별도 작성 |
| 인허가가 필요한 업종인 경우 | 허가·인가·등록·신고 수리 증명 등 영업 가능 증빙 | 목적/업태·종목을 정관 제2조와 신청서 목적에 넣기 전 인허가 필요 여부 확인 |
| 자본금 10억 원 이상 또는 소규모회사 특례를 벗어나는 경우 | 정관 공증, 의사록 인증, 감사/이사회 구성 등 추가 요건 | 저장된 표준 양식은 유지하되 공증·인증 필요 여부를 제출 전 체크 |
## 등록면허세·중과·소프트웨어 업종 체크
- **등록면허세**: 법인 설립등기 전에 납부하는 지방세다. 지방세법 제28조의 등록면허세 세율을 기준으로 자본금, 본점 소재지, 대도시 중과 여부에 따라 달라진다. 지방교육세가 추가된다.
- **과밀억제권역/대도시 중과**: 수도권 과밀억제권역 등 대도시 안에서 법인을 설립하면 지방세법 제28조 제2항의 법인등기 중과가 문제될 수 있다. 지방세법 제13조는 취득세 맥락에서만 별도로 다루고, “서울/수도권이면 무조건”으로 단정하지 말고 본점 주소와 법령상 예외 업종을 같이 확인한다.
- **소프트웨어 업종**: 소프트웨어 개발·공급, 정보통신업은 창업중소기업 세액감면(조세특례제한법 제6조) 또는 대도시 중과 제외 업종 검토 대상이 될 수 있다. 단, 감면·제외는 업종코드, 실제 사업내용, 창업 요건, 이전/합병/개인사업 전환 여부에 따라 달라지므로 세무사/지자체 확인 전에는 확정 표현을 쓰지 않는다.
- **응답 방식**: 세금은 “예상 체크리스트”로 안내하고, 최종 금액은 위택스/관할 시군구 계산 결과를 기준으로 한다.
## 응답 끝에 항상 붙일 문구
> 이 안내와 문서 초안은 참고용이며 법률·세무 자문이 아닙니다. 실제 등기 제출 전 관할 등기소, 위택스/지자체 세무부서, 법무사·변호사·세무사 확인을 권합니다.
## 출처 확인 우선순위
- `templates/official-form-sources.md`: 저장된 공식/공개 HWP 양식 목록, 문서별 양식 경로, 공식 출처 대조 메모
- 대법원 인터넷등기소: https://www.iros.go.kr (등기신청양식, 첨부서면예시)
- 온라인법인설립시스템: https://www.startbiz.go.kr
- 위택스: https://www.wetax.go.kr
- 국가법령정보센터 지방세법/지방세법 시행령/상법/상업등기법/상업등기규칙/상업등기신청서의 양식에 관한 예규: https://www.law.go.kr
- 국세청 창업중소기업 세액감면 안내 및 조세특례제한법 제6조: https://www.nts.go.kr / https://www.law.go.kr

View file

@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""Fill the bundled official Korean stock-company incorporation HWP form.
This script intentionally writes to a caller-provided output path and never
modifies the bundled official source form in place.
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
OFFICIAL_DIR = SKILL_DIR / "templates" / "official"
DEFAULT_FORM = OFFICIAL_DIR / "form-65-1-stock-company-incorporation-promoter.hwp"
DEFAULT_MAP = OFFICIAL_DIR / "form-65-1-fill-map.json"
def load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def stringify(value) -> str:
if value is None:
return ""
if isinstance(value, list):
return "\n".join(str(item) for item in value if item is not None)
if isinstance(value, (int, float)):
return f"{value:,.0f}"
return str(value)
def run_set_cell(current: Path, output: Path, spec: dict, text: str, cwd: Path) -> None:
cmd = [
"npx",
"k-skill-rhwp",
"set-cell-text",
str(current),
str(output),
"--section",
str(spec["section"]),
"--parent-paragraph",
str(spec["parentParagraph"]),
"--control",
str(spec["control"]),
"--cell",
str(spec["cell"]),
"--text",
text,
]
result = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or result.stdout.strip())
def fill_form(data: dict, form_path: Path, map_path: Path, output_path: Path, cwd: Path) -> list[str]:
fill_map = load_json(map_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = output_path.with_suffix(output_path.suffix + ".tmp")
shutil.copyfile(form_path, temp_path)
written: list[str] = []
for field_name, spec in fill_map["fields"].items():
if field_name in data:
text = stringify(data[field_name])
elif "default" in spec:
text = stringify(spec["default"])
else:
continue
next_path = output_path.with_suffix(output_path.suffix + f".{len(written)}.tmp")
run_set_cell(temp_path, next_path, spec, text, cwd)
temp_path.unlink(missing_ok=True)
temp_path = next_path
written.append(field_name)
shutil.move(str(temp_path), str(output_path))
return written
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Fill official form 65-1 HWP with JSON data")
parser.add_argument("--input-json", required=True, type=Path, help="JSON file with form field values")
parser.add_argument("--output", required=True, type=Path, help="Output HWP path outside the repository")
parser.add_argument("--form", type=Path, default=DEFAULT_FORM, help="Official HWP source form")
parser.add_argument("--map", dest="map_path", type=Path, default=DEFAULT_MAP, help="HWP cell fill map")
parser.add_argument("--cwd", type=Path, default=Path.cwd(), help="Directory where npx k-skill-rhwp is available")
args = parser.parse_args(argv)
data = load_json(args.input_json)
written = fill_form(data, args.form, args.map_path, args.output, args.cwd)
print(json.dumps({"ok": True, "output": str(args.output), "fields_written": written}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc: # noqa: BLE001 - CLI boundary
print(f"fill_official_hwp.py: {exc}", file=sys.stderr)
raise SystemExit(1)

View file

@ -1,124 +0,0 @@
{
"downloaded_at": "2026-05-02",
"note": "Publicly downloadable HWP templates collected from the listed source pages. Where a downloaded form contained real/sample company names, personal names, addresses, resident-registration-like numbers, bank names, or concrete sample dates, those values were sanitized to placeholders after download. The bundled standard articles reference is a general Ministry of Justice stock-company articles form suitable as a non-listed/startup reference, not a listed-company standard articles form. Verify suitability, licensing, and current official requirements before submission.",
"files": [
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "정관.hwp",
"url": "https://blog.kakaocdn.net/dna/kPhhH/btqvUyGvYvl/AAAAAAAAAAAAAAAAAAAAAMldtkYuC46ZKQT-PZBrQ5xlYghtGK_BcJDzi8M3oORj/%EC%A0%95%EA%B4%80.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=2tn9fayBdYzwsfZua4mPP2qzQtc%3D&attach=1&knm=tfile.hwp",
"path": "articles-of-incorporation.hwp",
"sha256": "244f5eddd3bda1b200ad28c2a4bd1295182949c502255821d418f6605dc8cb52",
"bytes": 21504
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "주식발행사항동의서.hwp",
"url": "https://blog.kakaocdn.net/dna/uWZvQ/btqvTVWemNI/AAAAAAAAAAAAAAAAAAAAAAv4YzUGetrPiGJ6nrBjJ0mHUZrBAHcP2SNRqEK06V3E/%EC%A3%BC%EC%8B%9D%EB%B0%9C%ED%96%89%EC%82%AC%ED%95%AD%EB%8F%99%EC%9D%98%EC%84%9C.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=5URf5oei3BwYKvi1vppdNRGziqg%3D&attach=1&knm=tfile.hwp",
"path": "share-issuance-consent.hwp",
"sha256": "cacdf5e9b95f734fc708b9258f72836c6acb9dfc0f0550a52a0ca36df6b1f3eb",
"bytes": 9216
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "주식인수증.hwp",
"url": "https://blog.kakaocdn.net/dna/cFhyio/btqvUPOKPBe/AAAAAAAAAAAAAAAAAAAAAFzsXsWe7nQ6V43IEUWYpDMn7zjIQs_36miFqzw0x2s7/%EC%A3%BC%EC%8B%9D%EC%9D%B8%EC%88%98%EC%A6%9D.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=S2yCAsjaWS%2Fs6XKOcPgq%2BTH3AQE%3D&attach=1&knm=tfile.hwp",
"path": "share-subscription.hwp",
"sha256": "4b6cd23af21211294362dea2ff5c0e3018c7a8ccc93543d28e1cdd1f8ef5dad8",
"bytes": 9216
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "발기인총회 기간단축 동의서.hwp",
"url": "https://blog.kakaocdn.net/dna/blsbID/btqvTVhH67A/AAAAAAAAAAAAAAAAAAAAAH87uTWGvHw4EvRi6tHMRf6XpLm2pCDhng0o98F5RteI/%EB%B0%9C%EA%B8%B0%EC%9D%B8%EC%B4%9D%ED%9A%8C%20%EA%B8%B0%EA%B0%84%EB%8B%A8%EC%B6%95%20%EB%8F%99%EC%9D%98%EC%84%9C.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=WU3918ycgKAZPT3UU%2BzZygmysfI%3D&attach=1&knm=tfile.hwp",
"path": "founder-meeting-period-shortening-consent.hwp",
"sha256": "d65e2f21cfa29ee98a24770965ba5f3c9e78dbaee4e6062f0e01c19e5bf1f31b",
"bytes": 9216
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "조사보고서.hwp",
"url": "https://blog.kakaocdn.net/dna/bzY2if/btqvUQ1dSyn/AAAAAAAAAAAAAAAAAAAAAKsezsmgGy42M8uVt_hjH5SKf4ix8QS2uqM70wZF8RsY/%EC%A1%B0%EC%82%AC%EB%B3%B4%EA%B3%A0%EC%84%9C.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=PkaZuqkfncOKJxmyaEkj%2BjULtkk%3D&attach=1&knm=tfile.hwp",
"path": "inspection-report.hwp",
"sha256": "7ad5c9bbfb416a4196352ab91715b6e7ed87d329714c81f624285a46b6c437ea",
"bytes": 10752
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "취임승낙서(감사).hwp",
"url": "https://blog.kakaocdn.net/dna/lmctu/btqvWp2GinG/AAAAAAAAAAAAAAAAAAAAAPjANE6MFT3qQ8FxVKmZtyWc3tp76wLpjBvx59m-Znka/%EC%B7%A8%EC%9E%84%EC%8A%B9%EB%82%99%EC%84%9C(%EA%B0%90%EC%82%AC).hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=KjnFd3mTy6ip1UNPWBNwd8bJD50%3D&attach=1&knm=tfile.hwp",
"path": "officer-acceptance-auditor.hwp",
"sha256": "6d9ca0907faf7ddb976b8d7e2569d87f7d5efc7c9e1550d12a7d90aed7a517c6",
"bytes": 8704
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "취임승낙서(이사.대표이사).hwp",
"url": "https://blog.kakaocdn.net/dna/bv9fD4/btqvTGrs7v5/AAAAAAAAAAAAAAAAAAAAAJxrlq6pQhlDomIn1bvd3jd_AVSEMyMmkUOriHaLw20E/%EC%B7%A8%EC%9E%84%EC%8A%B9%EB%82%99%EC%84%9C(%EC%9D%B4%EC%82%AC.%EB%8C%80%ED%91%9C%EC%9D%B4%EC%82%AC).hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=9WNbZDPbyDtjx09ZwrpNTzSGQQM%3D&attach=1&knm=tfile.hwp",
"path": "officer-acceptance-director-ceo.hwp",
"sha256": "7c5f4e7d44d69db6578c1a12640a2ef5ef0c2f3279f7faad7c65fca505878f24",
"bytes": 8704
},
{
"source_name": "위시라이트 블로그 공개 주식회사 설립 첨부서류 HWP",
"page_url": "https://wishright81.tistory.com/23",
"original_name": "이사회의사록.hwp",
"url": "https://blog.kakaocdn.net/dna/bR9OJQ/btqvUQNG0yZ/AAAAAAAAAAAAAAAAAAAAADd7sShQDM0Pv6_Df3LIL5H7wovm-TBSqcfMlaMpK_N2/%EC%9D%B4%EC%82%AC%ED%9A%8C%EC%9D%98%EC%82%AC%EB%A1%9D.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=gdXVuFaDSUNHNCuMoXihjRDGWSQ%3D&attach=1&knm=tfile.hwp",
"path": "board-minutes.hwp",
"sha256": "735eaece6ec2fc617d8768bd30098f1f4808099aeeeed91aebf48e988238d003",
"bytes": 10752
},
{
"source_name": "우택스 블로그 공개 주식회사 설립등기 첨부서류 HWP",
"page_url": "https://wootax.tistory.com/entry/%EC%A3%BC%EC%8B%9D%ED%9A%8C%EC%82%AC-%EC%84%A4%EB%A6%BD%ED%95%98%EA%B8%B0-%EB%B2%95%EC%9D%B8%EB%93%B1%EA%B8%B0%EB%B6%80%EB%93%B1%EB%B3%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0",
"original_name": "5. 발기인회의사록.hwp",
"url": "https://blog.kakaocdn.net/dna/bwE3eR/btso6ks6Rz1/AAAAAAAAAAAAAAAAAAAAAGXjN0fmZvlgAGf9McFFnfvkxxo5s51mVwOvolJ6MSFN/5.%20%EB%B0%9C%EA%B8%B0%EC%9D%B8%ED%9A%8C%EC%9D%98%EC%82%AC%EB%A1%9D.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=Gmc9qugkolm7QOXIFEvUg7AfO7c%3D&attach=1&knm=tfile.hwp",
"path": "founder-meeting-minutes.hwp",
"sha256": "62971efa2ace88d8df7fa827538fc6b9d31b60d14e945d0769327531fee6a77b",
"bytes": 58368
},
{
"source_name": "우택스 블로그 공개 주식회사 설립등기 첨부서류 HWP",
"page_url": "https://wootax.tistory.com/entry/%EC%A3%BC%EC%8B%9D%ED%9A%8C%EC%82%AC-%EC%84%A4%EB%A6%BD%ED%95%98%EA%B8%B0-%EB%B2%95%EC%9D%B8%EB%93%B1%EA%B8%B0%EB%B6%80%EB%93%B1%EB%B3%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0",
"original_name": "6. 주주명부.hwp",
"url": "https://blog.kakaocdn.net/dna/Yu8V2/btso10oEbPP/AAAAAAAAAAAAAAAAAAAAAAVv7HSE5W8icMvgAPu5O1oDuYfJU9papuMZ17SolMpe/6.%20%EC%A3%BC%EC%A3%BC%EB%AA%85%EB%B6%80.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=L%2F17AHZt9FAz%2Fuvkv4fhWQfrGSU%3D&attach=1&knm=tfile.hwp",
"path": "shareholder-register.hwp",
"sha256": "5dc27e306c0384bad4c59073c402f0bd3085d1049c24afe68cc75dc83702c3af",
"bytes": 36352
},
{
"source_name": "우택스 블로그 공개 주식회사 설립등기 첨부서류 HWP",
"page_url": "https://wootax.tistory.com/entry/%EC%A3%BC%EC%8B%9D%ED%9A%8C%EC%82%AC-%EC%84%A4%EB%A6%BD%ED%95%98%EA%B8%B0-%EB%B2%95%EC%9D%B8%EB%93%B1%EA%B8%B0%EB%B6%80%EB%93%B1%EB%B3%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0",
"original_name": "9. 인감신고서.hwp",
"url": "https://blog.kakaocdn.net/dna/K8StW/btso08AvjvK/AAAAAAAAAAAAAAAAAAAAAAg8rnGRLiZrbNJpvZ3uaK1vS0_QZ_wd9oAYMPZPWjJe/9.%20%EC%9D%B8%EA%B0%90%EC%8B%A0%EA%B3%A0%EC%84%9C.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=ZOKfYeOyzgqyx05a30tMlcPfhPQ%3D&attach=1&knm=tfile.hwp",
"path": "corporate-seal-report.hwp",
"sha256": "26aa32cce44a4cb1d3a92e5684c02fc210c65b2d344acaa470ebc91c2579bd5b",
"bytes": 52736
},
{
"source_name": "우택스 블로그 공개 주식회사 설립등기 첨부서류 HWP",
"page_url": "https://wootax.tistory.com/entry/%EC%A3%BC%EC%8B%9D%ED%9A%8C%EC%82%AC-%EC%84%A4%EB%A6%BD%ED%95%98%EA%B8%B0-%EB%B2%95%EC%9D%B8%EB%93%B1%EA%B8%B0%EB%B6%80%EB%93%B1%EB%B3%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0",
"original_name": "10. 위임장.hwp",
"url": "https://blog.kakaocdn.net/dna/beg039/btso1ui7lKh/AAAAAAAAAAAAAAAAAAAAAAwYRb3rbFNGA338GYRNAaxNjLxi81b6M-HeAya9eRDY/10.%20%EC%9C%84%EC%9E%84%EC%9E%A5.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=Wx7HctsqSHGJzFsBs9gNxTHrN%2Bs%3D&attach=1&knm=tfile.hwp",
"path": "power-of-attorney.hwp",
"sha256": "d9141458d779dd781178ca187c2466fe9c2ec7467971b9fcdb6ad52e4ff42d66",
"bytes": 33280
},
{
"source_name": "법무부 주식회사 표준정관 공개 재배포 HWP",
"page_url": "https://mbolt.tistory.com/60",
"original_name": "주식회사표준정관_법무부.hwp",
"url": "https://blog.kakaocdn.net/dna/bnYmGS/btqQmP25auG/AAAAAAAAAAAAAAAAAAAAAOftXuAiup09pc65AD9memsLydWwWBZ3dWCh-W5jgLvF/%EC%A3%BC%EC%8B%9D%ED%9A%8C%EC%82%AC%ED%91%9C%EC%A4%80%EC%A0%95%EA%B4%80_%EB%B2%95%EB%AC%B4%EB%B6%80.hwp?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=P9ehS7wc0K3d%2FsEuWvvUJvwxJrE%3D&attach=1&knm=tfile.hwp",
"path": "standard-articles-startup-moj.hwp",
"sha256": "64d84ff1bd9ba10db1f5ba9c7ee0442725385ce44b587730e7984c69bd3034d6",
"bytes": 19456,
"note": "상장회사 표준정관이 아니라 일반 비상장/스타트업 발기설립 정관 참고용으로 쓰는 법무부 주식회사 표준정관 HWP 재배포본."
}
]
}

View file

@ -1,95 +0,0 @@
# {{COMPANY_NAME}} 설립등기 첨부서류 묶음 초안
> 참고용 자동작성 템플릿입니다. 법률·세무 자문이 아니며 제출 전 원본성, 인감, 세금, 관할 등기소 요구사항을 확인하세요.
> 개인정보·민감정보 주의: 이 템플릿을 채울 때 주민등록번호 원문, 신분증 이미지, 인감증명서 스캔본은 꼭 필요한 로컬 제출본에만 넣고, 예시·로그·테스트·PR에는 마스킹하세요. 채워진 서류와 HWP 산출물은 레포에 커밋하지 마세요.
## 1. 등기신청 기본정보
- 법인명: {{COMPANY_NAME}}
- 본점 주소: {{HEAD_OFFICE_ADDRESS}}
- 자본금: {{CAPITAL_KRW}}원
- 1주의 금액: {{PAR_VALUE_KRW}}원
- 설립 시 발행주식 수: {{INCORPORATION_SHARES}}주
- 대표이사: {{CEO_NAME}}
- 등기 이사: {{DIRECTOR_NAMES}}
## 2. 첨부서류 체크리스트
| 순서 | 문서 | 쉬운 설명 | 준비 상태 |
| --- | --- | --- | --- |
| 1 | 설립등기신청서 | 등기소에 “이 회사를 등기해 달라”고 내는 표지 서류 | {{STATUS_APPLICATION}} |
| 2 | 정관 | 회사의 기본 규칙. 앞부분 제2조 목적/업태·종목과 맨 마지막 날짜·발기인 서명/기명날인·간인을 중점 확인 | {{STATUS_ARTICLES}} |
| 3 | 발기인 의사록/결정서 | 회사를 세우기로 정했다는 기록 | {{STATUS_FOUNDERS_MINUTES}} |
| 4 | 주식인수증 | 누가 몇 주를 가져가는지 적은 문서 | {{STATUS_SHARE_SUBSCRIPTION}} |
| 5 | 주금납입 증빙/잔고증명 | 자본금이 실제로 입금됐다는 증빙 | {{STATUS_BANK_BALANCE}} |
| 6 | 조사보고서 | 설립 과정과 재산 상태를 확인했다는 보고 | {{STATUS_INSPECTION_REPORT}} |
| 7 | 취임승낙서 | 이사/감사가 직책을 맡겠다고 동의한 문서 | {{STATUS_ACCEPTANCE}} |
| 8 | 인감신고서 | 법인인감을 등기소에 신고하는 서류. 실제 법인인감 도장을 미리 준비 | {{STATUS_SEAL_REPORT}} |
| 9 | 등록면허세 영수필확인서 | 등기 전 지방세를 냈다는 증명. 납부 후 반드시 발급/첨부 | {{STATUS_TAX_RECEIPT}} |
| 10 | 등기신청수수료 영수필확인서 | 등기 신청 수수료를 냈다는 증명. 등록면허세와 별도로 반드시 발급/첨부 | {{STATUS_REGISTRY_FEE_RECEIPT}} |
| 11 | 등기이사 개인 인감증명서 또는 본인서명사실확인서 | 등기이사 본인의 인감/서명을 확인하는 발급서류. 등기이사는 무조건 준비해야 함 | {{STATUS_DIRECTOR_SEAL_CERTS}} |
| 12 | 등기이사 주민등록초본/등본 등 주소 확인 증빙 | 등기이사 주소·주민등록번호/생년월일 확인 발급서류. 등기이사는 무조건 준비해야 함 | {{STATUS_DIRECTOR_RESIDENT_DOCS}} |
공유용 체크리스트에는 위 증빙의 보유 여부만 표시하고, 주민등록번호·상세 주소·인감증명서 번호 같은 개인정보/민감정보 원문은 적지 않는다. 등기이사에게는 개인 인감증명서/본인서명사실확인서와 주민등록초본/등본이 별도 발급서류로 필요하다는 점을 누락 없이 안내한다.
## 2-1. 양식별 수정 위치·간인·인감 체크
| 문서 | 어느 부분을 고칠지 | 제출 전 추가 확인 |
| --- | --- | --- |
| 설립등기신청서 | 상단 상호/본점, 등기 목적, 자본금·주식 수, 임원, 첨부서류 목록, 신청일·신청인 | 발기설립 양식 제65-1호인지 확인. 모집설립 양식은 사용하지 않음 |
| 정관 | 제1조 상호, **제2조 목적의 실제 사업 업태·종목**, 본점, 주식/임원/결산 규정, 부칙 | 맨 마지막 날짜와 발기인 전원 서명/기명날인, 여러 장이면 간인 |
| 주식인수/주식발행 동의 | 인수인, 주식 수, 1주의 금액, 인수가액, 납입기일 | 합계가 신청서·정관과 일치 |
| 발기인회의사록/결정서 | 일시·장소, 의안, 결의 내용, 발기인, 날짜, 날인란 | 여러 장이면 간인 필요 여부 확인 |
| 조사보고서 | 조사자, 주금납입 확인, 변태설립사항 유무, 결론 문구 | 최종 적법 판단을 에이전트가 단정하지 않음 |
| 취임승낙서 | 임원 성명, 주소, 생년월일, 직책, 취임일, 날짜, 개인 인감/서명 | 등기 임원 명단과 일치 |
| 인감신고서 | 대표자, 상호/본점, 법인인감 날인란 | 법인인감 도장 준비 및 날인 위치 확인 |
| 위임장 | 위임인·수임인, 위임 범위, 날짜, 날인란 | 대리 제출 시 원본 날인·간인 요구 확인 |
## 2-2. 조건부 추가서류
| 조건 | 추가서류/확인 | 준비 상태 |
| --- | --- | --- |
| 명의개서대리인을 둔 경우 | 명의개서대리인 계약 또는 선임 증명 | {{STATUS_TRANSFER_AGENT}} |
| 현물출자/재산인수 등 변태설립사항이 있는 경우 | 검사인·공증인 조사보고, 감정인 감정, 관련 재판서류 | {{STATUS_SPECIAL_FORMATION_DOCS}} |
| 인허가 업종인 경우 | 허가·인가·등록·신고 수리 증명 | {{STATUS_LICENSE_DOCS}} |
| 자본금 10억 원 이상 또는 소규모회사 특례 밖인 경우 | 정관 공증, 의사록 인증, 감사/이사회 요건 확인 | {{STATUS_NOTARIZATION_REVIEW}} |
## 3. 취임승낙서 초안
본인은 {{COMPANY_NAME}}의 {{OFFICER_ROLE}}로 선임되었음을 승낙합니다.
- 성명: {{OFFICER_NAME}}
- 주소: {{OFFICER_ADDRESS}}
- 생년월일: {{OFFICER_BIRTHDATE}}
- 취임일: {{APPOINTMENT_DATE}}
{{APPOINTMENT_DATE}}
{{OFFICER_NAME}} (인)
## 4. 조사보고서 초안
조사보고자 {{INSPECTOR_NAME}}은 {{COMPANY_NAME}}의 설립에 관하여 다음 사항을 조사하였습니다.
1. 정관의 작성 및 발기인의 기명날인 여부
2. 발행주식 총수와 인수 여부
3. 주금납입 또는 잔고증명 확인 여부
4. 현물출자, 재산인수 등 변태설립사항 유무: {{SPECIAL_FORMATION_ITEMS}}
5. 등록면허세 및 지방교육세 납부 여부
조사 결론: {{INSPECTION_CONCLUSION_AFTER_USER_OR_EXPERT_REVIEW}}
> 에이전트는 “설립 절차에 중대한 흠이 없다”는 최종 법률 판단을 임의로 쓰지 않습니다. 위 결론은 사용자, 조사보고자, 법무사·변호사 등 전문가가 근거를 확인한 뒤 확정하세요.
{{REPORT_DATE}}
조사보고자 {{INSPECTOR_NAME}} (인)
## 5. 등록면허세 확인 메모
- 본점 주소: {{HEAD_OFFICE_ADDRESS}}
- 과밀억제권역/대도시 중과 검토 결과: {{OVERCONCENTRATION_REVIEW}}
- 소프트웨어/정보통신 업종 감면 또는 중과 제외 검토: {{SOFTWARE_TAX_REVIEW}}
- 위택스/지자체 최종 납부번호: {{WETAX_PAYMENT_ID}}

View file

@ -1,91 +0,0 @@
# 주식회사 설립등기 공식 양식 출처 맵
> 이 파일은 스킬에 **이미 저장된 HWP 양식 경로**와 제출 전 대조 기준을 정리한 매핑 문서입니다. 에이전트는 새 양식을 찾는 것부터 시작하지 말고, 2026-05-02 기준 저장된 `templates/official/``templates/attachment-hwp/` 파일 사본을 레포 밖 작업 디렉터리에 복사해 채웁니다. 인터넷등기소/법원 제공 HWP/HWPX/PDF 원본은 수시로 바뀔 수 있으므로 제출 직전 최신본과 대조만 안내합니다.
## 저장 양식 우선 사용 순서
1. **번들 HWP 스냅샷** `templates/official/`
- `form-65-1-stock-company-incorporation-promoter.hwp`: [양식 제65-1호] 주식회사 설립 등기(발기설립).
- `form-65-2-stock-company-incorporation-subscription.hwp`: [양식 제65-2호] 주식회사 설립 등기(모집설립). 모집설립은 일반적이지 않으므로 이 스킬은 대응하지 않고 대조자료로만 둔다.
- `form-65-1-fill-map.json`: 발기설립 공식 HWP의 주요 입력 셀 매핑.
- `source-manifest.json`: 다운로드일, flSeq, SHA-256, 원천 URL 메타데이터.
2. **국가법령정보센터 등기예규** `상업등기신청서의 양식에 관한 예규`
- 대조할 양식: **양식 제65-1호(주식회사설립등기신청서·발기설립)**, **양식 제65-2호(주식회사설립등기신청서·모집설립)**.
- 용도: 인터넷등기소 양식이 최신 예규의 별지 양식과 맞는지 확인.
3. **찾기쉬운 생활법령정보: 주식회사 설립등기**
- 용도: 신청 방식, 신청정보, 첨부정보 목록의 쉬운 말 확인.
- 확인 포인트: 생활법령 페이지는 인터넷등기소에 등기신청서·첨부서류 양식 및 작성방식이 있다고 안내하고, 첨부서면은 등기예규 제65-1호/제65-2호를 보라고 안내한다.
4. **온라인법인설립시스템** `https://www.startbiz.go.kr`
- 용도: 온라인 법인설립 진행 시 실제 입력 흐름, 기관 연계 제출 흐름 확인.
## 실제 공개 배포 첨부서류 HWP 양식 묶음
공식 설립등기신청서 외에 실제 발기설립에서 자주 필요한 첨부서면은 `templates/attachment-hwp/`에 **공개 웹에서 실제 배포되는 HWP 파일**로 함께 둔다. 이 파일들은 에이전트가 임의 생성한 양식이 아니며, 각 파일의 출처 URL·원 파일명·SHA-256은 `templates/attachment-hwp/source-manifest.json`에 기록한다. 공개 배포본에 포함되어 있던 실제/샘플 법인명·성명·주소·주민등록번호형 문자열·은행명·구체 날짜는 HWP 안에서 자리표시자로 치환했다. 다만 공식 양식이 아닌 민간/공개 배포 양식은 제출 전 인터넷등기소 첨부서면예시, 상법 제289조, 상업등기규칙 제129조, 관할 등기소 요구와 반드시 대조한다.
- `articles-of-incorporation.hwp`: 공개 배포 정관 양식.
- `standard-articles-startup-moj.hwp`: 법무부 주식회사 표준정관 공개 재배포 HWP. 상장회사 표준정관이 아니라 비상장/스타트업 발기설립 정관 참고용으로 사용한다.
- `share-issuance-consent.hwp`: 주식발행사항동의서.
- `share-subscription.hwp`: 주식인수증.
- `founder-meeting-minutes.hwp`: 발기인회의사록.
- `founder-meeting-period-shortening-consent.hwp`: 발기인총회 기간단축 동의서.
- `shareholder-register.hwp`: 주주명부.
- `inspection-report.hwp`: 조사보고서.
- `officer-acceptance-director-ceo.hwp`: 이사/대표이사 취임승낙서.
- `officer-acceptance-auditor.hwp`: 감사 취임승낙서.
- `board-minutes.hwp`: 이사회의사록.
- `corporate-seal-report.hwp`: 인감신고서.
- `power-of-attorney.hwp`: 위임장.
## 표준 발기설립 문서별 공식/초안 대응
| 문서 | 공식 확인 위치 | 레포 내 HWP/보조자료 | 사용 원칙 |
| --- | --- | --- | --- |
| 주식회사설립등기신청서(발기설립) | 인터넷등기소 등기신청양식, 등기예규 양식 제65-1호, 번들 `templates/official/form-65-1-stock-company-incorporation-promoter.hwp` | `templates/incorporation-document-pack.md`의 기본정보 섹션 및 `scripts/fill_official_hwp.py` | 번들 HWP에 주요 값을 자동 작성하되, 제출 전 최신 공식본 대조와 사람 검토 필수 |
| 주식회사설립등기신청서(모집설립) | 등기예규 양식 제65-2호, 번들 `templates/official/form-65-2-stock-company-incorporation-subscription.hwp` | 기본 플로우에서는 사용하지 않음 | 모집설립은 일반적이지 않으므로 사용자가 별도 요청하지 않는 한 발기설립 양식을 채운다 |
| 정관 | 상법, 상업등기규칙, 인터넷등기소 첨부서면예시, 공개 배포 정관 HWP | `templates/attachment-hwp/articles-of-incorporation.hwp`, `templates/attachment-hwp/standard-articles-startup-moj.hwp` | 실제 공개 배포 HWP를 우선 복사해 작성한다. 앞부분 제2조 목적에는 실제 사업 업태·종목을 채우고, 맨 마지막 날짜·발기인 서명/기명날인·간인을 확인한다. 종류주식·스톡옵션·투자계약 등은 표준정관 구조를 우선 확인해 별도 조항으로 보강 |
| 발기인 의사록/결정서 | 인터넷등기소 첨부서면예시, 공개 배포 발기인회의사록 HWP | `templates/attachment-hwp/founder-meeting-minutes.hwp`, `templates/attachment-hwp/founder-meeting-period-shortening-consent.hwp` | 공개 배포 HWP를 복사해 회사 구조별 필수 결의사항을 공식 예시와 대조 |
| 주식인수증/주식청약서 | 인터넷등기소 첨부서면예시, 상업등기규칙 제129조, 공개 배포 HWP | `templates/attachment-hwp/share-subscription.hwp`, `templates/attachment-hwp/share-issuance-consent.hwp` | 발기설립은 주식 인수를 증명하는 정보, 모집설립은 청약 관련 정보가 달라질 수 있음 |
| 조사보고서 | 인터넷등기소 첨부서면예시, 상법 조사보고 조항, 공개 배포 HWP | `templates/attachment-hwp/inspection-report.hwp` | 공개 배포 HWP를 복사해 작성하되, 에이전트가 최종 적법 판단 문구를 단정하지 않음 |
| 취임승낙서 | 인터넷등기소 첨부서면예시, 상업등기규칙 제129조, 공개 배포 HWP | `templates/attachment-hwp/officer-acceptance-director-ceo.hwp`, `templates/attachment-hwp/officer-acceptance-auditor.hwp` | 성명·주소·생년월일 등 개인정보는 로컬 제출본에만 입력 |
| 등기이사 개인 인감증명서 또는 본인서명사실확인서 | 상업등기규칙 제129조 및 관할 등기소 요구 | 저장 양식 없음. 주민센터/정부24 등에서 등기이사 본인이 발급 | 등기이사는 무조건 필요한 발급서류로 안내하고 원문 정보는 로컬 제출본에만 보관 |
| 등기이사 주민등록초본/등본 등 주소 확인 증빙 | 상업등기규칙 제129조 및 관할 등기소 요구 | 저장 양식 없음. 주민센터/정부24 등에서 등기이사 본인이 발급 | 등기이사는 무조건 필요한 발급서류로 안내하고 발급일·주민등록번호 표시 범위 확인 |
| 인감신고서 | 인터넷등기소 등기신청양식/첨부서면예시, 공개 배포 HWP | `templates/attachment-hwp/corporate-seal-report.hwp` | 공개 배포 HWP를 참고하되 법인인감 날인·인감 관련 증빙은 공식 요구 확인 |
| 등록면허세 영수필확인서 | 위택스/관할 지자체 | `templates/incorporation-document-pack.md` 세금 확인 메모 | 필수 발급/첨부. 최종 세액·납부번호는 위택스/지자체 결과 기준 |
| 등기신청수수료 영수필확인서 | 인터넷등기소/등기소 수수료 납부 | `templates/incorporation-document-pack.md` 체크리스트 | 필수 발급/첨부. 등록면허세 영수필확인서와 별도 문서로 관리 |
## 조건부 추가서류 대조
- 명의개서대리인을 둔 경우: 명의개서대리인 계약 또는 선임 증명 정보를 첨부서류 목록에 추가한다.
- 현물출자·재산인수·설립비용 등 변태설립사항이 있는 경우: 상업등기규칙 제129조상 검사인/공증인 조사보고, 감정인 감정, 관련 재판서류 등 해당 증명정보를 별도로 확인한다.
- 인허가 업종인 경우: 허가·인가·등록·신고 수리 증명 등 영업 가능 증빙을 추가한다.
- 자본금 10억 원 이상 또는 소규모회사 특례 밖인 경우: 정관 공증, 의사록 인증, 감사/이사회 구성 필요 여부를 확인한다.
## 에이전트 답변에 포함할 공식 양식 안내 문구
- “실제 제출 양식은 인터넷등기소의 **등기신청양식**과 **첨부서면예시**에서 최신 HWP/HWPX/PDF를 다시 내려받아 사용하세요.”
- “주식회사 발기설립 신청서는 국가법령정보센터의 `상업등기신청서의 양식에 관한 예규` **양식 제65-1호**, 모집설립은 **양식 제65-2호**와 대조하세요.”
- “이 레포의 공개 배포 HWP 묶음과 Markdown 템플릿은 작성 보조자료이며, 최신 공식 양식·관할 등기소 요구를 대체하지 않습니다.”
## 저장 양식 기반 작성 흐름
1. 레포 밖 비공개 작업 디렉터리를 만든다.
2. 위 표의 저장된 HWP 양식을 작업 디렉터리로 복사한다.
3. 사용자 입력 JSON을 만든다. 주민등록번호 원문은 마스킹하거나 제출 직전 로컬 파일에만 둔다.
4. `scripts/fill_official_hwp.py`로 번들 [양식 제65-1호] HWP에 주요 셀을 채운다.
5. 첨부서류는 저장된 `templates/attachment-hwp/*.hwp` 사본을 한 장씩 확인하며 채운다. 단순 replace-all은 shortcut이므로 모든 양식을 순차 확인하고, 정관·의사록·위임장 등 간인 대상 가능 문서와 법인인감 준비 여부를 별도 체크한다.
```bash
workdir="$(mktemp -d "${TMPDIR:-/tmp}/corp-reg.XXXXXX")"
chmod 700 "$workdir"
python3 corporate-registration-consulting/scripts/fill_official_hwp.py \
--input-json "$workdir/form-data.json" \
--output "$workdir/form-65-1-filled.hwp"
npx k-skill-rhwp info "$workdir/form-65-1-filled.hwp"
```
## HWP/HWPX 처리 주의
- 공식 파일을 새로 내려받거나 번들 HWP를 채운 산출물은 레포 밖 임시 디렉터리에 보관한다.
- `k-skill-rhwp info <공식양식>`로 구조를 확인한 뒤 표/셀은 `set-cell-text`, 본문 자리표시자는 `replace-all`을 우선 사용한다. 다만 replace-all에 의존하지 말고 각 양식의 앞부분·본문·하단 날짜·서명/날인란을 순차 검토한다. 번들 발기설립 HWP는 `form-65-1-fill-map.json`의 셀 매핑을 사용한다.
- 공식 양식은 표와 칸이 많으므로 자동 치환 후 반드시 사람이 한컴오피스/호환 뷰어로 열어 누락 셀, 줄바꿈, 날인란, 첨부서류 목록을 확인한다.

View file

@ -1,32 +0,0 @@
{
"form": "form-65-1-stock-company-incorporation-promoter.hwp",
"title": "[양식 제65-1호] 주식회사 설립 등기(발기설립)",
"engine": "k-skill-rhwp set-cell-text",
"warning": "이 매핑은 국가법령정보센터 HWP 별지 양식의 표 셀 인덱스에 맞춘 보조 자동작성 맵입니다. 자동 작성 후 한컴오피스/호환 뷰어에서 셀 위치, 줄바꿈, 날인란, 첨부서면 통수를 반드시 사람이 확인하세요.",
"fields": {
"registration_purpose": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 18, "default": "주식회사설립" },
"registration_reason": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 19 },
"company_name": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 21 },
"head_office_address": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 23 },
"public_notice_method": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 25 },
"par_value_krw": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 27 },
"authorized_shares": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 29 },
"issued_shares_summary": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 31 },
"capital_krw": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 33 },
"purposes_text": { "section": 0, "parentParagraph": 2, "control": 0, "cell": 35 },
"directors_auditors_text": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 2 },
"ceo_name_address": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 4 },
"share_class_details": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 6, "default": "해당없음" },
"branches": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 8, "default": "해당없음" },
"duration_or_dissolution": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 10, "default": "해당없음" },
"other_registration_items": { "section": 0, "parentParagraph": 3, "control": 0, "cell": 12 },
"registration_tax_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 1 },
"local_education_tax_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 3 },
"special_rural_tax_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 5, "default": "0" },
"tax_total_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 7 },
"application_fee_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 9 },
"fee_payment_number": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 11 },
"tax_base_krw": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 13 },
"application_date": { "section": 0, "parentParagraph": 6, "control": 0, "cell": 17 }
}
}

View file

@ -1,27 +0,0 @@
{
"downloaded_at": "2026-05-02",
"source": {
"name": "국가법령정보센터 상업등기신청서의 양식에 관한 예규",
"page_url": "https://www.law.go.kr/행정규칙/상업등기신청서의양식에관한예규",
"adm_rul_seq": "2200000106061",
"note": "별지 양식 HWP 다운로드 링크(flDownload.do)에서 내려받은 공식 별지 서식입니다. 제출 전에는 최신 예규/인터넷등기소 양식과 다시 대조하세요."
},
"files": [
{
"path": "form-65-1-stock-company-incorporation-promoter.hwp",
"title": "[양식 제65-1호] 주식회사 설립 등기(발기설립)",
"flSeq": "146330897",
"bylClsCd": "200206",
"sha256": "793dc933df06bd7dee117c6613e369d6918be22114d8682d6e8c5f54850ddc98",
"bytes": 17408
},
{
"path": "form-65-2-stock-company-incorporation-subscription.hwp",
"title": "[양식 제65-2호] 주식회사 설립 등기(모집설립)",
"flSeq": "146330901",
"bylClsCd": "200206",
"sha256": "aefa665b1d57f856041a648bb8b9cbcfd291049112d683e9504310295d2a7848",
"bytes": 17408
}
]
}

View file

@ -1,6 +1,6 @@
---
name: coupang-product-search
description: retention-corp/coupang_partners의 로컬 Coupang MCP 호환 레이어로 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 상품 비교, 베스트 상품, 골드박스 특가를 조회한다.
description: coupang-mcp 서버를 통해 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 상품 비교, 베스트 상품, 골드박스 특가를 조회한다.
license: MIT
metadata:
category: retail
@ -12,9 +12,9 @@ metadata:
## What this skill does
[retention-corp/coupang_partners](https://github.com/retention-corp/coupang_partners) 저장소의 로컬 Coupang MCP 호환 레이어를 사용해 쿠팡 상품 조회 도구를 실행한다. 기존 유지보수형 HF Space MCP 엔드포인트 대신, 이 저장소의 `bin/coupang_mcp.py`가 제공하는 `local://coupang-mcp` 계약을 호출한다.
[coupang-mcp](https://github.com/uju777/coupang-mcp) 서버를 경유하여 쿠팡 상품을 검색하고 실시간 가격을 확인한다.
- 키워드 상품 검색
- 키워드 상품 검색 (로켓배송/일반배송 구분)
- 로켓배송 전용 필터 검색
- 가격대 범위 검색
- 상품 비교표 생성
@ -25,48 +25,23 @@ metadata:
## How it works
```
Claude Code / Codex
→ coupang-product-search/scripts/coupang_partners_mcp.py
→ git clone/update retention-corp/coupang_partners (user cache)
→ python3 bin/coupang_mcp.py
→ local://coupang-mcp compatible tool layer
├─ Coupang Partners API client (operator keys present)
└─ hosted fallback → https://a.retn.kr/v1/public/assist (no keys)
Claude Code
→ MCP Streamable HTTP (JSON-RPC)
→ HF Space (coupang-mcp 서버)
→ Netlify 프록시 (도쿄)
→ 다나와 가격 조회 (1차) / 쿠팡 API 폴백
```
Hard rules:
- API 키 불필요
- 다나와에서 정확한 판매가 우선 조회, 실패 시 쿠팡 API 가격 자동 폴백
- 해외 IP 차단 우회를 위해 도쿄 리전 프록시 경유
- `COUPANG_MCP_ENDPOINT`는 호환성 knob로만 유지한다. 기본값은 `local://coupang-mcp`다.
- 구형 HF Space hosted MCP 엔드포인트를 사용하거나 새로 지어내지 않는다.
- upstream 저장소는 `https://github.com/retention-corp/coupang_partners.git`만 사용한다.
- `tools``init`은 로컬 MCP 계약 확인용으로 먼저 실행한다.
## Execution paths
`retention-corp/coupang_partners`는 하나의 CLI 뒤에서 두 가지 경로를 자동으로 선택한다. 래퍼(`coupang_partners_mcp.py`)는 두 경로 모두를 그대로 통과시킨다.
1. **Operator (local HMAC) path**`COUPANG_ACCESS_KEY``COUPANG_SECRET_KEY`가 둘 다 설정된 경우. upstream이 Coupang Partners API를 HMAC 서명해 직접 호출한다. 키/시크릿은 절대 답변·문서·커밋에 노출하지 않는다.
2. **Credentialless hosted fallback path** — 위 두 키 중 하나라도 없는 경우(또는 `OPENCLAW_SHOPPING_FORCE_HOSTED=1`). upstream이 자동으로 Retention Corp의 hosted 백엔드(`https://a.retn.kr/v1/public/assist`)로 떨어진다. 이 경로는 `X-OpenClaw-Client-Id` allowlist로 게이트되어 있으며, upstream이 기본으로 실어 보내는 `openclaw-skill` 값이 현재 Retention Corp allowlist에 등록된 값이다. k-skill 래퍼는 `OPENCLAW_SHOPPING_CLIENT_ID`를 별도로 설정하지 않고 이 upstream 기본값을 그대로 사용한다.
두 경로 모두 JSON envelope(`ok`/`data.session_id`/`data.tool`/`data.payload`/`data.result`) 모양은 동일하므로, 답변 로직은 경로를 구별할 필요가 없다. short deeplink는 hosted fallback에서는 `https://a.retn.kr/s/...` 형태로, operator path에서는 `https://link.coupang.com/...` 형태로 온다.
### 관련 환경변수
| 환경변수 | 역할 | 기본값 |
|---------|------|--------|
| `COUPANG_ACCESS_KEY`, `COUPANG_SECRET_KEY` | 운영자 Coupang Partners API 크리덴셜. 둘 다 있을 때만 로컬 HMAC 경로가 활성화된다. | 없음 (없으면 hosted fallback) |
| `OPENCLAW_SHOPPING_CLIENT_ID` | hosted fallback이 보낼 `X-OpenClaw-Client-Id`. upstream이 `openclaw-skill`을 기본으로 실어 보내며 이 값이 현재 Retention Corp allowlist에 등록되어 있다. k-skill 래퍼는 이 변수를 오버라이드하지 않는 것을 권장한다. | `openclaw-skill` |
| `OPENCLAW_SHOPPING_FORCE_HOSTED` | `1`이면 키가 있어도 hosted 경로를 강제한다. | 비어있음 |
| `OPENCLAW_SHOPPING_BASE_URL` | hosted 백엔드 base URL 오버라이드. 스테이징/로컬 backend 테스트용. | `https://a.retn.kr` |
## MCP endpoint / contract
## MCP endpoint
```
local://coupang-mcp
https://yuju777-coupang-mcp.hf.space/mcp
```
프로토콜 호환 버전: MCP `2025-03-26`. 네트워크로 붙는 Streamable HTTP 서버가 아니라, upstream 저장소의 로컬 MCP 호환 CLI가 같은 도구 이름과 JSON-RPC 모양의 payload를 반환한다.
## When to use
- "쿠팡에서 생수 가격 좀 찾아줘"
@ -80,7 +55,6 @@ local://coupang-mcp
- 로그인, 장바구니, 결제 자동화가 필요한 경우
- 쿠팡 계정/session 접근이 필요한 경우
- 실시간 재고/품절 여부를 100% 보장해야 하는 경우 (hosted fallback과 Partners API 모두 캐시·지연이 있을 수 있다)
## Workflow
@ -90,125 +64,73 @@ local://coupang-mcp
- 권장 질문: `어떤 용도/예산/브랜드/용량을 우선할까요?`
### 2. Bootstrap and check the tool contract
### 2. Initialize MCP session
래퍼는 기본적으로 `~/.cache/k-skill/coupang_partners`에 upstream 저장소를 clone한다. 이미 clone되어 있으면 그대로 사용하고, 최신화가 필요할 때만 `--update`를 붙인다.
coupang-mcp는 MCP Streamable HTTP 프로토콜을 사용한다. 세션을 초기화한 뒤 도구를 호출한다.
```bash
python3 coupang-product-search/scripts/coupang_partners_mcp.py tools
python3 coupang-product-search/scripts/coupang_partners_mcp.py init
```
기존 checkout을 명시하거나 CI/검증에서 네트워크 clone을 막으려면:
```bash
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
--repo-dir /path/to/coupang_partners \
--no-clone \
tools
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
--repo-dir /path/to/coupang_partners \
--no-clone \
init
# Step 1: Initialize and get session ID
SESSION_ID=$(curl -s -X POST "https://yuju777-coupang-mcp.hf.space/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"k-skill","version":"1.0"}}}' \
-D /dev/stderr 2>&1 1>/dev/null | grep -i 'mcp-session-id' | awk '{print $2}' | tr -d '\r')
```
### 3. Call tools
구체적인 사용자 요청에 맞춰 upstream CLI 명령을 호출한다. 결과는 `ok`, `data.tool`, `data.payload`, `data.result`를 포함하는 JSON으로 반환된다.
세션 ID를 얻은 뒤 `tools/call` 로 원하는 도구를 호출한다.
```bash
# 일반 검색 (키 없이도 hosted fallback으로 작동)
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "32인치 4K 모니터"
# 로켓배송 필터
python3 coupang-product-search/scripts/coupang_partners_mcp.py rocket "에어팟"
# 가격대 검색
python3 coupang-product-search/scripts/coupang_partners_mcp.py budget "키보드" --max-price 100000
# 비교
python3 coupang-product-search/scripts/coupang_partners_mcp.py compare "아이패드 vs 갤럭시탭"
# 골드박스 (운영자 키가 필요한 upstream 경로)
python3 coupang-product-search/scripts/coupang_partners_mcp.py goldbox
```
### 4. (optional) hosted fallback 강제
운영자 키가 있는 상태에서도 hosted fallback 경로를 점검하고 싶으면 `OPENCLAW_SHOPPING_FORCE_HOSTED=1`만 추가하면 된다. `OPENCLAW_SHOPPING_CLIENT_ID`는 upstream이 보내는 기본값 `openclaw-skill`이 현재 Retention Corp allowlist에 등록된 값이므로 별도로 설정하지 않는다.
```bash
export OPENCLAW_SHOPPING_FORCE_HOSTED=1
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "에어팟"
curl -s -X POST "https://yuju777-coupang-mcp.hf.space/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_coupang_products","arguments":{"keyword":"32인치 4K 모니터"}}}' \
2>&1 | grep "^data:" | sed 's/^data: //'
```
## Available tools
| 도구명 | CLI 명령 | 기능 | 파라미터 예시 |
|--------|----------|------|-------------|
| `search_coupang_products` | `search` | 일반 상품 검색 | `"생수"` |
| `search_coupang_rocket` | `rocket` | 로켓배송만 필터링 | `"에어팟"` |
| `search_coupang_budget` | `budget` | 가격대 범위 검색 | `"키보드" --max-price 100000` |
| `compare_coupang_products` | `compare` | 상품 비교표 생성 | `"아이패드 vs 갤럭시탭"` |
| `get_coupang_recommendations` | `recommendations` | 인기 검색어 제안 | `--category 전자제품` |
| `get_coupang_seasonal` | `seasonal` | 계절/상황별 추천 | `"설날 선물"` |
| `get_coupang_best_products` | `best` | 카테고리별 베스트 | `--category-id 1016` |
| `get_coupang_goldbox` | `goldbox` | 당일 특가 정보 | `--limit 10` |
주의: `get_coupang_goldbox``get_coupang_best_products`는 upstream 기준 Coupang Partners API 권한이 필요한 경로이므로, 키가 없는 환경에서는 실패할 수 있다. 이런 경우 에러 메시지를 그대로 전달하고 hosted fallback이 커버하는 `search`/`rocket`/`budget`/`compare` 경로로 우회 제안한다.
| 도구명 | 기능 | 파라미터 예시 |
|--------|------|-------------|
| `search_coupang_products` | 일반 상품 검색 | `{"keyword":"생수"}` |
| `search_coupang_rocket` | 로켓배송만 필터링 | `{"keyword":"에어팟"}` |
| `search_coupang_budget` | 가격대 범위 검색 | `{"keyword":"키보드","min_price":0,"max_price":100000}` |
| `compare_coupang_products` | 상품 비교표 생성 | `{"keyword":"아이패드 vs 갤럭시탭"}` |
| `get_coupang_recommendations` | 인기 검색어 제안 | `{}` |
| `get_coupang_seasonal` | 계절/상황별 추천 | `{"keyword":"설날 선물"}` |
| `get_coupang_best_products` | 카테고리별 베스트 | `{"keyword":"전자제품"}` |
| `get_coupang_goldbox` | 당일 특가 정보 | `{}` |
## Response format
upstream CLI는 JSON을 출력한다. `data.result` 안의 상품 배열 또는 도구별 객체를 읽고, 답변에서는 로켓배송(rocket)과 일반배송(normal)을 구분한다.
```json
{
"ok": true,
"data": {
"session_id": "session-...",
"tool": "search_coupang_products",
"payload": {
"jsonrpc": "2.0",
"result": {
"content": [
{"type": "text", "text": "[...]"}
]
}
},
"result": []
}
}
```
사용자에게 보여줄 때는 다음처럼 짧게 정리한다.
결과는 로켓배송(rocket)과 일반배송(normal)으로 구분되어 반환된다.
```
## rocket (상위 후보)
## rocket (6)
1) LG전자 4K UHD 모니터
가격: 397,750원 (참고용)
보러가기: https://a.retn.kr/s/... # hosted fallback shortlink
또는: https://link.coupang.com/a/... # operator HMAC 경로 딥링크
옵션: 80cm / 32UR500K
가격: 397,750원 (39만원대)
보러가기: https://link.coupang.com/a/...
## normal (상위 후보)
## normal (4)
1) 삼성전자 QHD 오디세이 G5 게이밍 모니터
가격: 283,000원 (참고용)
보러가기: https://a.retn.kr/s/...
가격: 283,000원 (28만원대)
보러가기: https://link.coupang.com/a/...
```
## Response policy
- 후보가 여러 개면 상위 3~5개만 짧게 비교한다.
- 로켓배송/일반배송 구분을 명시한다.
- 가격/품절/배송 정보는 실시간 변동될 수 있음을 안내한다.
- upstream checkout, 권한, Coupang Partners 환경변수 문제로 실패하면 실패 원인과 재시도/설정 방법을 짧게 안내한다.
- **Affiliate 고지(필수)**: 응답에 포함되는 shortlink(`https://a.retn.kr/s/...`)와 직접 coupang 딥링크(`link.coupang.com/...?lptag=AF...`)는 Retention Corp의 쿠팡 파트너스(affiliate) 채널로 트래킹된다. upstream이 돌려주는 `disclosure` 문자열(`"파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"`)이 있으면 그대로 노출하고, 없으면 같은 취지의 고지를 답변 말미에 덧붙인다.
- 가격은 참고용임을 안내한다 (다나와 실패 시 쿠팡 API 추정가).
- MCP 서버가 응답하지 않으면 서버 상태를 알리고 나중에 재시도를 권한다.
## Done when
- `tools``init` 또는 실제 명령으로 retention-corp/coupang_partners 로컬 MCP 계약을 확인했다.
- 검색 결과가 로켓배송/일반배송으로 구분되어 정리되었다.
- 사용자 니즈에 맞는 추천 TOP 3이 제시되었다.
- 가격/배송 정보와 변동 가능성 안내가 포함되었다.
- affiliate 고지(disclosure)가 답변에 포함되었다.
- 가격/배송 정보가 포함되었다.

View file

@ -1,146 +0,0 @@
#!/usr/bin/env python3
"""Bootstrap and run retention-corp/coupang_partners Coupang MCP tools.
The k-skill repo intentionally does not vendor the third-party implementation.
This wrapper keeps the skill pointed at the approved upstream repository, clones it
into a user cache when needed, and then delegates to its local MCP-compatible CLI.
"""
from __future__ import annotations
import argparse
import os
import pathlib
import subprocess
import sys
from typing import Sequence
UPSTREAM_REPO_URL = "https://github.com/retention-corp/coupang_partners.git"
DEFAULT_MCP_ENDPOINT = "local://coupang-mcp"
DEFAULT_REPO_DIR = pathlib.Path(os.getenv("COUPANG_PARTNERS_REPO_DIR", "~/.cache/k-skill/coupang_partners")).expanduser()
UPSTREAM_CLI = pathlib.Path("bin") / "coupang_mcp.py"
class BootstrapError(RuntimeError):
"""Raised when the upstream checkout cannot be prepared."""
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the retention-corp/coupang_partners local Coupang MCP-compatible CLI.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" coupang_partners_mcp.py tools\n"
" coupang_partners_mcp.py init\n"
" coupang_partners_mcp.py search 생수\n"
" coupang_partners_mcp.py budget 키보드 --max-price 100000\n"
"\n"
"Honored upstream environment variables (forwarded as-is):\n"
" COUPANG_ACCESS_KEY, COUPANG_SECRET_KEY\n"
" Operator Coupang Partners API credentials. When set, upstream\n"
" uses the local HMAC-signed Coupang Partners path.\n"
" OPENCLAW_SHOPPING_CLIENT_ID\n"
" Allowlisted client id for the hosted fallback. upstream sends\n"
" openclaw-skill by default, which is the value currently on the\n"
" Retention Corp allowlist; k-skill does not override this.\n"
" OPENCLAW_SHOPPING_FORCE_HOSTED=1\n"
" Force the hosted fallback even when Coupang keys are present.\n"
" OPENCLAW_SHOPPING_BASE_URL\n"
" Override the hosted backend base URL. Default upstream target\n"
" is https://a.retn.kr and /v1/public/assist is the public entry.\n"
"\n"
"When both COUPANG_ACCESS_KEY and COUPANG_SECRET_KEY are missing,\n"
"upstream falls back to the hosted Retention Corp backend so this\n"
"skill keeps working without Coupang Partners credentials."
),
)
parser.add_argument(
"--repo-dir",
default=str(DEFAULT_REPO_DIR),
help="Checkout directory for retention-corp/coupang_partners (default: %(default)s).",
)
parser.add_argument(
"--no-clone",
action="store_true",
help="Do not clone the upstream repository if it is missing; fail with setup guidance instead.",
)
parser.add_argument(
"--update",
action="store_true",
help="Run git pull --ff-only in an existing upstream checkout before delegating.",
)
parser.add_argument(
"upstream_args",
nargs=argparse.REMAINDER,
help="Arguments passed to bin/coupang_mcp.py, for example: tools, search 생수, rocket 에어팟.",
)
args = parser.parse_args(argv)
if args.upstream_args and args.upstream_args[0] == "--":
args.upstream_args = args.upstream_args[1:]
if not args.upstream_args:
parser.error("missing upstream command; try: tools, init, search <keyword>, rocket <keyword>, budget <keyword>")
return args
def upstream_cli_path(repo_dir: pathlib.Path) -> pathlib.Path:
return repo_dir / UPSTREAM_CLI
def ensure_repo(repo_dir: pathlib.Path, *, clone: bool = True, update: bool = False) -> pathlib.Path:
cli_path = upstream_cli_path(repo_dir)
if cli_path.exists():
if update:
run_checked(["git", "-C", str(repo_dir), "pull", "--ff-only"], "failed to update upstream checkout")
return cli_path
if repo_dir.exists():
raise BootstrapError(
f"{repo_dir} exists but does not look like retention-corp/coupang_partners "
f"(missing {UPSTREAM_CLI}). Recreate it with: git clone {UPSTREAM_REPO_URL} {repo_dir}"
)
if not clone:
raise BootstrapError(
f"Missing retention-corp/coupang_partners checkout at {repo_dir}. "
f"Create it with: git clone {UPSTREAM_REPO_URL} {repo_dir}"
)
repo_dir.parent.mkdir(parents=True, exist_ok=True)
run_checked(["git", "clone", "--depth", "1", UPSTREAM_REPO_URL, str(repo_dir)], "failed to clone upstream checkout")
if not cli_path.exists():
raise BootstrapError(f"Cloned {UPSTREAM_REPO_URL}, but {UPSTREAM_CLI} was not found in {repo_dir}")
return cli_path
def run_checked(command: Sequence[str], context: str) -> None:
try:
subprocess.run(command, check=True)
except FileNotFoundError as exc:
raise BootstrapError(f"{context}: required executable not found: {command[0]}") from exc
except subprocess.CalledProcessError as exc:
raise BootstrapError(f"{context}: {exc}") from exc
def build_command(cli_path: pathlib.Path, upstream_args: Sequence[str]) -> list[str]:
return [sys.executable, str(cli_path), *upstream_args]
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
repo_dir = pathlib.Path(args.repo_dir).expanduser().resolve()
try:
cli_path = ensure_repo(repo_dir, clone=not args.no_clone, update=args.update)
except BootstrapError as exc:
print(f"coupang_partners_mcp.py: {exc}", file=sys.stderr)
return 2
env = os.environ.copy()
env.setdefault("COUPANG_MCP_ENDPOINT", DEFAULT_MCP_ENDPOINT)
completed = subprocess.run(build_command(cli_path, args.upstream_args), env=env)
return int(completed.returncode)
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,210 +0,0 @@
---
name: court-auction-notice-search
description: Browse 대법원경매정보(courtauction.go.kr) 부동산 매각공고 by 매각기일·법원·기일/기간 입찰, expand each notice into 사건번호·용도·주소·감정평가액·최저매각가, search property items by free conditions(지역·용도·가격·면적·유찰횟수), and look up a case directly by 법원+사건번호. Read-only, slow-by-design (~2s/call) to avoid IP blocks.
license: MIT
metadata:
category: real-estate
locale: ko-KR
phase: v1
---
# Court Auction Notice Search
## What this skill does
대한민국 법원이 운영하는 공식 **법원경매정보** 사이트(`courtauction.go.kr`) 의 매각공고와 사건정보를 에이전트가 활용할 수 있는 JSON 형태로 변환해서 돌려준다.
- 공식 OPEN API가 없어 사이트 내부의 WebSquare JSON XHR endpoint를 그대로 호출한다.
- 1차 transport 는 직접 HTTP다. Workflow C 자유검색에서 raw-HTTP WAF성 HTTP 400이 날 때만 Playwright fallback 으로 전환하며, 명시적 차단(`BLOCKED`/`ipcheck=false`)은 기본적으로 중단한다 (`rebrowser-playwright` 또는 `playwright-core` 가 있을 때만).
- 사이트는 **IP 단위 봇 차단** 이 매우 공격적이다 (16회/30초 정도면 1시간 차단). 이 패키지는 호출 간 최소 2초 jitter, 세션당 호출 budget(기본 10회), `data.ipcheck === false` 즉시 throw 로 보수적으로 동작한다.
- **참고용 도구**다. 실제 입찰 전에는 반드시 법원 원문 매각공고를 다시 확인해야 한다.
## When to use
- "오늘/내일 어디서 부동산 경매 열려?"
- "서울중앙지방법원 2026-04-27 매각공고 보여줘"
- "기일입찰 vs 기간입찰만 나눠서 보여줘"
- "이 매각공고 안의 사건번호/용도/주소/감정평가액 다 보여줘"
- "사건번호 2024타경100001 진행 상황 알려줘"
- "서울 강남구 아파트 최저가 5억 이하 유찰 1회 이상 물건 찾아줘"
- "법원사무소 코드 표 줘"
## When not to use
- 동산(자동차·중기) 경매 (이번 v1 범위 밖)
- 특정 매각기일 날짜의 모든 법원 일정을 한 번에 (Workflow D 별도 follow-up 이슈)
- 매각물건 사진(전경/개황/내부) URL 노출 (별도 follow-up 이슈)
- 매각물건명세서 / 현황조사서 / 감정평가서 PDF 다운로드 (별도 follow-up 이슈)
- 입찰서 자동 작성·자동 제출 (지원하지 않는다, 입찰은 반드시 법원에서 사람이 직접)
## Inputs
- `date` — 매각기일 월(YYYY-MM 또는 YYYYMM) 또는 특정일(YYYY-MM-DD 또는 YYYYMMDD). 필수. 실제 사이트 검색 버튼은 월(YYYYMM) 단위로 조회하므로 특정일 입력은 월 조회 후 해당 일자만 필터링한다.
- `courtCode` — 법원사무소코드 (예: `B000210` = 서울중앙지방법원). 비우면 전체. `getCourtCodes()` 또는 `codes courts` 로 받아온다.
- `bidType``date` (= 기일입찰, code 000331) 또는 `period` (= 기간입찰, code 000332). 빈값이면 둘 다.
- `caseNumber` — 사건번호. `2024타경100001` 형식 권장. `2024-100001` 도 받아서 `2024타경100001` 로 정규화한다.
## Mandatory honest framing
이 스킬은 사용자에게 다음 사실을 항상 알려야 한다.
1. 데이터는 법원경매정보 사이트의 공개 정보를 그대로 옮긴 것이며 **실제 입찰 전에 법원 원문을 재확인**해야 한다.
2. 사이트는 자동화 호출에 매우 민감해서 **빠른 연속 조회 시 IP가 1시간 차단**될 수 있다. 차단되면 같은 IP에서는 약 1시간을 기다려야 한다.
3. 가격(감정평가액·최저매각가격)·매각기일·매각장소는 **공고 시점 기준** 이며 정정·취하·연기로 변경될 수 있다 (`correctionCount`, `cancellationCount` 필드를 참고).
4. 본 스킬은 **read-only**다. 입찰 자체는 자동화하지 않는다.
## Official surfaces
- 법원경매정보 메인: `https://www.courtauction.go.kr`
- 부동산매각공고 진입: `https://www.courtauction.go.kr/pgj/index.on?w2xPath=/pgj/ui/pgj100/PGJ143M01.xml&pgjId=143M01`
- 경매사건검색 진입: `https://www.courtauction.go.kr/pgj/index.on?w2xPath=/pgj/ui/pgj100/PGJ159M00.xml&pgjId=159M00`
- 직접 호출 endpoint (이 스킬이 사용하는 것):
- `POST /pgj/pgj143/selectRletDspslPbanc.on` — 매각공고 목록
- `POST /pgj/pgj143/selectRletDspslPbancDtl.on` — 매각공고 상세 (사건/물건 펼치기)
- `POST /pgj/pgj15A/selectAuctnCsSrchRslt.on` — 사건 단건 조회
- `POST /pgj/pgjsearch/searchControllerMain.on` — 물건 자유 조건검색 (PGJ151F00 → PGJ151M01)
- `POST /pgj/pgjComm/selectCortOfcCdLst.on` — 법원사무소코드 전체
## Workflow A — 매각공고 → 사건/물건 펼치기
1. 사용자에게 **매각기일(YYYY-MM-DD)** 과 (선택) 법원·입찰구분을 받는다.
2. `searchSaleNotices({ date, courtCode, bidType })` 호출 → 그 날·그 법원의 매각공고 카드 목록.
3. 사용자가 카드를 고르면 카드 객체(또는 `raw`)를 그대로 `getSaleNoticeDetail(notice)` 에 넘긴다.
4. 응답의 `items[]``caseNumber`, `usage`, `address`, `appraisedPrice`, `minimumSalePrice`, `remarks` 를 가진다 (이슈 본문이 명시한 4필드 모두 포함).
5. 가격은 원 단위 정수다. 사용자에게 보여줄 때는 한국식 천단위 콤마 + 억/만 단위 환산을 같이 제시한다.
## Workflow B — 사건번호 직접 조회
1. 사용자에게 **법원사무소코드** + **사건번호(2024타경100001)** 를 받는다.
2. `getCaseByCaseNumber({ courtCode, caseNumber })` 호출.
3. `found:false / status:204` 면 사건이 존재하지 않거나 비공개. 사건번호 형식·법원이 맞는지 사용자에게 다시 확인한다.
4. `found:true``caseInfo`(사건명·접수일·청구액·재판부·진행상태), `items[]`(매각목적물 — 주소/배당요구종기), `schedule[]`(매각기일별 최저가/감정가/결과), `claimDeadline`, `relatedCases`, `stakeholders` 가 채워진다.
## Workflow C — 부동산 물건 자유 조건검색
1. 사용자의 조건을 `searchProperties()` 입력으로 매핑한다.
- `region: { sido, sigungu, dong }` — 코드 또는 대표 정적 sido 코드테이블의 한국어명. 지역을 주면 지번주소 검색(`cortStDvs:"2"`)으로, 지역이 없으면 매각공고 모드(`cortStDvs:"1"`)로 조회한다. 시군구/읍면동은 정적 표가 없으므로 코드로 직접 전달(예: `{ sido:"11", sigungu:"11680", dong:"11680101" }`)한다.
- `usage: { large, medium, small }` — 용도 대/중/소분류 코드(5자리, 예: 건물=`20000`) 또는 대분류 한국어명(`토지`/`건물`/`차량및운송장비`/`기타`).
- `priceRange` — 최저매각가격 원 단위 `{ min, max }` (실수 허용)
- `appraisedPriceRange` — 감정평가액 원 단위 `{ min, max }` (실수 허용)
- `saleDate``{ from, to }`
- `flbdCount` — 유찰횟수 `{ min, max }` **정수만**
- `area` — 면적(㎡) `{ min, max }` (실수 허용)
- `pageSize` — 페이지당 결과 수, upstream PGJ151 드롭다운에서 확인된 `10`/`20`/`50`/`100` 중 하나(기본 10). `1` 등 임의 값은 live endpoint 가 HTTP 400을 반환하므로 로컬에서 거부한다.
2. `searchProperties({ ... })` 호출 → `POST /pgj/pgjsearch/searchControllerMain.on`.
- 1차로 direct HTTP 시도. Workflow C raw-HTTP WAF의 HTTP 400을 만나면 자동으로 Playwright fallback 으로 재시도한다. fallback 을 끄려면 `{ fallback: false }`. `BLOCKED`(`ipcheck=false`)는 사이트의 명시적 차단 신호이므로 기본적으로 즉시 중단하며, 사용자가 위험을 이해하고 명시적으로 `{ fallbackOnBlocked: true }` 를 준 경우에만 재시도한다.
3. 응답의 `items[]` 는 핵심 raw 컬럼을 영문 키로 정규화한다:
- `saNo``caseNumber`, `srnSaNo`/`printCsNo``displayCaseNumber`
- `mokmulSer`/`maemulSer``itemNumber`
- `hjguSido + hjguSigu + hjguDong + daepyoLotno + buldNm``address`
- `gamevalAmt``appraisedPrice`, `minmaePrice``minimumSalePrice`
- `yuchalCnt``flbdCount`, `mulStatcd``statusCode`, `jinstatCd``progressStatusCode`
- `boCd``courtCode`, `jiwonNm``courtName`, `jpDeptNm``judgeDeptName`
- `lclsUtilCd/mclsUtilCd/sclsUtilCd``usageCodes.{large,medium,small}`
- `srchHjguSidoCd/SiguCd/DongCd``regionCodes.{sido,sigungu,dong}`
- `xCordi/yCordi``coordinates`, `wgs84Xcordi/Ycordi``coordinatesWgs84`
- `buldList/areaList/jimokList``buildingList/areaList/landCategoryList`
- `pjbBuldList``propertyDescription`, `mulBigo``remarks`
4. `getUsageCodes()` 는 4개 대분류(`10000=토지`, `20000=건물`, `30000=차량및운송장비`, `40000=기타`)와 일부 대표 중/소분류를 정적으로 반환한다. `getRegionCodes()` 는 19개 시도 + 코드만 반환한다. 시군구/읍면동은 upstream cascade XHR이 안정적이지 않아 정적 표에 포함하지 않으며 raw 코드를 그대로 전달하면 된다. 알 수 없는 값은 fail-open으로 통과한다.
5. **Same-name usage codes 보호**: `resolveUsageCode("아파트", "large")` 처럼 입력 이름이 다른 level 에만 존재하면, 같은 이름의 medium/small 코드를 잘못 리턴하지 않고 fail-open(원문 통과)한다.
## Throttling and call-budget rules
- 호출 간 최소 2초 (기본). 더 늘리려면 `--min-delay-ms 3000`.
- 기본 세션 budget 은 **10회**. 더 많은 조회가 필요하면 새 세션을 열거나 (`new CourtAuctionHttpClient`) `maxCallsPerSession` 을 명시적으로 늘린다.
- 차단(`data.ipcheck === false`)을 만나면 `BLOCKED` 에러를 즉시 throw 하고 멈춘다. 자동 retry 하지 않는다 (차단 연장 위험).
- 차단된 IP는 **약 1시간** 후 자연 복구된다. 그 사이에는 다른 IP/네트워크에서 작업하거나 사람이 브라우저로 사이트에 접속해서 차단 해제 화면을 거친다.
- **Workflow C 자유검색은 사이트 WAF 가 raw HTTP 호출을 더 엄격하게 차단**한다. `searchProperties()` 는 1차 direct HTTP에서 WAF성 HTTP 400을 만났을 때만 Playwright fallback 으로 재시도한다. 명시적 차단(`BLOCKED`/`ipcheck=false`)은 기본적으로 즉시 중단하며, 사용자가 위험을 이해하고 `fallbackOnBlocked:true` 를 준 경우에만 재시도한다. Playwright fallback 모듈(`rebrowser-playwright` 또는 `playwright-core`)이 없으면 첫 HTTP 400 실패가 그대로 throw 된다.
- searchProperties 는 같은 Playwright 클라이언트로 연속 호출하면 **10~15회 간격 호출에서 안정**하다. 그 이상 burst 호출이 필요하면 호출 사이 3~5초 sleep 을 두고 새 클라이언트를 열어라.
## Node.js example
```js
const {
searchSaleNotices,
getSaleNoticeDetail,
getCaseByCaseNumber,
getCourtCodes
} = require("court-auction-notice-search");
async function main() {
const courts = await getCourtCodes();
console.log(`법원사무소 ${courts.count}개 로드됨`);
const notices = await searchSaleNotices({
date: "2026-04-27",
courtCode: "B000210",
bidType: "date"
});
console.log(`서울중앙지방법원 매각공고 ${notices.count}건`);
if (notices.items.length > 0) {
const detail = await getSaleNoticeDetail(notices.items[0]);
for (const item of detail.items) {
console.log(
`${item.caseNumber} (${item.usage}) — 감정 ${item.appraisedPrice}원 / 최저 ${item.minimumSalePrice}원`
);
console.log(` 주소: ${item.address}`);
}
}
const caseInfo = await getCaseByCaseNumber({
courtCode: "B000210",
caseNumber: "2024타경100001"
});
if (caseInfo.found) {
console.log(`사건명: ${caseInfo.caseInfo.caseName}`);
console.log(`매각기일 횟수: ${caseInfo.schedule.length}`);
}
}
main().catch((error) => {
if (error.code === "BLOCKED") {
console.error("[BLOCKED] 사이트가 1시간 차단했습니다. 다른 IP에서 다시 시도하거나 1시간 뒤 재시도하세요.");
} else {
console.error(error);
}
process.exitCode = 1;
});
```
## CLI example
```bash
# 1. 법원사무소 코드표
court-auction-notice-search codes courts --pretty | head -40
# 2. 입찰구분 (정적 코드)
court-auction-notice-search codes bid-types --pretty
court-auction-notice-search codes usages --pretty
court-auction-notice-search codes regions --pretty
# 3. 매각공고 목록
court-auction-notice-search notices --date 2026-04 --court-code B000210 --bid-type date --pretty
# 4. 매각공고 상세 — list 응답의 row 의 raw 필드를 그대로 detail 호출에 사용한다.
# (CLI 단발 호출에서는 list -> detail 으로 결과를 파이프할 수 있도록 jq 등을 함께 사용)
# 5. 사건번호 직접 조회
court-auction-notice-search case --court-code B000210 --case-number "2024타경100001" --pretty
# 6. 자유 조건검색
court-auction-notice-search search --sido 서울특별시 --sigungu 11680 --usage-large 건물 --usage-medium 21200 \
--price-min 100000000 --price-max 500000000 --sale-from 2026-05-01 --sale-to 2026-05-20 --pretty
```
## Block / Error handling
- `error.code === "BLOCKED"``data.ipcheck === false`. 1시간 대기 후 다른 IP에서 재시도. 사용자에게 차단 사실과 대기 안내를 그대로 전달한다.
- `error.code === "BUDGET_EXCEEDED"` — 세션 budget 초과. 의도적인 안전장치다. 정말 필요하면 `--max-calls 20` 같이 늘리지만 차단 위험을 함께 안내한다.
- `error.code === "UPSTREAM_ERROR"` — 사이트가 일반적인 에러를 돌려준 경우. 세션 만료 또는 잘못된 jdbnCd 가 가장 흔한 원인. warmup 부터 다시.
- `error.code === "NETWORK_ERROR"` — 타임아웃/연결 실패.
- `error.code === "PLAYWRIGHT_UNAVAILABLE"` — Playwright fallback 을 명시적으로 쓰려는데 모듈이 깔려있지 않음. `npm i rebrowser-playwright` 또는 `npm i playwright-core` 로 해결.
## Done when
- 사용자에게 IP 차단 위험과 "참고용·실제 입찰 전 법원 원문 재확인" 고지를 했다.
- 매각공고를 펼쳐서 `caseNumber/usage/address/appraisedPrice/minimumSalePrice` 가 채워진 JSON을 돌려줬다.
- 사건번호로 직접 조회한 경우, `found:false` 일 때 사용자가 후속 조치를 알 수 있도록 안내했다.
- 차단 발생 시 자동 재시도하지 않고 즉시 멈췄다.
- 작업 후 호출 budget 이 남아있는지 사용자에게 알려서 추가 호출 여지를 명시했다.

View file

@ -1,101 +0,0 @@
---
name: daangn-cars-search
description: 당근중고차 공개 웹 데이터 표면으로 지역·가격 조건 기반 차량 검색과 상세 조회를 수행한다. 문의/구매 자동화는 제외한다.
license: MIT
metadata:
category: automotive
locale: ko-KR
phase: v1
---
# Daangn Cars Search
## What this skill does
당근중고차 공개 Remix `_data` JSON route를 사용해 차량 목록과 상세 정보를 읽기 전용으로 조회한다.
최종 사용자는 자연어로 요청해도 되고, 필요하면 아래의 Python helper를 직접 실행한다. 외부 패키지나 k-skill-proxy 없이 Python 표준 라이브러리만 사용한다.
## When to use
- "당근중고차 합정동 레이 찾아봐"
- "당근에서 천만원 이하 중고차 검색"
- "이 당근 중고차 URL 상세 봐줘"
## When not to use
- 당근 계정 로그인이 필요한 작업
- 채팅, 찜, 거래 제안, 문의, 지원, 예약, 계약, 구매처럼 상대방 또는 계정에 영향을 주는 작업
- CAPTCHA/봇 차단/로그인벽 우회가 필요한 작업
## Prerequisites
- 인터넷 연결
- Python 3.9+
- 이 저장소 루트에서 실행하거나, 스크립트 경로를 절대경로로 지정
## Data surfaces
- Region resolver: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
- Search `_data`: `/kr/cars/?in=<지역명>-<id>&onlyOnSale=1&_data=routes/kr.cars._index`
- Detail `_data`: `<car-url>?_data=routes%2Fkr.cars.%24car_post_id`
## Workflow
1. 사용자 요청에서 키워드, 지역명, 가격/거래 유형 같은 필터를 추출한다.
2. 지역명이 있으면 region resolver로 내부 region id를 찾는다.
3. 목록 검색은 category별 `_data` route를 호출한다.
4. 상세 URL이 주어지면 category별 detail route 또는 공개 HTML 메타를 조회한다.
5. 결과를 짧게 정리하되 source URL과 적용 지역을 보존한다.
## Commands
```bash
python3 daangn-cars-search/scripts/daangn_cars.py search "레이" --region "합정동" --limit 5
python3 daangn-cars-search/scripts/daangn_cars.py search --region "합정동" --price-max 10000000 --limit 5
python3 daangn-cars-search/scripts/daangn_cars.py detail "https://www.daangn.com/kr/cars/.../"
```
## Output fields
- title, price, price_text, region, status, driveDistance, carData, chatRoomCount, url
- detail: carPost 원문
## Region handling
지역 필터가 있으면 먼저 당근 지역 검색 API로 내부 지역 id를 해석한다.
```text
https://www.daangn.com/kr/api/v1/regions/keyword?keyword=합정동
→ 서울특별시 마포구 합정동, id=231
→ in=합정동-231
```
동일한 지명이 여러 지역에 있으면 다음 우선순위로 선택한다.
1. 사용자가 입력한 문자열이 `name`, `name1`, `name2`, `name3` 중 하나와 정확히 맞는 후보
2. 서울 `depth=3` 동 단위 후보
3. 첫 번째 후보
응답에는 항상 `effective_region` 또는 실제 적용된 지역명을 포함한다. 사용자의 의도와 다른 지역으로 보이면 결과를 단정하지 말고 후보 확인을 요청한다. IP/쿠키 기본 위치에 의존하지 않는다.
## Safety and scope
- 읽기 전용 검색/상세 조회만 수행한다.
- 로그인, 채팅, 찜, 거래 제안, 지원, 문의, 예약, 계약, 구매 자동화는 하지 않는다.
- 공개 웹 표면이 바뀌거나 빈 응답/봇 차단/로그인벽이 나오면 실패 모드로 보고하고 우회하지 않는다.
- 결과는 실시간 재고/공고 상태와 달라질 수 있으므로 source URL을 함께 제시한다.
## Failure modes
- 당근의 Remix route 이름이나 JSON shape가 변경되면 `_data` 조회가 실패할 수 있다.
- 지역명이 넓거나 중복되면 다른 행정동이 선택될 수 있다.
- 검색 결과가 0건이어도 사이트 정책/지역 기본값/필터 조합 때문일 수 있으므로 source URL을 보존한다.
- 상세 조회는 삭제/종료/비공개 전환된 글에서 실패할 수 있다.
## Done when
- 지역명이 있으면 지역 id를 해석하고 적용했다.
- 목록 조회 또는 상세 조회를 최소 1회 수행했다.
- 결과에 source URL과 effective region을 포함했다.
- 인증/거래성 액션은 수행하지 않았다.

View file

@ -1,72 +0,0 @@
#!/usr/bin/env python3
import argparse, json, re, sys, urllib.parse, urllib.request
from html import unescape
HEADERS = {"User-Agent":"Mozilla/5.0", "Accept":"application/json,text/html;q=0.9,*/*;q=0.8"}
def fetch_json(url):
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=25) as r:
return json.load(r)
def fetch_text(url):
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0", "Accept":"text/html"})
with urllib.request.urlopen(req, timeout=25) as r:
return r.read().decode('utf-8', 'ignore')
def won(v):
if v in (None, ''): return '-'
try: return f"{int(float(v)):,}"
except Exception: return str(v)
def resolve_region(region):
if not region: return None
url = 'https://www.daangn.com/kr/api/v1/regions/keyword?keyword=' + urllib.parse.quote(region)
data = fetch_json(url)
locs = data.get('locations') or []
if not locs: raise SystemExit(f'지역 후보 없음: {region}')
# Exact dong/name match first, then Seoul depth-3, then first candidate.
exact = [x for x in locs if region in (x.get('name'), x.get('name1'), x.get('name2'), x.get('name3'))]
seoul = [x for x in locs if x.get('name1') == '서울특별시' and x.get('depth') == 3]
sel = (exact or seoul or locs)[0]
return sel
def region_param(sel):
return urllib.parse.quote(f"{sel['name']}-{sel['id']}")
def absolute(href):
if not href: return ''
if href.startswith('http'): return href
return 'https://www.daangn.com' + href
def print_json(obj):
print(json.dumps(obj, ensure_ascii=False, indent=2))
def cmd_search(args):
sel=resolve_region(args.region) if args.region else None
params=[]
if sel: params.append(('in', f"{sel['name']}-{sel['id']}"))
if args.only_on_sale: params.append(('onlyOnSale','1'))
if args.price_max: params.append(('priceMax', str(args.price_max)))
if args.price_min: params.append(('priceMin', str(args.price_min)))
params.append(('_data','routes/kr.cars._index'))
url='https://www.daangn.com/kr/cars/?'+urllib.parse.urlencode(params)
data=fetch_json(url); arr=((data.get('carAllPage') or {}).get('carPosts') or [])
if args.keyword:
arr=[a for a in arr if args.keyword.lower() in (a.get('title') or '').lower()]
arr=arr[:args.limit]
items=[{'title':a.get('title'),'price':a.get('price'),'price_text':won(a.get('price')),'region':(a.get('region') or {}).get('name'),
'status':a.get('status'),'driveDistance':a.get('driveDistance'),'carData':a.get('carData'),
'chatRoomCount':a.get('chatRoomCount'),'url':absolute(a.get('href'))} for a in arr]
print_json({'source':url,'effective_region':data.get('searchRegion') or sel,'count':len(items),'items':items})
def cmd_detail(args):
u=args.url.rstrip('/')+'/?_data=routes%2Fkr.cars.%24car_post_id'
data=fetch_json(u); print_json({'source':u,'carPost':data.get('carPost') or data})
p=argparse.ArgumentParser(description='Daangn cars read-only search/detail')
sub=p.add_subparsers(dest='cmd', required=True)
s=sub.add_parser('search'); s.add_argument('keyword', nargs='?'); s.add_argument('--region'); s.add_argument('--price-min',type=int); s.add_argument('--price-max',type=int); s.add_argument('--only-on-sale',action='store_true',default=True); s.add_argument('--limit',type=int,default=10); s.set_defaults(func=cmd_search)
d=sub.add_parser('detail'); d.add_argument('url'); d.set_defaults(func=cmd_detail)
args=p.parse_args(); args.func(args)

View file

@ -1,100 +0,0 @@
---
name: daangn-jobs-search
description: 당근알바 공개 웹 데이터 표면으로 키워드·지역 기반 알바 공고 검색과 상세 조회를 수행한다. 지원/채팅 자동화는 제외한다.
license: MIT
metadata:
category: jobs
locale: ko-KR
phase: v1
---
# Daangn Jobs Search
## What this skill does
당근알바 공개 Remix `_data` JSON route로 채용/알바 공고 목록과 상세 정보를 읽기 전용으로 조회한다.
최종 사용자는 자연어로 요청해도 되고, 필요하면 아래의 Python helper를 직접 실행한다. 외부 패키지나 k-skill-proxy 없이 Python 표준 라이브러리만 사용한다.
## When to use
- "당근알바 합정동 카페 찾아봐"
- "홍대 근처 주말 알바 검색"
- "이 당근알바 공고 상세 봐줘"
## When not to use
- 당근 계정 로그인이 필요한 작업
- 채팅, 찜, 거래 제안, 문의, 지원, 예약, 계약, 구매처럼 상대방 또는 계정에 영향을 주는 작업
- CAPTCHA/봇 차단/로그인벽 우회가 필요한 작업
## Prerequisites
- 인터넷 연결
- Python 3.9+
- 이 저장소 루트에서 실행하거나, 스크립트 경로를 절대경로로 지정
## Data surfaces
- Region resolver: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
- Search `_data`: `/kr/jobs/?in=<지역명>-<id>&search=<keyword>&_data=routes/kr.jobs._index`
- Detail fallback: `<job-url>` redirects to `jobs.daangn.com/job-posts/<id>` and exposes public HTML title/meta/JSON-LD. The helper first tries the legacy `_data` route and falls back to HTML meta when that route returns an empty response.
## Workflow
1. 사용자 요청에서 키워드, 지역명, 가격/거래 유형 같은 필터를 추출한다.
2. 지역명이 있으면 region resolver로 내부 region id를 찾는다.
3. 목록 검색은 category별 `_data` route를 호출한다.
4. 상세 URL이 주어지면 category별 detail route 또는 공개 HTML 메타를 조회한다.
5. 결과를 짧게 정리하되 source URL과 적용 지역을 보존한다.
## Commands
```bash
python3 daangn-jobs-search/scripts/daangn_jobs.py search "카페" --region "합정동" --limit 5
python3 daangn-jobs-search/scripts/daangn_jobs.py detail "https://www.daangn.com/kr/jobs/.../"
```
## Output fields
- title, company, region, address, salary, salaryType, workDays, workTimeStart, workTimeEnd, closed, url
- detail: `jobPost` 원문 if the `_data` route is available; otherwise public page `title`, `meta`, and `json_ld`
## Region handling
지역 필터가 있으면 먼저 당근 지역 검색 API로 내부 지역 id를 해석한다.
```text
https://www.daangn.com/kr/api/v1/regions/keyword?keyword=합정동
→ 서울특별시 마포구 합정동, id=231
→ in=합정동-231
```
동일한 지명이 여러 지역에 있으면 다음 우선순위로 선택한다.
1. 사용자가 입력한 문자열이 `name`, `name1`, `name2`, `name3` 중 하나와 정확히 맞는 후보
2. 서울 `depth=3` 동 단위 후보
3. 첫 번째 후보
응답에는 항상 `effective_region` 또는 실제 적용된 지역명을 포함한다. 사용자의 의도와 다른 지역으로 보이면 결과를 단정하지 말고 후보 확인을 요청한다. IP/쿠키 기본 위치에 의존하지 않는다.
## Safety and scope
- 읽기 전용 검색/상세 조회만 수행한다.
- 로그인, 채팅, 찜, 거래 제안, 지원, 문의, 예약, 계약, 구매 자동화는 하지 않는다.
- 공개 웹 표면이 바뀌거나 빈 응답/봇 차단/로그인벽이 나오면 실패 모드로 보고하고 우회하지 않는다.
- 결과는 실시간 재고/공고 상태와 달라질 수 있으므로 source URL을 함께 제시한다.
## Failure modes
- 당근의 Remix route 이름이나 JSON shape가 변경되면 `_data` 조회가 실패할 수 있다.
- 지역명이 넓거나 중복되면 다른 행정동이 선택될 수 있다.
- 검색 결과가 0건이어도 사이트 정책/지역 기본값/필터 조합 때문일 수 있으므로 source URL을 보존한다.
- 상세 조회는 삭제/종료/비공개 전환된 글에서 실패할 수 있다.
## Done when
- 지역명이 있으면 지역 id를 해석하고 적용했다.
- 목록 조회 또는 상세 조회를 최소 1회 수행했다.
- 결과에 source URL과 effective region을 포함했다.
- 인증/거래성 액션은 수행하지 않았다.

View file

@ -1,98 +0,0 @@
#!/usr/bin/env python3
import argparse, json, re, sys, urllib.parse, urllib.request
from html import unescape
HEADERS = {"User-Agent":"Mozilla/5.0", "Accept":"application/json,text/html;q=0.9,*/*;q=0.8"}
def fetch_json(url):
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=25) as r:
body = r.read()
if not body:
raise ValueError(f'빈 JSON 응답: {url}')
return json.loads(body)
def fetch_text(url):
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0", "Accept":"text/html"})
with urllib.request.urlopen(req, timeout=25) as r:
return r.read().decode('utf-8', 'ignore')
def won(v):
if v in (None, ''): return '-'
try: return f"{int(float(v)):,}"
except Exception: return str(v)
def resolve_region(region):
if not region: return None
url = 'https://www.daangn.com/kr/api/v1/regions/keyword?keyword=' + urllib.parse.quote(region)
data = fetch_json(url)
locs = data.get('locations') or []
if not locs: raise SystemExit(f'지역 후보 없음: {region}')
# Exact dong/name match first, then Seoul depth-3, then first candidate.
exact = [x for x in locs if region in (x.get('name'), x.get('name1'), x.get('name2'), x.get('name3'))]
seoul = [x for x in locs if x.get('name1') == '서울특별시' and x.get('depth') == 3]
sel = (exact or seoul or locs)[0]
return sel
def region_param(sel):
return urllib.parse.quote(f"{sel['name']}-{sel['id']}")
def absolute(href):
if not href: return ''
if href.startswith('http'): return href
return 'https://www.daangn.com' + href
def print_json(obj):
print(json.dumps(obj, ensure_ascii=False, indent=2))
def parse_html_detail(url):
html = fetch_text(url)
title = re.search(r'<title>(.*?)</title>', html, re.S)
meta = {}
for m in re.finditer(r'<meta[^>]+(?:property|name)=["\']([^"\']+)["\'][^>]+content=["\']([^"\']*)["\']', html):
key, value = m.group(1), unescape(m.group(2)).strip()
if key in ('description', 'og:title', 'og:description', 'og:image'):
meta[key] = value
json_ld = []
for m in re.finditer(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', html, re.S):
try:
json_ld.append(json.loads(unescape(m.group(1))))
except Exception:
pass
return {
'source': url,
'title': unescape(title.group(1)).strip() if title else meta.get('og:title'),
'meta': meta,
'json_ld': json_ld[:3],
}
def cmd_search(args):
sel=resolve_region(args.region) if args.region else None
params=[]
if sel: params.append(('in', f"{sel['name']}-{sel['id']}"))
if args.keyword: params.append(('search', args.keyword))
params.append(('_data','routes/kr.jobs._index'))
url='https://www.daangn.com/kr/jobs/?'+urllib.parse.urlencode(params)
data=fetch_json(url); arr=((data.get('jobsAllPage') or {}).get('jobPosts') or [])[:args.limit]
items=[{'title':a.get('title'),'company':a.get('workplaceCompanyName'),'region':a.get('workplaceRegion'),
'address':a.get('workplaceRoadNameAddress'),'salary':a.get('salary'),'salaryType':a.get('salaryType'),
'workDays':a.get('workDays'),'workTimeStart':a.get('workTimeStart'),'workTimeEnd':a.get('workTimeEnd'),
'closed':a.get('closed'),'url':absolute(a.get('href') or a.get('jobsWebDetailUrl'))} for a in arr]
print_json({'source':url,'effective_region':data.get('searchRegion') or sel,'count':len(items),'items':items})
def cmd_detail(args):
u=args.url.rstrip('/')+'/?_data=routes%2Fkr.jobs.%24job_post_id'
try:
data=fetch_json(u)
print_json({'source':u,'jobPost':data.get('jobPost') or data})
except Exception:
detail = parse_html_detail(args.url)
detail['data_source_attempted'] = u
print_json(detail)
p=argparse.ArgumentParser(description='Daangn jobs read-only search/detail')
sub=p.add_subparsers(dest='cmd', required=True)
s=sub.add_parser('search'); s.add_argument('keyword', nargs='?'); s.add_argument('--region'); s.add_argument('--limit',type=int,default=10); s.set_defaults(func=cmd_search)
d=sub.add_parser('detail'); d.add_argument('url'); d.set_defaults(func=cmd_detail)
args=p.parse_args(); args.func(args)

View file

@ -1,101 +0,0 @@
---
name: daangn-realty-search
description: 당근부동산 공개 웹 데이터 표면으로 지역 기반 부동산 매물 검색과 상세 확인을 수행한다. 문의/예약/계약 자동화는 제외한다.
license: MIT
metadata:
category: real-estate
locale: ko-KR
phase: v1
---
# Daangn Realty Search
## What this skill does
당근부동산 목록의 공개 Remix `_data` JSON과 상세 페이지의 JSON-LD/HTML 메타를 읽어 매물 후보를 정리한다.
최종 사용자는 자연어로 요청해도 되고, 필요하면 아래의 Python helper를 직접 실행한다. 외부 패키지나 k-skill-proxy 없이 Python 표준 라이브러리만 사용한다.
## When to use
- "당근부동산 합정동 전세 찾아봐"
- "마포구 월세 매물 봐줘"
- "이 당근부동산 URL 상세 요약해줘"
## When not to use
- 당근 계정 로그인이 필요한 작업
- 채팅, 찜, 거래 제안, 문의, 지원, 예약, 계약, 구매처럼 상대방 또는 계정에 영향을 주는 작업
- CAPTCHA/봇 차단/로그인벽 우회가 필요한 작업
## Prerequisites
- 인터넷 연결
- Python 3.9+
- 이 저장소 루트에서 실행하거나, 스크립트 경로를 절대경로로 지정
## Data surfaces
- Region resolver: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
- Search `_data`: `/kr/realty/?in=<지역명>-<id>&_data=routes/kr.realty._index`
- Detail: `https://realty.daangn.com/articles/<id>``application/ld+json``<title>`
## Workflow
1. 사용자 요청에서 키워드, 지역명, 가격/거래 유형 같은 필터를 추출한다.
2. 지역명이 있으면 region resolver로 내부 region id를 찾는다.
3. 목록 검색은 category별 `_data` route를 호출한다.
4. 상세 URL이 주어지면 category별 detail route 또는 공개 HTML 메타를 조회한다.
5. 결과를 짧게 정리하되 source URL과 적용 지역을 보존한다.
## Commands
```bash
python3 daangn-realty-search/scripts/daangn_realty.py search --region "합정동" --limit 5
python3 daangn-realty-search/scripts/daangn_realty.py search --region "합정동" --sales-type "APARTMENT" --trade-type "MONTHLY_RENT"
python3 daangn-realty-search/scripts/daangn_realty.py detail "https://realty.daangn.com/articles/..."
```
## Output fields
- title, salesType, trade, area, areaPyeong, totalManageCost, url
- detail: JSON-LD, page title
## Region handling
지역 필터가 있으면 먼저 당근 지역 검색 API로 내부 지역 id를 해석한다.
```text
https://www.daangn.com/kr/api/v1/regions/keyword?keyword=합정동
→ 서울특별시 마포구 합정동, id=231
→ in=합정동-231
```
동일한 지명이 여러 지역에 있으면 다음 우선순위로 선택한다.
1. 사용자가 입력한 문자열이 `name`, `name1`, `name2`, `name3` 중 하나와 정확히 맞는 후보
2. 서울 `depth=3` 동 단위 후보
3. 첫 번째 후보
응답에는 항상 `effective_region` 또는 실제 적용된 지역명을 포함한다. 사용자의 의도와 다른 지역으로 보이면 결과를 단정하지 말고 후보 확인을 요청한다. IP/쿠키 기본 위치에 의존하지 않는다.
## Safety and scope
- 읽기 전용 검색/상세 조회만 수행한다.
- 로그인, 채팅, 찜, 거래 제안, 지원, 문의, 예약, 계약, 구매 자동화는 하지 않는다.
- 공개 웹 표면이 바뀌거나 빈 응답/봇 차단/로그인벽이 나오면 실패 모드로 보고하고 우회하지 않는다.
- 결과는 실시간 재고/공고 상태와 달라질 수 있으므로 source URL을 함께 제시한다.
## Failure modes
- 당근의 Remix route 이름이나 JSON shape가 변경되면 `_data` 조회가 실패할 수 있다.
- 지역명이 넓거나 중복되면 다른 행정동이 선택될 수 있다.
- 검색 결과가 0건이어도 사이트 정책/지역 기본값/필터 조합 때문일 수 있으므로 source URL을 보존한다.
- 상세 조회는 삭제/종료/비공개 전환된 글에서 실패할 수 있다.
## Done when
- 지역명이 있으면 지역 id를 해석하고 적용했다.
- 목록 조회 또는 상세 조회를 최소 1회 수행했다.
- 결과에 source URL과 effective region을 포함했다.
- 인증/거래성 액션은 수행하지 않았다.

View file

@ -1,85 +0,0 @@
#!/usr/bin/env python3
import argparse, json, re, sys, urllib.parse, urllib.request
from html import unescape
HEADERS = {"User-Agent":"Mozilla/5.0", "Accept":"application/json,text/html;q=0.9,*/*;q=0.8"}
def fetch_json(url):
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=25) as r:
return json.load(r)
def fetch_text(url):
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0", "Accept":"text/html"})
with urllib.request.urlopen(req, timeout=25) as r:
return r.read().decode('utf-8', 'ignore')
def won(v):
if v in (None, ''): return '-'
try: return f"{int(float(v)):,}"
except Exception: return str(v)
def resolve_region(region):
if not region: return None
url = 'https://www.daangn.com/kr/api/v1/regions/keyword?keyword=' + urllib.parse.quote(region)
data = fetch_json(url)
locs = data.get('locations') or []
if not locs: raise SystemExit(f'지역 후보 없음: {region}')
# Exact dong/name match first, then Seoul depth-3, then first candidate.
exact = [x for x in locs if region in (x.get('name'), x.get('name1'), x.get('name2'), x.get('name3'))]
seoul = [x for x in locs if x.get('name1') == '서울특별시' and x.get('depth') == 3]
sel = (exact or seoul or locs)[0]
return sel
def region_param(sel):
return urllib.parse.quote(f"{sel['name']}-{sel['id']}")
def absolute(href):
if not href: return ''
if href.startswith('http'): return href
return 'https://www.daangn.com' + href
def print_json(obj):
print(json.dumps(obj, ensure_ascii=False, indent=2))
def norm_trade(t):
if not t: return None
return t
def cmd_search(args):
sel = resolve_region(args.region) if args.region else None
params=[]
if sel: params.append(('in', f"{sel['name']}-{sel['id']}"))
if args.sales_type: params.append(('salesType', args.sales_type))
if args.trade_type: params.append(('tradeType', args.trade_type))
if args.only_verified: params.append(('onlyVerified','true'))
params.append(('_data','routes/kr.realty._index'))
url='https://www.daangn.com/kr/realty/?'+urllib.parse.urlencode(params)
data=fetch_json(url)
arr=((data.get('realtyPosts') or {}).get('realtyPosts') or [])
if args.keyword:
arr=[a for a in arr if args.keyword.lower() in json.dumps(a, ensure_ascii=False).lower()]
arr=arr[:args.limit]
items=[]
for a in arr:
tr=(a.get('trades') or [{}])[0]
items.append({'title':a.get('title'),'salesType':a.get('salesType') or a.get('salesTypeV2'),'trade':tr,
'area':a.get('area'),'areaPyeong':a.get('areaPyeong'),'totalManageCost':a.get('totalManageCost'),
'url':a.get('webUrl') or absolute(a.get('href'))})
print_json({'source':url,'effective_region':data.get('searchRegion') or sel,'count':len(items),'items':items})
def cmd_detail(args):
html=fetch_text(args.url)
lds=[]
for m in re.finditer(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', html, re.S):
try: lds.append(json.loads(unescape(m.group(1))))
except Exception: pass
title=re.search(r'<title>(.*?)</title>', html, re.S)
print_json({'source':args.url,'title':unescape(title.group(1)).strip() if title else None,'json_ld':lds[:3]})
p=argparse.ArgumentParser(description='Daangn realty read-only search/detail')
sub=p.add_subparsers(dest='cmd', required=True)
s=sub.add_parser('search'); s.add_argument('--region'); s.add_argument('--keyword'); s.add_argument('--sales-type'); s.add_argument('--trade-type'); s.add_argument('--only-verified',action='store_true'); s.add_argument('--limit',type=int,default=10); s.set_defaults(func=cmd_search)
d=sub.add_parser('detail'); d.add_argument('url'); d.set_defaults(func=cmd_detail)
args=p.parse_args(); args.func(args)

View file

@ -1,100 +0,0 @@
---
name: daangn-used-goods-search
description: 당근 중고거래 공개 웹 데이터 표면으로 키워드·지역 기반 매물 검색과 상세 조회를 수행한다. 로그인/채팅/찜/구매 자동화는 제외한다.
license: MIT
metadata:
category: marketplace
locale: ko-KR
phase: v1
---
# Daangn Used-Goods Search
## What this skill does
당근 중고거래 공개 Remix `_data` JSON route를 사용해 매물 목록과 상세 정보를 읽기 전용으로 조회한다.
최종 사용자는 자연어로 요청해도 되고, 필요하면 아래의 Python helper를 직접 실행한다. 외부 패키지나 k-skill-proxy 없이 Python 표준 라이브러리만 사용한다.
## When to use
- "당근에서 맥북 찾아봐"
- "합정동 아이폰 매물 검색"
- "이 당근 중고거래 URL 상세 봐줘"
## When not to use
- 당근 계정 로그인이 필요한 작업
- 채팅, 찜, 거래 제안, 문의, 지원, 예약, 계약, 구매처럼 상대방 또는 계정에 영향을 주는 작업
- CAPTCHA/봇 차단/로그인벽 우회가 필요한 작업
## Prerequisites
- 인터넷 연결
- Python 3.9+
- 이 저장소 루트에서 실행하거나, 스크립트 경로를 절대경로로 지정
## Data surfaces
- Region resolver: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
- Search `_data`: `/kr/buy-sell/all/?in=<지역명>-<id>&search=<keyword>&only_on_sale=true&_data=routes/kr.buy-sell._index`
- Detail `_data`: `<listing-url>?_data=routes%2Fkr.buy-sell.%24buy_sell_id`
## Workflow
1. 사용자 요청에서 키워드, 지역명, 가격/거래 유형 같은 필터를 추출한다.
2. 지역명이 있으면 region resolver로 내부 region id를 찾는다.
3. 목록 검색은 category별 `_data` route를 호출한다.
4. 상세 URL이 주어지면 category별 detail route 또는 공개 HTML 메타를 조회한다.
5. 결과를 짧게 정리하되 source URL과 적용 지역을 보존한다.
## Commands
```bash
python3 daangn-used-goods-search/scripts/daangn_used_goods.py search "맥북" --region "합정동" --limit 5
python3 daangn-used-goods-search/scripts/daangn_used_goods.py detail "https://www.daangn.com/kr/buy-sell/.../"
```
## Output fields
- title, price, price_text, status, region, url
- detail: product 원문, view/chat/count류 필드가 있으면 함께 확인
## Region handling
지역 필터가 있으면 먼저 당근 지역 검색 API로 내부 지역 id를 해석한다.
```text
https://www.daangn.com/kr/api/v1/regions/keyword?keyword=합정동
→ 서울특별시 마포구 합정동, id=231
→ in=합정동-231
```
동일한 지명이 여러 지역에 있으면 다음 우선순위로 선택한다.
1. 사용자가 입력한 문자열이 `name`, `name1`, `name2`, `name3` 중 하나와 정확히 맞는 후보
2. 서울 `depth=3` 동 단위 후보
3. 첫 번째 후보
응답에는 항상 `effective_region` 또는 실제 적용된 지역명을 포함한다. 사용자의 의도와 다른 지역으로 보이면 결과를 단정하지 말고 후보 확인을 요청한다. IP/쿠키 기본 위치에 의존하지 않는다.
## Safety and scope
- 읽기 전용 검색/상세 조회만 수행한다.
- 로그인, 채팅, 찜, 거래 제안, 지원, 문의, 예약, 계약, 구매 자동화는 하지 않는다.
- 공개 웹 표면이 바뀌거나 빈 응답/봇 차단/로그인벽이 나오면 실패 모드로 보고하고 우회하지 않는다.
- 결과는 실시간 재고/공고 상태와 달라질 수 있으므로 source URL을 함께 제시한다.
## Failure modes
- 당근의 Remix route 이름이나 JSON shape가 변경되면 `_data` 조회가 실패할 수 있다.
- 지역명이 넓거나 중복되면 다른 행정동이 선택될 수 있다.
- 검색 결과가 0건이어도 사이트 정책/지역 기본값/필터 조합 때문일 수 있으므로 source URL을 보존한다.
- 상세 조회는 삭제/종료/비공개 전환된 글에서 실패할 수 있다.
## Done when
- 지역명이 있으면 지역 id를 해석하고 적용했다.
- 목록 조회 또는 상세 조회를 최소 1회 수행했다.
- 결과에 source URL과 effective region을 포함했다.
- 인증/거래성 액션은 수행하지 않았다.

View file

@ -1,80 +0,0 @@
#!/usr/bin/env python3
import argparse, json, re, sys, urllib.parse, urllib.request
from html import unescape
HEADERS = {"User-Agent":"Mozilla/5.0", "Accept":"application/json,text/html;q=0.9,*/*;q=0.8"}
def fetch_json(url):
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=25) as r:
return json.load(r)
def fetch_text(url):
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0", "Accept":"text/html"})
with urllib.request.urlopen(req, timeout=25) as r:
return r.read().decode('utf-8', 'ignore')
def won(v):
if v in (None, ''): return '-'
try: return f"{int(float(v)):,}"
except Exception: return str(v)
def resolve_region(region):
if not region: return None
url = 'https://www.daangn.com/kr/api/v1/regions/keyword?keyword=' + urllib.parse.quote(region)
data = fetch_json(url)
locs = data.get('locations') or []
if not locs: raise SystemExit(f'지역 후보 없음: {region}')
# Exact dong/name match first, then Seoul depth-3, then first candidate.
exact = [x for x in locs if region in (x.get('name'), x.get('name1'), x.get('name2'), x.get('name3'))]
seoul = [x for x in locs if x.get('name1') == '서울특별시' and x.get('depth') == 3]
sel = (exact or seoul or locs)[0]
return sel
def region_param(sel):
return urllib.parse.quote(f"{sel['name']}-{sel['id']}")
def absolute(href):
if not href: return ''
if href.startswith('http'): return href
return 'https://www.daangn.com' + href
def print_json(obj):
print(json.dumps(obj, ensure_ascii=False, indent=2))
def cmd_search(args):
params = []
effective = None
path = '/kr/buy-sell/'
if args.region:
effective = resolve_region(args.region)
path = '/kr/buy-sell/all/'
params.append(('in', f"{effective['name']}-{effective['id']}"))
params.append(('search', args.keyword))
if args.only_on_sale: params.append(('only_on_sale','true'))
params.append(('_data','routes/kr.buy-sell._index'))
url = 'https://www.daangn.com' + path + '?' + urllib.parse.urlencode(params)
data = fetch_json(url)
arr = (((data.get('allPage') or {}).get('fleamarketArticles')) or [])[:args.limit]
print_json({
'source': url,
'effective_region': effective or data.get('region'),
'count': len(arr),
'items': [{
'title': a.get('title'), 'price': a.get('price'), 'price_text': won(a.get('price')),
'region': (a.get('region') or {}).get('name'), 'status': a.get('status'),
'url': absolute(a.get('href') or a.get('webUrl')),
} for a in arr]
})
def cmd_detail(args):
u = args.url.rstrip('/') + '/?_data=routes%2Fkr.buy-sell.%24buy_sell_id'
data = fetch_json(u); p = data.get('product') or data.get('article') or data
print_json({'source': u, 'product': p})
p=argparse.ArgumentParser(description='Daangn used-goods read-only search/detail')
sub=p.add_subparsers(dest='cmd', required=True)
s=sub.add_parser('search'); s.add_argument('keyword'); s.add_argument('--region'); s.add_argument('--limit',type=int,default=10); s.add_argument('--only-on-sale',action='store_true',default=True); s.set_defaults(func=cmd_search)
d=sub.add_parser('detail'); d.add_argument('url'); d.set_defaults(func=cmd_detail)
args=p.parse_args(); args.func(args)

View file

@ -1,148 +0,0 @@
---
name: daishin-report-search
description: 대신증권 리포트 GitHub Pages 미러에서 최신 HTML 리포트 목록과 원문/설명 페이지를 조회한다.
license: MIT
metadata:
category: finance
locale: ko-KR
phase: v1
---
# Daishin Report Search
## What this skill does
대신증권 리포트 HTML 미러(`jay-jo-0/github_pages_repo`)에서 최신 리포트 목록을 찾고, 특정 리포트의 원문 텍스트·제목·헤딩·Rating/Target 표·원문 링크를 에이전트가 재사용하기 쉬운 JSON으로 반환한다.
이 스킬은 투자 조언, 매매 자동화, 추천을 하지 않는다. 공개 HTML 리포트를 읽어 요약 가능한 자료로 정리하는 조회 전용 스킬이다.
## When to use
- "대신증권 최신 리포트 보여줘"
- "대신증권 반도체 리포트 찾아줘"
- "20260511082352 리포트 원문과 설명 페이지를 읽어줘"
- "대신증권 리포트 목록을 에이전트가 쓰기 좋은 JSON으로 줘"
## Prerequisites
- 인터넷 연결
- Node.js 18+
- 이 저장소의 `daishin-report-search` npm package 또는 동일 로직
## Public access path discovered
### Primary source: GitHub recursive tree API
- list endpoint: `https://api.github.com/repos/jay-jo-0/github_pages_repo/git/trees/main?recursive=1`
- selected paths: repository-root files matching `YYYYMMDDHHMMSS.html`
- optional companion paths: `YYYYMMDDHHMMSS_explain.html`
- detail raw HTML: `https://raw.githubusercontent.com/Jay-jo-0/github_pages_repo/main/<path>`
- browser detail URL: `https://jay-jo-0.github.io/github_pages_repo/<path>`
- reason selected: the sample GitHub Pages URL maps directly to a public GitHub repository. The recursive tree API exposes all timestamped HTML filenames without relying on a brittle directory listing screen scrape. Raw GitHub URLs provide stable unauthenticated detail fetches.
### Fallback source: GitHub contents API for an exact file
- exact-file endpoint: `https://api.github.com/repos/jay-jo-0/github_pages_repo/contents/<path>?ref=main`
- used automatically for a known timestamp when the raw detail URL is unavailable; it also provides GitHub content metadata for manual diagnostics.
No `k-skill-proxy` route is used because the upstream is public and does not require an API key.
## Workflow
### 1. List latest reports
```js
const { listReports } = require("daishin-report-search")
const result = await listReports({
limit: 10,
query: "반도체", // optional; matches title/headings/detail text
maxInspect: 100, // optional query crawl budget among newest pages
githubToken: process.env.GITHUB_TOKEN // optional; raises GitHub API limits when caller has one
})
console.log(result.items)
```
CLI:
```bash
node packages/daishin-report-search/src/cli.js --limit 10
node packages/daishin-report-search/src/cli.js 반도체 --limit 5 --max-inspect 100
```
Return each item with:
- `id` (`YYYYMMDDHHMMSS`)
- `date`, `time`, `timestamp` (filename-derived KST timestamp)
- `title`
- `headings`
- `excerpt`
- `ratingTargets` when a Rating/Target table is present
- `pageUrl`, `rawUrl`, `apiUrl`
- `hasExplain`, `explainUrl` when a companion explanation page exists
### 2. Fetch one report
```js
const { fetchReport } = require("daishin-report-search")
const report = await fetchReport("20260511082352", {
includeExplain: true
})
console.log(report.title)
console.log(report.text)
console.log(report.explain?.text)
```
CLI:
```bash
node packages/daishin-report-search/src/cli.js --id 20260511082352 --include-explain
```
### 3. Summarize conservatively
When answering a user, show:
```text
- 제목: ...
게시 추정 시각: 2026-05-11 08:23:52 KST (파일명 기준)
주요 헤딩: ...
Rating/Target: ... (있는 경우)
원문: https://jay-jo-0.github.io/github_pages_repo/...
설명 페이지: ... (있는 경우)
```
Always state that the timestamp is filename-derived and that report contents can change in the public mirror.
## Fallback order
1. GitHub recursive tree API → filter timestamped root HTML files → sort newest filename first → fetch raw detail HTML for selected/latest candidates.
2. If a query is present, inspect newer candidates up to `maxInspect` until enough matches are found or the budget is exhausted; return a warning if the budget is exhausted.
3. For a known id, fetch raw detail directly. If explanation is requested, fetch `<id>_explain.html`; if absent, return the original report plus a warning.
4. If the tree endpoint is truncated, blocked, rate-limited, or changed, report that as a source warning/failure instead of guessing hidden pages.
5. For a known id, if the raw detail URL fails, fall back to the GitHub contents API for that exact file path. Explanation pages use the same exact-file fallback but remain optional and return a warning if unavailable.
6. If the caller has authenticated GitHub access, pass `githubToken` / `githubHeaders` in library calls or set `DAISHIN_GITHUB_TOKEN` / `GITHUB_TOKEN` for the CLI; these credentials are scoped to `api.github.com` requests and are not sent to raw detail URLs. Do not require or proxy a token by default.
## Done when
- Latest report rows or a specific report are returned with direct source URLs.
- Query and limit were applied or explicitly left broad.
- Explanation pages were included only when requested or when listing metadata shows they exist.
- Empty results and upstream warnings are disclosed.
## Failure modes
- GitHub unauthenticated API rate limits can return 403/429; latest/search returns empty `items` plus `source.error.kind = "rate_limit"` and rate-limit reset metadata when GitHub exposes it. Retry later or use caller-supplied authenticated GitHub access if appropriate.
- The repository path or branch can change; then tree/raw URLs will fail.
- The tree response could become truncated; in that case the latest-list completeness is not guaranteed.
- HTML structure can change; title/headings/table extraction may be partial, but URLs and raw text fallback should still be returned when available.
- Some pages may not be authored by Daishin even though they are in the issue-scoped public mirror. Do not infer provenance beyond page title/content.
## Notes
- Read-only lookup only; no login, trading, order placement, recommendation, or investment advice.
- Do not scrape private Daishin services or bypass CAPTCHA/login walls.
- No secrets or API keys are required. Optional GitHub tokens are caller-owned, used only when explicitly supplied via options or environment, and scoped to GitHub API hosts.

View file

@ -62,9 +62,7 @@ metadata:
- product search summary: `https://www.daisomall.co.kr/ssn/search/Search`
- product search list: `https://www.daisomall.co.kr/ssn/search/SearchGoods`
- product summary list: `https://www.daisomall.co.kr/ssn/search/GoodsMummResult`
- auth (비로그인 JWT 발급): `https://www.daisomall.co.kr/api/auth/request`
- store pickup stock: `https://www.daisomall.co.kr/api/pd/pdh/selStrPkupStck` ← **인증 필요**
- pickup eligibility fallback: `https://www.daisomall.co.kr/api/ms/msg/selPkupStr`
- store pickup stock: `https://www.daisomall.co.kr/api/pd/pdh/selStrPkupStck`
- optional online stock cross-check: `https://www.daisomall.co.kr/api/pdo/selOnlStck`
## Workflow
@ -108,17 +106,7 @@ console.log(productResult.items)
### 3. Check the store pickup stock
`selStrPkupStck``Authorization` 헤더 없이 호출하면 **403**을 반환한다.
로그인 없이 `/api/auth/request`로 비로그인 JWT를 발급받아 AES-CBC로 암호화한 뒤 Bearer 헤더로 전달한다.
**Bearer 토큰 생성 방법:**
1. `GET /api/auth/request` → 응답 바디: JWT 평문, 응답 헤더 `x-dm-uid` 보존 (유효 30초)
2. 랜덤 16바이트 IV 생성 후 JWT를 AES-128-CBC / PKCS7 / 키 `"PRE_AUTH_ENC_KEY"`로 암호화
3. `bearer = base64(IV) + base64(암호문)` 으로 조합 후 `Authorization: Bearer <bearer>`, `X-DM-UID: <uid>` 헤더로 전달
바디는 `{pdNo, strCd}` 쌍 배열로 여러 매장을 한 번에 조회할 수 있다.
응답의 `stck` 필드가 `"0"` 또는 빈 값이면 재고 없음.
공식 매장 픽업 재고 API로 해당 매장의 재고를 확인한다.
```js
const { getStorePickupStock } = require("daiso-product-search")
@ -169,14 +157,9 @@ console.log(result.pickupStock)
- 상품명이 너무 넓으면 다른 용량/호수 후보가 많이 섞일 수 있다.
- 공식 재고는 시점 차이로 실제 방문 시 수량이 달라질 수 있다.
- 현재 확인된 공식 표면은 **매장 내 aisle/진열 위치**를 직접 주지 않을 수 있다.
- `selStrPkupStck` 403 → `/api/auth/request` 재호출 후 Bearer를 새로 빌드해 재시도한다.
- Bearer 재시도 후에도 401/403이면 재고 수량은 `retrievalStatus: "blocked"` 로 표시하고, `selPkupStr` 기반 `pickupEligibility`(픽업 가능 여부)만 보조 정보로 제공한다.
## Notes
- 조회형 스킬이다.
- 공식 표면 우선 원칙을 유지한다.
- 공식 표면이 위치를 주지 않으면 억지 추정을 하지 않는다.
- 인증 키(`PRE_AUTH_ENC_KEY`)는 JS 번들에 하드코딩되어 있으며 변경될 수 있다.
- `selStrPkupStck` 호출 시: `/api/auth/request` 호출 후 Bearer를 만들어 시도한다.
- fallback order: Bearer 재고 조회 → 401/403 시 토큰 재발급 후 1회 재시도 → 구조화된 blocked 재고 → 선택적 `selPkupStr` 픽업 가능 여부.

View file

@ -1,193 +0,0 @@
---
name: danawa-price-search
description: 다나와 공개 검색/가격비교 표면으로 상품 후보를 찾고, 쇼핑몰별 최저가·배송비 포함 실구매가·카드 할인가·무이자 할부 정보를 보수적으로 비교한다.
license: MIT
metadata:
category: retail
locale: ko-KR
phase: v1
---
# Danawa Price Search
## What this skill does
다나와의 로그인 없는 공개 검색/가격비교 표면을 읽기 전용으로 호출해 한국 쇼핑몰 가격을 비교한다.
- 상품명/검색어로 다나와 상품 후보와 `pcode`를 찾는다.
- 선택한 상품의 쇼핑몰별 오퍼를 조회한다.
- 상품가만이 아니라 배송비 포함 실구매가, 무료배송 여부, 카드 할인가, 무이자 할부 문구를 함께 정리한다.
- 구매, 로그인, 장바구니, 찜, 주문 액션은 하지 않는다.
## When to use
- "다나와에서 에어팟 최저가 찾아줘"
- "다나와 가격비교로 쇼핑몰별 가격 비교해줘"
- "무료배송인지, 카드 할인까지 보면 어디가 제일 싸?"
- "무이자 할부 붙은 최저가도 같이 봐줘"
## When not to use
- 실제 구매/주문/결제/로그인이 필요한 경우
- 회원 전용 쿠폰, 개인화 포인트, 앱 전용 혜택을 확정해야 하는 경우
- 대량 모니터링이나 고빈도 크롤링을 해야 하는 경우
- CAPTCHA, 접근 차단, fingerprint 우회를 해야 하는 경우
## Required inputs
상품명 또는 검색어가 필요하다. 검색어가 넓으면 브랜드, 모델명, 용량, 색상, 자급제/통신사 여부 등을 추가로 물어본다.
권장 질문:
> 찾을 다나와 상품명이나 모델명을 알려주세요. 예: 갤럭시 S25 울트라 256GB 자급제, 에어팟 프로 2세대 USB-C
## Public surfaces
현재 구현은 인증 없는 공개 표면만 사용한다.
- 검색 페이지: `https://search.danawa.com/dsearch.php?query=...`
- 상품 상세 페이지: `https://prod.danawa.com/info/?pcode=...`
- 가격비교 AJAX: `https://prod.danawa.com/info/ajax/getAllPriceCompareMallList.ajax.php`
AJAX endpoint는 HTML fragment를 반환한다. helper는 `.diff_item`, 쇼핑몰 로고 `alt`, `em.prc_c`/`em.prc_t`, 배송 문구, 결제조건 배지(`.ico.cash`/`.ico.point`/`.ico.coupon`/`.ico.discount`/`.ico.card`/`.ico.membership` 등), 카드 할인 라인, 무이자 할부 레이어, 다나와 bridge link를 파싱한다.
## Commands
스킬 디렉터리에서 실행한다.
```bash
python scripts/danawa_search.py search "에어팟 프로 2세대" --limit 8
python scripts/danawa_search.py offers 28208783 --limit 10
python scripts/danawa_search.py compare "에어팟 프로 2세대" --limit 5 --offers 5
```
helper는 JSON만 출력한다. 결과를 확인한 뒤 사용자에게는 한국어 표와 짧은 결론으로 정리한다.
## Output shape
### `search`
```json
{
"query": "...",
"source_url": "...",
"count": 0,
"items": []
}
```
`items[]` 주요 필드:
- `pcode`
- `title`
- `price`, `price_text`
- `mall_text`
- `url`
- `image_url`
- `spec`
### `offers`
```json
{
"pcode": "...",
"title": "...",
"source_url": "...",
"count": 0,
"normal_count": 0,
"conditional_count": 0,
"offers": [],
"meta": { "sort": "total_price" }
}
```
`offers[]`는 **배송비 포함 실구매가(`total_price`) 오름차순**으로 정렬된다. `count` / `normal_count` / `conditional_count``limit` 적용 후 실제 반환된 `offers[]` window 기준이다. 결제조건(현금/쿠폰/포인트/할인/특정카드/멤버십 한정)이 붙은 row도 같은 정렬에 그대로 참여한다 — 가장 싸면 1위로 올라온다. 결제조건은 분리 그룹이나 추가 필터링 없이 row 단위 `payment_badges` / `payment_condition_types` / `payment_condition_label` / `cash_only` / `point_only` / `coupon_only` / `card_only_badge` / `discount_badge` / `membership_badge` / `is_conditional_price` 필드로 노출한다. 호출자는 사용자의 결제 수단에 따라 직접 판단한다.
`offers[]` 주요 필드:
- `mall`
- `price`, `price_text`
- `shipping`
- `is_free_shipping`
- `shipping_fee`
- `total_price`, `total_price_text`
- `card_price`, `card_price_text`
- `card_name`
- `card_discount`, `card_discount_text`
- `installment`
- `installment_detail`
- `payment_badges` — Danawa가 가격 옆에 노출한 결제조건 배지의 표시 라벨 목록. 배지 텍스트가 비어 있고 `.ico.cash`처럼 클래스만 있는 경우도 정규화 라벨을 합성한다 (예: `["현금"]`, `["포인트"]`, `["쿠폰"]`, `["카드"]`, `["할인"]`, `["멤버십"]`)
- `payment_condition_types` — 화이트리스트 배지를 정규화한 조건 타입 목록 (`cash`/`point`/`coupon`/`card`/`discount`/`membership`)
- `payment_condition_label` — 사용자 응답용 결제조건 라벨 (예: `현금`, `할인`, `멤버십`, 복수 조건이면 `현금, 할인`)
- `cash_only` — 현금 결제 전용가
- `point_only` — 포인트 차감 적용가
- `coupon_only` — 쿠폰 적용가
- `card_only_badge` — 특정 카드 한정 노출가
- `discount_badge` — 할인 조건 배지 노출가
- `membership_badge` — 멤버십 조건 배지 노출가
- `is_conditional_price``payment_condition_types`가 하나 이상 있으면 True. **일반 결제가가 아니므로 카드 일반 결제 시 가격이 다르거나 불가능할 수 있음**
- `url`
항상 무료배송 여부, 배송비 포함 실구매가, 카드별 할인 가격, 무이자 할부 문구, **그리고 `payment_badges`/`payment_condition_label`/`is_conditional_price`를 함께 확인한다.** 조건부 가격을 일반가처럼 1위로 노출하면 비교 결과가 거짓이 된다.
### `compare`
`compare`는 검색 결과를 먼저 가져온 뒤 각 후보 상품에 대해 `offers[]`를 best-effort로 붙인다. 검색 결과가 애매하면 상위 후보의 제목과 `pcode`를 먼저 보여주고 선택을 요청한다.
## Response style
Discord/Telegram/chat 응답에서는 표 형식을 우선한다.
```md
| 순위 | 판매처 | 상품가 | 결제조건 | 배송 | 실구매가 | 카드할인가 | 무이자 | 링크 |
|---:|---|---:|---|---|---:|---:|---|---|
| 1 | 킴스클럽 | 979,000원 | **현금 전용** | 유/무료 | 979,000원 | - | - | 보기 |
| 2 | 롯데ON | 1,073,890원 | 일반 | 무료배송 | 1,073,890원 | - | - | 보기 |
| 3 | G마켓 | 1,089,590원 | 일반 | 무료배송 | 1,089,590원 | - | 최대 24개월 | 보기 |
| 4 | 옥션 | 1,121,780원 | **쿠폰 적용가** | 무료배송 | 1,121,780원 | 우리카드 303,720원 | 최대 24개월 | 보기 |
```
정렬 기준:
1. **`total_price` 오름차순 단일 기준.** 결제조건(현금/쿠폰/포인트/할인/특정카드/멤버십 한정)이 붙은 row도 같은 정렬에 그대로 참여한다 — 가장 싸면 1위로 올라온다. 결제조건은 분리 그룹화하지 않고 표의 "결제조건" 컬럼에 행별로 표시한다 (`payment_condition_label`이 있으면 그 값을 우선 표시, 없으면 "일반"; 세부 매핑은 `cash` → "현금 전용", `coupon` → "쿠폰 적용가", `point` → "포인트 적용가", `card` → 카드명/카드 조건, `discount` → "할인 조건", `membership` → "멤버십 조건"). 사용자는 자기 결제 수단에 따라 직접 판단한다.
2. `card_price`가 있고 카드 적용 시 승자가 바뀌면 표 아래에 "카드 기준 최저가"를 별도로 적는다.
3. 무이자 할부는 결제 조건이 달라질 수 있으므로 Danawa 노출 문구 기준이라고 밝힌다.
4. 1위가 조건부 가격이면 요약 문장에 결제수단 단서를 짧게 덧붙인다. 예: "**최저 실구매가: 킴스클럽 979,000원 / 현금 결제 한정**, 카드 결제 기준 최저가는 롯데ON 1,073,890원". 카드 결제 가능한 최저가도 같이 알려 사용자가 결제수단별 결과를 한 번에 비교할 수 있게 한다.
요약 예시:
```md
최저 실구매가: G마켓 217,950원 / 무료배송
카드 기준 최저가: 옥션 우리카드 303,720원
무이자: G마켓·옥션 최대 24개월 표기
```
카드 할인 markup이 없으면 "카드 할인가 표기 없음"이라고 쓰고, 체크아웃 할인 자체가 없다고 단정하지 않는다.
## Workflow
1. 검색어를 확인한다.
2. `python scripts/danawa_search.py search "<검색어>" --limit 5`로 후보를 확인한다.
3. 후보가 명확하면 해당 `pcode``offers`를 실행한다.
4. 후보가 애매하면 상위 3~5개 상품명/가격/`pcode`를 보여주고 선택을 요청한다.
5. 오퍼는 **`total_price` 오름차순 단일 기준으로 정렬한다 (결제조건 분리 그룹화하지 않음).** 결제조건은 표의 "결제조건" 컬럼과 row 단위 플래그로만 표기하고, 1위가 현금/쿠폰가여도 그대로 1위로 노출한다.
6. 카드 할인가가 있으면 카드 기준 최저가도 별도 요약한다. 1위가 조건부 가격이면 "카드 결제 기준 최저가"도 요약 문장에 함께 적어 결제수단별 최저가를 한 번에 알게 한다.
7. 조회 시점 기준이며 가격/배송/카드 혜택은 변동될 수 있음을 짧게 덧붙인다.
## Failure modes
- 검색 결과가 0개면 검색어를 더 구체화한다.
- Danawa HTML/AJAX 구조가 바뀌면 selector가 깨져 `offers`가 비거나 필드가 누락될 수 있다.
- 다나와가 새로운 결제조건 배지 클래스나 문구를 도입하면 결제조건 배지 화이트리스트(`cash`/`point`/`coupon`/`discount`/`card`/`membership` 클래스, `현금`/`포인트`/`쿠폰`/`할인`/`카드`/`멤버십` 텍스트 키워드)와 `payment_condition_types`/`payment_condition_label` 매핑을 함께 갱신해야 한다.
- 검색 결과 가격과 오퍼 AJAX 가격은 갱신 시점·카드가·제휴 링크 기준 차이로 다를 수 있다.
- 카드 할인과 무이자 문구는 Danawa가 노출한 경우에만 확정적으로 보여준다.
- 공개 표면 기반이므로 고빈도 요청에는 throttling/backoff를 추가해야 한다.
- 접근 차단이나 CAPTCHA가 나오면 우회를 시도하지 말고 실패 모드로 보고한다.
## Done when
- 검색어 또는 모델명을 확인했다.
- 상품 후보를 최소 1개 이상 반환하거나, 반환 실패 이유를 설명했다.
- 쇼핑몰별 상품가, 배송비, 실구매가, 카드 할인가, 무이자 문구를 조회 시점 기준으로 정리했다.
- 사용자 응답은 표 형식으로 제공했다.
- 로그인/구매/차단 우회 범위를 벗어나지 않았다.

View file

@ -1,354 +0,0 @@
#!/usr/bin/env python3
"""Read-only Danawa search/price comparison helper for Hermes.
Usage:
python scripts/danawa_search.py search "에어팟 프로 2세대" --limit 8
python scripts/danawa_search.py offers 28208783 --limit 10
python scripts/danawa_search.py compare "에어팟 프로 2세대" --limit 5 --offers 5
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
import urllib.parse
import urllib.request
from html import unescape
from typing import Any, Dict, List, Optional
try:
from bs4 import BeautifulSoup
except ImportError as exc: # pragma: no cover - environment guard
raise SystemExit("beautifulsoup4 is required: python -m pip install beautifulsoup4") from exc
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/121 Safari/537.36"
def fetch(url: str, *, method: str = "GET", data: Optional[dict] = None, referer: Optional[str] = None) -> str:
headers = {
"User-Agent": UA,
"Accept-Language": "ko-KR,ko;q=0.9,en;q=0.8",
}
body = None
if data is not None:
body = urllib.parse.urlencode(data).encode("utf-8")
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
headers["X-Requested-With"] = "XMLHttpRequest"
if referer:
headers["Referer"] = referer
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=25) as resp:
return resp.read().decode("utf-8", "replace")
def soup_for(html: str) -> BeautifulSoup:
return BeautifulSoup(html, "html.parser")
def clean_text(s: Optional[str]) -> Optional[str]:
if s is None:
return None
return " ".join(unescape(s).split())
def parse_int(s: Optional[str]) -> Optional[int]:
if not s:
return None
digits = re.sub(r"\D", "", s)
return int(digits) if digits else None
def abs_url(url: Optional[str]) -> Optional[str]:
if not url:
return None
if url.startswith("//"):
return "https:" + url
if url.startswith("/"):
return "https://prod.danawa.com" + url
return url
def search(query: str, limit: int = 10) -> Dict[str, Any]:
url = "https://search.danawa.com/dsearch.php?query=" + urllib.parse.quote(query)
html = fetch(url)
soup = soup_for(html)
items: List[Dict[str, Any]] = []
for li in soup.select("li.prod_item"):
pid = (li.get("id") or "").replace("productItem", "") or None
name_el = li.select_one(".prod_name a") or li.select_one("p.prod_name a") or li.select_one('a[name="productName"]')
if not name_el:
continue
name = clean_text(name_el.get_text(" ", strip=True))
link = abs_url(name_el.get("href"))
min_input = li.select_one(f"#min_price_{pid}") if pid else None
price = parse_int(min_input.get("value") if min_input else None)
if price is None:
price_el = li.select_one(".price_sect strong") or li.select_one(".prod_pricelist strong")
price = parse_int(price_el.get_text() if price_el else None)
img = li.select_one(".thumb_image img")
image = abs_url((img.get("data-original") or img.get("src")) if img else None)
mall_el = li.select_one(".prod_pricelist .memory_sect") or li.select_one(".meta_item")
spec = " / ".join(clean_text(e.get_text(" ", strip=True)) or "" for e in li.select(".spec_list a, .spec_list span")[:10])
items.append(
{
"pcode": pid,
"title": name,
"price": price,
"price_text": f"{price:,}" if price else None,
"mall_text": clean_text(mall_el.get_text(" ", strip=True)) if mall_el else None,
"url": link,
"image_url": image,
"spec": spec[:300] if spec else None,
}
)
if len(items) >= limit:
break
return {"query": query, "source_url": url, "count": len(items), "items": items, "meta": {"extraction": "danawa-search-html", "ts": int(time.time())}}
def js_value(html: str, key: str) -> str:
patterns = [
rf"{re.escape(key)}\s*:\s*\"([^\"]*)\"",
rf"{re.escape(key)}\s*:\s*'([^']*)'",
rf"{re.escape(key)}\s*:\s*([0-9]+)",
]
for pat in patterns:
m = re.search(pat, html)
if m:
raw = m.group(1)
if "\\u" in raw or "\\/" in raw:
try:
return json.loads('"' + raw.replace('"', '\\"') + '"')
except Exception:
return raw.replace("\\/", "/")
return raw
return ""
def product_meta(pcode: str) -> Dict[str, str]:
url = f"https://prod.danawa.com/info/?pcode={urllib.parse.quote(str(pcode))}"
html = fetch(url)
meta = {
"pcode": str(pcode),
"source_url": url,
"cate1": js_value(html, "nCategoryCode1"),
"cate2": js_value(html, "nCategoryCode2"),
"cate3": js_value(html, "nCategoryCode3"),
"cate4": js_value(html, "nCategoryCode4") or "0",
"UICategoryCode": js_value(html, "nCategoryCode"),
"powerLinkKeyword": js_value(html, "powerLinkKeyword"),
"minPrice": js_value(html, "nMinPrice"),
"keyword": js_value(html, "sKeyword"),
"NaPm": js_value(html, "sNaPm"),
"sProductFullName": js_value(html, "sProductName"),
"makerCode": js_value(html, "makerCode"),
"makerName": js_value(html, "makerName"),
}
title = soup_for(html).select_one(".prod_tit .title")
if title:
meta["sProductFullName"] = clean_text(title.get_text(" ", strip=True)) or meta["sProductFullName"]
return meta
def offers(pcode: str, limit: int = 20, include_shipping: bool = False) -> Dict[str, Any]:
meta = product_meta(pcode)
post_price = "Y" if include_shipping else "N"
data = {
"pcode": meta["pcode"],
"cate1": meta.get("cate1", ""),
"cate2": meta.get("cate2", ""),
"cate3": meta.get("cate3", ""),
"cate4": meta.get("cate4", "0"),
"UICategoryCode": meta.get("UICategoryCode", "0"),
"powerLinkKeyword": meta.get("powerLinkKeyword", ""),
"minPrice": meta.get("minPrice", ""),
"keyword": meta.get("keyword", ""),
"NaPm": meta.get("NaPm", ""),
"bDeliveryLeftRightYN": "N",
"bQuickPostSortYN": "N",
"sSortType": "minPrice",
"sProductFullName": meta.get("sProductFullName", ""),
"bPostPriceYN": post_price,
"bBadgeDefaultYN": "N",
"bWarrantyDefaultYN": "N",
"nOpenMarketMoreCount": "30",
"nAffiliateMoreCount": "30",
"nOverseasShoppingMoreCount": "30",
"nGeneralAffiliateMoreCount": "3",
"sRelationMenuType": "",
"sRelationType": "",
"bCoupangSortYN": "N",
"makerCode": meta.get("makerCode", ""),
"makerName": meta.get("makerName", ""),
}
html = fetch("https://prod.danawa.com/info/ajax/getAllPriceCompareMallList.ajax.php", method="POST", data=data, referer=meta["source_url"])
soup = soup_for(html)
rows: List[Dict[str, Any]] = []
for div in soup.select(".diff_item"):
mall_img = div.select_one(".d_mall img")
mall = mall_img.get("alt") if mall_img else None
price_el = div.select_one("em.prc_c") or div.select_one("em.prc_t")
price = parse_int(price_el.get_text() if price_el else None)
if not mall or price is None:
continue
ship_el = div.select_one(".ship") or div.select_one(".stxt")
shipping = clean_text(ship_el.get_text(" ", strip=True)) if ship_el else None
shipping_fee = 0 if shipping and "무료" in shipping else parse_int(shipping)
card_line = div.select_one(".card_line")
card_price_el = card_line.select_one(".card_prc") if card_line else None
card_name_el = card_line.select_one(".txt") if card_line else None
card_price = parse_int(card_price_el.get_text() if card_price_el else None)
installment_el = div.select_one(".btn_foi .txt")
installment_detail_el = div.select_one(".foi_layer .ly_cont")
link = div.select_one("a.priceCompareBuyLink")
# 결제조건 ico만 캡처. 다른 ico(빠른배송, 안내, 상품리뷰 등)는 노이즈라 제외.
# 클래스만 있고 텍스트가 비어 있는 아이콘도 row 라벨이 누락되지 않도록
# 같은 정규화 테이블에서 표시 라벨/타입/boolean 필드를 모두 파생한다.
payment_condition_labels = {
"cash": "현금",
"point": "포인트",
"coupon": "쿠폰",
"card": "카드",
"discount": "할인",
"membership": "멤버십",
}
payment_condition_types: List[str] = []
payment_badges: List[str] = []
for el in div.select(".prc_line .ico, .d_dsc .ico"):
classes = set(el.get("class") or [])
text = clean_text(el.get_text(" ", strip=True)) or ""
matched_types = [
kind
for kind, label in payment_condition_labels.items()
if kind in classes or label in text
]
if not matched_types:
continue
for kind in matched_types:
if kind not in payment_condition_types:
payment_condition_types.append(kind)
label = payment_condition_labels[kind]
if label not in payment_badges:
payment_badges.append(label)
cash_only = "cash" in payment_condition_types
point_only = "point" in payment_condition_types
coupon_only = "coupon" in payment_condition_types
card_only_badge = "card" in payment_condition_types
discount_badge = "discount" in payment_condition_types
membership_badge = "membership" in payment_condition_types
payment_condition_label = ", ".join(payment_badges) or None
is_conditional_price = bool(payment_condition_types)
rows.append(
{
"mall": clean_text(mall),
"price": price,
"price_text": f"{price:,}",
"shipping": shipping,
"is_free_shipping": bool(shipping and "무료" in shipping),
"shipping_fee": shipping_fee,
"total_price": price + (shipping_fee or 0),
"total_price_text": f"{price + (shipping_fee or 0):,}",
"card_price": card_price,
"card_price_text": f"{card_price:,}" if card_price else None,
"card_name": clean_text(card_name_el.get_text(" ", strip=True)) if card_name_el else None,
"card_discount": (price - card_price) if card_price else None,
"card_discount_text": f"{price - card_price:,}" if card_price else None,
"installment": clean_text(installment_el.get_text(" ", strip=True)) if installment_el else None,
"installment_detail": clean_text(installment_detail_el.get_text(" ", strip=True)) if installment_detail_el else None,
"payment_badges": payment_badges,
"cash_only": cash_only,
"point_only": point_only,
"coupon_only": coupon_only,
"card_only_badge": card_only_badge,
"discount_badge": discount_badge,
"membership_badge": membership_badge,
"payment_condition_types": payment_condition_types,
"payment_condition_label": payment_condition_label,
"is_conditional_price": is_conditional_price,
"url": abs_url(link.get("href") if link else None),
}
)
# 정렬은 단순히 배송비 포함 실구매가 오름차순. 결제조건(현금/쿠폰/포인트/특정카드)은
# 분리 그룹으로 묶지 않고 row 단위로 payment_badges / payment_condition_types /
# payment_condition_label 및 세부 boolean 플래그로 노출한다. 호출자(또는 사용자)는 자기 결제수단에 맞춰 판단한다.
rows.sort(key=lambda row: (
row["total_price"] is None,
row["total_price"] or row["price"],
row["price"],
row["mall"] or "",
))
rows = rows[:limit]
return {
"pcode": str(pcode),
"title": meta.get("sProductFullName"),
"source_url": meta["source_url"],
"count": len(rows),
"normal_count": sum(1 for r in rows if not r.get("is_conditional_price")),
"conditional_count": sum(1 for r in rows if r.get("is_conditional_price")),
"offers": rows,
"meta": {
"extraction": "danawa-price-ajax",
"include_shipping": include_shipping,
"sort": "total_price",
"ts": int(time.time()),
},
}
def compare(query: str, limit: int, offer_limit: int) -> Dict[str, Any]:
result = search(query, limit=limit)
enriched = []
for item in result["items"]:
row = dict(item)
if item.get("pcode"):
try:
off = offers(item["pcode"], limit=offer_limit)
row["offers"] = off.get("offers", [])
except Exception as exc: # keep search result usable if a detail call fails
row["offers_error"] = f"{type(exc).__name__}: {exc}"
enriched.append(row)
result["items"] = enriched
result["meta"]["detail_extraction"] = "best-effort"
return result
def positive_int(raw: str) -> int:
value = int(raw)
if value < 1:
raise argparse.ArgumentTypeError("must be >= 1")
return value
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
s = sub.add_parser("search")
s.add_argument("query")
s.add_argument("--limit", type=positive_int, default=10)
o = sub.add_parser("offers")
o.add_argument("pcode")
o.add_argument("--limit", type=positive_int, default=20)
o.add_argument("--include-shipping", action="store_true")
c = sub.add_parser("compare")
c.add_argument("query")
c.add_argument("--limit", type=positive_int, default=5)
c.add_argument("--offers", type=positive_int, default=5)
args = ap.parse_args()
try:
if args.cmd == "search":
out = search(args.query, args.limit)
elif args.cmd == "offers":
out = offers(args.pcode, args.limit, args.include_shipping)
else:
out = compare(args.query, args.limit, args.offers)
print(json.dumps(out, ensure_ascii=False, indent=2))
return 0
except Exception as exc:
print(json.dumps({"error": f"{type(exc).__name__}: {exc}"}, ensure_ascii=False), file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -13,7 +13,7 @@
| 유형 | 설명 | 예시 |
|------|------|------|
| **SKILL.md 전용** | 문서만으로 동작 (에이전트가 bash/python 직접 실행) | `kakaotalk-mac`, `srt-booking` |
| **npm 패키지** | `packages/` 아래 Node.js 라이브러리로 구현 | `k-lotto`, `daiso-product-search` |
| **npm 패키지** | `packages/` 아래 Node.js 라이브러리로 구현 | `k-lotto`, `blue-ribbon-nearby` |
| **프록시 경유** | `k-skill-proxy`가 upstream API 키를 보관하고 HTTP로 중계 | `seoul-subway-arrival`, `fine-dust-location` |
| **Python 스크립트** | `scripts/`의 Python 파일 직접 실행 | `korean-spell-check`, `sillok-search` |
@ -156,38 +156,6 @@ upstream API 키를 사용자에게 노출하지 않으려면 `k-skill-proxy`를
---
## 크롤링/검색 스킬을 만들 때: site-agnostic discovery 먼저
웹사이트를 조회하거나 크롤링하는 스킬의 최종 산출물은 결국 **그 사이트에 맞는 site-dependent 접근 방법**이다. 다만 처음부터 특정 화면 구조나 임시 우회법을 감으로 고정하지 않는다. 먼저 `insane-search`식 접근처럼 **사이트에 상관없이 반복 가능한 탐색 절차**를 적용해 대상 사이트에서 실제로 안정적인 경로를 찾아낸 뒤, 그 발견 결과를 해당 스킬의 site-dependent 지식으로 패키징한다.
적용 대상:
- 검색 결과/상세 페이지를 읽어야 하는 스킬
- 공식 API 문서가 없거나 불완전한 사이트
- PC 페이지, 모바일 페이지, RSS, sitemap, 정적 JSON, 공개 데이터 호출 등 여러 입구가 있을 수 있는 사이트
- 브라우저에서는 보이지만 단순 HTTP 요청에서는 빈 화면/차단/로그인 유도만 보이는 사이트
권장 절차:
1. **공개 입구부터 찾기**: 공식 API, 공개 JSON, RSS/Atom, sitemap, 검색 폼, 모바일 페이지, 정적 파일처럼 사이트가 공개적으로 제공하는 경로를 먼저 확인한다.
2. **브라우저 동작을 관찰하기**: 화면을 직접 긁기 전에 검색/상세 화면이 어떤 공개 데이터 요청을 통해 채워지는지 확인한다.
3. **안정적인 경로를 우선하기**: 화면 선택자보다 공개 데이터 호출, 문서화된 endpoint, RSS/sitemap처럼 구조가 덜 흔들리는 경로를 선호한다.
4. **차단과 빈 응답을 실패로 분리하기**: HTTP 성공만으로 완료로 보지 말고, 실제 결과 본문이 있는지 확인한다. 로그인벽, 봇 검사, 빈 껍데기 페이지는 별도 실패 모드로 적는다.
5. **site-dependent 방법을 명시적으로 패키징하기**: 탐색 과정에서 확인한 검색 URL, 필수 파라미터, 결과 해석 규칙, fallback 순서를 `SKILL.md`와 패키지 코드에 좁고 명확하게 기록한다.
6. **권한 경계를 지키기**: 인증, 결제, CAPTCHA, 약관상 제한이 필요한 경로는 자동화하지 말고 사용자 개입 또는 실패 모드로 처리한다.
`SKILL.md`에는 최소한 아래 내용을 남긴다.
- 어떤 공개 접근 경로를 선택했는지와 그 이유
- 검색/상세 조회의 입력값과 출력값
- 기본 경로가 실패했을 때의 fallback 순서
- 빈 결과, 차단, 로그인 필요, upstream 변경 등 실패 모드
- 시크릿/인증이 필요한지 여부와 저장소에 절대 넣지 않을 값
새 dependency는 기본값으로 추가하지 않는다. 기존 Node.js/Python 표준 기능, 이미 있는 패키지, 또는 `k-skill-proxy`의 좁은 allowlist route로 해결할 수 있는지 먼저 확인한다.
---
## 스킬 등록 & 검증
스킬은 **별도 레지스트리 없이 디렉토리 스캔으로 자동 발견**된다.
@ -236,7 +204,6 @@ npm run ci
- [ ] npm 패키지라면 `packages/`에 구현체와 테스트 추가
- [ ] npm 패키지라면 `.changeset/*.md` 파일 추가 (반드시 **기능 PR에서**, Version Packages PR에서 추가하지 말 것)
- [ ] 프록시 경유라면 `k-skill-proxy/src/server.js`에 route 추가하고 main에 merge
- [ ] 크롤링/검색 스킬이라면 공개 접근 경로, fallback 순서, 차단/로그인/빈 결과 실패 모드 문서화
- [ ] 시크릿이 있다면 `KSKILL_` 접두사 규칙 준수 및 `docs/setup.md` 업데이트
- [ ] `docs/features/my-new-skill.md` 작성 (선택, 상세 가이드)

View file

@ -1,211 +0,0 @@
# k-skill-proxy 배포 가이드 (Cloud Run + GitHub Actions)
`k-skill-proxy`는 Google Cloud Run에서 운영되고, `main` 브랜치에 머지되면 GitHub Actions가 자동으로 재배포합니다.
이 문서는 그 자동 배포 파이프라인의 **1회성 셋업 절차**와 **운영 점검 절차**를 정리합니다. 일반 contributor는 읽지 않아도 되며, 프록시 운영을 담당하는 maintainer(현재 `jeffrey@markr.ai`)가 인프라를 처음 만들거나 수리할 때 참고합니다.
## 운영 사실
| 항목 | 값 |
| --- | --- |
| GCP project ID | `k-skill-proxy` |
| Region | `asia-northeast1` (도쿄) |
| Cloud Run service | `k-skill-proxy` |
| Artifact Registry repo | `asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill` |
| 공개 도메인 | `https://k-skill-proxy.nomadamas.org` (Cloud Run domain mapping) |
| 컨테이너 이미지 정의 | `packages/k-skill-proxy/Dockerfile` |
| 워크플로 | `.github/workflows/deploy-k-skill-proxy.yml` |
| 인증 | Workload Identity Federation (long-lived JSON key 없음) |
| 시크릿 저장소 | GCP Secret Manager (이름 = 환경변수 이름) |
## 배포 흐름
1. `dev` 브랜치에서 작업, PR을 `dev`에 보낸다.
2. `dev``main` 머지 PR이 `@vkehfdl1`에 의해 머지된다.
3. `main` push가 `.github/workflows/deploy-k-skill-proxy.yml`을 트리거한다.
4. 워크플로가:
- WIF로 `${GCP_DEPLOY_SERVICE_ACCOUNT}`로 impersonate
- `packages/k-skill-proxy/Dockerfile`로 컨테이너 빌드
- Artifact Registry에 `:${GITHUB_SHA}` 태그로 push
- Cloud Run `k-skill-proxy` 서비스를 새 이미지로 재배포 (Secret Manager 시크릿 + 런타임 env 주입)
- 새 revision의 `*.run.app` URL과 `https://k-skill-proxy.nomadamas.org/health`에 smoke test
5. 실패 시 GitHub Actions 페이지에서 로그 확인. Cloud Run 자체는 마지막 healthy revision에 트래픽을 유지한다.
## 1회성 GCP 셋업
> 이미 한 번 셋업되어 있다면 다시 실행할 필요 없음. 새 maintainer가 인계받거나 SA를 새로 만들 때만 사용.
```bash
export PROJECT_ID="k-skill-proxy"
export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')"
export GH_REPO="NomaDamas/k-skill" # owner/repo
export POOL_ID="github-actions-pool"
export PROVIDER_ID="github-actions-provider"
export DEPLOY_SA="k-skill-proxy-deploy"
export DEPLOY_SA_EMAIL="${DEPLOY_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
```
### 1) 필요한 API 활성화
```bash
gcloud services enable \
iamcredentials.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
--project="$PROJECT_ID"
```
### 2) Workload Identity Pool + GitHub OIDC provider
```bash
gcloud iam workload-identity-pools create "$POOL_ID" \
--project="$PROJECT_ID" \
--location=global \
--display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
--project="$PROJECT_ID" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--display-name="GitHub OIDC" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository == '${GH_REPO}'"
```
> `attribute-condition`은 토큰 발급 단계에서 우리 저장소만 허용해 풀 자체를 좁힙니다. 임의의 다른 repo가 같은 풀을 통해 SA를 impersonate하지 못하게 막는 핵심 가드입니다.
### 3) Deploy service account 생성
```bash
gcloud iam service-accounts create "$DEPLOY_SA" \
--project="$PROJECT_ID" \
--display-name="GitHub Actions k-skill-proxy deployer"
```
### 4) 풀 → service account impersonation 허용
```bash
gcloud iam service-accounts add-iam-policy-binding "$DEPLOY_SA_EMAIL" \
--project="$PROJECT_ID" \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.repository/${GH_REPO}"
```
### 5) deploy SA에 필요한 권한 부여
```bash
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/run.admin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/artifactregistry.writer
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/iam.serviceAccountUser
```
`iam.serviceAccountUser`는 Cloud Run의 런타임 service account(`${PROJECT_NUMBER}-compute@developer.gserviceaccount.com`)를 deploy SA가 대신 지정할 수 있게 하기 위함입니다.
### 6) Cloud Run 런타임 SA에 Secret Manager accessor 부여
```bash
RUNTIME_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
for s in \
AIR_KOREA_OPEN_API_KEY KMA_OPEN_API_KEY SEOUL_OPEN_API_KEY HRFCO_OPEN_API_KEY \
OPINET_API_KEY DATA_GO_KR_API_KEY KEDU_INFO_KEY \
DATA4LIBRARY_AUTH_KEY FOODSAFETYKOREA_API_KEY KAKAO_REST_API_KEY KRX_API_KEY \
KOSIS_API_KEY NAVER_SEARCH_CLIENT_ID NAVER_SEARCH_CLIENT_SECRET LAW_OC; do
gcloud secrets add-iam-policy-binding "$s" \
--project="$PROJECT_ID" \
--member="serviceAccount:${RUNTIME_SA}" \
--role=roles/secretmanager.secretAccessor \
--condition=None >/dev/null
done
```
### 7) WIF provider 리소스 이름 확인
```bash
gcloud iam workload-identity-pools providers describe "$PROVIDER_ID" \
--project="$PROJECT_ID" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--format='value(name)'
# 예: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
```
이 값과 `${DEPLOY_SA_EMAIL}`을 GitHub에 등록합니다.
## GitHub repository secrets
다음 두 개의 **secret**을 `Settings → Secrets and variables → Actions → Repository secrets`에 등록합니다.
| Name | Value |
| --- | --- |
| `GCP_WIF_PROVIDER` | 위 7번에서 얻은 provider 리소스 전체 이름 |
| `GCP_DEPLOY_SERVICE_ACCOUNT` | `k-skill-proxy-deploy@k-skill-proxy.iam.gserviceaccount.com` |
> 값 자체가 민감하진 않지만, 외부에 노출되면 reconnaissance에 도움이 될 수 있으므로 secret으로 둡니다. variable로 옮겨도 동작은 동일합니다.
## Secret Manager에 upstream key 업로드
```bash
KEYS=(
AIR_KOREA_OPEN_API_KEY KMA_OPEN_API_KEY SEOUL_OPEN_API_KEY HRFCO_OPEN_API_KEY
OPINET_API_KEY DATA_GO_KR_API_KEY KEDU_INFO_KEY
DATA4LIBRARY_AUTH_KEY FOODSAFETYKOREA_API_KEY KAKAO_REST_API_KEY KRX_API_KEY
KOSIS_API_KEY NAVER_SEARCH_CLIENT_ID NAVER_SEARCH_CLIENT_SECRET LAW_OC
)
set -a; source ~/.config/k-skill/secrets.env; set +a
for k in "${KEYS[@]}"; do
value="${!k:-}"
[[ -z "$value" ]] && { echo "skip $k (empty)"; continue; }
if gcloud secrets describe "$k" --project="$PROJECT_ID" >/dev/null 2>&1; then
printf '%s' "$value" | gcloud secrets versions add "$k" --data-file=- --project="$PROJECT_ID"
else
printf '%s' "$value" | gcloud secrets create "$k" --data-file=- --replication-policy=automatic --project="$PROJECT_ID"
fi
done
```
키 값을 회전(rotate)할 때도 같은 명령을 다시 실행하면 새 version이 추가됩니다. Cloud Run은 `:latest`로 바인딩되어 있어 다음 배포부터 자동 반영됩니다(즉시 적용이 필요하면 새 revision을 한 번 더 deploy).
## 운영 점검 절차
- 자동 배포 상태: GitHub `Actions` 탭의 "Deploy k-skill-proxy to Cloud Run" 워크플로
- 라이브 헬스체크: `curl -fsS https://k-skill-proxy.nomadamas.org/health`
- Cloud Run revision/로그: GCP Console → Cloud Run → `k-skill-proxy` (`asia-northeast1`)
- 이미지 태그: `asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:<commit-sha>`
- 트래픽 롤백: 이전 revision으로 traffic split을 100% 되돌리거나, 직전 commit을 revert해서 main에 머지 → 워크플로가 다시 돈다.
## 로컬에서 동일한 배포를 수동으로 돌리고 싶을 때
`gcloud auth login`으로 maintainer 계정에 로그인된 상태에서:
```bash
SHA="$(git rev-parse HEAD)"
IMAGE_URI="asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:${SHA}"
gcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet
docker build -t "$IMAGE_URI" -f packages/k-skill-proxy/Dockerfile .
docker push "$IMAGE_URI"
gcloud run deploy k-skill-proxy \
--image="$IMAGE_URI" \
--region=asia-northeast1 \
--platform=managed \
--allow-unauthenticated \
--execution-environment=gen2 \
--cpu=1 --memory=512Mi --min-instances=0 --max-instances=3 \
--concurrency=80 --timeout=60 --cpu-boost \
--project=k-skill-proxy
```
이 명령은 평상시에는 필요 없습니다. GitHub Actions가 같은 일을 하기 때문입니다.

View file

@ -1,45 +0,0 @@
# 사업자 실사 종합 (biz-health-check)
`biz-health-check` 스킬은 사업자등록번호(+상호/지역) 하나로 무료 공공 데이터 6종을 한 번에 교차 조회해 실사 리포트 한 장을 만든다. 같은 레포의 단품 스킬 helper를 그대로 재사용한다(단일 진실원천).
## 묶는 단품 스킬
| 섹션 | 단품 스킬 | 경로 |
| --- | --- | --- |
| 국세청 사업자등록 상태 | `nts-business-registration` | proxy |
| 국민연금 가입 사업장 | `national-pension-workplace` | proxy |
| 국세 체납 명단공개 | `nts-tax-delinquency` | 직접(무인증) |
| 금융위 기업기본정보 | `fsc-corporate-info` | proxy |
| 조달청 부정당제재 | `g2b-sanctioned-supplier` | proxy |
| 지방행정 인허가 영업상태 | `localdata-business-status` | 직접(무인증) |
## 설계 원칙
- 점수·등급·"위험" 같은 해석 라벨을 산출하지 않는다. 각 항목의 사실 + 출처 + 조회시각만 병렬한다.
- 한 항목 조회가 실패해도 전체를 막지 않고 그 항목만 `unavailable` + 사유로 강등한다.
- 단품 helper를 찾지 못하면 해당 섹션만 건너뛰고 나머지를 진행한다.
## 인증/시크릿
- 사용자 측 필수 시크릿 없음.
- proxy 섹션(국세청 상태·국민연금·금융위·부정당)은 운영 서버의 `DATA_GO_KR_API_KEY`로 동작한다.
- 무인증 섹션(체납·인허가)은 키 없이 사용자 머신에서 직접 동작한다.
## 예시
```bash
python3 biz-health-check/scripts/biz_health_check.py 124-81-00998 --name "삼성전자"
python3 biz-health-check/scripts/biz_health_check.py --name "호텔샬롬" --region 제주제주시 --industry 숙박업
```
## 입력
- `b_no`: 사업자등록번호 10자리(하이픈 허용) — 상태조회·부정당제재에 필요
- `--name`: 상호·법인명 — 국민연금·금융위·체납·인허가에 필요
- `--region`: 시군구 — 인허가(동네 사업장) 조회에 필요
- `--industry`: 인허가 업종(여러 번 지정 가능)
## 공식 출처
- 각 단품 스킬 문서의 공식 출처를 따른다. 통합 목록은 [sources](../sources.md)의 "사업자 실사" 항목 참조.

View file

@ -1,90 +0,0 @@
# 캐치테이블 예약 스나이핑 가이드
## 이 기능으로 할 수 있는 일
- 로그인된 Chrome 세션을 재사용해 캐치테이블 예약 페이지 진입
- 원하는 식당의 취소 슬롯/빈자리 폴링
- 여러 식당을 순차 감시하다가 먼저 열린 슬롯에 예약 시도
- 예약 오픈 시간에 맞춘 오픈런 시도
- dry-run 모드로 빈자리 발견까지만 알리고 최종 예약은 사용자에게 넘기기
## 먼저 알아둘 점
- 이 기능은 **Chrome MCP + 로그인된 캐치테이블 세션**이 있어야만 동작한다.
- 카카오/네이버 로그인 자동화는 하지 않는다.
- 결제 정보 자동 입력은 하지 않는다.
- 선결제 매장은 결제 단계에서 반드시 사용자가 직접 확인해야 한다.
- 서버 부하를 줄이기 위해 폴링 간격은 **30초 이상**으로 유지한다.
## 입력 형태
다음 정보를 자연어에서 추출해 사용한다.
- 식당명 또는 캐치테이블 URL
- 날짜 또는 날짜 범위
- 인원 수
- 시간대(선택)
- dry-run 여부
- 인원 유연 매칭 여부
- 예약 오픈 시각(오픈런 모드일 때)
예시:
- `온지음 5월 토요일 저녁 2인 빈자리 나오면 예약해줘`
- `온지음, 밍글스, 라연 중 5월 주말 2인 아무데나 먼저 뜨는 거 잡아줘`
- `라연 5월 예약 오픈이 4월 30일 오전 10시야, 그때 맞춰 2인 잡아줘`
- `밍글스 빈자리 뜨면 예약은 내가 할게, dry-run으로`
- `https://app.catchtable.co.kr/ct/shop/mingles 토요일 4명 자동예약`
## 동작 흐름
1. 캐치테이블 홈 또는 식당 페이지에 접속한다.
2. 로그인 상태를 확인한다.
3. 오픈런 모드면 지정 시각까지 대기 후 즉시 예약을 시도한다.
4. 일반 스나이핑 모드면 30초 간격으로 새로고침/재조회하며 슬롯을 감시한다.
5. 슬롯이 열리면 날짜/인원/시간을 선택하고 예약 흐름으로 진입한다.
6. 무료 예약이면 최종 예약 버튼까지 진행하고, 선결제 매장이면 결제 직전 단계에서 사용자 확인을 요구한다.
## 멀티 타겟 / 인원 유연 모드
### 멀티 타겟
- 여러 식당을 순차적으로 감시한다.
- 한 곳에서 예약 가능한 슬롯을 발견하면 나머지 감시는 즉시 중단한다.
### 인원 유연 매칭
- 예를 들어 2인 자리가 없을 때 4인 자리를 대안으로 확인할 수 있다.
- 대안 인원 슬롯을 발견하면 사용자에게 확인을 받고 다음 단계로 진행한다.
## dry-run 모드
`알림만`, `dry-run` 같은 표현이 있으면 예약 완료 대신 다음까지만 수행한다.
- 빈자리 발견
- 식당/날짜/시간/인원 요약
- 사용자가 직접 예약할 수 있도록 알림
## 제한사항
- 로그인 자동화 없음
- 카드/간편결제 정보 자동 입력 없음
- 캐치테이블 외 예약 플랫폼 미지원
- UI 변경 시 selector/흐름이 깨질 수 있음
## 검증 메모
2026-04-22 기준 로컬 검증에서 다음을 확인했다.
- 캐치테이블 식당 페이지 진입
- 예약 가능한 식당에서 날짜/인원/시간 선택
- 방문 확인 단계 진입
- 결제 방식 선택 단계 진입
다만 최종 예약 완료는 **로그인된 캐치테이블 세션이 없는 Chrome 프로필**에서는 검증할 수 없었다. 이 기능의 최종 성공 여부는 로그인된 사용자 세션과 실시간 좌석 상황에 직접 의존한다.
## 원칙
- 사용자의 로그인 자격 증명을 새 env var나 repo 문서에 추가하지 않는다.
- 사용자가 이미 로그인해 둔 브라우저 세션만 재사용한다.
- 결제나 취소 수수료가 얽힌 단계에서는 사용자 확인을 우선한다.

View file

@ -1,38 +0,0 @@
# 법인등기 신청 컨설팅
`corporate-registration-consulting`은 일반 영리 주식회사 **발기설립** 등기를 처음 진행하는 사용자를 위해 저장된 HWP 양식 사본을 채우는 법인설립등기 준비 스킬이다. 모집설립은 일반적이지 않으므로 기본 플로우에서 제외한다.
## 핵심 원칙
- 런타임 지침은 `corporate-registration-consulting/SKILL.md`가 담당한다.
- 실제 작성은 Markdown 초안이 아니라 저장된 HWP 양식 사본을 레포 밖 비공개 작업 디렉터리에 복사해 진행한다. 최종 산출물은 실제 `.hwp` 사본이다.
- 정관은 `templates/attachment-hwp/standard-articles-startup-moj.hwp` 또는 `templates/attachment-hwp/articles-of-incorporation.hwp`를 우선 사용한다.
- 단순 `replace-all`은 shortcut일 뿐이며, 각 HWP의 상단·본문·표 셀·하단 날짜·서명/날인란을 한 장 한 장 순차 확인한다.
- 자리표시자와 실제 사용자 입력값을 구분하고, 개인정보가 들어간 산출물은 레포에 커밋하지 않는다.
## 필수 문서와 양식
인터넷등기소/온라인법인설립시스템 제출 전 대조용 저장된 양식 경로와 공식 출처 대조는 `corporate-registration-consulting/templates/official-form-sources.md`, 제출 체크리스트는 `corporate-registration-consulting/templates/incorporation-document-pack.md`를 기준으로 한다.
반드시 포함할 항목:
- 주식회사설립등기신청서(발기설립): `templates/official/form-65-1-stock-company-incorporation-promoter.hwp`
- 정관, 주식발행사항 동의/상법 제291조 사항 증명정보, 주식인수증, 발기인회의사록, 주주명부, 조사보고서, 취임승낙서, 이사회의사록, 인감신고서, 위임장: `templates/attachment-hwp/*.hwp`
- 등록면허세 영수필확인서, 등기신청수수료 영수필확인서
- 등기이사 개인 인감증명서 또는 본인서명사실확인서
- 등기이사 주민등록초본/등본 등 주소 확인 증빙
- 주금납입/잔고증명
조건부로 명의개서대리인, 현물출자·재산인수 등 변태설립사항, 인허가 업종, 정관 공증·의사록 인증 필요 여부를 확인한다.
## 중점 확인
- 정관 제2조 목적에는 실제 사업 업태·종목을 채운다.
- 정관 맨 마지막 작성일자, 발기인 성명, 서명/기명날인, 여러 장 문서의 간인을 확인한다.
- 인감신고서 제출을 위해 실제 법인인감 도장을 준비하도록 안내한다.
- 등록면허세·지방교육세·과밀억제권역/대도시 중과는 지방세법 제28조 및 위택스/관할 지자체 결과를 기준으로 확인한다.
- 소프트웨어 업종은 조세특례제한법 제6조 등 감면/중과 제외 가능성을 체크하되 확정하지 않는다.
## 면책
이 기능은 참고용이며 법률·세무 자문, 법무사 대행이 아니다. 에이전트는 인터넷등기소/위택스 로그인, 전자서명, 세금 납부, 등기 제출, 사용자 사칭, 최종 법률 판단, 최종 세무 판단을 지원하지 않는다. 실제 제출은 사용자가 직접 수행하고 제출 전 관할 등기소·위택스/지자체·법무사·변호사·세무사 확인을 권한다.

View file

@ -2,9 +2,9 @@
## 이 기능으로 할 수 있는 일
[retention-corp/coupang_partners](https://github.com/retention-corp/coupang_partners)의 로컬 Coupang MCP 호환 레이어를 이용해 쿠팡 상품 조회 도구를 실행한다. 기존 HF Space 기반 `coupang-mcp` 서버 대신 upstream 저장소의 `bin/coupang_mcp.py``local://coupang-mcp` 계약으로 호출한다.
[coupang-mcp](https://github.com/uju777/coupang-mcp) 서버를 통해 쿠팡 상품을 검색하고 실시간 가격을 확인한다.
- 키워드 상품 검색
- 키워드 상품 검색 (로켓배송/일반배송 구분)
- 로켓배송 전용 필터 검색
- 가격대 범위 검색
- 상품 비교표 생성
@ -14,153 +14,90 @@
## 동작 방식
```
Codex/Claude Code → coupang_partners_mcp.py → retention-corp/coupang_partners checkout → bin/coupang_mcp.py → local://coupang-mcp
├─ operator: Coupang Partners API (local HMAC)
└─ credentialless: hosted fallback → https://a.retn.kr/v1/public/assist
Claude Code → MCP JSON-RPC → HF Space (coupang-mcp) → Netlify 프록시 (도쿄) → 다나와/쿠팡
```
- **구형 hosted endpoint 제거** — 이전 HF Space 기반 MCP 서버를 사용하지 않는다.
- **upstream 고정** — 래퍼는 `https://github.com/retention-corp/coupang_partners.git`를 clone/update한 뒤 upstream CLI에 위임한다.
- **이중 실행 경로**`COUPANG_ACCESS_KEY`/`COUPANG_SECRET_KEY`가 둘 다 있으면 upstream이 로컬 HMAC으로 Coupang Partners API를 호출하고, 없으면 자동으로 Retention Corp의 hosted 백엔드(`https://a.retn.kr/v1/public/assist`)로 떨어져 공개 추천/검색을 반환한다(hosted fallback). 래퍼는 두 경로를 자동 선택한다.
- **allowlist** — hosted fallback은 `X-OpenClaw-Client-Id` allowlist로 게이트되어 있다. upstream이 기본으로 실어 보내는 `openclaw-skill` 값이 현재 Retention Corp allowlist에 등록되어 있어 credentialless 호출이 200을 받는다. k-skill 래퍼는 이 기본값을 그대로 사용하고 `OPENCLAW_SHOPPING_CLIENT_ID`를 오버라이드하지 않는다. Retention Corp 측 allowlist 정책이 바뀌면 그때 맞춰 가이드를 갱신한다.
- **secret은 runtime 환경변수** — 운영자 모드에서는 `COUPANG_ACCESS_KEY`, `COUPANG_SECRET_KEY`를 환경변수로 주입한다. 키를 저장소나 답변에 노출하지 않는다.
- **계약 확인 우선**`tools`/`init` 명령으로 로컬 MCP 호환 도구 목록과 JSON-RPC payload 형태를 먼저 확인한다.
- **API 키 불필요** — coupang-mcp가 다나와 가격 조회를 1차로, 쿠팡 API를 폴백으로 사용
- 해외 IP 차단 우회를 위해 도쿄 리전 Netlify 프록시 경유
## MCP 계약
## MCP 엔드포인트
```
local://coupang-mcp
https://yuju777-coupang-mcp.hf.space/mcp
```
프로토콜 호환 버전: MCP `2025-03-26`. 네트워크 Streamable HTTP 서버가 아니라 upstream 저장소의 로컬 CLI가 같은 도구 이름을 제공한다.
## 환경변수
| 환경변수 | 역할 | 기본값 |
|---------|------|--------|
| `COUPANG_ACCESS_KEY`, `COUPANG_SECRET_KEY` | 운영자 Coupang Partners API 크리덴셜. 둘 다 있을 때만 로컬 HMAC 경로가 활성화된다. | 없음 (없으면 hosted fallback) |
| `OPENCLAW_SHOPPING_CLIENT_ID` | hosted fallback의 `X-OpenClaw-Client-Id`. upstream이 `openclaw-skill`을 기본으로 실어 보내며 이 값이 현재 Retention Corp allowlist에 등록되어 있다. k-skill 래퍼는 이 변수를 오버라이드하지 않는다. | `openclaw-skill` |
| `OPENCLAW_SHOPPING_FORCE_HOSTED` | `1`이면 키가 있어도 hosted 경로를 강제한다. | 비어있음 |
| `OPENCLAW_SHOPPING_BASE_URL` | hosted 백엔드 base URL 오버라이드. 스테이징/로컬 backend 테스트용. | `https://a.retn.kr` |
k-skill 쪽 래퍼(`coupang_partners_mcp.py`)는 위 환경변수를 **오버라이드/디폴트 설정 없이 그대로 upstream에 전달**한다. 사용자가 export한 값이 최종 결정을 가져간다.
프로토콜: MCP Streamable HTTP (JSON-RPC 2.0)
## 사용 가능한 도구
| 도구명 | CLI 명령 | 기능 | 사용 예시 |
|--------|----------|------|----------|
| `search_coupang_products` | `search` | 일반 상품 검색 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py search "맥북"` |
| `search_coupang_rocket` | `rocket` | 로켓배송만 필터링 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py rocket "에어팟"` |
| `search_coupang_budget` | `budget` | 가격대 범위 검색 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py budget "키보드" --max-price 100000` |
| `compare_coupang_products` | `compare` | 상품 비교표 생성 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py compare "아이패드 vs 갤럭시탭"` |
| `get_coupang_recommendations` | `recommendations` | 인기 검색어 제안 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py recommendations --category 전자제품` |
| `get_coupang_seasonal` | `seasonal` | 계절/상황별 추천 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py seasonal "설날 선물"` |
| `get_coupang_best_products` | `best` | 카테고리별 베스트 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py best --category-id 1016` |
| `get_coupang_goldbox` | `goldbox` | 당일 특가 정보 | `python3 coupang-product-search/scripts/coupang_partners_mcp.py goldbox --limit 10` |
주의: `get_coupang_goldbox``get_coupang_best_products`는 upstream 기준 Coupang Partners API 권한이 필요한 경로이므로, 키 없이 hosted fallback으로만 실행 중이면 실패할 수 있다. 실패 메시지를 그대로 전달하고 hosted fallback이 커버하는 `search`/`rocket`/`budget`/`compare`로 우회 제안한다.
| 도구명 | 기능 | 사용 예시 |
|--------|------|----------|
| `search_coupang_products` | 일반 상품 검색 | "맥북 검색해줘" |
| `search_coupang_rocket` | 로켓배송만 필터링 | "로켓배송 에어팟 찾아줘" |
| `search_coupang_budget` | 가격대 범위 검색 | "10만원 이하 키보드" |
| `compare_coupang_products` | 상품 비교표 생성 | "아이패드 vs 갤럭시탭" |
| `get_coupang_recommendations` | 인기 검색어 제안 | "요즘 뭐가 인기야?" |
| `get_coupang_seasonal` | 계절/상황별 추천 | "설날 선물 추천" |
| `get_coupang_best_products` | 카테고리별 베스트 | "전자제품 베스트" |
| `get_coupang_goldbox` | 당일 특가 정보 | "오늘 특가 뭐있어?" |
## 기본 흐름
1. 검색어를 받는다. 너무 넓으면 용도/예산/브랜드를 먼저 물어본다.
2. `tools``init` 명령으로 retention-corp/coupang_partners 로컬 MCP 도구 목록과 handshake payload를 확인한다.
3. 요청에 맞는 CLI 명령을 실행한다(키가 없어도 hosted fallback으로 `search`/`rocket`/`budget`/`compare`는 작동한다).
4. `data.result`를 읽고 로켓배송/일반배송을 구분하여 정리한다.
5. 상위 3~5개 추천과 함께 가격/배송 정보, 변동 가능성, affiliate 고지를 제공한다.
2. MCP 세션을 초기화한다 (`initialize``Mcp-Session-Id` 확보).
3. `tools/call`로 적절한 도구를 호출한다.
4. 결과를 로켓배송/일반배송으로 구분하여 정리한다.
5. 상위 3~5개 추천과 함께 가격/배송 정보를 제공한다.
## 호출 예시
```bash
# 1. 최초 실행: upstream checkout을 자동 clone하고 도구 목록 확인
python3 coupang-product-search/scripts/coupang_partners_mcp.py tools
python3 coupang-product-search/scripts/coupang_partners_mcp.py init
# 1. 세션 초기화
curl -s -X POST "https://yuju777-coupang-mcp.hf.space/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-03-26",
"capabilities":{},
"clientInfo":{"name":"k-skill","version":"1.0"}
}}'
# → 응답 헤더에서 Mcp-Session-Id 확보
# 2. 이미 clone된 upstream을 명시해서 네트워크 없이 계약 확인
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
--repo-dir ~/.cache/k-skill/coupang_partners \
--no-clone \
tools
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
--repo-dir ~/.cache/k-skill/coupang_partners \
--no-clone \
init
# 3. 기존 checkout을 fast-forward로 최신화한 뒤 계약 확인
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
--repo-dir ~/.cache/k-skill/coupang_partners \
--update \
tools
# 4. 상품 검색 (키 없이도 hosted fallback으로 동작)
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "생수"
# 5. 로켓배송 필터
python3 coupang-product-search/scripts/coupang_partners_mcp.py rocket "에어팟"
# 6. hosted fallback 강제 (upstream 기본 allowlist client-id 유지)
OPENCLAW_SHOPPING_FORCE_HOSTED=1 \
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "무선청소기"
# 2. 상품 검색
curl -s -X POST "https://yuju777-coupang-mcp.hf.space/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
"name":"search_coupang_products",
"arguments":{"keyword":"생수"}
}}'
```
## 결과 형식
upstream CLI는 다음과 같은 JSON envelope를 반환한다.
```json
{
"ok": true,
"data": {
"session_id": "session-...",
"tool": "search_coupang_products",
"payload": {
"jsonrpc": "2.0",
"result": {
"content": [
{"type": "text", "text": "[...]"}
]
}
},
"result": []
}
}
```
- hosted fallback 경로는 각 상품의 `productUrl``https://a.retn.kr/s/...` 형태의 short deeplink를 붙여 돌려준다.
- operator (로컬 HMAC) 경로는 `https://link.coupang.com/...?lptag=AF...` 형태의 직접 딥링크를 돌려준다.
- 두 경로 모두 Retention Corp의 쿠팡 파트너스 채널로 트래킹된다. affiliate 고지를 반드시 포함한다.
사용자 답변은 짧은 비교표 형태로 정리한다.
```
## rocket (상위 후보)
## rocket (6)
1) LG전자 4K UHD 모니터
가격: 397,750원 (참고용)
보러가기: https://a.retn.kr/s/...
옵션: 80cm / 32UR500K
가격: 397,750원 (39만원대)
보러가기: https://link.coupang.com/a/...
## normal (상위 후보)
## normal (4)
1) 삼성전자 QHD 오디세이 G5 게이밍 모니터
가격: 283,000원 (참고용)
보러가기: https://a.retn.kr/s/...
가격: 283,000원 (28만원대)
보러가기: https://link.coupang.com/a/...
```
## Affiliate 고지 (필수)
hosted fallback이 반환하는 응답에는 `disclosure` 필드가 포함될 수 있다. 예시 문구: `"파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"`. 이 문구가 오면 **답변에 그대로 노출**한다. 응답에 disclosure가 없더라도 `a.retn.kr/s/` shortlink나 `lptag=AF` 딥링크가 포함되는 이상 같은 취지의 문구를 반드시 포함한다.
## 제한사항
- 가격/품절/배송 정보는 실시간으로 바뀔 수 있다.
- 가격은 참고용이다. 다나와 조회 실패 시 쿠팡 API 추정가가 표시된다.
- 로그인, 장바구니, 결제 자동화는 지원하지 않는다.
- hosted fallback(`https://a.retn.kr/v1/public/assist`)은 allowlist로 게이트되어 있어 upstream 정책에 따라 응답 형태나 client-id 요구가 바뀔 수 있다.
- `goldbox`/`best-products` 등 Coupang Partners API 권한이 필요한 도구는 hosted fallback에서는 대체되지 않으므로 실패할 수 있다.
- upstream checkout이 없고 네트워크 clone도 막힌 환경에서는 `--repo-dir`로 기존 checkout을 지정해야 한다.
- MCP 서버(HF Space)가 다운되면 일시적으로 사용 불가하다.
## 출처
- [retention-corp/coupang_partners GitHub](https://github.com/retention-corp/coupang_partners)
- 로컬 MCP 계약: `local://coupang-mcp`
- hosted fallback endpoint: `https://a.retn.kr/v1/public/assist`
- hosted fallback을 upstream에 추가한 PR: <https://github.com/retention-corp/coupang_partners/pull/1> (merged)
- 래퍼: `coupang-product-search/scripts/coupang_partners_mcp.py`
- [coupang-mcp GitHub](https://github.com/uju777/coupang-mcp)
- MCP 엔드포인트: `https://yuju777-coupang-mcp.hf.space/mcp`

View file

@ -1,112 +0,0 @@
# 법원 경매 부동산 매각공고 조회
대한민국 법원이 운영하는 공식 **법원경매정보** 사이트(`courtauction.go.kr`) 의 매각공고와 사건정보를 에이전트가 활용할 수 있는 JSON 형태로 변환해서 돌려준다.
> **참고용입니다.** 실제 입찰 전에는 반드시 해당 법원의 원문 매각공고와 매각물건명세서를 직접 확인하세요. 본 스킬은 read-only이며, 입찰서 자동 작성·자동 제출은 지원하지 않습니다.
## 무엇을 할 수 있나
- ✅ Workflow A — **매각공고 브라우징**: 매각기일·법원·기일/기간 입찰을 조건으로 매각공고 목록 → 그 공고 안의 사건번호·용도·주소·감정평가액·최저매각가격 펼치기
- ✅ Workflow B — **사건번호 직접 조회**: 법원사무소코드 + 사건번호(`2024타경100001`) → 사건정보·물건내역·매각기일별 이력·배당요구종기
- ✅ Workflow C — **부동산 물건 자유 조건검색**: 지역·용도·가격대·면적·유찰횟수·매각기일 조건 → 물건 목록 JSON
- ✅ 법원사무소 코드(60+개) + 입찰구분 코드(기일입찰=`000331`, 기간입찰=`000332`) + Workflow C용 대표 용도/지역 코드 변환
- ✅ 2-tier transport — direct HTTP 1차, Playwright fallback 옵션
- ✅ 안티봇 가드 — 호출 간 ≥2초 jitter, 세션당 호출 budget, `data.ipcheck === false` 즉시 `BLOCKED` throw
## 무엇을 할 수 없나 (별도 follow-up 이슈)
- ❌ Workflow D 일별/월별 캘린더
- ❌ 매각물건 사진(전경/개황/내부) URL 노출
- ❌ 매각물건명세서·현황조사서·감정평가서 PDF 다운로드
- ❌ 동산(자동차·중기) 경매
## 차단(BLOCKED) 정책
`courtauction.go.kr` 은 자동화 호출에 매우 민감해서 빠른 연속 조회 시 IP가 약 1시간 차단됩니다. 본 스킬은 다음과 같이 보수적으로 동작합니다.
- 호출 간 최소 2초 + jitter 0~1초 대기 (override: `--min-delay-ms 3000`)
- 세션당 호출 budget 10회 (override: `--max-calls 5`)
- `data.ipcheck === false` 또는 응답 메시지에 "차단" 포함 시 → `BLOCKED` 에러를 즉시 throw, **자동 재시도 금지** (차단 연장 위험)
차단되면 같은 IP에서 약 1시간을 기다려야 합니다. 그 사이에는 다른 IP 또는 사람이 직접 사이트에 접속해서 차단 해제 화면을 거칩니다.
## CLI 사용
```bash
court-auction-notice-search -h
court-auction-notice-search codes courts --pretty | head -40
court-auction-notice-search codes bid-types --pretty
court-auction-notice-search codes usages --pretty
court-auction-notice-search codes regions --pretty
court-auction-notice-search notices --date 2026-04 --court-code B000210 --bid-type date --pretty
court-auction-notice-search search --sido 서울특별시 --sigungu 11680 --usage-large 건물 --usage-medium 21200 \
--price-min 100000000 --price-max 500000000 --sale-from 2026-05-01 --sale-to 2026-05-20 --pretty
court-auction-notice-search case --court-code B000210 --case-number "2024타경100001" --pretty
```
## Node.js 사용
```js
const {
searchSaleNotices,
getSaleNoticeDetail,
getCaseByCaseNumber,
searchProperties
} = require("court-auction-notice-search");
const notices = await searchSaleNotices({
date: "2026-04", // 월 전체 조회. 일자 입력은 같은 월 조회 후 해당일만 필터링
courtCode: "B000210",
bidType: "date"
});
if (notices.items.length > 0) {
const detail = await getSaleNoticeDetail(notices.items[0]);
for (const item of detail.items) {
console.log(item.caseNumber, item.usage, item.address);
console.log(" 감정 ", item.appraisedPrice, "최저 ", item.minimumSalePrice);
}
}
const caseInfo = await getCaseByCaseNumber({
courtCode: "B000210",
caseNumber: "2024타경100001"
});
const properties = await searchProperties({
region: { sido: "서울특별시", sigungu: "11680", dong: "11680101" },
usage: { large: "건물" },
priceRange: { min: 100000000, max: 500000000 },
saleDate: { from: "2026-05-01", to: "2026-05-20" },
flbdCount: { min: 1 },
page: 1,
pageSize: 20
});
```
## 사이트 내부 endpoint (직접 캡처한 것)
| 목적 | 메소드 + 경로 | request body |
| --- | --- | --- |
| 매각공고 목록 | `POST /pgj/pgj143/selectRletDspslPbanc.on` | `{"dma_srchDspslPbanc":{"srchYmd","cortOfcCd","bidDvsCd","srchBtnYn":"Y"}}` (`srchYmd`는 사이트 검색 버튼과 동일하게 `YYYYMM`) |
| 매각공고 상세 | `POST /pgj/pgj143/selectRletDspslPbancDtl.on` | `{"dma_srchGnrlPbanc":{"cortOfcCd","dspslDxdyYmd","jdbnCd",...}}` |
| 사건 단건 | `POST /pgj/pgj15A/selectAuctnCsSrchRslt.on` | `{"dma_srchCsDtlInf":{"cortOfcCd","csNo"}}` |
| 물건 자유 조건검색 | `POST /pgj/pgjsearch/searchControllerMain.on` | canonical body captured via Playwright (`scripts/capture-pgj151-submit.cjs`); fixture at `packages/court-auction-notice-search/test/fixtures/canonical-search-body.json`. `pageNo/pageSize/statNum` 은 number, `pageSize` 는 upstream 드롭다운 값 `10`/`20`/`50`/`100`만 허용, `notifyLoc` 기본 `"off"`. |
| 법원사무소 코드 | `POST /pgj/pgjComm/selectCortOfcCdLst.on` | `{}` |
세션 cookie(`JSESSIONID`, `WMONID`)는 endpoint별 진입 화면을 먼저 열어 받아둡니다. 매각공고/상세는 `GET /pgj/index.on?w2xPath=/pgj/ui/pgj100/PGJ143M01.xml&pgjId=143M01`, 물건 자유 조건검색(Workflow C)은 `GET /pgj/index.on?w2xPath=/pgj/ui/pgj100/PGJ151F00.xml&pgjId=151F00` 으로 warmup 합니다.
## 설치
```bash
npm install court-auction-notice-search
# Playwright fallback 을 쓰려면 (선택)
npm install rebrowser-playwright # 권장
# 또는
npm install playwright-core
```
## 관련 이슈
- 이 패키지는 [Issue #167](https://github.com/NomaDamas/k-skill/issues/167) 에서 출발했고, #184에서 Workflow C 자유 조건검색을 추가했습니다.
- 캘린더·물건 사진·PDF·동산 경매는 별도 follow-up 이슈로 분리되어 추적됩니다.

View file

@ -1,43 +0,0 @@
# 당근중고차 검색 가이드 (`daangn-cars-search`)
당근중고차 공개 웹 데이터 표면을 사용해 지역·키워드·가격 조건 기반 차량을 검색하고, 개별 차량 상세를 읽기 전용으로 확인하는 스킬입니다.
## 사용 시나리오
- "당근중고차 합정동 레이 찾아봐"
- "당근에서 천만원 이하 중고차 검색해줘"
- "이 당근 중고차 URL 상세 요약해줘"
## 구현 표면
브라우저 자동화, 로그인, 채팅, 문의, 구매 자동화를 사용하지 않습니다.
1. 지역 해석: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
2. 검색: `https://www.daangn.com/kr/cars/?in=<지역명>-<id>&onlyOnSale=1&_data=routes/kr.cars._index`
3. 상세: `<차량 URL>?_data=routes%2Fkr.cars.%24car_post_id`
## 로컬 실행
```bash
python3 daangn-cars-search/scripts/daangn_cars.py search "레이" --region "합정동" --limit 5
python3 daangn-cars-search/scripts/daangn_cars.py search --region "합정동" --price-max 10000000 --limit 5
python3 daangn-cars-search/scripts/daangn_cars.py detail "https://www.daangn.com/kr/cars/.../"
```
## 지역 필터
지역명은 당근 region API로 내부 id를 해석한 뒤 `in=<지역명>-<id>` 형태로 검색 URL에 넣습니다.
```text
합정동 → 서울특별시 마포구 합정동, id=231 → in=합정동-231
```
## 출력 해석
검색 결과는 `title`, `price`, `price_text`, `region`, `status`, `driveDistance`, `carData`, `chatRoomCount`, `url`을 우선 확인합니다. 차량 연식, 주행거리, 사고/정비 이력처럼 원문 의존도가 높은 정보는 상세 조회의 `carPost` 원문을 함께 확인합니다.
## 제한사항
- 공개 Remix `_data` route 이름이나 JSON shape가 바뀌면 실패할 수 있습니다.
- 문의, 시승 예약, 구매, 결제, 채팅 자동화는 실행하지 않습니다.
- 가격·판매 상태는 실시간으로 바뀔 수 있어 원문 URL을 함께 제시합니다.

View file

@ -1,42 +0,0 @@
# 당근알바 검색 가이드 (`daangn-jobs-search`)
당근알바 공개 웹 데이터 표면을 사용해 키워드·지역 기반 알바 공고를 검색하고, 개별 공고 상세를 읽기 전용으로 확인하는 스킬입니다.
## 사용 시나리오
- "당근알바 합정동 카페 알바 찾아봐"
- "홍대 근처 주말 알바 검색해줘"
- "이 당근알바 공고 상세 요약해줘"
## 구현 표면
브라우저 자동화, 로그인, 채팅, 지원, 문의 자동화를 사용하지 않습니다.
1. 지역 해석: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
2. 검색: `https://www.daangn.com/kr/jobs/?in=<지역명>-<id>&search=<키워드>&_data=routes/kr.jobs._index`
3. 상세: `<공고 URL>``jobs.daangn.com/job-posts/<id>` 공개 HTML의 title/meta/JSON-LD(헬퍼는 legacy `_data`를 먼저 시도 후 빈 응답이면 HTML 메타로 fallback)
## 로컬 실행
```bash
python3 daangn-jobs-search/scripts/daangn_jobs.py search "카페" --region "합정동" --limit 5
python3 daangn-jobs-search/scripts/daangn_jobs.py detail "https://www.daangn.com/kr/jobs/.../"
```
## 지역 필터
지역명은 당근 region API로 내부 id를 해석한 뒤 `in=<지역명>-<id>` 형태로 검색 URL에 넣습니다.
```text
합정동 → 서울특별시 마포구 합정동, id=231 → in=합정동-231
```
## 출력 해석
검색 결과는 `title`, `company`, `region`, `address`, `salary`, `salaryType`, `workDays`, `workTimeStart`, `workTimeEnd`, `closed`, `url`을 우선 확인합니다. 상세 조회는 가능하면 `jobPost` 원문을 사용하고, 공개 `_data`가 빈 응답이면 HTML title/meta/JSON-LD를 근거로 정리합니다.
## 제한사항
- 공개 Remix `_data` route 이름이나 JSON shape가 바뀌면 실패할 수 있습니다.
- 마감·삭제·비공개 전환된 공고는 상세 조회가 실패할 수 있습니다.
- 지원, 채팅, 문의, 개인정보 제출 자동화는 범위 밖입니다.

View file

@ -1,43 +0,0 @@
# 당근부동산 검색 가이드 (`daangn-realty-search`)
당근부동산 공개 웹 데이터 표면을 사용해 지역 기반 부동산 매물 후보를 검색하고, 상세 페이지의 공개 메타를 읽기 전용으로 확인하는 스킬입니다.
## 사용 시나리오
- "당근부동산 합정동 월세 매물 찾아봐"
- "마포구 전세 후보 당근에서 봐줘"
- "이 당근부동산 URL 상세 요약해줘"
## 구현 표면
브라우저 자동화, 로그인, 채팅, 문의, 예약, 계약 자동화를 사용하지 않습니다.
1. 지역 해석: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
2. 검색: `https://www.daangn.com/kr/realty/?in=<지역명>-<id>&_data=routes/kr.realty._index`
3. 상세: `https://realty.daangn.com/articles/<id>``application/ld+json``<title>`
## 로컬 실행
```bash
python3 daangn-realty-search/scripts/daangn_realty.py search --region "합정동" --limit 5
python3 daangn-realty-search/scripts/daangn_realty.py search --region "합정동" --sales-type "APARTMENT" --trade-type "MONTHLY_RENT"
python3 daangn-realty-search/scripts/daangn_realty.py detail "https://realty.daangn.com/articles/..."
```
## 지역 필터
지역명은 당근 region API로 내부 id를 해석한 뒤 `in=<지역명>-<id>` 형태로 검색 URL에 넣습니다.
```text
합정동 → 서울특별시 마포구 합정동, id=231 → in=합정동-231
```
## 출력 해석
검색 결과는 `title`, `salesType`, `trade`, `area`, `areaPyeong`, `totalManageCost`, `url`을 우선 확인합니다. 부동산 판단에는 실시간 상태, 보증금/월세, 관리비, 면적, 중개/직거래 여부가 중요하므로 원본 URL을 함께 제시합니다.
## 제한사항
- 당근부동산 목록 JSON과 `realty.daangn.com` 상세 HTML 구조 변경에 영향을 받습니다.
- 문의, 방문 예약, 계약, 결제, 채팅은 실행하지 않습니다.
- 공고 내용은 실시간 상태와 달라질 수 있어 최종 판단 전 원문 확인이 필요합니다.

View file

@ -1,45 +0,0 @@
# 당근 중고거래 검색 가이드 (`daangn-used-goods-search`)
당근 중고거래 공개 웹 데이터 표면을 사용해 키워드·지역 기반 매물을 검색하고, 개별 매물 상세를 읽기 전용으로 확인하는 스킬입니다.
## 사용 시나리오
- "당근에서 합정동 맥북 매물 찾아봐"
- "이 당근 중고거래 URL 상세 요약해줘"
- "아이폰 15 Pro 중고 매물 중 판매중인 것만 봐줘"
## 구현 표면
브라우저 자동화, 로그인, 채팅, 찜, 거래 제안, 구매 자동화를 사용하지 않습니다.
1. 지역 해석: `https://www.daangn.com/kr/api/v1/regions/keyword?keyword=<지역명>`
2. 검색: `https://www.daangn.com/kr/buy-sell/all/?in=<지역명>-<id>&search=<키워드>&only_on_sale=true&_data=routes/kr.buy-sell._index`
3. 상세: `<매물 URL>?_data=routes%2Fkr.buy-sell.%24buy_sell_id`
## 로컬 실행
```bash
python3 daangn-used-goods-search/scripts/daangn_used_goods.py search "맥북" --region "합정동" --limit 5
python3 daangn-used-goods-search/scripts/daangn_used_goods.py detail "https://www.daangn.com/kr/buy-sell/.../"
```
## 지역 필터
지역명은 바로 URL에 넣지 않고 당근 region API로 내부 id를 먼저 조회합니다.
```text
합정동 → 서울특별시 마포구 합정동, id=231 → in=합정동-231
```
동일 지명이 여러 곳에 있으면 정확 일치 후보, 서울 동 단위 후보, 첫 번째 후보 순으로 선택합니다. 결과에는 적용 지역(`effective_region`)과 원본 URL을 함께 남깁니다.
## 출력 해석
검색 결과는 `title`, `price`, `price_text`, `status`, `region`, `url` 중심으로 1차 후보를 고릅니다. 조회수, 채팅수, 설명 같은 상세 판단은 상세 조회 결과의 `product` 원문을 확인한 뒤 정리합니다.
## 제한사항
- 공개 Remix `_data` route 이름이나 JSON shape가 바뀌면 실패할 수 있습니다.
- 삭제·판매완료·비공개 전환된 글은 상세 조회가 실패할 수 있습니다.
- CAPTCHA, 로그인벽, 봇 차단이 나오면 실패 모드로 보고하고 우회하지 않습니다.
- 상대방에게 영향을 주는 채팅, 찜, 거래 제안, 구매 자동화는 범위 밖입니다.

View file

@ -1,45 +0,0 @@
# 대신증권 리포트 조회 가이드
`daishin-report-search``jay-jo-0/github_pages_repo` GitHub Pages 미러에 올라오는 대신증권 리포트 HTML을 최신순으로 찾고 원문/설명 페이지를 JSON으로 정리하는 조회 전용 스킬이다.
## 공개 접근 경로
- 목록: `https://api.github.com/repos/jay-jo-0/github_pages_repo/git/trees/main?recursive=1`
- 원문 HTML: `https://raw.githubusercontent.com/Jay-jo-0/github_pages_repo/main/<YYYYMMDDHHMMSS.html>`
- exact-file fallback: `https://api.github.com/repos/jay-jo-0/github_pages_repo/contents/<YYYYMMDDHHMMSS.html>?ref=main`
- 브라우저 URL: `https://jay-jo-0.github.io/github_pages_repo/<YYYYMMDDHHMMSS.html>`
- 설명 페이지: `<YYYYMMDDHHMMSS_explain.html>`이 있을 때만 제공
파일명 timestamp를 KST 게시 추정 시각으로 표시한다. GitHub API와 raw 파일은 공개 unauthenticated endpoint라서 proxy를 쓰지 않는다.
## 사용 예시
```bash
node packages/daishin-report-search/src/cli.js --limit 10
GITHUB_TOKEN=... node packages/daishin-report-search/src/cli.js --limit 10
node packages/daishin-report-search/src/cli.js 반도체 --limit 5 --max-inspect 100
node packages/daishin-report-search/src/cli.js --id 20260511082352 --include-explain
```
```js
const { listReports, fetchReport } = require("daishin-report-search")
const latest = await listReports({ limit: 10 })
const semis = await listReports({ query: "반도체", limit: 5, maxInspect: 100 })
const withToken = await listReports({ githubToken: process.env.GITHUB_TOKEN })
const detail = await fetchReport("20260511082352", { includeExplain: true })
```
## 출력 필드
목록 항목은 `id`, `date`, `time`, `timestamp`, `title`, `headings`, `excerpt`, `ratingTargets`, `pageUrl`, `rawUrl`, `apiUrl`, `hasExplain`, `explainUrl`을 포함한다.
상세 조회는 원문 `text`를 추가하고, `includeExplain`이 켜져 있으면 `explain` 객체에 설명 페이지의 `title`, `headings`, `text`, `excerpt`, `pageUrl`을 포함한다.
## 주의 사항
- 투자 판단이나 매매 추천이 아니라 공개 리포트 조회 보조 기능이다.
- GitHub unauthenticated API rate limit, upstream repository 변경, HTML 구조 변경 시 경고나 오류가 반환될 수 있다. 목록 조회의 GitHub tree API가 403/429로 막히면 예외 대신 빈 `items``source.error`/rate-limit metadata를 반환한다.
- API limit을 높여야 할 때는 caller-owned `githubToken`/`githubHeaders` 옵션 또는 CLI 환경변수 `DAISHIN_GITHUB_TOKEN`/`GITHUB_TOKEN`을 사용할 수 있다. 이 값은 GitHub API host(tree discovery와 exact-file fallback)에만 전송되고 raw 원문 URL에는 전송되지 않는다. 기본 동작에는 토큰이나 proxy가 필요 없다.
- 상세 조회는 raw 원문 URL을 먼저 읽고, 실패하면 알려진 timestamp 경로의 GitHub contents API로 fallback한다.
- 검색어가 있으면 최신 파일부터 `maxInspect`개까지 원문을 읽어 매칭하므로 너무 낮게 잡으면 결과가 누락될 수 있다.

View file

@ -4,14 +4,8 @@
- 다이소 매장명으로 공식 매장 후보 찾기
- 상품명/검색어로 공식 상품 후보 찾기
- 특정 매장의 **매장 픽업 재고 수량** 확인 (Bearer 토큰 인증 기반 공식 `selStrPkupStck` 표면)
- 필요하면 `referenceOnly: true` 온라인 재고 참고값 함께 확인
## 이 기능으로 할 수 없는 일 (스킬 범위 한계)
- 매장 내 진열 위치(aisle/매대)는 공식 표면이 제공하지 않으므로 답하지 않습니다.
- 결제·주문·픽업 예약 자동화는 범위가 아닙니다.
- 비공식 크롤링·헤드리스 브라우저 우회·계정 세션 재사용은 범위가 아닙니다.
- 특정 매장의 **매장 픽업 재고** 확인
- 필요하면 온라인 재고 참고값 함께 확인
## 먼저 필요한 것
@ -33,9 +27,7 @@
- store detail: `https://www.daisomall.co.kr/api/dl/dla-api/selStrInfo`
- product search list: `https://www.daisomall.co.kr/ssn/search/SearchGoods`
- product summary list: `https://www.daisomall.co.kr/ssn/search/GoodsMummResult`
- auth (비로그인 JWT 발급): `https://www.daisomall.co.kr/api/auth/request`
- store pickup stock: `https://www.daisomall.co.kr/api/pd/pdh/selStrPkupStck` (Bearer 인증 필요)
- pickup eligibility fallback: `https://www.daisomall.co.kr/api/ms/msg/selPkupStr`
- store pickup stock: `https://www.daisomall.co.kr/api/pd/pdh/selStrPkupStck`
- optional online stock: `https://www.daisomall.co.kr/api/pdo/selOnlStck`
## 기본 흐름
@ -44,12 +36,9 @@
2. 상품명이 없으면 상품명/검색어를 한 번 더 물어봅니다.
3. `selStr` 로 매장 후보를 찾고, 필요하면 `selStrInfo` 로 매장 상세를 확인합니다.
4. `SearchGoods` 로 상품 후보를 찾습니다.
5. `GET /api/auth/request` 로 비로그인 JWT를 받아 AES-128-CBC / 키 `"PRE_AUTH_ENC_KEY"` 로 암호화한 뒤 Bearer 헤더를 빌드합니다.
6. `selStrPkupStck` 에 Bearer 헤더를 실어 해당 매장의 상품 재고를 확인합니다.
7. 403 응답이 오면 `/api/auth/request` 를 재호출해 Bearer를 새로 빌드한 뒤 한 번 재시도합니다.
8. Bearer 재시도 후에도 401/403이면 `pickupStock.retrievalStatus: "blocked"` 를 반환하고, 선택적으로 `selPkupStr` 기반 `pickupEligibility` 로 픽업 가능 여부를 보조 확인합니다.
9. 필요하면 `SearchGoods` 응답의 `onldPdNo` 를 함께 보존해 `selOnlStck` 온라인 재고 교차 확인에 사용합니다.
10. 공식 표면이 매장 내 위치를 주지 않으면 재고 중심으로 답합니다.
5. `selStrPkupStck` 로 해당 매장의 상품 재고를 확인합니다.
6. 필요하면 `SearchGoods` 응답의 `onldPdNo` 를 함께 보존해 `selOnlStck` 온라인 재고 교차 확인에 사용합니다.
7. 공식 표면이 매장 내 위치를 주지 않으면 재고 중심으로 답합니다.
## 예시
@ -82,20 +71,13 @@ main().catch((error) => {
- 상품 후보가 여러 개면 브랜드, 용량, 호수까지 같이 보여 주는 편이 덜 헷갈립니다.
- 재고 수량은 실시간 100% 보장값이 아니므로, 필요하면 `방문 직전 다시 확인` 문구를 같이 줍니다.
- 공식 표면이 매장 내 위치를 주지 않으면 `공식 표면에서는 매장 재고까지만 확인된다`고 답합니다.
- 매장 픽업 재고의 `status` 는 조회 결과 범주입니다. 상품 재고 여부는 `inStock` 또는 `inventoryStatus` 로 설명하고, `status: "available"` 만으로 재고가 있다고 말하지 않습니다.
- 인증 키(`PRE_AUTH_ENC_KEY`)는 JS 번들에 하드코딩되어 있으며 변경될 수 있습니다. 403이 지속되면 키가 교체된 것일 수 있습니다.
## 라이브 확인 메모
2026-03-27 기준으로 `selStrPkupStck` 는 실제 매장 픽업 재고를 반환했습니다.
2026-05-15 기준 Bearer 토큰 인증(`/api/auth/request` + AES-128-CBC)으로 정상 접근 가능합니다.
2026-03-27 기준으로 다음 공식 호출이 실제 응답을 반환했습니다.
현재 운영 원칙은 다음과 같습니다.
- `POST /api/ms/msg/selStr``강남역2호점` 매장 후보
- `GET /ssn/search/SearchGoods?searchTerm=리들샷...``1049275` 포함 상품 후보
- `POST /api/pd/pdh/selStrPkupStck``strCd=10224`, `pdNo=1049275` 조합의 매장 픽업 재고
- `POST /api/ms/msg/selStr` → 매장 후보 확인
- `GET /ssn/search/SearchGoods?searchTerm=...` → 상품 후보 및 `onldPdNo` 확인
- `GET /api/auth/request` → 비로그인 JWT 발급, 헤더 `x-dm-uid` 보존 (유효 30초)
- JWT를 AES-128-CBC / 키 `"PRE_AUTH_ENC_KEY"` 로 암호화 → `bearer = base64(IV) + base64(암호문)` 조합
- `POST /api/pd/pdh/selStrPkupStck` + `Authorization: Bearer <bearer>`, `X-DM-UID: <uid>` → 성공 시 `status: "available"`, `retrievalStatus: "resolved"`. 실제 재고 여부는 `inStock` / `inventoryStatus` 로 표시
- 403 → `/api/auth/request` 재호출 후 Bearer 재빌드 후 1회 재시도
- `POST /api/pdo/selOnlStck` → 가능한 경우 온라인 재고 참고값 표시
같은 날짜 smoke test 에서 `강남역2호점 + VT 리들샷 100` 조합은 재고 수량 `0` 으로 응답했습니다. 즉, **공식 경로가 실제로 동작함은 확인했지만 당시 해당 매장 재고는 없었습니다.**

View file

@ -1,55 +0,0 @@
# 다나와 최저가 비교 (`danawa-price-search`)
다나와 공개 검색/가격비교 표면을 사용해 상품 후보를 찾고, 쇼핑몰별 가격을 배송비 포함 실구매가 기준으로 비교하는 스킬입니다.
## 사용 시나리오
- "다나와에서 맥북 에어 M4 최저가 비교해줘"
- "이 다나와 pcode 쇼핑몰별 가격 표로 보여줘"
- "배송비랑 카드할인까지 포함해서 어디가 제일 싼지 봐줘"
## 구현 표면
브라우저 자동화나 로그인을 사용하지 않습니다.
1. 검색: `https://search.danawa.com/dsearch.php?query=...`
2. 상품 상세 확인: `https://prod.danawa.com/info/?pcode=...`
3. 쇼핑몰별 가격비교 AJAX: `https://prod.danawa.com/info/ajax/getAllPriceCompareMallList.ajax.php`
## 로컬 실행
```bash
python3 danawa-price-search/scripts/danawa_search.py search "맥북 에어 M4" --limit 5
python3 danawa-price-search/scripts/danawa_search.py offers 28208783 --limit 10
python3 danawa-price-search/scripts/danawa_search.py compare "갤럭시 S25" --limit 3 --offers 5
```
## 출력 해석
`offers``compare` 결과에는 다음 필드가 포함됩니다.
- `mall`: 쇼핑몰명
- `price`: 표시 가격
- `shipping_fee`: 배송비 숫자. 무료배송이면 `0`, 파싱 불가면 `null`
- `is_free_shipping`: 무료배송 여부
- `total_price`: 가격 + 배송비 기준 실구매가 후보
- `card_price`: 카드 적용 표시가
- `card_discount`: 표시가와 카드가 차액
- `installment`: 무이자 할부 문구
- `payment_badges`: Danawa가 가격 옆에 노출한 결제조건 배지의 표시 라벨 목록. 배지 텍스트가 비어 있고 `.ico.cash`처럼 클래스만 있는 경우도 정규화 라벨을 합성합니다 (예: `["현금"]`, `["쿠폰"]`, `["포인트"]`, `["카드"]`, `["할인"]`, `["멤버십"]`)
- `payment_condition_types`: 화이트리스트 배지를 정규화한 조건 타입 목록 (`cash`/`point`/`coupon`/`card`/`discount`/`membership`)
- `payment_condition_label`: 사용자 응답용 결제조건 라벨. 복수 조건이면 쉼표로 연결
- `cash_only` / `point_only` / `coupon_only` / `card_only_badge` / `discount_badge` / `membership_badge`: 각각 현금·포인트·쿠폰·특정 카드·할인·멤버십 조건 가격 여부
- `is_conditional_price`: `payment_condition_types`가 하나 이상 있으면 True. 일반 카드 결제로는 가격이 다르거나 적용 불가할 수 있음
- `url`: 다나와 경유 링크
`count`, `normal_count`, `conditional_count``limit` 적용 후 실제 반환된 `offers[]` 기준입니다.
사용자에게는 `total_price` 기준으로 정렬한 Markdown 표를 먼저 보여주고, 카드가는 별도 열에 표시합니다.
## 주의사항
- 다나와의 공개 HTML/AJAX 구조가 바뀌면 selector와 파싱 규칙을 갱신해야 합니다.
- 자동 구매, 로그인, CAPTCHA 우회, 결제 단계 자동화는 이 스킬의 범위가 아닙니다.
- 동일 상품명이라도 옵션/용량/모델명이 섞일 수 있으므로 검색 후보를 먼저 확인한 뒤 가격비교를 진행합니다.
- 결제조건 배지(현금/쿠폰/포인트/할인/특정 카드/멤버십 한정)는 사용자 응답 표에 반드시 `payment_condition_label` 기반 라벨로 표시해야 합니다. 정렬은 `total_price` 단일 기준이라 조건부 가격이 1위로 올라올 수 있고, 라벨이 없으면 카드 결제 사용자에게 적용 불가능한 가격을 일반 최저가로 안내하게 됩니다.

View file

@ -1,34 +0,0 @@
# 기부처 조회 가이드
`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 확인 보조 링크와 후보 공식 홈페이지를 함께 제시하며, 후보별 등록 검증이 이미 완료됐다고 표현하지 않는다.

View file

@ -1,65 +0,0 @@
# 근처 응급실 병상 상태 확인
`emergency-room-beds` 스킬은 사용자가 알려준 위치 기준으로 가까운 응급실을 찾고, E-Gen 공개 응급실 찾기 표면에서 제공하는 응급실/입원실 운영 상태 플래그를 정리한다.
## 핵심 원칙
- 위치를 자동 추적하지 않는다. 위치가 없으면 먼저 현재 위치를 질문한다.
- 데이터 출처는 NEMC/E-Gen 공개 페이지와 E-Gen nearby 응급실 목록 endpoint다.
- E-Gen nearby 목록은 응급실 운영 여부와 입원실/병상 운영 플래그를 제공하지만, 병원별 정확한 실시간 잔여 병상 수나 병상 가동률 수치를 제공하지 않는다.
- 긴급 상황에서는 결과와 별개로 119 또는 병원 대표전화 확인을 안내한다.
## 사용 예
```text
현재 위치를 알려주세요. 동네/역명/랜드마크/위도·경도 중 편한 형식으로 보내주시면 근처 응급실 상태를 찾아볼게요.
```
위치를 받으면 `emergency-room-beds` 패키지의 `searchNearbyEmergencyRoomsByLocationQuery()`를 사용한다.
## Node.js 예시
```js
const { searchNearbyEmergencyRoomsByLocationQuery } = require("emergency-room-beds");
async function main() {
const result = await searchNearbyEmergencyRoomsByLocationQuery("광화문", {
limit: 3,
radius: 5
});
console.log(result.anchor);
console.log(result.items.map((item) => ({
name: item.name,
distanceKm: item.distanceKm,
emergencyRoomOperating: item.bedStatus.emergencyRoomOperating,
inpatientBedsOperating: item.bedStatus.inpatientBedsOperating,
updatedAt: item.updatedAt,
phone: item.phone,
mapUrl: item.mapUrl
})));
console.log(result.meta.bedCountLimitation);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
```
## 응답 필드
- 병원명, 거리, 응급의료기관 등급, 병원 유형
- 응급실 운영 여부 (`emergencyRoomOperating`)
- 입원실/병상 운영 플래그 (`inpatientBedsOperating`)
- 권역외상센터/소아전문/소아야간진료 여부
- 주소, 대표전화, 갱신시각, 지도 링크
- 공개 데이터 한계 문구: 정확한 실시간 잔여 병상 수/가동률 미제공
## 참고 표면
- NEMC 모니터링: <https://dw.nemc.or.kr/nemcMonitoring/mainmgr/Main.do>
- E-Gen 응급실 찾기: <https://www.e-gen.or.kr/egen/search_emergency_room.do>
- E-Gen nearby endpoint: `https://www.e-gen.or.kr/egen/retrieve_emergency_room_list.do`
- Kakao Map 모바일 검색: `https://m.map.kakao.com/actions/searchView?q=<query>`
- Kakao Map 장소 패널 JSON: `https://place-api.map.kakao.com/places/panel3/<confirmId>`

View file

@ -1,40 +0,0 @@
# 고속버스 예매 가이드
## 이 기능으로 할 수 있는 일
- KOBUS 고속버스 터미널/노선 후보 확인
- 배차 시간표, 버스 등급, 잔여석, 요금 확인
- 좌석 선택 단계 진입 가능 여부 확인
- 필요한 경우 임시 좌석 선점 후 공식 결제정보 입력 페이지로 handoff
- 진행하지 않을 때 임시 선점 해제
## 먼저 필요한 것
- 별도 사용자 계정/비밀번호는 기본 조회·좌석 단계에서 필요하지 않음
- 결제는 공식 KOBUS 페이지에서 사용자가 직접 진행
- 브라우저 자동화보다 `https://www.kobus.co.kr` 공식 HTTP 흐름을 우선 사용
## 입력값
- 출발 터미널
- 도착 터미널
- 날짜: `YYYYMMDD`
- 희망 시간대
- 인원 수와 좌석 선호
## 기본 흐름
1. 쿠키 jar를 만들고 KOBUS 메인/예매 페이지를 열어 세션을 시작한다.
2. `POST /mrs/readRotLinInf.ajax` 로 터미널/노선 코드를 확인한다.
3. `POST /mrs/alcnSrch.do` 로 배차를 조회한다.
4. 결과 HTML의 `fnSatsChc(...)` 인자를 파싱해 후보를 정리한다.
5. 선택 후보는 `POST /mrs/satschc.do` 로 좌석/요금 단계 진입을 확인한다.
6. 사용자가 원하면 `POST /mrs/setPcpy.ajax` 로 임시 선점 후 공식 결제정보 입력 페이지 링크를 제공한다.
7. 사용자가 진행하지 않으면 `POST /mrs/cancPcpy.ajax` 로 선점을 해제한다.
## 주의할 점
- 결제 자동화는 포함하지 않는다. 공식 페이지의 결제 직전 단계까지 보조하는 assisted checkout 흐름이다.
- KOBUS 모바일 페이지는 좁은 화면에서 `/mblIdx.do` 로 리다이렉트할 수 있어 helper 링크 caveat를 확인한다.
- KOBUS 터미널 코드는 티머니 시외버스 코드와 다르므로 혼용하지 않는다.
- stateless POST보다 쿠키와 referer를 유지하는 흐름이 안정적이다.

View file

@ -1,179 +0,0 @@
# 항공권 가격 조회 (`flight-ticket-search`)
[`fast-flights`](https://pypi.org/project/fast-flights/) 라이브러리를 통해 Google Flights 공개 검색 표면을 조회해 항공권 후보, 예약 검색 링크, 날짜·월·연도별 최저가·평균가 비교를 보수적으로 제공하는 스킬입니다. API key, 로그인, 결제, CAPTCHA 우회 없이 무료 공개 표면만 사용합니다.
## 사용 시나리오
- "인천에서 나리타 다음 달 최저가 알려줘"
- "6월 ICN-NRT 월별 비교"
- "올해랑 내년 6월 1일 항공권 가격 비교"
- "ICN-LAX 비즈니스 가격 대략 비교해줘"
- "서울에서 도쿄 왕복 예약 링크 줘"
## 구현 표면
브라우저 자동화나 로그인을 사용하지 않습니다.
1. `fast-flights==2.2` 가 Google Flights 의 공개 검색 결과를 파싱합니다.
2. 예약 링크는 특정 판매자 결제 deep link 가 아니라 **Google Flights 검색 결과 링크**입니다. 실제 구매·결제·좌석 선택은 사용자가 브라우저에서 직접 진행합니다.
3. 첫 실행 시 `~/.cache/k-skill/flight-ticket-search/venv``fast-flights` 가 격리 설치되고 이후 그 venv 로 재실행합니다. 저장소에는 의존성 vendoring 이나 API key 를 두지 않습니다.
## 로컬 실행
### 단일 검색
편도:
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py search \
--from ICN \
--to NRT \
--date 2026-06-01 \
--adults 1 \
--seat economy \
--limit 5 \
--format markdown
```
왕복:
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py search \
--from ICN \
--to NRT \
--date 2026-06-01 \
--return-date 2026-06-08 \
--adults 1 \
--seat economy \
--limit 5
```
### 월별 비교
지정 월의 날짜들을 실제 검색해 각 날짜의 최저가·평균가를 비교합니다. 기본은 주 1회 샘플링입니다.
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py compare-month \
--from ICN \
--to NRT \
--month 2026-06 \
--sample weekly \
--limit 5
```
일별 전체 조회가 필요하면 `--sample daily` 를 씁니다. 28~31 회 요청이 발생하므로 rate limit 보호를 위해 `--sleep` 을 1.5 초 이상 유지합니다.
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py compare-month \
--from ICN \
--to NRT \
--month 2026-06 \
--sample daily \
--sleep 2 \
--limit 10
```
### 사용자 정의 범위 비교
"다음주부터 2주간", "6월 1일부터 20일까지"처럼 범위를 받을 때 사용합니다.
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py compare-range \
--from ICN \
--to BKK \
--start-date 2026-06-01 \
--end-date 2026-06-20 \
--step-days 3 \
--limit 5
```
`--step-days 1` 은 일별 비교, `7` 은 주별 비교입니다.
### 연도 비교
같은 월일을 여러 연도에 대해 조회합니다.
```bash
python3 flight-ticket-search/scripts/flight_ticket_search.py compare-years \
--from ICN \
--to NRT \
--years 2026,2027 \
--month-day 06-01 \
--limit 5
```
## 출력 해석
### 단일 검색 응답 주요 필드
- `meta.booking_search_url` — Google Flights 예약 검색 링크
- `meta.price_band` — Google 이 표시하는 `low` / `typical` / `high` 가격 band
- `stats.min_price`, `stats.avg_price`, `stats.max_price`
- `flights[].name`, `departure`, `arrival`, `duration`, `stops`, `price_text`
- `flights[].quality``complete` 또는 `partial` (Google Flights 응답 일부가 누락될 수 있음을 표시)
### 비교 검색 응답 주요 필드
- `stats.min_price` — 샘플 날짜 중 최저가
- `stats.avg_of_daily_min` — 날짜별 최저가의 평균
- `stats.max_of_daily_min` — 날짜별 최저가 중 최고값
- `cheapest_dates[]` — 가장 싼 날짜와 예약 검색 링크
- `rows[]` — 날짜별 성공/실패 및 요약
- `failures[]` — 너무 먼 미래 날짜 등 실패 케이스 (숨기지 않고 보고)
## 입력 가이드
- 출발/도착 공항 IATA 코드: `ICN`, `GMP`, `PUS`, `NRT`, `HND`, `LAX`, `CJU`
- 출발일: `YYYY-MM-DD`
- 선택: 왕복 귀국일, 성인 수(기본 1), 좌석 등급(`economy` / `premium-economy` / `business` / `first`), 비교 샘플 방식(`weekly` / `daily`)
사용자가 도시명만 말하면 IATA 코드를 추론합니다. 흔한 기본값:
- 서울/인천 국제선: `ICN`
- 서울 국내선/제주: `GMP`
- 도쿄: 나리타 `NRT` 또는 하네다 `HND` — 명시 없으면 사용자에게 확인
- 제주: `CJU`
## 예약 링크 정책
- `booking_search_url` 은 Google Flights 검색 URL 입니다.
- 특정 항공사/OTA 결제 단계 deep link 를 자동 추출하거나 클릭하지 않습니다.
- 결제·예약 확정·로그인·여권 정보 입력은 스킬 범위 밖입니다.
- 사용자가 예약까지 원하면 링크를 열어 직접 확인하도록 안내합니다.
## 검증된 노선 (2026-05-10 로컬 프로브 기준)
- 국내선: `GMP-CJU`, `ICN-CJU`
- 동북아: `ICN-NRT`, `ICN-PVG`, `ICN-HKG`, `ICN-TPE`
- 동남아: `ICN-SIN`, `ICN-BKK`
- 중동: `ICN-DXB`
- 북미: `ICN-LAX`, `ICN-JFK`
- 유럽: `ICN-LHR`, `ICN-CDG`, `ICN-FRA`
- 오세아니아: `ICN-SYD`
- 남미: `ICN-GRU`
- 왕복/좌석 등급/성인 다수: `ICN↔NRT`, `GMP↔CJU`, business, 성인 2명
## 실패 모드
- Google Flights HTML/프론트엔드 구조 변경으로 항공사명·시간 파싱이 비거나 `partial` 로 떨어질 수 있습니다.
- 일부 노선은 가격만 나오고 항공편 상세가 누락될 수 있습니다.
- 잘못된 IATA 코드, 동일 출도착 공항, 실제 항공편이 없는 구간은 실패합니다.
- 너무 먼 미래 날짜는 upstream 에 결과가 없을 수 있습니다.
- 비교 기능은 날짜별 실시간 조회라 요청 수가 많습니다. daily 월별 비교는 30 회 안팎의 요청이 발생합니다.
- `fast-flights` fallback 이 외부 fetch helper 를 쓰는 경우 `401 no token provided` 가 날 수 있어, 동일 입력의 실사용성이 낮은 케이스면 사전 validation 으로 막거나 잠시 후 재시도합니다.
- Skyscanner: CAPTCHA/403 으로 직접 provider 부적합 (사용하지 않음).
- Kiwi Tequila API: 무료 계정 API key 가 필요해 기본 no-key 경로에서는 사용하지 않습니다.
## 비범위
- 실제 예약/결제/취소/좌석 지정 자동화
- 로그인 회원가, 카드 할인, 쿠폰, 마일리지 적용가 확정
- CAPTCHA, fingerprint, bot-block 우회
- 스카이스캐너 직접 조회 (CAPTCHA/403 으로 안정 provider 가 아님)
## 출처
- 스킬 정의: [`flight-ticket-search/SKILL.md`](../../flight-ticket-search/SKILL.md)
- 헬퍼 스크립트: [`flight-ticket-search/scripts/flight_ticket_search.py`](../../flight-ticket-search/scripts/flight_ticket_search.py)
- `fast-flights`: <https://pypi.org/project/fast-flights/>
- Google Flights: <https://www.google.com/travel/flights>

View file

@ -1,150 +0,0 @@
# 자연휴양림 빈 객실 조회 가이드
대상 사이트는 숲나들e 공식 사이트 `https://foresttrip.go.kr/index.jsp` 이다. 이 기능은 해당 사이트의 자연휴양림 예약 가능 객실 **조회 자동화**만 수행한다.
## 이 기능으로 할 수 있는 일
- 숲나들e/자연휴양림 예약 가능 객실 조회
- 특정 날짜 또는 여러 날짜 기준 조회
- 전체 자연휴양림 또는 휴양림명/ID 기준 조회
- 숙박/야영 카테고리별 조회
- JSON 또는 사람이 읽기 좋은 텍스트 출력
이 기능은 **조회 전용 자동화**이다. 예약 신청, 결제, 캡차 처리, 대기열 우회, 반복 스나이핑은 하지 않는다.
## 먼저 필요한 것
- Python 3.9+
- Playwright Chromium
- [공통 설정 가이드](../setup.md) 완료
- [보안/시크릿 정책](../security-and-secrets.md) 확인
```bash
python3 -m pip install playwright
python3 -m playwright install chromium
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --check-deps
```
`--check-deps` 는 숲나들e 로그인이나 네트워크 조회를 수행하지 않고, 로컬 Python/Playwright Chromium 준비 상태만 확인한다.
## 필요한 환경변수
- `KSKILL_FORESTTRIP_ID`
- `KSKILL_FORESTTRIP_PASSWORD`
선택:
- 없음
### Credential resolution order
1. **이미 환경변수에 있으면** 그대로 사용한다.
2. **에이전트가 자체 secret vault(1Password CLI, Bitwarden CLI, macOS Keychain 등)를 사용 중이면** 거기서 꺼내 환경변수로 주입해도 된다.
3. **`~/.config/k-skill/secrets.env`** (기본 fallback) — plain dotenv 파일, 퍼미션 `0600`.
4. **아무것도 없으면** 유저에게 물어서 2 또는 3에 저장한다.
helper는 `KSKILL_FORESTTRIP_ID`, `KSKILL_FORESTTRIP_PASSWORD` 환경변수를 읽는다. secret vault나 `secrets.env` 는 에이전트/사용자가 값을 꺼내 실행 환경에 주입하기 위한 저장 위치이며, helper가 임의로 계정 정보를 다른 곳에 저장하지 않는다.
## 처음 실행 순서
처음 쓰는 사용자는 의존성 확인 후 환경변수를 현재 shell에만 주입해서 1개 휴양림으로 먼저 조회한다.
```bash
export KSKILL_FORESTTRIP_ID="your-foresttrip-id"
export KSKILL_FORESTTRIP_PASSWORD="your-foresttrip-password"
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --check-deps
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --forest-name 유명산 --text --dates 20260504
```
성공 여부를 먼저 보려면 전체 조회보다 `--forest-name` 또는 `--forest-id` 로 범위를 좁혀 실행한다. JSON 결과가 필요하면 같은 조건에 `--json` 을 사용한다.
## 입력값
- 날짜: `YYYYMMDD`
- 여러 날짜: `YYYYMMDD,YYYYMMDD`
- 조회 범위: 전체 자연휴양림, 휴양림 ID, 휴양림명 부분 일치
- 카테고리:
- `01`: 숙박
- `02`: 야영/캠핑
- `01,02`: 숙박 + 야영/캠핑
- 고급 옵션:
- `--week-range N`: `--dates` 를 생략했을 때만 오늘부터 N주 조회
- `--concurrency N`: 병렬 조회 worker 수, 1-5 범위
- `--session-cache PATH`: 로그인 세션 캐시 경로 override
## 기본 흐름
1. `KSKILL_FORESTTRIP_ID`, `KSKILL_FORESTTRIP_PASSWORD` 를 확보한다.
2. 필요한 경우 `python3 -m pip install playwright``python3 -m playwright install chromium` 을 실행한다.
3. helper로 read-only 월별예약조회 endpoint를 실행한다.
4. helper가 로그인 세션, CSRF, 공식 휴양림 ID 목록을 확보한다.
5. 날짜, 휴양림명, 객실/시설명, 숙박/야영 구분, 정원 중심으로 요약한다.
6. 응답 정제: API가 `srchDate` 기준 최대 5일 윈도우를 반환할 수 있어 helper가 요청 범위 밖 `useDt`, 운영자 보유분("예비" 포함 객실), 같은 객실 중복 행을 자동 제거한다.
2026-04-29 확인 기준, 로그인 없이 월별예약조회 화면에 접근하면 `401 Unauthorized`가 반환되고, 조회 endpoint는 JSON 대신 안내 HTML을 반환한다. 따라서 현재 구현은 로그인 세션/CSRF 확보를 필수 전제로 둔다.
## 검증 방식
메인테이너가 별도 숲나들e 계정을 새로 만들 필요는 없다.
- CI/리뷰 검증: `./scripts/validate-skills.sh`, `python3 -m py_compile ...`, `--help`, `--check-deps` 로 진행한다.
- 실제 조회 검증: 기여자 또는 이미 숲나들e 계정을 가진 사용자가 개인 계정으로 선택 실행한다.
- PR에는 실제 조회 결과의 `forests_scanned`, `fetch_failures`, `filter_hits` 같은 비민감 요약값만 기록하고, 계정 정보와 세션 쿠키는 공유하지 않는다.
## 예시
전체 자연휴양림에서 하루 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --all --text --dates 20260504
```
JSON으로 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --all --json --dates 20260504
```
여러 날짜 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --all --text --dates 20260504,20260505
```
야영/캠핑만 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --all --text --dates 20260504 --categories 02
```
휴양림명으로 좁혀 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --forest-name 유명산 --text --dates 20260504
```
로그인 세션 캐시를 무시하고 새로 조회:
```bash
python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --all --text --dates 20260504 --refresh-session
```
## 주의할 점
- 예약 자동화가 아니다.
- 결제, 캡차 처리, 대기열 우회는 하지 않는다.
- aggressive polling은 피한다.
- 조회 결과는 시점 차이로 숲나들e 화면과 달라질 수 있다.
- 로그인 실패 시 계정 정보 또는 숲나들e 정책 변경을 먼저 확인한다.
- API가 요청 날짜보다 넓은 5일 윈도우를 반환해도 출력에는 요청 범위(`today``last_day`) 안의 행만 포함된다.
- "예비" 표기가 있는 객실은 사용자 예약 화면에 노출되지 않는 운영자 보유분이라 결과에서 자동 제외된다.
## 흔한 문제 해결
- `Playwright browser missing`: `python3 -m playwright install chromium` 을 실행한다.
- `Missing KSKILL_FORESTTRIP_ID` 또는 `Missing KSKILL_FORESTTRIP_PASSWORD`: 환경변수가 현재 shell에 주입됐는지 확인한다.
- 로그인 실패: 숲나들e 웹사이트에서 같은 계정으로 직접 로그인되는지 먼저 확인한다.
- 날짜/카테고리/출력 옵션 오류: helper가 로그인 전에 argparse error로 중단하므로 메시지에 맞춰 값을 고친다.
- JSON 대신 HTML 안내 페이지가 반환됨: 세션/CSRF가 없거나 만료된 상태일 수 있으므로 `--refresh-session` 으로 1회 재조회한다.
- 일부 휴양림 fetch failure: 성공한 결과와 실패 개수를 함께 보고하고, 반복 polling으로 보정하지 않는다.

View file

@ -1,35 +0,0 @@
# 금융위 기업기본정보 조회 (fsc-corporate-info)
`fsc-corporate-info` 스킬은 공공데이터포털의 **금융위원회_기업기본정보 서비스**(15043184, `getCorpOutline_V2`)를 `k-skill-proxy` 경유로 호출한다.
## 제공 기능
- 법인명(`corpNm`) 기준 후보: 대표자·설립일·업종 등 upstream 필드 원문
- 사업자번호 교차검증: 응답에 `bzno`가 있으면 입력 번호와 정확 일치하는 후보를 분리(없으면 교차검증 불가 표기)
## 인증/시크릿
사용자 로컬 시크릿은 필요 없다. upstream `DATA_GO_KR_API_KEY`는 프록시 서버에만 둔다(15043184 활용신청 필요). self-host 프록시는 `KSKILL_PROXY_BASE_URL`로 지정한다.
## 입력 제한
검색 파라미터가 `crno`(법인등록번호 13자리)/`corpNm`(법인명)뿐이라 **사업자번호 단독 조회가 불가**하다. 법인명으로 조회한다. `crno`는 사업자등록번호와 별개 번호다.
## 예시
```bash
python3 fsc-corporate-info/scripts/fsc_corporate_info.py --name "삼성전자" --b-no 124-81-00998
```
## 실패 모드
- `400 bad_request`: 법인명 미입력
- `503 upstream_not_configured`: 프록시에 `DATA_GO_KR_API_KEY` 없음
- `502 upstream_forbidden`: 프록시 키가 15043184에 미신청
- 빈 결과: 법인명 불일치 — 표기를 바꿔 재시도
## 공식 출처
- 공공데이터포털: <https://www.data.go.kr/data/15043184/openapi.do>
- upstream: `https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2`
- 프록시 route: `GET /v1/fsc/corp-outline`

View file

@ -1,41 +0,0 @@
# 부정당제재업체 조회 (g2b-sanctioned-supplier)
`g2b-sanctioned-supplier` 스킬은 공공데이터포털의 **조달청 나라장터 사용자정보 서비스**(15129466, `getUnptRsttCorpInfo02`)를 `k-skill-proxy` 경유로 호출한다.
## 제공 기능
- 사업자등록번호 정확 일치(`inqryDiv=1`)로 **조회시점 현재 유효한** 부정당제재 조회
- 반환: 제재 시작/종료일자, 제재기관명, 계약법구분, 제재근거법률 등 upstream 필드 원문
## 적용 범위 한계
upstream 명세상 다음은 제공되지 않는다(과거 이력 조회가 아니다).
- 조회시점에 제재만료·해제된 건
- 나라장터 미등록업체·개인에 대한 제재
만료 이력까지 보려면 나라장터(<https://www.g2b.go.kr>)에서 수동 확인이 필요하다.
## 인증/시크릿
사용자 로컬 시크릿은 필요 없다. upstream `DATA_GO_KR_API_KEY`는 프록시 서버에만 둔다(15129466 활용신청 필요). self-host 프록시는 `KSKILL_PROXY_BASE_URL`로 지정한다.
## 예시
```bash
python3 g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py --bizno 124-81-00998
```
## 실패 모드
- `400 bad_request`: 사업자번호가 10자리가 아님
- `503 upstream_not_configured`: 프록시에 `DATA_GO_KR_API_KEY` 없음
- `502 upstream_forbidden`: 프록시 키가 15129466에 미신청
- `total_count: 0`: 조회시점 유효 제재 없음(만료·미등록업체는 미제공임에 유의)
## 공식 출처
- 공공데이터포털: <https://www.data.go.kr/data/15129466/openapi.do>
- upstream: `https://apis.data.go.kr/1230000/ao/UsrInfoService02/getUnptRsttCorpInfo02`
- 수동 대조: 나라장터 <https://www.g2b.go.kr>
- 프록시 route: `GET /v1/g2b/sanctioned-supplier`

View file

@ -1,32 +0,0 @@
# 강남언니 병원 조회 가이드
`gangnamunni-clinic-search`는 강남언니 공개 검색 페이지에서 병원 후보를 조회하는 read-only 스킬입니다.
## 공개 접근 경로
- 검색 URL: `https://www.gangnamunni.com/search?q=<keyword>`
- 데이터 위치: HTML 안의 `__NEXT_DATA__` JSON (`props.pageProps.hospitals`)
- 인증/시크릿: 불필요
- 프록시: 사용하지 않음
## 예시
```bash
npx gangnamunni-clinic-search "강남 성형외과" --limit 5
```
```js
const { searchClinics } = require("gangnamunni-clinic-search")
const result = await searchClinics({ query: "코성형", limit: 3 })
```
## 출력
각 후보는 공개 검색 페이지에 포함된 병원명, 평점, 리뷰 수, 지원 언어, 이미지 URL, 공개 병원 링크를 포함합니다.
## 제한사항
- 조회 시점 공개 검색 결과 기준입니다.
- 로그인, 상담, 예약, 결제, 찜, 리뷰 작성은 자동화하지 않습니다.
- CAPTCHA/차단/로그인벽/빈 shell 페이지는 실패 모드로 처리합니다.
- 의료 판단이나 병원 선택 보증을 대신하지 않습니다.

View file

@ -1,93 +0,0 @@
# 개별공시지가 조회 가이드
## 이 기능으로 할 수 있는 일
- 한국 국토교통부 부동산공시가격알리미(`realtyprice.kr`)에서 지번 단위 **개별공시지가**(원/㎡) 조회
- 다년도 추이(과거 수년치)와 전년 대비 변동률 정규화 JSON 출력
- 17개 광역자치단체(서울, 세종특별자치시 포함) 모든 시·군·구 지원
- 산 지번 / 본번-부번 모두 지원
## 가장 중요한 규칙
`realtyprice.kr`는 **API 키가 필요 없는 완전 공개 엔드포인트**이므로 이 스킬은 `k-skill-proxy`를 경유하지 않는다. 사용자 머신에서 직접 upstream을 호출한다. (저장소의 *k-skill-proxy inclusion rule* — 프록시는 API 키가 필요한 upstream만 다룬다.)
## 무엇을 가져오나
- 공시지가는 매년 1월 1일 기준, 4~5월에 공시된다.
- 재산세, 종합부동산세, 양도소득세 등 **세금 산정의 법적 기준 단가**다.
- 공시지가 ≠ 시세. 시세는 통상 공시지가의 1.5~3배.
> 시세, 실거래가, 매매가, 호가가 필요하면 [`real-estate-search`](real-estate-search.md) 또는 다른 스킬을 사용한다.
## 먼저 필요한 것
없음. 인터넷 연결과 Node.js 18+ 만 있으면 된다.
## 사용 방법
### 설치
```bash
npm install gongsijiga-search
```
### 기본 호출
```js
const { lookupGongsijiga } = require("gongsijiga-search");
const result = await lookupGongsijiga("서울특별시 강남구 역삼동 736");
console.log(result.latest.price_per_sqm); // 72340000
console.log(result.yoy_change_pct); // 5.45
```
### 입력 주소 형식
`<시도> <시군구> <읍면동…> [산] <본번[-부번]>`
| 형식 | 예시 |
| --- | --- |
| 일반 | `서울특별시 강남구 역삼동 736` |
| 약칭 시도 | `서울 강남구 역삼동 736` |
| 부번 있음 | `경기 성남시 분당구 정자동 178-3` |
| 산 지번 | `서울 서초구 서초동 산 1-2` |
| 다토큰 읍면동 | `전남 무안군 청계면 청천리 100-5` |
| 세종 (시군구 없음) | `세종 어진동 575` 또는 `세종특별자치시 어진동 575` |
### 응답 모양
```json
{
"address": "서울특별시 강남구 역삼동 736",
"jibun": "736번지",
"san": false,
"latest": {
"year": 2026,
"price_per_sqm": 72340000,
"notice_date": "2026-04-30",
"base_date": "2026-01-01"
},
"history": [
{ "year": 2026, "price_per_sqm": 72340000, "notice_date": "2026-04-30" },
{ "year": 2025, "price_per_sqm": 68600000, "notice_date": "2025-04-30" }
],
"yoy_change_pct": 5.45,
"source_url": "https://www.realtyprice.kr/notice/gsindividual/search.htm"
}
```
## 실패 모드
| `error.code` | 의미 | 처리 |
| --- | --- | --- |
| `ADDRESS_PARSE_FAILED` | 주소 파싱 실패 / 미인식 시도 | "행정구역 + 본번이 포함된 주소가 필요합니다" 안내 후 재요청 |
| `INVALID_BUNJI` | 본번 비숫자 또는 4자리 초과 | 본번 형식 재요청 |
| `REGION_NOT_FOUND` | 시군구/읍면동 매칭 실패 | `err.candidates` 후보(최대 3개) 제안 |
| `LAND_NOT_FOUND` | 해당 지번 미등재 | "본번/부번 오타이거나 도로/하천 등 미과세 토지" 설명 |
| `UPSTREAM_ERROR` | `realtyprice.kr` 비정상 응답 | "데이터 출처 일시 장애. 잠시 후 재시도" + `source_url` |
| `UPSTREAM_TIMEOUT` | 30초 타임아웃 | UPSTREAM_ERROR와 동일 처리 |
## 출처
- [부동산공시가격알리미](https://www.realtyprice.kr/notice/gsindividual/search.htm) — 국토교통부
- 패키지 소스: [`packages/gongsijiga-search/`](../../packages/gongsijiga-search)

View file

@ -1,68 +0,0 @@
# 시외버스 예매 가이드
## 이 기능으로 할 수 있는 일
- 티머니 시외버스 터미널/노선 후보 확인
- 배차 시간표, 운수사, 잔여석, 요금 확인
- 좌석/요금 단계 진입 가능 여부 확인
- 공식 카드정보 입력 페이지로 handoff
## 먼저 필요한 것
- 별도 사용자 계정/비밀번호는 기본 조회·좌석 단계에서 필요하지 않음
- 결제는 공식 티머니 시외버스 페이지에서 사용자가 직접 진행
- 브라우저 자동화보다 `https://intercitybus.tmoney.co.kr` 공식 HTTP 흐름을 우선 사용
## 입력값
- 출발 터미널
- 도착 터미널
- 날짜: `YYYYMMDD`
- 희망 시간대
- 인원 수와 좌석 선호
## 기본 흐름
1. 쿠키 jar를 만들고 티머니 시외버스 페이지를 열어 세션을 시작한다.
2. `POST /otck/readAlcnList.do` 로 배차를 조회한다. 이때 브라우저 JS가 붙이는 `bef_Aft_Dvs=D`, `req_Rec_Num=10`을 반드시 같이 보낸다.
3. 결과의 `readSasFeeInf(...)` 인자를 파싱해 후보를 정리한다.
4. 선택 후보는 `POST /otck/readSatsFee.do` 로 좌석/요금 단계 진입을 확인한다.
5. 사용자가 원하면 `POST /otck/readPcpySats.do` 로 공식 카드정보 입력 페이지에 진입하도록 handoff한다.
6. 뒤로가기/취소성 이동으로 좌석 선택 단계에 복귀해 임시 선점을 해제할 수 있는지 확인한다.
## read-only 조회 helper
```bash
python3 intercity-bus-booking/scripts/intercity_bus_search.py \
--depart-code 0511601 \
--arrive-code 2482701 \
--depart-name 동서울 \
--arrive-name 속초 \
--date 20260520
```
이 helper는 쿠키 세션을 시작하고 공식 배차 조회 POST를 수행한 뒤 출발시각, 운수사, 등급, 요금, 잔여/총 좌석을 JSON으로 출력한다. 기본은 read-only이며, `--hold-seat` 또는 `--hold-first-seat`를 주면 좌석/요금 단계에 진입해 `readPcpySats.do`로 임시 좌석 선점을 만들고 공식 카드정보 입력 HTML과 cancel/back 필드를 저장한다. 결제 정보 입력·제출은 수행하지 않는다.
### 임시 선점 예시
```bash
python3 intercity-bus-booking/scripts/intercity_bus_search.py \
--depart-code 0511601 \
--arrive-code 2482701 \
--depart-name 동서울 \
--arrive-name 속초 \
--date 20260520 \
--select-index 1 \
--hold-first-seat \
--output-dir /tmp/tmoney-hold
```
성공 조건은 JSON의 `hold.success=true`, `hold.hold_id` 존재, 저장된 HTML에 `카드정보 입력` 표시가 있는 것이다. 라이브 응답 페이지에는 정확한 만료 카운트다운 문구가 노출되지 않았으므로, 선점 후 결제는 즉시 진행하게 안내하고 방치된 선점은 저장된 cancel/back 필드로 해제한다.
## 주의할 점
- 결제 자동화는 포함하지 않는다. 공식 페이지의 결제 직전 단계까지 보조하는 assisted checkout 흐름이다.
- 티머니 시외버스 터미널 코드는 KOBUS 고속버스 코드와 다르므로 혼용하지 않는다.
- 일부 표면은 `txbus` 계열 URL과 연결될 수 있지만, 검증된 기본 URL은 `intercitybus.tmoney.co.kr` 이다.
- stateless POST보다 쿠키와 referer를 유지하는 흐름이 안정적이다.
- `bef_Aft_Dvs` 또는 `req_Rec_Num`을 누락하면 실제 배차가 있어도 `errorCont`가 포함된 일반 오류 페이지가 반환될 수 있다.

View file

@ -1,170 +0,0 @@
# 등기부등본 자동화 가이드
`iros-registry-automation`은 인터넷등기소(IROS)에서 법인/부동산 등기부등본(등기사항증명서)을 여러 건 발급해야 할 때, 사용자가 직접 로그인·결제하는 브라우저 흐름을 전제로 장바구니, 열람, 저장 작업을 보조하는 스킬이다.
이 문서는 원 저작자 `challengekim`의 MIT 참고 구현 [`challengekim/iros-registry-automation`](https://github.com/challengekim/iros-registry-automation)을 기준으로 작성했다. 스킬 답변이나 파생 문서에도 이 원 저작자 링크를 남긴다.
## 할 수 있는 일
- 법인등기부등본: 법인등록번호 기반(`iros_cart_by_corpnum.py`) 또는 상호명 기반(`iros_cart.py`)으로 장바구니에 담고, 사용자가 직접 결제한 뒤 열람·저장한다.
- 부동산등기부등본: 주소/동호수 JSON을 사용해 `iros_cart_realty.py`로 장바구니에 담는다. 결제·열람·다운로드는 인터넷등기소 웹 UI에서 수동 처리하는 것을 기본 권장한다.
- TouchEn nxKey 설치, Playwright/Chromium 준비, 입력 파일 형식, 저장 폴더를 점검한다.
- 다운로드된 PDF와 법인정보 리포트 같은 산출물을 저장소 밖 안전한 경로에 두도록 안내한다.
## 먼저 알아둘 점
- 로그인은 사용자가 직접 한다. 아이디/비밀번호, 공동인증서 비밀번호, 간편인증, OTP, 보안카드 입력을 에이전트에게 맡기지 않는다.
- 결제는 사용자가 직접 한다. 카드 번호와 승인 절차는 브라우저에서 사람이 처리한다.
- 법인 발급은 upstream 문서 기준 **페이지당 10건** 결제 제약을 전제로 한다. 10건을 넘으면 사용자가 10건 단위로 반복 결제한다.
- 부동산은 인터넷등기소 웹 UI의 10만원 미만 일괄 결제와 일괄열람출력/일괄저장 기능을 쓰는 편이 빠르고 안전한 경우가 많다. 이 스킬은 부동산 주소 목록을 장바구니에 반복 담는 부분에 초점을 둔다.
- TouchEn nxKey가 설치되어 있지 않으면 중간에 보안 프로그램 설치 페이지가 뜰 수 있다. 설치 후 브라우저/PC를 재시작하고 처음부터 다시 실행한다.
- 이 기능은 참고용 발급 자동화 가이드다. 법률 자문, 권리관계 해석, 발급 결과의 법적 효력 판단을 하지 않는다.
## 설치
이 스킬은 로그인·인증·결제 인접 브라우저 자동화를 mutable upstream `HEAD`에 맡기지 않는다. 실행 전 이 저장소의 `iros-registry-automation/scripts/upstream.pin`에 적힌 reviewed SHA로 checkout한다.
```bash
git clone https://github.com/challengekim/iros-registry-automation.git
cd iros-registry-automation
git checkout 7c6924b2ff88d693a12556659188cb91041e5097
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
cp config.json.example config.json
```
업스트림 핀 업데이트는 새 upstream diff 검토가 필요한 보안/신뢰 경계 변경이다. `scripts/upstream.pin`과 설치 예시의 `git checkout` SHA를 같은 PR에서 함께 갱신한다.
Chrome/Chromium, Python 3.10+, IROS 로그인 수단, 결제 카드, TouchEn nxKey가 필요하다.
## 안전한 작업 폴더
발급 대상 목록과 PDF에는 법인등록번호, 주소, 동호수, 회사명 등 개인정보/민감정보가 들어갈 수 있다. 저장소 밖 비공개 디렉터리에서 다루고, PR·테스트 로그·공개 문서에 실제 값을 커밋하지 않는다.
```bash
workdir="$(mktemp -d "${TMPDIR:-/tmp}/iros-registry.XXXXXX")"
chmod 700 "$workdir"
mkdir -p "$workdir/downloads" "$workdir/logs" "$workdir/output" "$workdir/tmp-downloads"
```
실제 입력은 upstream repo `data/`가 아니라 `$workdir/corp-input.json`, `$workdir/realty-input.json`, `$workdir/customer-list.xlsx`처럼 저장소 밖에 둔다. upstream `data/` 디렉터리는 샘플 형식 확인용으로만 보고, 실제 법인등록번호·주소·동호수·고객 Excel 원문을 넣지 않는다.
```bash
cat > "$workdir/corp-input.json" <<'JSON'
{
"1101111234567": "예시 주식회사",
"1101117654321": "샘플 주식회사"
}
JSON
python3 - "$workdir" <<'PY'
import json
import pathlib
import sys
workdir = pathlib.Path(sys.argv[1])
corp_input = json.loads((workdir / "corp-input.json").read_text())
companies = list(corp_input.values())
(workdir / "companies-input.json").write_text(
json.dumps(companies, ensure_ascii=False, indent=2) + "\n"
)
PY
```
`iros_download.py`는 결제 후 열람·저장 단계에서 `companies_list`를 열어 저장 파일명을 맞춘다. 법인등록번호 기반 `iros_cart_by_corpnum.py`를 쓰더라도 결제 전에 `$workdir/companies-input.json`을 준비해야 결제 후 다운로드가 로컬 `FileNotFoundError` 없이 이어진다.
`config.json`의 입력·로그·save_dir 관련 값을 `$workdir`로 돌리면 upstream 스크립트를 실행해도 저장소 하위 `data/`, `logs/`, `output/`에 실제 업무 정보가 남지 않는다.
```bash
python3 - "$workdir" <<'PY'
import json
import pathlib
import sys
workdir = pathlib.Path(sys.argv[1])
config = json.loads(pathlib.Path("config.json").read_text())
config.update({
"corpnum_list": str(workdir / "corp-input.json"),
"companies_list": str(workdir / "companies-input.json"),
"realty_list": str(workdir / "realty-input.json"),
"excel_path": str(workdir / "customer-list.xlsx"),
"save_dir": str(workdir / "downloads"),
"realty_save_dir": str(workdir / "downloads" / "realty"),
"pdf_dir": str(workdir / "downloads"),
"report_output": str(workdir / "output" / "corp-report.xlsx"),
"extract_output": str(workdir / "output" / "corp-extract.json"),
"bizno_cache": str(workdir / "logs" / "bizno-cache.json"),
"bizno_results": str(workdir / "logs" / "bizno-results.json"),
"realty_cart_log": str(workdir / "logs" / "cart-realty-log.json"),
"realty_download_log": str(workdir / "logs" / "download-realty-log.json"),
"cart_log": str(workdir / "logs" / "cart-log.json"),
"cart_corpnum_log": str(workdir / "logs" / "cart-corpnum-log.json"),
"download_log": str(workdir / "logs" / "download-log.json"),
"download_temp": str(workdir / "tmp-downloads"),
})
pathlib.Path("config.json").write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n")
PY
```
## 법인등기부등본 흐름
1. 법인등록번호 또는 상호명 목록을 준비한다.
2. 법인등록번호가 있으면 `iros_cart_by_corpnum.py`를 우선 사용한다. 상호명만 있으면 `iros_cart.py`를 사용한다.
3. 브라우저가 열리면 사용자가 직접 IROS에 로그인한다.
4. 자동 처리로 법인 검색 → 말소사항포함 등 선택 → 장바구니 담기를 진행한다.
5. 결제대상목록 페이지가 뜨면 사용자가 직접 카드 결제를 완료한다. 법인은 페이지당 10건 단위 제약을 전제로 한다.
6. 결제 후 `iros_download.py` 또는 마법사 메뉴 2번으로 열람·저장한다.
```bash
python iros_cart_by_corpnum.py
python iros_download.py
```
위 명령은 로컬 `config.json`을 읽으므로, 먼저 `corpnum_list`, `companies_list`, `save_dir`가 각각 `$workdir/corp-input.json`, `$workdir/companies-input.json`, `$workdir/downloads`를 가리키는지 확인한다.
## 부동산등기부등본 흐름
1. 주소/동호수 JSON을 준비한다.
2. `iros_cart_realty.py`로 주소 검색 → 소재지번 선택 → 용도(열람)/등기기록유형(전부)/미공개 → 장바구니 담기를 진행한다.
3. 사용자가 인터넷등기소 웹 UI에서 직접 결제, 일괄열람출력, 일괄저장을 수행한다.
4. 자동 저장이 꼭 필요한 경우에만 `iros_download_realty.py`를 별도로 검토한다.
```bash
python iros_cart_realty.py
```
부동산은 결제·열람·다운로드까지 무조건 자동으로 밀어붙이기보다, 장바구니 단계 자동화 후 브라우저의 일괄 기능을 쓰는 경로를 먼저 권한다.
## 마법사 사용
처음 쓰는 사용자는 upstream 마법사를 먼저 실행한다.
```bash
python iros_wizard.py
```
마법사는 법인/부동산 장바구니, 결제 후 열람·저장, 사업자번호 기반 법인정보 조회, 다운로드된 법인 PDF 종합 리포트 생성을 메뉴로 제공한다. 사업자번호/고객 workbook 경로는 `excel_path``$workdir/customer-list.xlsx`를 가리키게 한 뒤 사용하고, upstream repo `data/고객리스트.xlsx`에는 실제 고객 Excel을 두지 않는다.
## 트러블슈팅
| 증상 | 원인 | 대응 |
| --- | --- | --- |
| 보안 프로그램 설치 페이지가 뜸 | TouchEn nxKey 미설치 | 설치 후 브라우저/PC 재시작, 스크립트 처음부터 재실행 |
| 법인 상호 검색 결과가 맞지 않음 | 사명변경, 특수문자, 동명 법인 | 법인등록번호 기반으로 재시도 |
| 10건 초과 법인 결제가 한 번에 되지 않음 | 페이지당 10건 제약 | 10건 단위로 반복 결제 |
| 부동산 저장 자동화가 느림 | 웹 UI 일괄 기능이 더 적합 | 장바구니만 자동화하고 결제·일괄열람출력·일괄저장은 수동 처리 |
## 보안/개인정보 원칙
- IROS 계정, 인증서 비밀번호, 카드 정보를 저장하지 않는다.
- 발급 대상 JSON, 다운로드 PDF, Excel 리포트는 저장소 밖에 둔다.
- 테스트와 PR에는 샘플/마스킹 값만 사용한다.
- 산출물 경로가 개인 이름·주소를 포함하면 공유 요약에서 경로도 마스킹한다.
## 출처
- 인터넷등기소(IROS): https://www.iros.go.kr
- 원 저작자 참고 구현: `challengekim/iros-registry-automation` — https://github.com/challengekim/iros-registry-automation
- upstream license: MIT

View file

@ -1,67 +0,0 @@
# 잡코리아 인재검색 가이드
## 이 기능으로 할 수 있는 일
- 잡코리아 기업회원 인재검색 화면에서 구인/채용 조건을 입력해 후보를 찾는다.
- 사용자가 직접 로그인한 브라우저 세션에서 현재 보이는 마스킹된 목록/이력서 정보를 읽는다.
- 유료 이력서 열람 전에 후보 적합도를 비교하고 shortlist를 만든다.
- 영업, 마케팅, 디자인, PM/PO, HR, 재무, 운영, 개발/데이터 등 전 직무에 사용할 수 있다.
## 먼저 알아둘 점
- 잡코리아 구인자/채용 담당자가 접근 가능한 기업회원 계정과 사용자 직접 로그인이 필요하다.
- 에이전트는 비밀번호, OTP, 세션 쿠키를 요청하거나 저장하지 않는다.
- 유료 열람, 마스킹 해제, 연락처 확인, 포지션 제안, 스크랩, 메모, 후보 상태 변경은 자동으로 하지 않는다.
- 비로그인 공개 목록 fallback은 가능하지만 정확도가 낮으므로 `목록 기반 1차 shortlist`로 표시한다.
## 공식 표면
- 잡코리아 기업 인재검색: https://www.jobkorea.co.kr/corp/person/find
## 입력값
- 채용 직무명
- 경력 범위
- 지역
- 필수 경험/스킬/업종
- 우대 경험/성과/툴
- 제외할 업무/업종/경력 패턴
- 유료 열람 추천 인원 수
## 기본 흐름
1. 잡코리아 기업 인재검색 페이지를 연다.
2. 로그인 상태를 확인한다. 로그인되지 않았으면 사용자가 열린 브라우저에서 직접 로그인한다.
3. 직무/키워드/경력/지역/제외 조건을 입력한다.
4. 결과 목록에서 후보 pool을 만든다.
5. 유료 열람이나 연락처 확인이 아닌 일반 상세/마스킹 이력서만 연다.
6. 현재 보이는 정보만 근거로 점수화한다.
7. URL과 검토 수준을 포함해 유료 열람 추천 후보를 정리한다.
## 결과 형식
```text
잡코리아 인재 shortlist
검색 조건
- 포지션: ...
- 필수 조건: ...
- 우대 조건: ...
- 제외 조건: ...
- 경력/지역: ...
- 모드: 로그인 마스킹 이력서 / 비로그인 목록 fallback
유료 열람 추천 Top N
1. 후보 A
- 점수: ...
- 근거: ...
- 리스크: ...
- 추천 액션: 채용 담당자가 유료 열람 검토
- URL: ...
```
## 제한사항
- 사이트 UI 변경 시 브라우저 추출 selector를 조정해야 할 수 있다.
- 계정 권한, 유료 상품 상태, 마스킹 정책에 따라 보이는 정보가 다르다.
- 후보 개인정보를 장기 저장하거나 대량 수집하지 않는다.

View file

@ -1,75 +0,0 @@
# 금감원 DART 전자공시 조회 가이드
## 이 기능으로 할 수 있는 일
- 공시검색 (최근 공시 목록, 기업별 공시 이력)
- 기업개황 (대표자, 업종, 주소, 결산월 등)
- 재무제표 (매출액, 영업이익, 당기순이익, 자산/부채/자본 등)
- 배당에 관한 사항
- 증자(감자) 현황
- 자기주식 취득 및 처분 현황
- 회계감사인의 명칭 및 감사의견
- 직원 현황 (부문별/성별 정규직·계약직 인원)
- 주요사항보고서: 유무상증자 결정, 소송, 해외 상장/상장폐지, 전환사채, 교환사채, 회사분할합병
## 먼저 필요한 것
`API_K_DART` 환경변수에 DART OpenAPI 인증키를 설정해야 한다.
키 발급: <https://opendart.fss.or.kr/uss/umt/EgovMberInsertView.do>
## 추천 조회 순서
1. `corpCode.xml` ZIP을 다운로드해 회사명 또는 종목코드로 `corp_code`(8자리 고유번호)를 찾는다.
2. `corp_code`로 기업개황, 재무제표, 감사의견 등 원하는 API를 호출한다.
3. 주요사항보고서는 날짜 범위(`bgn_de`, `end_de`)가 필요하다.
## 검색 예시
공시검색:
```bash
curl -fsS --get 'https://opendart.fss.or.kr/api/list.json' \
--data-urlencode "crtfc_key=$API_K_DART" \
--data-urlencode 'corp_code=00126380' \
--data-urlencode 'bgn_de=20260101' \
--data-urlencode 'end_de=20260419' \
--data-urlencode 'page_count=5'
```
재무제표 (연결, 사업보고서):
```bash
curl -fsS --get 'https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json' \
--data-urlencode "crtfc_key=$API_K_DART" \
--data-urlencode 'corp_code=00126380' \
--data-urlencode 'bsns_year=2024' \
--data-urlencode 'reprt_code=11011' \
--data-urlencode 'fs_div=CFS'
```
## 응답 해석 팁
- `status: "000"`이 정상. 그 외는 에러 (010=미등록 키, 011=사용 불가 키, 012=접근 불가 IP, 013=조회된 데이터 없음, 020=요청 제한 초과, 100=필드 오류).
- 재무제표의 `thstrm_amount`=당기, `frmtrm_amount`=전기, `bfefrmtrm_amount`=전전기.
- `reprt_code`: 11011(사업보고서), 11012(반기), 11013(1분기), 11014(3분기).
- `fs_div`: CFS(연결), OFS(개별).
## 답변 템플릿 권장
- 회사명 / 시장 / 종목코드
- 회계연도 / 보고서 종류
- 핵심 재무 수치 (매출, 영업이익, 순이익, 자산/부채/자본)
- 마지막 한 줄: `금감원 DART 공시 데이터 기준이며 투자 조언은 아닙니다.`
## 에러/제약
- `API_K_DART` 미설정 시 API 호출 불가 → 키 발급 안내
- DART API 요청 한도: 공식 가이드(020 메시지) 기준 "일반적으로 20,000건 이상의 요청"에 대해 020 (요청 제한 초과)이 발생하며, 키별로 별도 한도가 설정된 경우 다른 임계치에서도 동일 코드가 반환될 수 있음. 분당 throttle 등 세부 수치는 공개 가이드에 명시되지 않음. 본인 키의 정확한 사용 현황은 로그인 후 OpenDART 사이트의 [오픈API 이용현황](https://opendart.fss.or.kr/mng/apiUsageStatusView.do) 페이지에서 확인 가능.
- 상장폐지·오래된 비상장 법인은 데이터가 없을 수 있음
- DART OpenAPI `list.json` 의 공식 요청 파라미터 표에 `corp_name` 은 존재하지 않는다. 회사명 기준으로 좁히려면 `corpCode.xml` ZIP을 받아 `corp_code`(8자리 고유번호)를 먼저 확보한 뒤 `corp_code` 로 호출한다. `corp_code` 자체는 선택사항(공식 가이드: N)이지만, 공식 spec 기준 미지정 시 `bgn_de`~`end_de` 검색 기간이 3개월 이내로 제한된다.
## 참고 링크
- 공식 DART OpenAPI: <https://opendart.fss.or.kr/intro/main.do>
- API 목록: <https://opendart.fss.or.kr/intro/infoApiList.do>

View file

@ -1,34 +0,0 @@
# K-스킬 클리너 가이드
`k-skill-cleaner`는 K-스킬 묶음에서 사용자가 쓰지 않는 스킬을 찾기 위한 정리 보조 스킬이다. 몇 가지 인터뷰 답변과 로컬 코딩 에이전트 로그의 트리거 횟수 신호를 합쳐 삭제 후보와 검토 후보를 나눈다.
## 기본 흐름
1. 먼저 인터뷰로 보존할 스킬, 절대 쓰지 않는 스킬, 주로 쓰는 에이전트, 분석 기간을 확인한다.
2. 설치된 단독 스킬에서는 `python3 scripts/k_skill_cleaner.py``k-skill-cleaner` 스킬 디렉터리 안에서 실행한다. 전체 저장소 checkout에서는 `python3 k-skill-cleaner/scripts/k_skill_cleaner.py` 또는 호환 wrapper `python3 scripts/k_skill_cleaner.py`를 사용할 수 있다.
3. helper는 root-level `SKILL.md` 디렉터리를 찾고, 사용자가 제공한 usage JSON 또는 로컬 로그를 스캔한다.
4. 결과 JSON의 `candidates`를 읽어 `remove``review`를 분리한다.
5. 삭제는 추천 이후 사용자가 명시적으로 승인한 경우에만 진행한다.
## 트리거 횟수 확인 방법
| 에이전트 | 확인 위치 | 주의점 |
| --- | --- | --- |
| Claude Code | `~/.claude/projects/**/*.jsonl`, `~/.claude/transcripts/**/*.jsonl` | 스킬 이벤트, `$skill` 언급, `SKILL.md` 로드 흔적을 best-effort로 센다. |
| Codex | `~/.codex/sessions/**/*.jsonl`, `~/.codex/log/**/*.log`, `.omx/logs/**/*.log` | 라우팅된 스킬명, `$skill` 호출, 스킬 파일 읽기 흔적을 센다. |
| OpenCode | `~/.local/share/opencode/**/*.jsonl`, `~/.config/opencode/**/*.jsonl` | 설치별 schema가 다를 수 있어 export된 transcript가 더 정확할 수 있다. |
| OpenClaw/ClawHub | `~/.openclaw/**/*.jsonl`, `~/.clawhub/**/*.jsonl` | 공개적으로 고정된 trigger-count schema를 가정하지 않는다. 가능하면 사용자가 export한 통계를 받는다. |
| Hermes Agent | `~/.hermes/**/*.jsonl`, `~/.config/hermes/**/*.jsonl` | 공개적으로 고정된 trigger-count schema를 가정하지 않는다. 가능하면 사용자가 export한 통계를 받는다. |
## 예시
```bash
python3 scripts/k_skill_cleaner.py \
--skills-root . \
--scan-default-logs \
--days 90 \
--never-use lotto-results \
--keep k-skill-setup,k-skill-cleaner
```
`--days 90`은 최근 90일 window만 카운트한다. timestamp가 없는 로그 줄은 파일 mtime으로 포함/제외를 결정한다. 단, `--usage-json`으로 넣은 값은 이미 집계된 count로 간주하므로 `--days`/`--since`로 다시 필터링하지 않는다. 같은 기간의 통계를 export하거나 직접 전처리한 JSON을 넣어야 한다. 출력은 `usage_json``scanned_logs` provenance를 포함하고, 파일 삭제를 하지 않는 JSON 리포트다. `zero_triggers``low_usage`만 있는 항목은 바로 삭제하지 말고 검토 후보로 남긴다. `interview_never_use`가 포함된 항목은 사용자의 의도가 확인된 삭제 후보로 보고한다.

View file

@ -18,10 +18,6 @@ client/skill -> k-skill-proxy -> upstream public API
- `GET /v1/fine-dust/report`
- `GET /v1/korea-weather/forecast`
- `GET /v1/seoul-subway/arrival`
- `GET /v1/seoul-density/citydata` (서울 실시간 도시데이터 핫스팟 혼잡도/추정 인구, `SEOUL_OPEN_API_KEY`)
- `GET /v1/seoul-bike/realtime` (서울 따릉이 실시간 대여정보 `bikeList`, `SEOUL_OPEN_API_KEY`)
- `GET /v1/seoul-bike/stations` (서울 따릉이 대여소 마스터 `tbCycleStationInfo`, `SEOUL_OPEN_API_KEY`)
- `GET /v1/seoul-bike/nearby` (좌표 주변 따릉이 실시간 대여소 필터링, `SEOUL_OPEN_API_KEY`)
- `GET /v1/han-river/water-level`
- `GET /v1/household-waste/info` (생활쓰레기 배출정보, `DATA_GO_KR_API_KEY`; 쿼리 `pageNo`·`numOfRows` 필수, 값 `1`·`100`)
- `GET /v1/mfds/drug-safety/lookup` (식약처 의약품개요정보 + 안전상비의약품 정보, `DATA_GO_KR_API_KEY`)
@ -29,28 +25,17 @@ client/skill -> k-skill-proxy -> upstream public API
- `GET /v1/korean-stock/search`
- `GET /v1/korean-stock/base-info`
- `GET /v1/korean-stock/trade-info`
- `GET /v1/naver-shopping/search` (네이버 검색 Open API 쇼핑 검색 우선, 키가 없으면 공개 BFF JSON 기반 상품/가격 후보 조회)
- `GET /v1/opinet/around`
- `GET /v1/opinet/detail`
- `GET /v1/neis/school-search` (나이스 학교기본정보, `KEDU_INFO_KEY`)
- `GET /v1/neis/school-meal` (나이스 급식식단정보, `KEDU_INFO_KEY`)
- `GET /v1/data4library/library-search` (도서관 정보나루 정보공개 도서관 조회, `DATA4LIBRARY_AUTH_KEY`)
- `GET /v1/data4library/book-search` (도서관 정보나루 도서 검색, `DATA4LIBRARY_AUTH_KEY`)
- `GET /v1/data4library/book-detail` (도서관 정보나루 도서 상세 조회, `DATA4LIBRARY_AUTH_KEY`)
- `GET /v1/data4library/libraries-by-book` (도서 소장 도서관 조회, `DATA4LIBRARY_AUTH_KEY`)
- `GET /v1/data4library/book-exists` (도서관별 도서 소장여부, `DATA4LIBRARY_AUTH_KEY`)
- `GET /v1/kstartup/business-info` (창업진흥원 K-Startup 통합공고 지원사업 정보, `DATA_GO_KR_API_KEY`)
- `GET /v1/kstartup/announcements` (창업진흥원 K-Startup 지원사업 공고 정보, `DATA_GO_KR_API_KEY`)
- `GET /v1/kstartup/contents` (창업진흥원 K-Startup 창업 콘텐츠 정보, `DATA_GO_KR_API_KEY`)
- `GET /v1/kstartup/statistics` (창업진흥원 K-Startup 통계보고서 정보, `DATA_GO_KR_API_KEY`)
- `GET /B552584/:service/:operation` (허용된 AirKorea route passthrough)
## 권장 환경변수
클라이언트(스킬) 쪽:
- 일반 hosted client는 `KSKILL_PROXY_BASE_URL`을 unset/empty로 비워 두면 hosted `https://k-skill-proxy.nomadamas.org`를 기본값으로 사용합니다.
- `KSKILL_PROXY_BASE_URL=https://your-proxy.example.com`은 self-host 또는 alternate proxy를 명시적으로 쓰는 경우에만 설정하는 override 예시입니다.
- `KSKILL_PROXY_BASE_URL=https://your-proxy.example.com`
프록시 서버 쪽:
@ -62,39 +47,43 @@ client/skill -> k-skill-proxy -> upstream public API
- `DATA_GO_KR_API_KEY=...`
- `FOODSAFETYKOREA_API_KEY=...` (선택: 식품안전나라 회수 live 결과, 없으면 sample fallback)
- `KEDU_INFO_KEY=...` (나이스 교육정보 개방 포털 Open API 인증키)
- `DATA4LIBRARY_AUTH_KEY=...` (도서관 정보나루 Open API 인증키)
- `KRX_API_KEY=...`
- `NAVER_SEARCH_CLIENT_ID=...`, `NAVER_SEARCH_CLIENT_SECRET=...` (선택: 네이버 검색 Open API 쇼핑 검색)
- `KSKILL_PROXY_PORT=4020`
## 프로덕션 배포 구조
프로덕션 proxy 서버는 **Google Cloud Run**에서 운영한다.
프로덕션 proxy 서버는 개발 repo와 분리된 별도 clone으로 운영한다.
- GCP project: `k-skill-proxy`
- Region: `asia-northeast1` (도쿄)
- Cloud Run service: `k-skill-proxy`
- 공개 도메인: `k-skill-proxy.nomadamas.org` (Cloud Run domain mapping)
- 컨테이너 이미지 정의: `packages/k-skill-proxy/Dockerfile`
- 시크릿(upstream API key): GCP Secret Manager에 보관, Cloud Run runtime에 주입
- 배포 디렉토리: `~/.local/share/k-skill-proxy` (main 브랜치 단독 clone)
- PM2 프로세스: `k-skill-proxy`
- Cloudflare Tunnel ingress: `k-skill-proxy.nomadamas.org -> http://localhost:4020`
### 자동 배포 (GitHub Actions)
### 자동 배포 (cron)
`main` 브랜치에 push/merge되면 `.github/workflows/deploy-k-skill-proxy.yml` 워크플로가 실행되어 다음 순서로 동작한다.
`~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh`가 매시 정각에 실행된다.
1. Workload Identity Federation으로 GCP 인증
2. `packages/k-skill-proxy/Dockerfile`로 이미지 빌드
3. Artifact Registry (`asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:<sha>`)에 push
4. Cloud Run service `k-skill-proxy` 재배포 (Secret Manager 시크릿 + 런타임 환경변수 주입)
5. 직접 Cloud Run URL과 `https://k-skill-proxy.nomadamas.org/health` smoke test
```
0 * * * * PATH=/usr/bin:/opt/homebrew/bin:/opt/homebrew/lib/node_modules/.bin:$PATH ~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh >> /tmp/k-skill-proxy-update.log 2>&1
```
동작 순서:
1. `git fetch origin main`
2. local SHA == remote SHA 이면 종료 (up-to-date)
3. `git pull --ff-only`
4. `package-lock.json` 변경 시 `npm ci`
5. `pm2 restart k-skill-proxy --update-env`
따라서 **main에 merge되어야 프로덕션에 반영**된다. dev 브랜치 변경은 프로덕션에 영향 없음.
배포 상태와 로그는 GitHub Actions의 "Deploy k-skill-proxy to Cloud Run" 워크플로 실행 페이지와 GCP Console의 Cloud Run revision/log에서 확인한다.
로그: `/tmp/k-skill-proxy-update.log`
### 초기 셋업 (운영자 1회 수행)
### 초기 설정 (PM2 + cloudflared)
WIF pool/provider, deploy service account, Secret Manager 시크릿 생성 등 1회성 GCP 셋업 절차와 GitHub repository secrets/variables 등록 방법은 [`docs/deploy-k-skill-proxy.md`](../deploy-k-skill-proxy.md)에 정리되어 있다.
1. `pm2 start ecosystem.config.cjs`
2. `pm2 save`
3. `pm2 startup` 출력대로 launchd 등록
4. Cloudflare Tunnel ingress 에 `k-skill-proxy.nomadamas.org -> http://localhost:4020` 추가
## 기본 공개 정책
@ -117,30 +106,14 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/fine-dust/report' \
서울 지하철 도착정보 endpoint:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/seoul-subway/arrival" \
curl -fsS --get 'http://127.0.0.1:4020/v1/seoul-subway/arrival' \
--data-urlencode 'stationName=강남'
```
서울 실시간 혼잡도 endpoint:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/seoul-density/citydata" \
--data-urlencode 'area=강남역'
# 서울 따릉이 주변 대여소
curl -fsS --get "${BASE}/v1/seoul-bike/nearby" \
--data-urlencode 'lat=37.5717' \
--data-urlencode 'lon=126.9763' \
--data-urlencode 'radius_m=500'
```
한국 날씨 endpoint:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/korea-weather/forecast" \
curl -fsS --get 'http://127.0.0.1:4020/v1/korea-weather/forecast' \
--data-urlencode 'lat=37.5665' \
--data-urlencode 'lon=126.9780'
```
@ -212,76 +185,6 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/mfds/food-safety/search'
--data-urlencode 'limit=5'
```
KOSIS 통계 조회 endpoint (`KOSIS_API_KEY` 필요, caller `apiKey`는 무시하고 서버 쪽 키를 주입):
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/kosis/search' \
--data-urlencode 'q=1인 가구' \
--data-urlencode 'limit=3'
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/kosis/meta' \
--data-urlencode 'tableId=DT_1JC1501' \
--data-urlencode 'metaType=ITM'
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/kosis/data' \
--data-urlencode 'tableId=DT_1JC1501' \
--data-urlencode 'prdSe=Y' \
--data-urlencode 'start=2020' \
--data-urlencode 'end=2023' \
--data-urlencode 'objL1=ALL'
```
Kakao Local geocoding endpoint (`KAKAO_REST_API_KEY` 필요, caller `apiKey`는 무시하고 서버 쪽 키를 주입):
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/kakao-local/geocode' \
--data-urlencode 'q=서울역' \
--data-urlencode 'limit=1'
```
도서관 정보나루 도서 검색 endpoint (`DATA4LIBRARY_AUTH_KEY` 필요):
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/data4library/book-search' \
--data-urlencode 'keyword=역사' \
--data-urlencode 'pageNo=1' \
--data-urlencode 'pageSize=10'
```
도서 상세/소장 조회 endpoint:
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/data4library/book-detail' \
--data-urlencode 'isbn13=9788971998557' \
--data-urlencode 'loaninfoYN=Y'
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/data4library/libraries-by-book' \
--data-urlencode 'isbn=9788971998557' \
--data-urlencode 'region=11'
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/data4library/book-exists' \
--data-urlencode 'libraryCode=111001' \
--data-urlencode 'isbn13=9788971998557'
```
프록시는 caller가 넘긴 `authKey`/`format`을 무시하고 서버 쪽 `DATA4LIBRARY_AUTH_KEY``format=json`을 주입한다.
네이버 쇼핑 가격비교 endpoint (`NAVER_SEARCH_CLIENT_ID`/`NAVER_SEARCH_CLIENT_SECRET`이 있으면 공식 Search API 우선):
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/naver-shopping/search' \
--data-urlencode 'q=에어팟 프로 2세대' \
--data-urlencode 'limit=10'
```
키가 없는 no-key fallback은 `search.shopping.naver.com/search/all` HTML 페이지 대신
`ns-portal.shopping.naver.com/api/v2/shopping-paged-slot?query=<검색어>&source=shp_gui`
공개 JSON path를 사용한다. `page`는 BFF에 전달한 뒤 해당 페이지 카드만 정규화하고, no-key
`price_asc`/`price_dsc`/`review` 정렬은 선택된 BFF 페이지 안에서 로컬 적용한다. BFF에는 날짜
필드가 없어 no-key `date` 요청은 `meta.sort_applied: "unsupported"`로 표시한다.
한국 주식 검색 endpoint:
```bash
@ -317,5 +220,5 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/B552584/ArpltnInforInqireSv
- upstream key는 프록시 서버에서만 관리합니다.
- 한국 주식 route도 사용자에게 `KRX_API_KEY` 를 배포하지 않습니다.
- client 쪽에는 upstream API key를 배포하지 않습니다.
- 도서관 정보나루 route도 사용자에게 `DATA4LIBRARY_AUTH_KEY` 를 배포하지 않습니다.
- self-host proxy 운영자는 동일 route를 local/self-host URL 로도 검증합니다.
- public hosted route rollout 이 끝나기 전에는 서울 지하철/한국 날씨 예시를 local/self-host URL 로 검증합니다.
- public hosted route rollout 이 끝나기 전에는 한강 수위 route도 local/self-host 또는 배포 확인이 끝난 proxy URL 로 검증합니다.

View file

@ -1,104 +0,0 @@
# 카카오맵 가이드
## 이 기능으로 할 수 있는 일
- **장소 검색**: 키워드(`스타벅스`)·카테고리(`FD6`=음식점)·좌표 중심으로 가게·시설 검색 (Kakao Local API)
- **좌표 ↔ 주소 변환**: 좌표 → 도로명/지번 주소, 좌표 → 행정구역(법정동/행정동)
- **자동차 길찾기**: 출발지·목적지 좌표 기준 거리·소요시간·통행료·예상 택시 요금 (Kakao Mobility Directions)
- 모두 `k-skill-proxy` 경유. 사용자 키 발급 불필요.
## 먼저 필요한 것
- [공통 설정 가이드](../setup.md) 확인
- 사용자는 별도 Kakao Developers 앱 생성/키 발급 필요 없음
- 운영자(proxy 서버)는 `KAKAO_REST_API_KEY` 보유
## 기본 경로
기본 hosted path: `https://k-skill-proxy.nomadamas.org/v1/kakao-map/*`, `https://k-skill-proxy.nomadamas.org/v1/kakao-mobility/*`
`KSKILL_PROXY_BASE_URL` 환경변수로 override 가능.
## Proxy routes
| endpoint | upstream | 주요 입력 |
|---|---|---|
| `GET /v1/kakao-map/search/keyword` | `https://dapi.kakao.com/v2/local/search/keyword.json` | `q`, `x`, `y`, `radius`, `category_group_code`, `sort`, `page`, `size` |
| `GET /v1/kakao-map/search/category` | `https://dapi.kakao.com/v2/local/search/category.json` | `category_group_code`, `x`, `y`, `radius`, `sort`, `page`, `size` |
| `GET /v1/kakao-map/coord2address` | `https://dapi.kakao.com/v2/local/geo/coord2address.json` | `x`, `y`, `input_coord` |
| `GET /v1/kakao-map/coord2region` | `https://dapi.kakao.com/v2/local/geo/coord2regioncode.json` | `x`, `y`, `input_coord` |
| `GET /v1/kakao-mobility/directions` | `https://apis-navi.kakaomobility.com/v1/directions` | `origin=x,y`, `destination=x,y`, `waypoints`, `priority`(RECOMMEND\|TIME\|DISTANCE), `car_fuel`, `car_hipass`, `alternatives`, `avoid`(ferries\|toll\|motorway\|schoolzone\|uturn; `\|` 구분) |
## 기본 흐름
1. 사용자가 장소 키워드/카테고리/좌표/길찾기 질문을 한다.
2. 적합한 endpoint를 골라 proxy 로 호출한다 (위 표 참고).
3. proxy는 `KAKAO_REST_API_KEY` 를 서버측에서만 `Authorization: KakaoAK ...` 헤더로 주입한다.
4. 응답에서 핵심 필드만 추려 사용자에게 정리해 전달한다.
5. 성공 응답은 proxy cache(기본 TTL 5분)로 보관해 다음 동일 쿼리를 빠르게 돌려준다.
## 예시
키워드 검색:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/kakao-map/search/keyword" \
--data-urlencode 'q=스타벅스' \
--data-urlencode 'x=127.0276' \
--data-urlencode 'y=37.4979' \
--data-urlencode 'radius=500' \
--data-urlencode 'sort=distance'
```
좌표 → 주소:
```bash
curl -fsS --get "${BASE}/v1/kakao-map/coord2address" \
--data-urlencode 'x=127.0276' \
--data-urlencode 'y=37.4979'
```
자동차 길찾기:
```bash
curl -fsS --get "${BASE}/v1/kakao-mobility/directions" \
--data-urlencode 'origin=126.9706,37.5559' \
--data-urlencode 'destination=127.0276,37.4979' \
--data-urlencode 'priority=RECOMMEND' \
--data-urlencode 'avoid=toll'
```
응답 요약(예):
```text
자동차 경로: (126.9706,37.5559) → (127.0276,37.4979)
- 거리: 12.3km / 예상 소요시간: 25분
- 통행료: 1,200원 / 예상 택시요금: 18,500원
- 옵션: RECOMMEND, avoid=toll
```
## fallback / 대체 흐름
- 키 누락(`503 upstream_not_configured`) → 사용자에게 운영자 설정 필요 안내
- 인증 실패(401/403) → `503` 으로 변환 (key revoke / 쿼터 초과)
- 좌표 형식 오류 / 미존재 카테고리 코드 → `400 bad_request`
- 경로 미발견·출발지=도착지 근접 등 semantic 실패 → `502 upstream_semantic_error` + `result_msg`
- 네트워크 실패 → `502 upstream_error`
## 주의할 점
- Kakao Mobility는 **자동차 전용**이다. 대중교통 길찾기는 [한국 대중교통 길찾기 가이드](korean-transit-route.md) 를 쓴다.
- 카테고리 검색은 좌표 중심(`x`, `y`)이 필수다.
- waypoints 는 최대 5개 (Kakao Mobility 정책).
- 통행료 회피는 `avoid=toll`을 사용한다. `priority=DISTANCE`는 최단거리 우선순위일 뿐 통행료 회피와 동의어가 아니다.
- Kakao Mobility 무료 일일 쿼터는 1,000건 수준이다. proxy cache + rate-limit이 보호 역할을 하지만, 대량 호출은 자제한다.
- 본 스킬은 데이터 조회 전용이다. 예약·결제·자동 운전은 하지 않는다.
- secret/token/.env 원문은 응답에 노출되지 않는다 (proxy가 키를 서버측에서만 주입).
## 참고 표면
- Kakao Developers Console: `https://developers.kakao.com`
- Kakao Local API 문서: `https://developers.kakao.com/docs/latest/ko/local/dev-guide`
- Kakao Mobility 안내: `https://developers.kakao.com/docs/latest/ko/kakaonavi/common`
- proxy 운영 안내: [k-skill 프록시 서버 가이드](k-skill-proxy.md)

View file

@ -1,113 +1,84 @@
# 카카오톡 Mac 아카이브 검색 가이드
# 카카오톡 Mac CLI 가이드
## 이 기능으로 할 수 있는 일
- Apple Silicon macOS에서 `katok`으로 카카오톡 로컬 대화 아카이브 생성
- keyword, BM25, semantic 검색
- 검색 결과의 chunk id로 원문, 주변 맥락, parent window 조회
- 검색 전 freshness 확인과 sync/index 필요 여부 판단
이 가이드는 기존 `kakaotalk-mac` 스킬 경로를 유지하지만 실행 표면은 `katok` CLI다. 메시지 전송, 삭제, UI 자동화, 직접 DB 읽기, 인증 캐시 처리, 복호화 material 처리는 포함하지 않는다.
- macOS에서 카카오톡 최근 대화 목록 확인
- 특정 채팅방 최근 메시지 읽기
- 키워드로 전체 대화 검색
- 나와의 채팅으로 안전하게 테스트 전송
- 사용자 확인 후 특정 채팅방으로 메시지 전송
## 먼저 필요한 것
- Apple Silicon macOS
- macOS
- KakaoTalk for Mac 설치
- Homebrew 또는 Cargo
- `katok` CLI
- 현재 터미널 앱의 Full Disk Access 권한
- Homebrew
- `brew install silver-flight-group/tap/kakaocli`
- `python3` 3.10+
- 이 저장소의 helper `scripts/kakaotalk_mac.py`
- 터미널 앱에 **Full Disk Access****Accessibility** 권한 부여
## 설치
Homebrew:
카카오톡 앱이 없으면 `mas` 로 먼저 설치할 수 있다.
```bash
brew tap NomaDamas/katok https://github.com/NomaDamas/katok.git
brew install katok
brew install mas
mas account
mas install 869223134
```
Cargo:
## 입력값
```bash
cargo install katok
export PATH="$HOME/.cargo/bin:$PATH"
```
Cargo 설치 후 `katok`이 보이지 않으면 `$HOME/.cargo/bin`을 shell PATH에 추가한다.
## 개인 정보와 안전 규칙
- Do not inspect local database internals from this skill.
- Do not directly read KakaoTalk DB files.
- Do not handle auth caches or decryption material.
- live macOS 카카오톡 ingestion은 `katok sync --source macos --json`으로만 수행한다.
- 검색 결과는 snippet과 chunk id 중심으로 먼저 다룬다.
- 사용자가 특정 결과를 열어 달라고 하거나 chunk id를 제공했을 때만 chunk 원문을 조회한다.
- 채팅방 이름
- 검색 키워드
- 최근 범위(`--since 1h`, `--since 7d` 등)
- 전송 메시지 본문
- 테스트 여부(`--me`, `--dry-run`)
## 기본 흐름
1. `katok doctor --json`으로 freshness와 준비 상태를 확인한다.
2. Full Disk Access 설정이 필요하면 `katok permissions macos`로 시스템 설정 화면을 연다.
3. 앱 설치, container, DB 파일 접근 진단이 필요할 때만 `katok doctor --macos-probe --json`을 실행한다.
4. 최신성이 중요하거나 sync 권장이 있으면 `katok sync --source macos --json`을 실행한다.
5. semantic search 전에 index 권장이 있으면 `katok index --json`을 실행한다.
6. 질의 성격에 따라 `katok search keyword`, `katok search bm25`, `katok search semantic`을 선택한다.
7. 사용자가 지정한 결과만 `katok chunk get`, `katok chunk context`, `katok chunk parent`로 연다.
1. KakaoTalk for Mac 과 `kakaocli` 가 설치되어 있는지 확인한다.
2. `kakaocli status`, `kakaocli auth` 로 권한과 DB 접근이 되는지 먼저 확인한다.
3. `user_id` 자동 감지가 실패하면 helper `python3 scripts/kakaotalk_mac.py auth --refresh` 로 복구한다.
4. 읽기/검색은 JSON 모드로 실행한 뒤 사람이 읽기 쉽게 요약한다.
5. 전송은 먼저 `--me` 또는 `--dry-run` 으로 테스트한다.
6. 다른 사람에게 보내는 메시지는 항상 최종 확인 후에만 전송한다.
## 예시
```bash
katok doctor --json
katok permissions macos
katok doctor --macos-probe --json
katok sync --source macos --json
katok index --json
katok search keyword "계약서" --json
katok search bm25 "지난주 미팅 자료" --json
katok search semantic "최근에 논의한 세금 신고 일정" --json
katok chunk get <chunk-id> --json
katok chunk context <chunk-id> --json
katok chunk parent <chunk-id> --json
kakaocli status
kakaocli auth
python3 scripts/kakaotalk_mac.py auth --refresh
python3 scripts/kakaotalk_mac.py chats --limit 10 --json
python3 scripts/kakaotalk_mac.py messages --chat "지수" --since 1d --json
python3 scripts/kakaotalk_mac.py search "회의" --json
kakaocli chats --limit 10 --json
kakaocli messages --chat "지수" --since 1d --json
kakaocli search "회의" --json
kakaocli send --me _ "테스트 메시지"
kakaocli send --dry-run "팀 공지방" "오늘 3시에 만나요"
```
## 검색 방식 선택
## helper 가 해결하는 문제
`katok search keyword`는 정확한 문자열, 이름, 계좌번호, 고유명사처럼 그대로 기억나는 값을 찾을 때 쓴다.
`kakaocli auth` 실패가 항상 “DB 파일이 없음”을 의미하지는 않는다. 실제 Mac 환경에서는:
`katok search bm25`는 여러 단어가 섞인 일반 질의에 쓴다.
- container 안에 `KakaoTalk.db` 라는 이름 대신 **78자 hex 파일**이 DB 로 존재할 수 있다.
- `kakaocli status` 는 정상이어도 `auth``user_id 자동 감지 실패` 로 끝날 수 있다.
- 이 경우 plist 의 `AlertKakaoIDsList` 후보만으로는 부족하고, `DESIGNATEDFRIENDSREVISION:<SHA-512(user_id)>` 에서 실제 `user_id` 를 더 오래 찾아야 할 수 있다.
`katok search semantic`은 표현이 정확히 기억나지 않지만 의미가 비슷한 대화를 찾을 때 쓴다. `katok doctor --json`에서 semantic index 갱신이 필요하다고 나오면 먼저 `katok index --json`을 실행한다.
helper `scripts/kakaotalk_mac.py` 는 그 얇은 read-only 어댑터 역할을 한다.
## chunk 조회
검색 결과에서 더 넓은 맥락이 필요할 때만 chunk 명령을 사용한다.
```bash
katok chunk get <chunk-id> --json
katok chunk context <chunk-id> --json
katok chunk parent <chunk-id> --json
```
- `chunk get`: 해당 chunk 원문 조회
- `chunk context`: 같은 채팅방의 바로 앞뒤 micro chunk 조회
- `chunk parent`: semantic search가 사용한 더 큰 parent window 조회
## Synthetic QA
실제 카카오톡 설치 없이 upstream fixture로 테스트할 때만 아래 경로를 쓴다.
```bash
katok sync --source fixture tests/fixtures/kakao/replies.jsonl --json
KATOK_EMBEDDER=local-test katok index --json
KATOK_EMBEDDER=mock katok index --json
```
실사용 경로에서는 fixture, mock embedder, 원격 embedding endpoint를 사용하지 않는다.
- plist 에서 후보 `user_id` 와 active hash 를 읽는다.
- hash recovery 가 필요하면 더 긴 검색으로 실제 `user_id` 를 찾는다.
- 검증된 DB 경로와 SQLCipher key 를 `~/.cache/k-skill/kakaotalk-mac-auth.json` 에 캐시한다.
- 이후 read-only helper 명령 `chats`, `messages`, `search`, `schema` 를 cached `--db` / `--key` 와 함께 다시 실행한다.
## 주의할 점
- Apple Silicon macOS 전용이다.
- Intel macOS는 packaged local EmbeddingGemma 경로의 지원 대상이 아니다.
- Full Disk Access는 사용자가 System Settings에서 직접 허용해야 한다.
- `katok doctor --macos-probe --json`은 macOS app-data 접근 prompt를 띄울 수 있으므로 setup 진단이 필요할 때만 실행한다.
- 이 스킬은 read/search/retrieve 전용이며 메시지 전송과 삭제를 지원하지 않는다.
- **Full Disk Access** 가 없으면 읽기 명령도 실패할 수 있다.
- **Accessibility** 가 없으면 전송과 harvest 계열 자동화가 실패한다.
- macOS 전용이므로 Windows/Linux 대체 구현으로 넘어가지 않는다.
- 다른 사람에게 보내는 메시지는 자동 전송하지 말고 확인을 먼저 받는다.
- helper cache 는 로컬 auth material 을 담으므로 본인 장비에서만 보관한다.
- 기본 `auth` 텍스트 출력은 key 를 다시 보여주지 않는다. 자동화가 필요할 때만 `--format json` 또는 `--format shell` 을 사용한다.

View file

@ -10,13 +10,13 @@
- [공통 설정 가이드](../setup.md) 완료
- [보안/시크릿 정책](../security-and-secrets.md) 확인
- optional: `KSKILL_PROXY_BASE_URL` (self-host·별도 프록시를 쓸 때만 설정. 비우면 기본 hosted `https://k-skill-proxy.nomadamas.org` 를 사용)
- self-host 또는 배포 확인이 끝난 proxy base URL: `KSKILL_PROXY_BASE_URL`
## 필요한 환경변수
- 없음. `KSKILL_PROXY_BASE_URL` 은 선택 사항이며, 비우면 기본 hosted `https://k-skill-proxy.nomadamas.org` 를 사용한다.
- `KSKILL_PROXY_BASE_URL` (필수: self-host 또는 배포 확인이 끝난 proxy base URL)
사용자가 공공데이터포털 기상청 단기예보 API key를 직접 발급할 필요는 없다. `/v1/korea-weather/forecast` route는 기본 hosted proxy에서 호출하고, upstream `KMA_OPEN_API_KEY` 는 proxy 서버에서만 관리한다. 별도 proxy를 쓰는 경우에만 `KSKILL_PROXY_BASE_URL` 을 설정한다.
사용자가 공공데이터포털 기상청 단기예보 API key를 직접 발급할 필요는 없다. 대신 `KSKILL_PROXY_BASE_URL``/v1/korea-weather/forecast` route가 실제로 배포된 proxy 를 가리켜야 한다. upstream `KMA_OPEN_API_KEY` 는 proxy 서버에서만 관리한다.
## 입력값
@ -26,7 +26,7 @@
## 기본 흐름
1. `KSKILL_PROXY_BASE_URL` 이 있으면 그 값을 사용하고, 없거나 비어 있으면 기본 hosted proxy `https://k-skill-proxy.nomadamas.org` 를 사용한다.
1. `KSKILL_PROXY_BASE_URL` 로 self-host 또는 배포 확인이 끝난 proxy base URL 을 확인한다.
2. `/v1/korea-weather/forecast` 로 한국 기상청 단기예보를 조회한다.
3. `baseDate` / `baseTime` 을 생략하면 proxy 가 KST 기준 최신 발표 시각을 자동으로 선택한다.
4. 응답의 `item[]` 에서 `TMP`, `SKY`, `PTY`, `POP`, `PCP`, `SNO`, `REH`, `WSD` 를 우선 요약한다.
@ -36,8 +36,7 @@
위도/경도 기준:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/korea-weather/forecast" \
curl -fsS --get 'https://your-proxy.example.com/v1/korea-weather/forecast' \
--data-urlencode 'lat=37.5665' \
--data-urlencode 'lon=126.9780'
```
@ -45,8 +44,7 @@ curl -fsS --get "${BASE}/v1/korea-weather/forecast" \
격자 좌표 기준:
```bash
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
curl -fsS --get "${BASE}/v1/korea-weather/forecast" \
curl -fsS --get 'https://your-proxy.example.com/v1/korea-weather/forecast' \
--data-urlencode 'nx=60' \
--data-urlencode 'ny=127' \
--data-urlencode 'baseDate=20260405' \
@ -68,4 +66,5 @@ curl -fsS --get "${BASE}/v1/korea-weather/forecast" \
- 단기예보는 5km 격자 기반이라 행정구역 경계와 완전히 일치하지 않을 수 있다.
- 발표 시각 직후에는 최신 `baseTime` 이 아직 준비되지 않았을 수 있다. proxy 는 보수적으로 직전 발표 시각을 선택한다.
- public hosted route rollout 이 끝나기 전까지는 `KSKILL_PROXY_BASE_URL` 을 반드시 명시한다.
- self-host proxy 설정은 [k-skill 프록시 서버 가이드](k-skill-proxy.md)를 본다.

View file

@ -1,95 +0,0 @@
# 영화관 검색 가이드
원본 [`hmmhmmhm/daiso-mcp`](https://github.com/hmmhmmhm/daiso-mcp) 와 npm package [`daiso`](https://www.npmjs.com/package/daiso) 를 사용해 CGV, 메가박스, 롯데시네마의 영화관 검색, 상영작, 시간표, 잔여석 조회를 한다.
## 가장 중요한 규칙
`k-skill` 안에 별도 영화관 수집기를 추가하지 않는다.
기본 경로는 **MCP 서버를 직접 설치하지 않고 CLI로 먼저 확인하는 방식**이다.
1. `npx --yes daiso ...`
2. 필요하면 `git clone https://github.com/hmmhmmhm/daiso-mcp.git && cd daiso-mcp && npm install && npm run build`
3. clone fallback에서는 `node dist/bin.js ...`
## 빠른 확인
날짜가 있는 요청은 Asia/Seoul 기준 `YYYYMMDD` 로 정규화하고 `--playDate <YYYYMMDD>` 를 항상 붙인다. 예를 들어 오늘을 물으면 KST 오늘 날짜를 계산해서 넣는다.
```bash
npx --yes daiso health
npx --yes daiso get /api/cgv/theaters --keyword 강남 --limit 5 --json
npx --yes daiso get /api/cgv/movies --keyword 강남 --playDate <YYYYMMDD> --json
npx --yes daiso get /api/cgv/timetable --keyword 강남 --playDate <YYYYMMDD> --json
npx --yes daiso get /api/megabox/theaters --keyword 코엑스 --limit 5 --json
npx --yes daiso get /api/megabox/movies --keyword 코엑스 --playDate <YYYYMMDD> --json
npx --yes daiso get /api/megabox/seats --keyword 코엑스 --playDate <YYYYMMDD> --limit 10 --json
npx --yes daiso get /api/lottecinema/theaters --keyword 월드타워 --limit 5 --json
npx --yes daiso get /api/lottecinema/movies --keyword 월드타워 --playDate <YYYYMMDD> --json
npx --yes daiso get /api/lottecinema/seats --keyword 월드타워 --playDate <YYYYMMDD> --limit 10 --json
```
## 원본 저장소 clone fallback
```bash
git clone https://github.com/hmmhmmhm/daiso-mcp.git
cd daiso-mcp
npm install
npm run build
node dist/bin.js health
node dist/bin.js get /api/cgv/theaters --keyword 강남 --limit 5 --json
node dist/bin.js get /api/cgv/timetable --keyword 강남 --playDate <YYYYMMDD> --json
node dist/bin.js get /api/megabox/seats --keyword 코엑스 --playDate <YYYYMMDD> --limit 10 --json
node dist/bin.js get /api/lottecinema/seats --keyword 월드타워 --playDate <YYYYMMDD> --limit 10 --json
```
## 입력값
- 체인: CGV, 메가박스, 롯데시네마
- 지역 또는 지점: 강남, 코엑스, 월드타워 등
- 영화명: 잔여석이나 시간표를 특정 영화로 좁힐 때 사용
- 날짜: 사용자가 날짜를 말하면 그 날짜를 우선하고, 없으면 Asia/Seoul 기준 오늘을 `YYYYMMDD` 로 계산한다.
| 체인 | 후보 조회 | 상영작 | 시간표 또는 잔여석 | 날짜 |
| --- | --- | --- | --- | --- |
| CGV | `keyword`, 선택 `limit` | `keyword` 또는 `theaterId`, `playDate` | `keyword` 또는 `theaterId`, `movieId`, `playDate` | 필수로 명시 |
| 메가박스 | `keyword`, 선택 `limit` | `keyword` 또는 `theaterId`, `playDate` | `keyword` 또는 `theaterId`, `movieId`, `playDate` | 필수로 명시 |
| 롯데시네마 | `keyword`, 선택 `limit` | `keyword` 또는 `theaterId`, `playDate` | `keyword` 또는 `theaterId`, `movieId`, `playDate` | 필수로 명시 |
## 사용 흐름
1. `npx --yes daiso health` 로 endpoint 상태를 확인한다.
2. `/api/cgv/theaters`, `/api/megabox/theaters`, `/api/lottecinema/theaters` 로 영화관 후보를 찾는다.
3. 날짜 표현은 Asia/Seoul 기준 `YYYYMMDD` 로 바꾼다.
4. `/api/cgv/movies`, `/api/megabox/movies`, `/api/lottecinema/movies` 로 상영작을 확인한다.
5. CGV는 `/api/cgv/timetable` 로 시간표를 본다.
6. 메가박스와 롯데시네마는 `/api/megabox/seats`, `/api/lottecinema/seats` 로 잔여석을 본다.
7. 예매와 결제는 자동화하지 않는다.
## 응답 원칙
- 기준 체인과 지점을 먼저 쓴다.
- 상영작과 시간표는 필요한 만큼만 보여준다.
- 잔여석은 조회 시점의 참고값으로 말한다.
- 영화관 공식 앱이나 웹에서 예매 직전 다시 확인하라고 안내한다.
## 실패 모드
- public endpoint가 일시적으로 5xx를 줄 수 있다.
- 넓은 지역 키워드는 여러 지점을 섞을 수 있다.
- 시간표와 잔여석은 빠르게 바뀔 수 있다.
- theaterId, movieId가 있으면 keyword보다 그 값을 우선한다.
## 출처
- 원본 repo: `https://github.com/hmmhmmhm/daiso-mcp`
- npm package: `https://www.npmjs.com/package/daiso`
- CGV theaters API: `https://mcp.aka.page/api/cgv/theaters`
- CGV movies API: `https://mcp.aka.page/api/cgv/movies`
- CGV timetable API: `https://mcp.aka.page/api/cgv/timetable`
- Megabox theaters API: `https://mcp.aka.page/api/megabox/theaters`
- Megabox movies API: `https://mcp.aka.page/api/megabox/movies`
- Megabox seats API: `https://mcp.aka.page/api/megabox/seats`
- Lotte Cinema theaters API: `https://mcp.aka.page/api/lottecinema/theaters`
- Lotte Cinema movies API: `https://mcp.aka.page/api/lottecinema/movies`
- Lotte Cinema seats API: `https://mcp.aka.page/api/lottecinema/seats`

View file

@ -1,60 +0,0 @@
# 한국어 AI 윤문 (korean-humanizer) 가이드
## 이 기능으로 할 수 있는 일
- ChatGPT·Claude·Gemini 등이 쓴 "AI 티 나는" 한국어 글을 자연스러운 사람 글로 윤문
- 번역체, AI 상투어, 과도한 명사화·피동, 3의 법칙, 과장된 의의 부여, 마무리 상투구, 챗봇 잔재, 줄표·이모지·곡선따옴표 같은 흔적을 **심각도(S1/S2/S3)** 로 분류해 탐지
- "이 글에서 AI 흔적 찾아줘"처럼 고치지 않고 진단만 (탐지 리포트 + 심각도)
- 목표 글자수 지정 시(`length=1000`, "1000자로") ±5% 안으로 분량 조정, 공백 포함/제외 글자수 보고
- 사용자 글 샘플을 주면 그 말투(voice)로 재작성
## 왜 별도 스킬이 필요한가
- 영어권 humanizer(QuillBot·Undetectable AI 등)는 한국어에 약하다. 한국어 AI 글의 티는 대부분 **영어 번역투**와 격식을 가장한 **상투어**에서 나온다.
- 단순 맞춤법 교정(`korean-spell-check`)이나 유행어 입히기(`korean-slang-writing`)와 달리, 이 스킬은 의미를 보존하면서 **문체·리듬·표현**만 사람답게 되돌린다.
- 과교정을 막기 위해 4대 철칙(의미 불변 · 근거 기반 · 장르 유지 · 과윤문 금지)과 변경률 가드(30% 경고, 50% 중단)를 둔다.
## 먼저 필요한 것
- 추가 설치·API 키 없음. 이 스킬은 프롬프트/지식 기반이며 외부 호출이나 스크립트가 없다.
- (선택) 정확한 글자수 카운팅이 필요하면 `korean-character-count` 스킬과 연동된다.
## 기본 흐름 (탐지 → 윤문 → 감사 → 등급)
1. **트리아지** — 흔적이 무더기인지, 서식만 문제인지, 산문까지 다시 써야 하는지 먼저 정한다.
2. **탐지** — A~J 분류 카탈로그로 흔적을 span·심각도로 표시한다. S1부터 본다.
3. **윤문** — 흔적을 자연스러운 표현으로 교체한다. 의미·사실·고유명사·수치는 100% 보존한다.
4. **감사** — "왜 아직 AI 같은가?"를 다시 묻고, 자가검증 6항과 변경률을 점검한다. 위반이면 롤백 후 재윤문.
5. **등급** — A~D로 자가 채점한다. C·D면 추가 윤문이나 사람 검토를 권한다.
전체 패턴 표(A~J, 60+ 서브 패턴)는 스킬 디렉터리의 [`references/ai-tell-taxonomy.md`](../../korean-humanizer/references/ai-tell-taxonomy.md)에 있다.
## 사용 예시
```text
이 글 AI 티 안 나게 자연스럽게 다듬어줘:
[ChatGPT/Claude 초안 붙여넣기]
```
```text
이 글에서 AI 흔적만 찾아줘 (고치지 말고 심각도까지)
```
```text
1000자로 맞춰서 번역체 고쳐줘
```
## 제한사항
- 문체만 고친다. 사실관계 확인·출처 보강은 하지 않는다(필요하면 별도 리서치).
- 원문에 없는 내용을 창작해 채우지 않는다(의미 보존이 원칙).
- 변경률이 50%를 넘으면 작업을 중단하고 사람 검토를 권한다.
## 감사의 말 (Acknowledgments)
이 스킬은 두 기여 위에 만들어졌다.
- **[happy-nut](https://github.com/happy-nut) (Hyungsun Song)** 님이 PR [#311](https://github.com/NomaDamas/k-skill/pull/311)로 최초 `korean-humanizer` 스킬과 33개 한국어 패턴 카탈로그·예문, triage/length-control 설계를 기여했다. 이 가이드와 v2 스킬의 토대다.
- **[epoko77-ai/im-not-ai](https://github.com/epoko77-ai/im-not-ai)** (Humanize KR, MIT)의 방법론을 중심으로 v2를 재구성했다. A~J 분류 체계, S1/S2/S3 심각도, 4대 철칙, 변경률 30%/50% 가드, 품질 등급(A~D), 그리고 A-16(그/그녀 강박)·A-18(관계절 좌향 수식)·A-19(이중 조사)·C-11(연결어미 뒤 쉼표)·E-7(경어법 일관성) 같은 한국어 고유 패턴이 여기서 왔다.
원형은 영어권 [blader/humanizer](https://github.com/blader/humanizer)와 [Wikipedia: Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing)이다. 두 프로젝트와 happy-nut 님의 기여에 감사한다.

View file

@ -1,90 +0,0 @@
# 한국 사업자 장부 자동화 가이드
## 이 기능으로 할 수 있는 일
- 카드명세서 PDF, 은행 CSV, 엑셀, 영수증/세금계산서 이미지·PDF를 표준 거래내역으로 정리
- 거래처·MCC·정규식·학습 규칙을 이용한 계정과목 매핑
- 세무용 BS·PL, 세무사 전달 CSV, 종합소득세 준비 체크리스트 생성
- 경영용 현금흐름, cash burn, 카드별 분석, 대시보드 리포트 생성
- 사용자가 직접 발급한 CODEF 키(**BYOK**)로 홈택스·은행·카드 자동 수집 흐름 연결
## 가장 중요한 규칙
본 스킬은 [`kimlawtech/korean-jangbu-for`](https://github.com/kimlawtech/korean-jangbu-for) 업스트림의 **thin wrapper** 이다. 원저작자·관리자는 **[@kimlawtech](https://github.com/kimlawtech) (SpeciAI)** 이며, k-skill 측은 원본 링크, attribution, 설치 진입점, 회계·세무 면책 고지를 책임진다.
이 스킬을 사용한 답변은 반드시 아래를 함께 언급한다.
- 원본: https://github.com/kimlawtech/korean-jangbu-for
- 원저작자: @kimlawtech (SpeciAI)
- 라이선스: Apache-2.0
- 생성물은 참고용 초안이며 공식 회계감사·세무신고를 대체하지 않는다.
- 신고/제출 전 세무사 검토, 외감 대상 법인은 공인회계사 감사가 필요하다.
## 먼저 필요한 것
- 인터넷 연결 (업스트림 clone 용)
- `git`, `bash`, Python 3.11+
- macOS 또는 Linux
- OCR/PDF 처리 의존성 (업스트림 install/verify 흐름이 안내)
- 자동 수집을 사용할 경우 사용자가 직접 발급한 CODEF Client ID/Secret (BYOK)
## 설치 흐름
`~/.claude/skills/korean-jangbu-for/upstream/``~/.agents/skills/korean-jangbu-for/upstream/` 양쪽에 pinned SHA 로 업스트림을 설치한다. 동시에 업스트림 `skills/jangbu-*``~/.claude/skills/<skill-name>``~/.agents/skills/<skill-name>` top-level 경로로 등록해 slash skill discovery 가 중첩 `upstream/` 탐색에 의존하지 않게 한다. `/korean-jangbu-for` 는 wrapper top-level skill 로 유지한다.
홈 디렉터리 wrapper 에는 재실행 가능한 `scripts/install.sh`, `scripts/upstream.pin`, `LICENSE.upstream`, `DISCLAIMER.md`, `NOTICE` 까지 함께 복사된다. Promoted `jangbu-*` top-level 스킬은 wrapper 가 이전에 설치한 managed copy 만 자동 갱신하며, 같은 이름의 unrelated/user-authored 스킬이 있으면 덮어쓰지 않고 중단한다. 의도적으로 교체해야 하는 경우에만 `KOREAN_JANGBU_FOR_OVERWRITE_SKILLS=1` 로 재실행한다.
```bash
bash korean-jangbu-for/scripts/install.sh
```
설치 확인:
```bash
cat korean-jangbu-for/scripts/upstream.pin
git -C ~/.claude/skills/korean-jangbu-for/upstream rev-parse HEAD
git -C ~/.agents/skills/korean-jangbu-for/upstream rev-parse HEAD
```
세 SHA 가 모두 같고 `~/.agents/skills/jangbu-import/SKILL.md` 같은 top-level 하위 스킬이 존재하면 wrapper 설치가 성공한 것이다. 실제 OCR/MCP 런타임까지 검증해야 할 때는 업스트림 설치 후 verify 를 실행한다.
```bash
bash ~/.claude/skills/korean-jangbu-for/upstream/scripts/install.sh
bash ~/.claude/skills/korean-jangbu-for/upstream/scripts/verify.sh
```
업스트림 installer 는 Claude Code 스킬 symlink 를 등록하므로 wrapper 개발 중이면 실행 후 홈 디렉토리 wrapper 를 다시 sync 한다. 레포 내부에 repo-local `.claude/` 또는 `.agents/` 디렉토리는 만들지 않는다.
## 사용 전 intake
사용자에게 한 번에 1~3문항씩 확인한다.
- 사업자 유형: 개인사업자 / 법인 / 프리랜서 / 복수 사업장
- 목표 산출물: 표준 거래내역 / 계정과목 분류 / 세무사 전달 CSV / BS·PL / 현금흐름·경영 리포트 / 종소세 체크리스트
- 입력 자료: 은행 CSV, 카드 PDF, 엑셀, 영수증 이미지, 세금계산서 PDF, CODEF 자동 수집
- 대상 기간: 월 / 분기 / 연도
- 민감정보 처리: 마스킹 필요 수준, 외부 LLM 경유 허용 여부
- 공식 제출 예정 여부: 세무사 검토 또는 공인회계사 감사 필요성 판단
## upstream 하위 스킬 라우팅
- `/korean-jangbu-for` — 전체 메뉴 라우팅
- `/jangbu-connect` — CODEF API 자격증명 설정(BYOK)
- `/jangbu-import` — 원본 데이터 표준화
- `/jangbu-tag` — 계정과목 매핑
- `/jangbu-tax` — 세무용 BS·PL / 세무사 전달 CSV
- `/jangbu-dash` — 경영 리포트 / 카드별 분석 / 현금흐름
- `/jangbu-jongso` — 종합소득세 준비 체크리스트
## 보안 / 자격증명
CODEF 자동 수집은 BYOK 원칙을 따른다. 사용자가 직접 발급한 Client ID/Secret 만 사용하고, 값은 채팅에 노출하지 않는다. 저장은 업스트림 정책에 따라 macOS Keychain 또는 `~/.jangbu/credentials.env`(0o600) 같은 로컬 저장소에 한정한다. 금융기관 조회, 간편인증, 로그인 등 side-effect 는 사용자의 명시 승인 없이 실행하지 않는다.
## 라이선스 / 출처
- 업스트림: https://github.com/kimlawtech/korean-jangbu-for (Apache-2.0)
- 원저작자·관리자: @kimlawtech (SpeciAI)
- upstream pin 파일: `korean-jangbu-for/scripts/upstream.pin`
- 회계·세무 면책 전문: `korean-jangbu-for/DISCLAIMER.md`
- Apache-2.0 전문: `korean-jangbu-for/LICENSE.upstream`
- attribution notice: `korean-jangbu-for/NOTICE`

View file

@ -2,101 +2,126 @@
## 이 기능으로 할 수 있는 일
- `k-skill-proxy` 로 법령명/조문/판례/유권해석/자치법규 검색
- 검색 결과 식별자로 조문·판례 본문(상세) 조회
- 별도 API key나 로컬 설치 없이 hosted proxy로 바로 사용
- `korean-law-mcp` 로 법령명 검색
- 특정 법령의 조문 본문 조회
- 판례 / 유권해석 / 자치법규 검색
- MCP 또는 CLI 경로 중 현재 환경에 맞는 방식 선택
- 기존 경로 장애 시 `법망` fallback으로 이어가기
## 가장 중요한 규칙
한국 법령 관련 검색/조회는 기본 hosted proxy(`k-skill-proxy.nomadamas.org`)의 `/v1/korean-law/...` endpoint로 처리합니다. 사용자 쪽 `LAW_OC` 가 불필요합니다. 별도 repo package, 별도 python package, 임의 크롤러를 새로 만들지 않습니다.
이 endpoint는 법제처(국가법령정보센터) 공식 Open API(`open.law.go.kr` 의 DRF `lawSearch.do`/`lawService.do`)를 감싼 것이고, read-only 도구 표면 설계는 `chrisryugj/korean-law-mcp` 를 참고했습니다.
한국 법령 관련 검색/조회가 필요할 때는 **`korean-law-mcp`를 먼저 사용**합니다.
기존 서비스가 동작하지 않을 때만 승인된 fallback 표면인 **`법망`(`https://api.beopmang.org`)** 으로 전환합니다.
별도 repo package, 별도 python package, 임의 크롤러를 새로 만들지 않습니다.
## 먼저 필요한 것
- 인터넷 연결
- (선택) `KSKILL_PROXY_BASE_URL` — self-host proxy를 쓸 때만
- `node` 18+
- `npm install -g korean-law-mcp` (로컬 CLI/로컬 MCP server 경로일 때)
- remote MCP endpoint를 쓸 MCP 클라이언트
- `법망` fallback (`https://api.beopmang.org`) 에 접근할 수 있는 네트워크
사용자는 별도 API key를 준비할 필요가 없습니다. upstream `LAW_OC` 는 proxy 서버에서만 주입합니다. 무료 발급처(운영자용): `https://open.law.go.kr`
무료 API key 발급처: `https://open.law.go.kr`
## 기본 경로
`KSKILL_PROXY_BASE_URL` 환경변수가 있으면 그 값을 사용하고, 없으면 기본 경로 `https://k-skill-proxy.nomadamas.org` 를 사용합니다.
## 지원 endpoint
### 검색/목록 조회
```
GET /v1/korean-law/search?target={target}&query={검색어}
```
| target | 설명 |
|---|---|
| `law` | 현행법령 |
| `eflaw` | 시행일 법령 |
| `prec` | 판례 |
| `detc` | 헌재결정례 |
| `expc` | 법령해석례(유권해석) |
| `admrul` | 행정규칙 |
| `ordin` | 자치법규 |
| `trty` | 조약 |
지원 필터: `query`(검색어), `display`, `page`, `sort`, `date`, `prncYd`(선고일자), `nb`(사건번호), `datSrcNm`(데이터출처명), `curt`(법원) 등. 활성 필터만 넘기고, 요약 전에 반환 메타데이터를 확인합니다.
### 본문/상세 조회
```
GET /v1/korean-law/detail?target={target}&ID={일련번호}
```
검색 결과의 식별자(`ID` 또는 `MST`/`LID`)를 넘겨 상세 본문을 가져옵니다. 조문 지정은 `JO`(예: `000200` = 제2조)로 넘깁니다.
## 예시
로컬 CLI 또는 로컬 MCP server 경로는 `LAW_OC` 가 필요하다.
remote MCP endpoint는 사용자 `LAW_OC` 없이 `url`만으로 연결한다.
```bash
# 법령명 검색
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-law/search' \
--data-urlencode 'target=law' \
--data-urlencode 'query=관세법'
npm install -g korean-law-mcp
export LAW_OC=your-api-key
# 판례 검색
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-law/search' \
--data-urlencode 'target=prec' \
--data-urlencode 'query=부당해고'
korean-law list
korean-law help search_law
```
# 판례 본문 조회
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-law/detail' \
--data-urlencode 'target=prec' \
--data-urlencode 'ID=228541'
로컬 설치가 막히면 먼저 `https://korean-law-mcp.fly.dev/mcp` remote endpoint를 사용한다. 그 경로도 응답하지 않거나 서비스 장애가 나면 `법망`(`https://api.beopmang.org`) MCP/REST를 fallback으로 사용한다.
## MCP 연결 예시
```json
{
"mcpServers": {
"korean-law": {
"command": "korean-law-mcp",
"env": {
"LAW_OC": "your-api-key"
}
}
}
}
```
remote endpoint 예시:
```json
{
"mcpServers": {
"korean-law": {
"url": "https://korean-law-mcp.fly.dev/mcp"
}
}
}
```
위 remote 예시는 upstream 문서 기준으로 사용자 `LAW_OC` 를 따로 넣지 않는다. 사용자 쪽에서 준비할 것은 `url` 등록뿐이다.
## fallback: 법망
기존 `korean-law-mcp` 경로가 동작하지 않을 때만 `법망`을 사용한다.
### MCP fallback
```json
{
"mcpServers": {
"beopmang": {
"url": "https://api.beopmang.org/mcp"
}
}
}
```
### REST fallback 예시
```bash
curl "https://api.beopmang.org/api/v4/law?action=search&q=관세법"
curl "https://api.beopmang.org/api/v4/tools?action=overview&law_id=001706"
curl "https://api.beopmang.org/api/v4/law?action=get&law_id=001706&article=제750조"
```
## 기본 흐름
1. 질의가 법령/판례/행정해석/자치법규 중 어디에 가까운지 분류한다.
2. 법령명만 찾으면 `target=law``search` 한다.
3. 특정 조문이 필요하면 `search` 로 식별자(`MST`/`ID`)를 확인한 뒤 `detail` 을 호출한다.
4. 판례는 `target=prec`, 유권해석은 `target=expc`, 자치법규는 `target=ordin` 로 조회한다.
5. 범주가 애매하면 `target=law` 부터 시작한다.
6. 검색 결과가 0건이어도 바로 "관련 규범이 없다"고 단정하지 말고 검색어와 범주를 다시 확인한다.
2. 법령명만 찾으면 `search_law` 를 먼저 쓴다.
3. 특정 조문이 필요하면 `search_law` 또는 `search_all` 로 식별자(`mst`)를 확인한 뒤 `get_law_text` 를 호출한다.
4. 판례는 `search_precedents`, 유권해석은 `search_interpretations`, 자치법규는 `search_ordinance` 를 우선 사용한다.
5. 범주가 애매하면 `search_all` 로 시작한다.
6. `korean-law-mcp` 경로가 설치/네트워크/서비스 장애로 막히면 `법망` fallback으로 전환한다.
7. fallback 검색 결과가 0건이어도 바로 "관련 규범이 없다"고 단정하지 말고 검색어와 범주를 다시 확인한다.
## 실패 모드
## CLI 예시
- `target` 이 없거나 허용되지 않은 값이면 400 응답
- 검색어/식별자가 없으면 400 응답
- 프록시 서버에 `LAW_OC` 가 없으면 503 응답
- 법제처 API가 사용자 검증 실패를 반환하면 502 + `law_user_verification_failed` (운영자가 서버 OC/UA/Referer 점검)
- 법제처 API가 일시적으로 빈/HTML 응답이면 proxy가 재시도 후 502 + `upstream_unstable`
- 일부 출처는 본문을 제공하지 않을 수 있다. 본문을 못 가져오면 목록 메타데이터(사건번호·법원·선고일자·출처·요지)까지만 제공하고 본문이 없다는 점을 명시한다.
```bash
korean-law search_law --query "관세법"
korean-law get_law_text --mst 160001 --jo "제38조"
korean-law search_precedents --query "부당해고"
```
## 운영 팁
- `화관법` 같은 약칭은 `target=law` 로 정식 법령명을 먼저 확인한다.
- 조문 번호가 헷갈리면 `detail` 전에 법령 식별자부터 다시 확인한다.
- `화관법` 같은 약칭은 `search_law` / `search_all` 로 정식 법령명을 먼저 확인한다.
- 조문 번호가 헷갈리면 `get_law_text` 전에 법령 식별자부터 다시 확인한다.
- 로컬 CLI/MCP 경로를 쓰는데 `LAW_OC` 가 없으면 credential resolution order에 따라 확보를 안내한다.
- remote MCP endpoint를 쓰면 사용자 `LAW_OC` 없이 `url` 등록 상태만 확인한다.
- 기존 `korean-law-mcp` 경로가 실패하면 `https://api.beopmang.org/mcp` 또는 `/api/v4/law?action=search` 경로를 fallback으로 쓴다.
- 요약은 할 수 있지만 법률 자문처럼 단정적으로 결론을 내리지는 않는다.
## 출처
## 라이브 확인 메모
- 설계 참고(upstream): `https://github.com/chrisryugj/korean-law-mcp`
- 공식 데이터 출처: 법제처 국가법령정보 공동활용 (`https://open.law.go.kr`, DRF `lawSearch.do`/`lawService.do`)
- 운영자(proxy) 전용 시크릿: `LAW_OC` (사용자는 불필요)
2026-04-01 기준 smoke test 에서 아래 명령은 실제로 정상 동작했다.
- `korean-law list`
- `korean-law help search_law`
즉, `korean-law-mcp` CLI 설치와 기본 명령 진입은 검증했다. 실제 법령 검색은 로컬 CLI/MCP 경로라면 `LAW_OC` 가 준비된 환경에서 바로 이어서 사용할 수 있고, remote MCP endpoint는 사용자 `LAW_OC` 없이 URL 등록만으로 붙일 수 있다. 기존 경로 장애 시에는 `법망` fallback을 사용할 수 있다.

View file

@ -1,66 +0,0 @@
# 한국 마라톤 일정 조회 가이드
`korean-marathon-schedule` 스킬은 공개 웹 표면을 읽어 한국 마라톤/러닝 대회 일정을 조회하고, 요청 시 철인3종 대회도 함께 확인합니다.
## 제공 정보
각 결과는 가능한 범위에서 아래 정보를 반환합니다.
- 대회명
- 개최일
- 지역과 장소
- 신청 마감일 및 접수 기간
- 종목/코스
- 주최자
- 공식 웹사이트 또는 공개 상세 링크
## 공개 접근 경로
| 구분 | 공개 표면 | 사용 정보 | 인증 |
| --- | --- | --- | --- |
| 마라톤/러닝 | `https://gorunning.kr/races/``/races/<id>/<slug>/` 상세 페이지 | 일정, 장소, 접수 기간, 종목, 주최자, 웹사이트 | 불필요 |
| 철인3종 | `https://triathlon.or.kr/events/tour/?sYear=<YYYY>&vType=list` 및 상세 페이지 | 일정, 장소, 접수 기간, 코스, 주최자 | 불필요 |
두 표면 모두 API 키가 필요 없는 공개 읽기 경로이므로 `k-skill-proxy`를 사용하지 않습니다.
## 사용 예시
```js
const { searchEvents } = require("korean-marathon-schedule")
const result = await searchEvents({
query: "서울",
from: "2026-05-01",
to: "2026-12-31",
includeTriathlon: true,
limit: 10
})
console.log(result.items)
```
CLI:
```bash
node packages/korean-marathon-schedule/src/cli.js 서울 --from 2026-05-01 --to 2026-12-31 --include-triathlon --limit 10
```
## 응답 작성 원칙
```text
- 대회명: 소아암환우돕기 제23회 서울시민마라톤
일정: 2026-05-10
장소: 서울 여의도 한강 물빛광장
신청 마감: 2026-02-28 (접수기간 2026-01-12 ~ 2026-02-28)
종목: Half, 10km, 5km, 3km 걷기
링크: https://gorunning.kr/races/...
```
신청 마감일이 공개 페이지에서 확인되지 않으면 추정하지 말고 `신청 마감일 미확인`으로 표시합니다.
## 실패/주의 사항
- 일정과 접수 상태는 수시로 바뀌므로 조회 시각 기준 참고값으로 안내합니다.
- 공개 HTML 구조가 바뀌면 일부 필드가 비거나 파싱이 실패할 수 있습니다.
- 접수/결제/로그인/CAPTCHA가 필요한 경로는 자동화하지 않습니다.
- 행사별 공식 사이트가 없으면 GoRunning 또는 대한철인3종협회 상세 링크를 대신 제공합니다.

View file

@ -1,86 +0,0 @@
# 한국 중세 국어풍 변환 가이드
## 이 기능으로 할 수 있는 일
- 한국어 입력문을 창작용 **중세국어풍 문체**로 변환
- `은/는`, `을/를`, `에서` 같은 일부 조사를 `ᄋᆞᆫ`, `ᄋᆞᆯ`, `애`처럼 변환
- `했다`, `하는`, `말하는` 같은 일부 어미를 `ᄒᆞ엿다〮`, `ᄒᆞᄂᆞᆫ`, `ᄆᆞᆯᄒᆞᄂᆞᆫ`처럼 변환
- 날짜 단위를 `年`, `月`, `日`로 변환
- 일부 한자어를 `熱愛說`, `俳優`, `學校`처럼 Hanja 힌트로 변환
- URL, 이메일, Markdown 링크, inline/fenced code span은 구조 토큰으로 보고 변환하지 않음
- 인명·숫자·고유명사는 완전 보존이 아니라, 규칙이 맞지 않을 때 원문을 남기는 best-effort 방식으로 처리
## 왜 별도 스킬이 필요한가
LLM에게 "중세 국어처럼"이라고만 요청하면 변환 강도와 표기가 매번 달라진다. 이 스킬은 밈/창작용 변환에서 필요한 최소 계약을 고정한다.
- 동일 입력은 동일 출력으로 변환한다.
- 어떤 규칙이 적용됐는지 `replacements` 배열로 확인할 수 있다.
- 학술적 복원이 아니라 스타일 변환임을 문서화한다.
## 기본 계약
프로필은 `middle-korean-style-v1`이다.
- 날짜 단위 정규화를 먼저 적용한다. `2015년 7월 21일``2015年 7月 21日`처럼 바뀐다.
- 그다음 결정론적 lexicon 치환을 적용한다.
- 일부 현대 조사를 중세국어풍 조사로 바꾼다.
- 일부 현대 어미를 `ᄒᆞ-` 계열 중세국어풍 어미로 바꾼다.
- URL, 이메일, Markdown 링크, inline/fenced code span은 먼저 보호한 뒤 마지막에 원문 그대로 복원한다.
- 한자어 힌트는 넓은 전역 치환으로 적용되므로 합성어·고유명사처럼 보이는 문자열 안에서도 바뀔 수 있다.
- 변환하지 못한 내용은 원문 의미 보존을 위해 그대로 둔다.
`middle-korean-style-v1`의 출력 변경은 호환성에 영향을 주는 계약 변경으로 본다. 새 규칙을 추가하거나 순서를 바꿀 때는 회귀 테스트와 문서 예시를 함께 갱신한다.
## CLI 사용 예시
### 기본 JSON 출력
```bash
node scripts/korean_middle_korean.js --text "민수는 3월 5일 학교에서 공부했다."
```
예상 출력 일부:
```json
{
"profile": "middle-korean-style-v1",
"input": "민수는 3월 5일 학교에서 공부했다.",
"output": "민수ᄋᆞᆫ 3月 5日 學校애 공부ᄒᆞ엿다〮.",
"replacements": [
{ "kind": "date", "from": "월→月", "to": "$1月", "count": 1 }
]
}
```
### 변환문만 출력
```bash
node scripts/korean_middle_korean.js --text "열애설을 인정했다." --format text
```
예상 출력:
```text
熱愛說ᄋᆞᆯ 인졍ᄒᆞ엿다〮.
```
### 파일/stdin 입력
```bash
node scripts/korean_middle_korean.js --file ./input.txt --format text
cat input.txt | node scripts/korean_middle_korean.js --stdin --format json
```
## 응답 원칙
- 결과는 `output` 필드를 중심으로 전달한다.
- "정확한 중세국어 번역"이 아니라 "중세국어풍/창작용 변환"이라고 설명한다.
- 사용자가 학술적 정확성을 요구하면 이 스킬의 한계를 먼저 알리고, 전문 고문헌 검토가 필요하다고 안내한다.
## 검증
```bash
node --test scripts/test_korean_middle_korean.js
node scripts/korean_middle_korean.js --text "민수는 3월 5일 학교에서 공부했다." --format text
```

View file

@ -1,87 +0,0 @@
# 한국 개인정보처리방침·이용약관 자동 생성 가이드
## 이 기능으로 할 수 있는 일
- Next.js (13 ~ 16) App Router 프로젝트에 **개인정보처리방침 페이지** 생성
- 동일 프로젝트에 **이용약관 페이지** 생성 (공정위 전자상거래 표준약관 제10023호 기반)
- shadcn/ui 기반 **쿠키 배너**, **회원가입 동의 모달**, **라벨링 카드** 컴포넌트 설치
- 서비스 유형(SaaS · 쇼핑몰 · 커뮤니티 · 블로그 · 핀테크 · AI) 별 분기
- 한국어 / 영문 / 한영 병기 출력 (GDPR 관할 추가 시 EU 분기 지원)
## 가장 중요한 규칙
본 스킬은 [`kimlawtech/korean-privacy-terms`](https://github.com/kimlawtech/korean-privacy-terms) (Apache-2.0) 업스트림의 **thin wrapper** 이다. k-skill 측은 발견 · 인용 · 법률 면책 · 설치 진입점만 책임지고, 인터뷰 · 렌더 · 파일 생성 로직은 업스트림에 위임한다.
생성되는 모든 문서는 **참고용 초안**이며 **법률 자문이 아니다**. 실서비스 배포 전 반드시 **변호사 검토**를 받아야 한다. 2026.9.11 시행 개정 개인정보보호법 기준(§30 법정 항목 확장, 과징금 매출액 최대 10%, 사업주 책임)이 업스트림 pin 시점으로 반영돼 있으며, 이후 개정 반영은 upstream pin 을 bump 해 갱신한다.
## 먼저 필요한 것
- 인터넷 연결 (업스트림 clone 용; 오프라인 환경에서는 동작하지 않는다)
- `git` 2.20+
- `bash`
- `node` 18+ (업스트림이 권장하는 Next.js 실행 요건)
- 대상 프로젝트가 Next.js 13 ~ 16 App Router 기반 (Pages Router 단독 프로젝트는 업스트림이 실행 중단)
## 설치 흐름
dual-install: `~/.claude/skills/korean-privacy-terms/upstream/``~/.agents/skills/korean-privacy-terms/upstream/` 양쪽에 pinned SHA 로 업스트림을 체크아웃한다.
```bash
bash korean-privacy-terms/scripts/install.sh
```
설치 확인:
```bash
git -C ~/.claude/skills/korean-privacy-terms/upstream rev-parse HEAD
git -C ~/.agents/skills/korean-privacy-terms/upstream rev-parse HEAD
cat korean-privacy-terms/scripts/upstream.pin
```
세 값이 모두 동일한 40자리 SHA 여야 한다. AGENTS.md 규칙에 따라 레포 내부에 repo-local `.claude/` 또는 `.agents/` 디렉토리는 생성하지 않는다. `~/.agents/skills` 가 symlink 로 관리되는 환경에서는 그 indirection 을 존중한다.
## upstream pin 갱신
법령 개정 / 업스트림 CHANGELOG 업데이트가 반영되면 `korean-privacy-terms/scripts/upstream.pin` 을 최신 tag SHA 로 수동 교체한 뒤 PR 을 올린다.
```bash
git ls-remote --tags https://github.com/kimlawtech/korean-privacy-terms.git
```
새 SHA 를 `upstream.pin` 에 덮어쓰고 `bash korean-privacy-terms/scripts/install.sh` 를 다시 실행해 양쪽 홈 경로가 새 SHA 로 갱신되는지 확인한다. pin 만 올리지 말고 upstream CHANGELOG 를 함께 읽어 법률 개정 포인트를 PR 본문에 기록한다.
## 인터뷰 게이트 (필수)
사용자가 "개인정보처리방침 만들어줘" 처럼 생성을 요청하면 바로 파일을 만들지 말고 업스트림 `scripts/interview.md` 프로토콜을 따라 **먼저 되묻는다**. 최소한 아래를 확인한 뒤 생성 단계로 넘어간다.
- 대상 사용자 범위 — 한국만 / 해외 위주 / 양쪽 글로벌 (관할법 결정)
- 운영 주체 소재지 — 한국 / 해외
- 서비스 유형 — SaaS / 쇼핑몰 / 커뮤니티 / 블로그 / 핀테크 / AI
- Next.js 버전 (13 ~ 16), App Router 여부, `.ts` / `.js`
- Tailwind 버전 (v3 / v4), 번들러 (Turbopack / Webpack)
- shadcn/ui 기설치 여부, 기존 디자인 시스템
- 출력 언어 (한국어만 / 영문만 / 한영 병기)
- 14세 미만 대상 여부
- 자동화된 결정(AI) · 행태정보(쿠키) · 맞춤형 광고 처리 여부
- 사업자 상호 · 대표자 · 주소, 개인정보 보호책임자(CPO) 정보
인터뷰는 agent-neutral 톤으로 한 번에 1 ~ 2문항씩 진행하고, 법률 공포감(과태료 폭탄 등) 유발 표현은 피한다. 모르는 항목은 "넘어가도 됩니다" 로 연다.
## 응답 정책
이 스킬이 실행한 모든 답변은 아래 고정 블록을 끝에 포함한다.
- 생성된 문서는 **참고용 초안**이며 법률 자문이 아니다.
- 실서비스 적용 전 **변호사 검토**가 필수이다.
- 2026.9.11 시행 개정 개인정보보호법 기준은 업스트림 pin 시점에 반영되었다. 이후 개정에 대한 최신 반영 여부는 사용자에게 확인 책임이 있다.
- 법정 필수 11개 항목(처리 목적 · 처리 항목 · 처리/보유 기간 · 파기 · 정보주체 권리 · 보호책임자 · 안전조치 · 방침 변경 · 열람/정정/삭제/처리정지 요구권 · 열람청구 부서 · 권익침해 구제) 중 하나라도 누락돼선 안 된다.
- 2025.4.21 개정 작성지침 반영 항목(전송요구권 · 자동화된 결정 · 행태정보 · 고충처리 부서)도 해당되면 반드시 포함한다.
## 라이선스 / 출처
- 업스트림: https://github.com/kimlawtech/korean-privacy-terms (Apache-2.0)
- upstream pin 파일: `korean-privacy-terms/scripts/upstream.pin`
- 업스트림 저자 · 커뮤니티 attribution: `korean-privacy-terms/NOTICE` (@kimlawtech, SpeciAI community)
- 법률 면책 전문 (한 / 영): `korean-privacy-terms/DISCLAIMER.md`
- Apache-2.0 전문 (업스트림 `LICENSE` verbatim 복사): `korean-privacy-terms/LICENSE.upstream` — Apache License, Version 2.0 §4(a) 는 재배포자가 "give any other recipients of the Work or Derivative Works a copy of this License" 하도록 요구한다. `install.sh` 실행 전에도 레포 체크아웃 상태에서 이 요건이 충족되도록 번들해 두었다.
- 레포 루트 `LICENSE` (MIT) 는 k-skill 자체 라이선스이며 이 스킬 하위에는 적용되지 않는다. 본 스킬은 업스트림 산출물의 재배포이므로 Apache License, Version 2.0 §4 요건 (LICENSE 번들, NOTICE 포함, 수정 고지) 을 준수한다.

View file

@ -1,103 +0,0 @@
# 한국어 유행어 글쓰기 가이드
## 이 기능으로 할 수 있는 일
- SNS·홍보문·댓글·자기소개를 "유행어 섞인 요즘 말투" 로 작성하거나 리라이팅
- 무드·문맥·안전성(safety)·강도(intensity) 기준으로 한국 유행어 후보를 조회
- 개별 유행어의 뜻을 나무위키 best-effort 요약으로 확인
- 외부 JSON 인덱스로 유행어 풀을 교체·확장 (확장성)
## 왜 별도 스킬이 필요한가
- 한국 유행어는 생성·변형·소멸 속도가 빠르다. LLM 단독 지식에 의존하면 knowledge cutoff 이후 표현은 놓치거나 잘못 쓰기 쉽다.
- 유행어를 아무 때나 자동으로 끼워 넣는 것은 위험하다. `korean-slang-writing`**사용자가 명시적으로 원할 때만** 활성화되고, 근거 링크와 안전성 메타를 함께 돌려준다.
- 다른 한국어 스킬(자기소개서 교정, SNS 카피 작성 등)이 공통으로 참조할 수 있는 유행어 사전 레이어를 제공한다.
## 먼저 필요한 것
- `python3` 3.10+
- 인터넷 연결 (나무위키 lookup 에만 사용, 시드 검색은 오프라인)
- 추가 API 키 없음
## 입력값
- 시드 검색: `--query`, `--mood`, `--context`, `--safety`, `--intensity`, `--limit`, `--include-deprecated`, `--index-path`
- 원문 lookup: 유행어 단어 또는 나무위키 URL
- 출력: JSON(기본) 또는 텍스트
## 기본 계약
- 스킬은 **데이터와 조회 도구** 만 제공한다. 실제 글 작성은 호출하는 에이전트(LLM)가 한다.
- 시드 인덱스 (`data/seed-slang.json`) 는 나무위키 `분류:유행어` 를 참고한 수작업 큐레이션 결과이며, 각 항목은 `term`, `aliases`, `meaning_short`, `usage_context`, `mood_tags`, `intensity`, `safety`, `example_usage`, `namuwiki_url`, `era`, `still_usable` 을 가진다.
- `safety``safe` / `spicy` / `risky` 중 하나다. v1 시드에는 `risky` 항목을 포함하지 않는다.
- 검색 결과의 `match_reason` 우선순위는 `exact > alias > substring > no-query` 다.
- 나무위키 fetch 는 **best-effort** 다. Cloudflare 차단(403/429) 이나 404 시 `fetched: false``block_reason` 으로 보고하고, 에이전트는 추측하지 않는다.
## 기본 흐름
1. 사용자 의도(목적, 톤, 문맥, 강도)를 정리한다.
2. `slang_search.py` 로 시드 인덱스에서 후보를 고른다 (무드/문맥/safety 필터 조합).
3. 필요한 경우 `slang_lookup.py` 로 특정 유행어의 나무위키 원문 요약을 확인한다.
4. 에이전트가 후보 중 1~3개를 골라 자연스럽게 녹여 문장을 완성한다.
5. 결과물과 함께 "사용한 표현 · 의미 · 근거 링크" 를 사용자에게 보여준다.
## CLI 사용 예시
```bash
# 1) 특정 유행어 즉시 조회 (시드 정확 매칭)
python3 scripts/slang_search.py --query "갓생" --format text
# 2) 긍정·유머 무드, SNS/마케팅 문맥에서 안전한 후보 5개
python3 scripts/slang_search.py \
--mood "긍정,유머" \
--context "SNS,마케팅" \
--safety safe \
--limit 5
# 3) 구형 유행어까지 포함해 넓게 조회
python3 scripts/slang_search.py --query "인싸" --include-deprecated
# 4) 나무위키 원문 요약 시도 (실패하면 warning 반환)
python3 scripts/slang_lookup.py "중꺾마" --timeout 10 --format json
# 5) 외부 인덱스 파일로 대체 (PR 없이 커스텀 풀 사용)
python3 scripts/slang_search.py \
--query "럭키비키" \
--index-path ./my-custom-slang.json
```
## 응답 스타일 권장
```text
[추천 문안]
...
[사용한 유행어]
- 갓생: 부지런하고 생산적인 삶 — https://namu.wiki/w/...
- 오운완: 오늘 운동 완료 — https://namu.wiki/w/...
[더 과한 버전]
...
[더 무난한 버전]
...
```
## 제한사항
- 시드 인덱스는 약 30개 항목의 큐레이션 모음이며 전체 나무위키 `분류:유행어` 를 망라하지 않는다.
- 나무위키는 Cloudflare 를 사용하므로 요청량이 많거나 IP 가 의심받으면 403/429 가 나올 수 있다. 이 스킬은 실패를 조용히 덮지 않고 `block_reason` 으로 보고한다.
- `still_usable` 플래그는 수작업으로 관리하며, 자동 만료 로직은 v1 범위가 아니다.
- 스킬은 LLM 호출을 하지 않는다. "유행어 추천 → 문장 작성" 판단은 호출 에이전트가 한다.
## 안전 가이드
- 민감한 상황 (사과문, 공문, 보도자료, 법률 문서) 에서는 사용을 자제한다.
- `spicy` 로 표시된 표현은 비격식 · 친근한 컨텍스트에서만 사용하고, 이유를 한 줄 덧붙인다.
- 유행어의 의미를 시드 또는 나무위키에서 확인하지 못했다면 그 유행어는 쓰지 않는다.
- 사용자가 별도 요청하지 않는 한, 유행어는 **적게** 쓰는 것이 기본값이다.
## 확장 아이디어
- `data/` 에 커뮤니티·브랜드 전용 유행어 인덱스를 별도 파일로 추가 (의도적 사용)
- 자동 만료 점수 (`last_reviewed` + 현재 날짜 기반) 로 구형 항목 자동 demote
- 다중 소스 병합 (나무위키 외 공개 사전/뉴스 기사 요약)

View file

@ -1,72 +0,0 @@
# 한국 대중교통 길찾기 가이드
## 이 기능으로 할 수 있는 일
- 출발지→도착지 도어투도어 대중교통 경로 조회 (지하철 + 버스 + 도보)
- ODsay LIVE API 기반 환승 정보, 소요시간, 요금 확인
- Kakao Local geocoding으로 주소·장소명→좌표 변환
- 추천순 / 최소시간 / 최소환승 옵션 선택
## 먼저 필요한 것
- [공통 설정 가이드](../setup.md) 완료
- [보안/시크릿 정책](../security-and-secrets.md) 확인
- ODsay Server API Key 발급 및 호출 IP 화이트리스트 등록: https://lab.odsay.com
- Kakao Local geocoding은 기본 hosted `k-skill-proxy` 경유. 사용자 쪽 Kakao 키는 불필요하며, self-host proxy 운영자만 Kakao REST API Key를 발급해 서버에 설정한다: https://developers.kakao.com
## 필요한 환경변수
- `ODSAY_API_KEY` — ODsay LIVE API Server 키
`ODSAY_API_KEY``~/.config/k-skill/secrets.env` 에 저장하거나 환경변수로 주입한다. 별도 self-host proxy를 쓰는 경우에만 `KSKILL_PROXY_BASE_URL` 을 설정한다.
## 입력값
- 출발지 (주소, 장소명, 또는 좌표)
- 도착지 (주소, 장소명, 또는 좌표)
- 선택 사항: 경로 옵션 (`OPT=0` 추천순, `4` 최소시간, `5` 최소환승), `SearchPathType` (`0` 지하철+버스, `1` 지하철만, `2` 버스만)
## 기본 흐름
1. 출발지/도착지를 `k-skill-proxy``/v1/kakao-local/geocode`로 geocoding하여 좌표를 확보한다. Proxy 내부에서 Kakao Local `address.json``keyword.json` 순서로 시도한다.
2. ODsay `searchPubTransPathT`에 출발/도착 좌표와 옵션을 전달한다.
3. 응답의 `result.path[]`를 3개 이내로 정리한다.
4. 각 경로의 `subPath[]``trafficType`별로 표시하며, 첫/끝 도보 구간을 반드시 포함한다.
## 예시
### 좌표 직접 입력
```bash
set -a; . ~/.config/k-skill/secrets.env; set +a
KEY=$(python3 -c "import os,urllib.parse;print(urllib.parse.quote(os.environ['ODSAY_API_KEY'],safe=''))")
curl -s "https://api.odsay.com/v1/api/searchPubTransPathT?apiKey=${KEY}&SX=126.9706&SY=37.5559&EX=127.0276&EY=37.4979&OPT=0&SearchPathType=0"
```
### 주소→좌표→경로 (Python)
```python
import os, urllib.parse, urllib.request, json
PROXY = os.environ.get('KSKILL_PROXY_BASE_URL', 'https://k-skill-proxy.nomadamas.org').rstrip('/')
def geocode(q):
url = PROXY + '/v1/kakao-local/geocode?q=' + urllib.parse.quote(q)
with urllib.request.urlopen(url, timeout=10) as resp:
d = json.loads(resp.read())
if d.get('documents'):
doc = d['documents'][0]
return float(doc['x']), float(doc['y']), doc.get('place_name') or doc.get('address_name')
return None
sx, sy, s_name = geocode('서울역')
ex, ey, e_name = geocode('강남역')
# 이후 ODsay searchPubTransPathT 호출
```
## 주의할 점
- ODsay Server 키는 **호출 IP 화이트리스트 등록이 필수**이다. 등록되지 않은 IP에서는 `error` 응답이 반환된다.
- 현재 ODsay 공식 Basic 상품 기준 무료 체험은 일 1,000건(6개월)이다. `searchPubTransPathT``searchStation` 호출이 합산된다.
- 한국 외 좌표는 지원하지 않는다.
- 카카오맵/네이버지도 directions API는 대중교통 라우팅을 공개하지 않으므로 사용하지 말 것.

View file

@ -1,178 +0,0 @@
# 국가데이터처 KOSIS 통계 조회 가이드
대상 사이트는 **국가데이터처**(구 통계청)가 운영하는 **KOSIS(국가통계포털)** 공식 Open API `https://kosis.kr/openapi/` 이다. 이 기능은 한국 공식 통계 자료의 **조회 자동화**만 수행한다.
## 이 기능으로 할 수 있는 일
- 키워드로 KOSIS 통계표 검색 (`statisticsSearch.do`)
- 통계표 메타데이터(분류·항목·단위) 조회 (`statisticsData.do?method=getMeta`)
- 통계표 데이터 셀 조회 (`statisticsParameterData.do`)
- 사용자별로 KOSIS에 등록한 대용량 자료 조회 (`statisticsBigData.do`)
- JSON 또는 사람이 읽기 좋은 텍스트 출력
이 기능은 **조회 전용 자동화**이다. 통계 작성, 데이터 변경, 대시보드 등록, 사용자별 통계자료(`userStatsId`) 신규 등록은 하지 않는다.
## 먼저 필요한 것
- Python 3.9+ (stdlib only, 외부 패키지 없음)
- 일반 `search`/`meta`/`data`: 기본 hosted `k-skill-proxy` 접근
- `bigdata` 또는 `--direct`: KOSIS Open API 인증키 (무료, https://kosis.kr/openapi/ 에서 회원가입 후 활용신청)
- [공통 설정 가이드](../setup.md) 완료
- [보안/시크릿 정책](../security-and-secrets.md) 확인
```bash
python3 kosis-stats/scripts/run_kosis_stats.py --help
```
## 필요한 환경변수
- 일반 `search`/`meta`/`data`: 없음
- `KSKILL_PROXY_BASE_URL` — self-host·별도 프록시를 쓸 때만 설정. 비우면 기본 hosted `https://k-skill-proxy.nomadamas.org` 사용
- `KSKILL_KOSIS_API_KEY``bigdata` 또는 `--direct` 전용
### Credential resolution order (`bigdata` 또는 `--direct` 전용)
1. **이미 환경변수에 있으면** 그대로 사용한다.
2. **에이전트가 자체 secret vault(1Password CLI, Bitwarden CLI, macOS Keychain 등)를 사용 중이면** 거기서 꺼내 환경변수로 주입해도 된다.
3. **`~/.config/k-skill/secrets.env`** (기본 fallback) — plain dotenv 파일, 퍼미션 `0600`.
4. **아무것도 없으면** 유저에게 물어서 2 또는 3에 저장한다.
일반 조회 helper는 proxy URL만 읽고, KOSIS 인증키는 proxy 서버에서만 주입한다. `bigdata`/`--direct` 호출만 `KSKILL_KOSIS_API_KEY` 환경변수와 위 secrets 파일을 읽는다.
## 처음 실행 순서
처음 쓰는 사용자는 proxy 기반 검색 → 메타 → 작은 슬라이스 순으로 점검한다. 사용자 KOSIS 키는 일반 조회에 필요 없다.
```bash
python3 kosis-stats/scripts/run_kosis_stats.py search --query "1인 가구" --text
python3 kosis-stats/scripts/run_kosis_stats.py meta --table-id DT_1JC1501 --text
python3 kosis-stats/scripts/run_kosis_stats.py data \
--table-id DT_1ES4I001S --prd-se Y --start 2020 --end 2023 --text
```
발급 절차와 호출 한도, 에러 코드는 [`kosis-stats/references/kosis-openapi-guide.md`](../../kosis-stats/references/kosis-openapi-guide.md) 에 정리되어 있다.
## 입력값
서브커맨드: `search`, `meta`, `data`, `bigdata`. 공통 출력 옵션은 **서브커맨드 뒤에** 둔다.
- `search`
- `--query "키워드"`
- `--result-count N` (1-5000, 기본 20)
- `--start-count N` (페이징, 기본 1)
- `meta`
- `--org-id 101` (기본 101=통계청)
- `--table-id DT_1IN0001`
- `--meta-type TBL|ITM|OBJ` (기본 TBL)
- `data`
- `--org-id 101`
- `--table-id DT_1IN0001`
- `--prd-se M|Q|S|Y|F|IR`
- `--start YYYY[MM|QQ|HH]`, `--end YYYY[MM|QQ|HH]`
- `--itm-id ALL`
- `--obj-l 1=ALL --obj-l 2=00` (반복)
- `bigdata`
- `--user-stats-id <KOSIS 등록 ID>`
- `--format json|sdmx|csv` (xls는 바이너리라 helper 미지원 — 필요 시 KOSIS 웹에서 직접 다운로드)
- `--prd-se`, `--new-est-prd-cnt`
공통 옵션:
- `--text` / `--json` (기본 JSON)
- `--dry-run` (인증키 없이 URL/파라미터만 출력)
- `--timeout N` (기본 30)
- `--proxy-base-url URL` (기본 hosted proxy 대신 self-host/alternate proxy 사용)
- `--direct` (proxy를 우회하고 `KSKILL_KOSIS_API_KEY` 로 KOSIS 직접 호출)
## 기본 흐름
1. 일반 조회는 기본 hosted proxy를 사용한다. self-host를 쓰면 `KSKILL_PROXY_BASE_URL`을 설정한다.
2. `search` 로 후보 통계표를 본다.
3. `meta` 로 분류·단위·주기를 확인한다.
4. `data` 로 작은 슬라이스를 먼저 받는다.
5. 한도 초과(코드 `31`/`41`)면 기간/분류를 분할하거나 `bigdata` 로 전환한다.
6. 결과 요약 시 `org_id`, `tbl_id`, 기간, 단위, endpoint URL을 함께 적는다.
## 검증 방식
메인테이너가 일반 조회를 검토하기 위해 별도 KOSIS 인증키를 새로 발급받을 필요는 없다.
- CI/리뷰 검증: `./scripts/validate-skills.sh`, `python3 -m py_compile ...`, `--help`, `--dry-run`, 단위 테스트(`python3 -m unittest discover -s kosis-stats/tests`).
- 실제 direct 조회 검증: 기여자 또는 이미 KOSIS 키를 가진 사용자가 `--direct`로 선택 실행한다. Proxy live smoke는 배포 proxy에 `KOSIS_API_KEY`가 설정된 뒤 수행한다.
- PR에는 호출 endpoint, 파라미터, 응답 행 수 같은 비민감 요약만 남긴다. 인증키와 개인 조회 세부 내역은 공유하지 않는다.
## 예시
키워드 검색:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py search --query "1인 가구" --text
```
JSON 검색:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py search --query "고령" --result-count 50 --json
```
테이블 메타데이터:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py meta --table-id DT_1IN0001 --text
```
연간 데이터 작은 슬라이스:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py data \
--table-id DT_1ES4I001S --prd-se Y --start 2020 --end 2023 --json
```
월간 데이터:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py data \
--table-id DT_1J22001 --prd-se M --start 202401 --end 202412 --json
```
분류 필터:
```bash
python3 kosis-stats/scripts/run_kosis_stats.py data \
--table-id DT_1B040A3 --prd-se Y --start 2024 --end 2024 \
--obj-l 1=ALL --json
```
대용량 자료 (사전 등록한 `userStatsId` 필요):
```bash
python3 kosis-stats/scripts/run_kosis_stats.py bigdata \
--user-stats-id "<KOSIS에서 등록한 ID>" \
--format json --new-est-prd-cnt 5
```
URL/파라미터만 확인 (인증키 없이):
```bash
python3 kosis-stats/scripts/run_kosis_stats.py search --query "인구" --dry-run --text
```
## 주의할 점
- 분당 1,000건 호출 한도. 반복 호출 시 호출 간 sleep을 둔다.
- 1회 응답 40,000셀 한도. 초과하면 코드 `31`/`41` 반환 → 쿼리 분할 또는 `bigdata` 사용.
- `bigdata``userStatsId` 는 KOSIS 웹에서 사용자가 직접 등록해야 하며, 이 helper로 자동 등록하지 않는다. `openapisample/...` 같은 타인 등록 ID는 인증되지 않아 코드 `11` 을 반환한다.
- 2026-03-05 이후 HTTPS 전용. 모든 URL은 `https://`.
- KOSIS는 가끔 따옴표 없는 키의 비표준 JSON을 반환한다. helper가 자동 보정한다.
## 흔한 문제 해결
- `missing required environment variable: KSKILL_KOSIS_API_KEY`: `bigdata` 또는 `--direct` 호출에서만 발생한다. 환경변수가 현재 shell에 주입됐는지 확인한다. 없다면 https://kosis.kr/openapi/ 에서 발급한다.
- `KOSIS error 10` (인증키 누락) / `11` (만료): 키를 재확인하거나 갱신한다. `bigdata` 호출에서 `11` 이 뜨면 해당 `userStatsId` 가 본인 KOSIS 계정에 등록되어 있지 않을 가능성이 높다.
- `KOSIS error 20` (필수 분류 누락): 표마다 필수 차원 수가 다르다. `meta --table-id <ID> --meta-type OBJ` 로 차원 수를 확인하고(OBJ가 비어 있으면 `--meta-type ITM`), `--obj-l 1=<코드> --obj-l 2=<코드>` 형태로 모두 지정한 뒤 재호출한다. 예: `data --table-id DT_1J22001 --prd-se M --start 202401 --end 202401 --obj-l 1=ALL` → 코드 20 → meta 확인 → `--obj-l 1=T10 --obj-l 2=0` 추가 → 성공.
- `KOSIS error 21` (잘못된 요청 변수): `org_id`/`tbl_id`/`prdSe`/`startPrdDe` 형식과 분류 인덱스를 재확인한다. 표에 존재하지 않는 `objL3=ALL` 같은 인덱스는 거부된다. tblId 의심 시 `search --query <키워드>` 로 정확한 ID를 다시 찾는다.
- `KOSIS error 30` (결과 없음): 키워드 또는 기간 필터를 완화한다.
- `KOSIS error 31` / `41` (한도 초과): 기간을 좁히거나(예: 5년 → 1년) 분류 ALL 을 특정 코드로 바꾼다(예: `--obj-l 1=ALL``--obj-l 1=11` 서울만). 그래도 부족하면 `bigdata` 로 전환한다. 행정구역 코드는 시도 2자리, 시군구 5자리 관례.
- `KOSIS error 40` (호출 한도): 분당 1,000건 한도 도달. 잠시 대기.
- `KOSIS error 50` (서버 오류): 1~2초 대기 후 재시도.
- `argparse: unrecognized arguments: --text`: `--text`/`--json`/`--dry-run`/`--timeout` 은 서브커맨드(`search`/`meta`/`data`/`bigdata`) **뒤에** 둔다.

View file

@ -1,60 +0,0 @@
# 창업진흥원 K-Startup 조회 가이드
공공데이터포털 데이터셋 `15125364` (창업진흥원_K-Startup(사업소개,사업공고,콘텐츠 등)_조회서비스) 기반 4개 endpoint를 `k-skill-proxy` 경유로 조회한다. **조회 전용** 이며 사업 신청·결제·계좌 연결은 자동화하지 않는다.
스킬 이름: `kstartup-search`
호출 helper: `kstartup-search/scripts/run_kstartup.py`
## 어떤 데이터를 조회하나
| 서브커맨드 | upstream operation | 설명 |
| --- | --- | --- |
| `business-info` | `getBusinessInformation01` | 통합공고 지원사업 정보 (예산, 규모, 수행기관, 사업절차, 문의처) |
| `announcements` | `getAnnouncementInformation01` | 지원사업 공고 정보 (공고명, 접수기간, 지역, 신청대상, 모집진행여부 등) |
| `contents` | `getContentInformation01` | 창업관련 콘텐츠 (공지·뉴스·우수사례) |
| `statistics` | `getStatisticalInformation01` | 창업관련 통계보고서 |
`announcements` 가 가장 활용도 높다. 지역·대상·기간·모집 진행 여부로 필터링해 답변할 공고 후보를 좁히고, 자세한 신청 절차는 응답의 `detl_pg_url` 로 사용자가 K-Startup 사이트에서 직접 확인한다.
> **주의**: `supt_regin`은 라이브 호출에서 upstream이 서버 측에서 적용하지 않는 사례가 관측됐다 (서울만 요청해도 타 지역 공고가 섞여 돌아온다). 지역 필터가 중요한 답변이라면 helper가 받은 응답 JSON을 client에서 `supt_regin` 으로 한 번 더 거른다.
## 사용자 시크릿
- 일반 조회는 hosted proxy(`https://k-skill-proxy.nomadamas.org`)가 K-Startup 인증키를 서버 측에서 주입한다. 사용자에게 키를 요구하지 않는다.
- `--direct` 사용 시에만 `KSKILL_KSTARTUP_API_KEY` (또는 `DATA_GO_KR_API_KEY` fallback) 가 필요하다.
- 자세한 credential resolution order 는 [공통 설정 가이드](../setup.md) 와 [보안/시크릿 정책](../security-and-secrets.md) 참고.
## 예시
```bash
# 서울 모집 중 공고 5건 (hosted proxy 사용, 사용자 키 불필요)
python3 kstartup-search/scripts/run_kstartup.py announcements \
--supt-regin 서울특별시 --rcrt-prgs-yn Y --per-page 5 --text
# 2024년 사업화 분야 통합공고
python3 kstartup-search/scripts/run_kstartup.py business-info \
--biz-yr 2024 --biz-category-cd cmrczn_Tab3
# 정책/공지 콘텐츠 dry-run (인증 호출 없이 URL 검증만)
python3 kstartup-search/scripts/run_kstartup.py contents \
--clss-cd notice_matr --per-page 10 --dry-run
# 본인 키로 직접 호출
python3 kstartup-search/scripts/run_kstartup.py announcements \
--supt-regin 부산광역시 --direct
```
## 실패 모드 요약
- `400 bad_request`: 잘못된 날짜/Y·N/페이지 범위, 시작일 > 종료일 등 입력 검증 실패.
- `503 upstream_not_configured`: 프록시 서버에 `DATA_GO_KR_API_KEY` 가 없거나 `15125364` 활용신청이 미승인 상태.
- `502 upstream_error`: data.go.kr이 `resultCode != "00"` 또는 `errMsg` 를 반환 (API 키 미등록·만료·IP 미등록·요청 초과 등).
- 빈 `data` 배열: 필터에 맞는 공고나 콘텐츠가 없는 경우 → 키워드·지역·대상 범위를 완화한다.
- 데이터 갱신 주기: 공식 서비스설계서는 **일 1회**, 공공데이터포털 dataset 메타데이터에는 "실시간" 으로 표기돼 있다. 두 표면이 일치하지 않으니 분 단위 마감 시계열에는 쓰지 말고, 최종 마감·접수 상태는 응답의 `detl_pg_url` 에서 직접 확인한다.
## 한도와 출처
- 일 호출 한도: 개발계정 10,000, 운영계정 활용사례 등록 시 증가 가능.
- 라이선스: 이용허락범위 제한 없음 (data.go.kr 명시).
- 공식 표면: `https://www.data.go.kr/data/15125364/openapi.do`
- 서비스 URL: `https://apis.data.go.kr/B552735/kisedKstartupService01`

Some files were not shown because too many files have changed in this diff Show more