Compare commits

..

No commits in common. "main" and "feature/#0" have entirely different histories.

529 changed files with 3069 additions and 79947 deletions

View file

@ -0,0 +1,5 @@
---
"lck-analytics": minor
---
Add the first LCK analytics package and skill pack adapted from jerjangmin's original upstream implementation.

View file

@ -0,0 +1,5 @@
---
"cheap-gas-nearby": minor
---
Publish the first official Opinet-powered nearby cheapest gas station lookup package and skill docs.

View file

@ -0,0 +1,5 @@
---
"hipass-receipt": minor
---
Publish the first logged-in-session helper package and skill docs for Hi-Pass receipt workflows.

View file

@ -0,0 +1,5 @@
---
"blue-ribbon-nearby": patch
---
Handle Blue Ribbon `PREMIUM_REQUIRED` nearby responses with a domain error and document the current premium gate on live nearby results.

View file

@ -0,0 +1,5 @@
---
"market-kurly-search": minor
---
Publish the first reusable Market Kurly product search package and skill docs for unauthenticated price lookups.

View file

@ -0,0 +1,5 @@
---
"used-car-price-search": minor
---
Publish the first reusable used-car-price-search package with the SK direct inventory parser and skill docs.

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

@ -0,0 +1,26 @@
# PRD: Issue 56 - Han River water-level proxy endpoint
## Goal
한강홍수통제소(HRFCO) Open API의 `waterlevel/info` + `waterlevel/list/10M``k-skill-proxy` 의 공개 read-only endpoint로 감싸서, 최종 사용자가 별도 ServiceKey 없이 한강 수위·유량을 조회할 수 있게 한다.
## User story
- 사용자는 "한강대교 지금 수위 어때?"처럼 관측소명 또는 관측소코드로 현재 수위와 유량을 빠르게 확인하고 싶다.
- 에이전트는 proxy가 주는 현재 관측값과 기준 수위를 요약해 답변한다.
## Scope
- `k-skill-proxy` 에 HRFCO waterlevel summary endpoint 추가
- proxy 환경변수/README/가이드 문서 반영
- 신규 han-river-water-level skill 및 기능 문서 추가
- 문서 회귀 테스트 + proxy server tests 추가
## Non-goals
- `rainfall` / `dam` / `bo` / `fldfct` 전체 확장
- private/auth-required proxy 도입
- 지도 기반 위치 추천 또는 관측소 선택 UX 고도화
## Acceptance criteria
1. proxy server 가 공개 read-only endpoint 로 HRFCO 현재 수위/유량을 요약 제공한다.
2. upstream HRFCO ServiceKey 는 proxy 서버 환경변수로만 관리된다.
3. endpoint 는 관측소명/관측소코드 기준 최신 관측시각, 수위, 유량, 기준수위를 포함한 JSON 을 반환한다.
4. 신규 skill/docs 는 hosted proxy 기본 경로와 무-key client workflow 를 문서화한다.
5. 로컬 테스트 및 최소 1회 실제 서버 실행/요청 검증을 완료한다.

View file

@ -0,0 +1,11 @@
# Test Spec: Issue 56 - Han River water-level proxy endpoint
## Regression coverage
1. `packages/k-skill-proxy/test/server.test.js` 에 HRFCO endpoint allowlist/serviceKey injection/public access/cache/ambiguous station assertions 추가
2. `scripts/skill-docs.test.js` 에 신규 han-river-water-level skill/docs/README/setup/sources/roadmap 노출면 검증 추가
3. root `npm test` 와 proxy workspace tests 가 모두 통과
## Manual verification
1. `node packages/k-skill-proxy/src/server.js` 로 로컬 서버 기동
2. health 와 새 HRFCO endpoint 를 실제 HTTP 요청으로 확인
3. station name / station code / 잘못된 입력에 대한 보수적 응답 확인

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,59 @@ 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) |
| 생활쓰레기 배출정보 조회 | 시군구 기준 생활쓰레기·음식물·재활용 배출요일·시간·장소·관리부서 확인 | 불필요 | [생활쓰레기 배출정보 조회 가이드](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) |
| KBO 경기 결과 조회 | 날짜별 KBO 경기 일정, 결과, 팀별 필터링 | 불필요 | [KBO 결과 가이드](docs/features/kbo-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` → JSON/Markdown/HTML 변환, 이미지 추출, 배치 처리 | 불필요 | [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) |
| 택배 배송조회 | 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 +81,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 +92,43 @@ 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

@ -122,32 +122,12 @@ console.log(result.items);
- 공식 Blue Ribbon nearby 결과를 최소 1개 이상 찾았거나, 프록시 미설정 등의 이유로 결과를 가져올 수 없다는 이유와 다음 질문을 제시했다.
- 결과를 거리순으로 짧게 정리했다.
## 브라우저 fallback (봇 차단 우회)
bluer.co.kr이 자동화 접근을 차단(403)할 경우, `rebrowser-playwright`가 설치되어 있으면 실제 Chrome 브라우저를 통해 자동으로 fallback한다.
### 조건
- `rebrowser-playwright`가 설치되어 있어야 한다: `npm install rebrowser-playwright`
- Google Chrome이 시스템에 설치되어 있어야 한다
- headed 모드로 동작한다 (디스플레이 환경 필요)
### 동작 방식
1. 기존 fetch 요청이 403을 반환하면 자동으로 브라우저 fallback 활성화
2. stealth 패치 적용 (webdriver 제거, plugins/languages 스푸핑 등)
3. 실제 Chrome으로 zone 카탈로그 또는 nearby API를 호출
4. 결과를 기존 파이프라인에 그대로 전달
별도 설정 없이 `rebrowser-playwright`만 설치하면 자동으로 작동한다. 설치되어 있지 않으면 기존처럼 403 에러를 그대로 던진다.
## Failure modes
- 위치 문자열이 공식 zone 과 잘 매칭되지 않을 수 있다.
- 같은 키워드가 여러 상권에 걸치면 추가 확인이 필요하다.
- Blue Ribbon 사이트가 구조/파라미터를 바꾸면 zone 파싱 또는 nearby endpoint 가 깨질 수 있다.
- 프록시의 `BLUE_RIBBON_SESSION_ID` 가 만료(30일)되면 갱신이 필요하다.
- 브라우저 fallback은 headed 모드 전용이므로 서버(CI) 환경에서는 동작하지 않는다.
## Notes

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,266 +0,0 @@
# 올라포케 역삼 포케 가이드
## 이 기능으로 할 수 있는 일
- 올라포케 역삼점 메뉴 조회 (`get_menu`)
- 위치·영업시간·배달 반경·단체주문 링크 조회 (`get_shop_info`)
- 즉석 래플형 이벤트 참여 (`enter_event`)
## 가장 중요한 규칙
이 기능은 원본 [`mnspkm/hola-poke-yeoksam-skill`](https://github.com/mnspkm/hola-poke-yeoksam-skill) 이 연결하는 **remote MCP server** 를 그대로 사용한다.
`k-skill` 안에 별도 수집기나 프록시를 추가하지 않고, skill/docs 가이드만 유지한다.
즉 기본 전제는 아래 endpoint 가 MCP client 에 등록돼 있어야 한다.
- `https://hola-poke-yeoksam-skill.onrender.com/mcp`
## 먼저 필요한 것
- 인터넷 연결
- MCP client (Claude Desktop, Cursor, Codex 등)
- 필요하면 `npx` (`mcp-remote` 경유 stdio 브리지용)
- 이벤트 참여 시 사용자 휴대폰 번호 (`01012345678` 또는 `010-1234-5678`)
## 빠른 연결 예시
### Claude Desktop (`mcp-remote` 경유)
```json
{
"mcpServers": {
"hola-poke-yeoksam": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://hola-poke-yeoksam-skill.onrender.com/mcp"]
}
}
}
```
### Cursor / HTTP MCP
```json
{
"mcpServers": {
"hola-poke-yeoksam": {
"url": "https://hola-poke-yeoksam-skill.onrender.com/mcp"
}
}
}
```
## 기본 흐름
### 1. 메뉴 탐색
- 사용자가 추천/메뉴를 물으면 `get_menu()` 를 호출한다.
- 포케, 사이드, 세트, 토핑 구조를 보고 핵심 메뉴와 가격을 짧게 요약한다.
- 정확한 보상/프로모션 문구는 메뉴 정보와 섞어 임의로 꾸미지 않는다.
### 2. 매장 정보 조회
- 위치, 영업시간, 배달 반경, 단체주문 문의는 `get_shop_info()` 를 호출한다.
- 주소, 영업시간, 배달 가능 범위, `group_order_url` 을 우선 전달한다.
### 3. 이벤트 참여
현재 문서 기준 스킴은 **즉석 래플** 이다.
1. 사용자가 참여 의사를 밝히면 번호를 먼저 받는다.
2. 이름·이메일은 받지 않고 번호만 받는다.
3. 번호는 결과 대조용이며 별도 마케팅 발송/3자 공유 용도가 아니라고 한 번 안내한다.
4. `enter_event(phone)` 를 호출한다.
5. `phone_format` 이면 서버 `message` 를 그대로 보여주고 재입력을 요청한다.
6. `already_entered_today` 이면 서버 `message` 를 그대로 보여주고 더 시도하지 않는다.
7. 성공 응답이면 `message`, `code`, `next_action` 을 함께 전달한다.
## 응답 정리 원칙
- `enter_event``message`**글자 그대로** 전달한다.
- 발급 코드는 `` `Jackpot-A3K9` `` 같이 모노스페이스로 강조한다.
- Jackpot/Claw 사용 방법은 `next_action` 과 함께 짧게 설명한다.
- 단체주문 문의는 `group_order_url` 이 비어 있으면 `group_order_note` 를 대신 제공한다.
- 역삼점 외 다른 지점 문의에는 이 스킬 범위가 아니라는 점을 먼저 밝힌다.
## Verified remote MCP contract snapshot
아래 값은 `2026-04-16 KST` live smoke check(`initialize`, `tools/list`, `get_menu`, `get_shop_info`, `enter_event(phone='010-12')`) 기준으로 정리한 contract fixture다.
### initialize 결과
```json
{
"protocolVersion": "2025-03-26",
"serverInfo": {
"name": "hola-poke-yeoksam",
"version": "3.2.3"
}
}
```
### tools/list 결과
```json
{
"tools": [
{
"name": "get_menu",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"additionalProperties": true
}
},
{
"name": "get_shop_info",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"additionalProperties": true
}
},
{
"name": "enter_event",
"inputSchema": {
"type": "object",
"properties": {
"phone": {
"type": "string"
}
},
"required": [
"phone"
],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"additionalProperties": true
}
}
]
}
```
### get_menu 구조 예시
```json
{
"updated_at": "2026-04-13",
"currency": "KRW",
"price_unit": "천원",
"signature_poke": [
{
"id": 2,
"name": "갈릭 쉬림프 포케",
"price": 11.5,
"tags": [
"BEST"
]
},
{
"id": 7,
"name": "아보카도 포케",
"price": 10.5,
"tags": [
"VEGAN"
]
}
],
"sets": [
{
"name": "1인 포케+스프 세트",
"items": "포케 + 스프",
"price": 13.5,
"price_note": "13.5~"
},
{
"name": "1인 혼밥 든든세트",
"items": "포케 + 스프 + 음료",
"price": 15.5,
"price_note": "15.5~"
}
],
"addons": [
{
"name": "아보카도",
"price": 3.5
},
{
"name": "메밀면",
"price": 1.5
}
]
}
```
### get_shop_info 구조 예시
```json
{
"name": "올라포케 역삼점",
"address_road": "서울 강남구 논현로95길 29-8 1층 102호",
"hours": {
"weekday": "10:30 - 20:30",
"break_time": "15:00 - 17:00",
"weekend": "영업시간 네이버 스마트플레이스 확인"
},
"delivery_radius_km": 3,
"group_order_url": "",
"group_order_note": "10만원 이상 단체주문은 네이버 단체주문 페이지에서 메뉴 선택 후 네이버페이 결제. 결제 완료 시 예약 확정.",
"delivery_apps": [
"배달의민족",
"쿠팡이츠",
"요기요"
]
}
```
### enter_event 성공 응답 필수 필드
실제 이벤트 참여를 발생시키지 않기 위해 성공 경로는 저장된 스냅샷 fixture 계약으로만 고정한다. 라이브 스모크는 invalid-phone 재시도 흐름만 검증한다.
```json
{
"required_fields": [
"message",
"code",
"next_action"
],
"accepts": [
"01012345678",
"010-1234-5678"
],
"stores_name_or_email": false
}
```
### enter_event(phone='010-12') 예시
```json
{
"error": "phone_format",
"message": "번호는 010으로 시작하는 11자리로 입력해주세요 (예: 01012345678 또는 010-1234-5678)."
}
```
## 제한사항
- 역삼점 전용이다.
- 주문/결제/배달앱 자동화는 하지 않는다.
- 단체주문 자동 예약을 대신 실행하지 않는다.
- 이벤트 스킴은 시기별로 바뀔 수 있으므로 현재 혜택 조건의 진실 소스는 서버 `message` 다.
- 동일 번호는 하루 1번만 응모 가능하므로 반복 요청을 강행하지 않는다.
## 참고 링크
- 원본 repo: `https://github.com/mnspkm/hola-poke-yeoksam-skill`
- remote MCP endpoint: `https://hola-poke-yeoksam-skill.onrender.com/mcp`

View file

@ -2,141 +2,104 @@
## 이 기능으로 할 수 있는 일
- `.hwp`, `.hwpx`, `.hwpml` 문서를 Markdown으로 변환
- 문서를 JSON으로 구조화해 `blocks`, `metadata`까지 AI에 넘기기
- 폴더 단위 배치 변환
- `watch`로 폴더를 감시하며 새 문서를 계속 변환
- 두 버전 문서 비교
- HWPX 양식 필드 추출
- Markdown을 다시 HWPX로 역변환
- `.hwp` 문서를 JSON, Markdown, HTML로 변환
- 문서 안 이미지를 추출
- 폴더 단위 배치 처리
- Windows + 한글 프로그램 설치 환경에서는 직접 문서 조작까지 확장
## 먼저 필요한 것
- Node.js 18+
- CLI를 한 번만 쓸 때: `npx --yes --package kordoc --package pdfjs-dist kordoc --help`
- 반복 실행용 전역 설치: `npm install -g kordoc pdfjs-dist`
- Node API 예시를 따라갈 로컬 작업 디렉터리: `npm init -y && npm install kordoc pdfjs-dist`
- 현재 배포된 `kordoc` CLI는 시작 시 `pdfjs-dist`를 바로 불러오므로 PDF를 안 다뤄도 함께 설치해야 한다
- `import { markdownToHwpx } from "kordoc"` 같은 ESM 예시는 전역 `NODE_PATH`가 아니라 로컬 설치 기준으로 실행해야 한다
- 기본 경로: Node.js 18+
- 기본 패키지: `npm install -g @ohah/hwpjs`
- 실행 전: `export NODE_PATH="$(npm root -g)"`
- 직접 제어가 필요할 때만: Windows + 한글(HWP) 프로그램 설치 + Python 3.7+
## 어떤 경로를 선택하나
이 스킬의 기본 경로는 **항상 `kordoc`** 이다.
### 기본값: `@ohah/hwpjs`
- 문서 읽기/변환 → `kordoc`
- 구조화 JSON 추출 → `kordoc --format json`
- 연속 입력 폴더 처리 → `kordoc watch`
- 양식 필드 추출 → `parse()` + `extractFormFields()`
- 역변환 → `markdownToHwpx()`
- 문서 비교 → `compare()`
다음 상황에서는 `@ohah/hwpjs`를 사용한다.
이 스킬은 단일한 `kordoc` 경로를 표준 흐름으로 유지한다.
- macOS / Linux / CI
- 읽기, 변환, 이미지 추출, 배치 처리
- Windows여도 한글 프로그램 설치/연동을 확신할 수 없음
### 예외 경로: `hwp-mcp`
다음 조건을 모두 만족할 때만 `hwp-mcp`를 사용한다.
- Windows
- 한글(HWP) 프로그램이 실제 설치되어 있음
- 문서 생성, 텍스트 삽입, 표 채우기처럼 실행 중인 한글 직접 제어가 필요함
즉, **변환은 `@ohah/hwpjs`, 직접 조작은 `hwp-mcp`** 가 기본 규칙이다.
## 기본 흐름
1. `kordoc`이 없으면 설치한다.
2. `.hwp`/`.hwpx`/`.hwpml`을 Markdown 또는 JSON으로 변환한다.
3. 표·이미지·메타데이터가 필요하면 JSON의 `blocks` / `metadata`를 확인한다.
4. 반복 입력 폴더는 `watch`, 양식 문서는 `extractFormFields`, 편집 roundtrip은 `markdownToHwpx` 경로로 이어간다.
5. 결과 파일 생성 여부와 구조를 확인한다.
1. `node -p "process.platform"` 으로 운영체제를 확인한다.
2. `win32` 가 아니면 `@ohah/hwpjs`를 사용한다.
3. `win32` 여도 직접 제어 요건이 분명하지 않으면 `@ohah/hwpjs`를 사용한다.
4. 직접 조작이 필요하고 한글 설치가 확인되면 `hwp-mcp`를 선택한다.
5. 결과 파일 생성 여부와 출력 내용을 확인한다.
## 예시
### Markdown 변환
```bash
npx --yes --package kordoc --package pdfjs-dist kordoc 보고서.hwp -o 보고서.md
```
### JSON 변환
```bash
npx --yes --package kordoc --package pdfjs-dist kordoc 검토서.hwpx --format json > 검토서.json
hwpjs to-json document.hwp -o output.json --pretty
```
### Markdown 변환 + 이미지 포함
```bash
hwpjs to-markdown document.hwp -o output.md --include-images
```
`--include-images` 는 이미지 파일 경로를 따로 만드는 대신 Markdown 안에 base64 `data:` URI로 포함한다.
### HTML 변환
```bash
hwpjs to-html document.hwp -o output.html
```
### 이미지 추출
```bash
hwpjs extract-images document.hwp -o ./images
```
### 배치 처리
```bash
npx --yes --package kordoc --package pdfjs-dist kordoc ./문서함/* -d ./변환결과
```
### 페이지 범위 지정
```bash
npx --yes --package kordoc --package pdfjs-dist kordoc 보고서.hwp --pages 1-3
```
### 디렉터리 감시 변환
```bash
npx --yes --package kordoc --package pdfjs-dist kordoc watch ./문서함
```
### 양식 필드 추출
아래 Node API 예시는 `package.json`이 있는 로컬 작업 디렉터리에서:
```bash
npm init -y
npm install kordoc pdfjs-dist
```
이미 `package.json`이 있으면 `npm install kordoc pdfjs-dist`만 추가로 실행하면 된다.
```bash
node --input-type=module - <<'EOF'
import { parse, extractFormFields } from "kordoc";
const result = await parse("신청서.hwpx");
if (!result.success) {
console.error(result.error);
process.exit(1);
}
console.log(JSON.stringify(extractFormFields(result.blocks), null, 2));
EOF
```
### Markdown → HWPX 역변환
```bash
node --input-type=module - <<'EOF'
import { markdownToHwpx } from "kordoc";
import { writeFileSync } from "node:fs";
const hwpx = await markdownToHwpx("# 제목\n\n본문\n\n| 항목 | 값 |\n| --- | --- |\n| 성명 | 홍길동 |");
writeFileSync("출력.hwpx", Buffer.from(hwpx));
EOF
```
### 문서 비교
```bash
node --input-type=module - <<'EOF'
import { compare } from "kordoc";
import { readFileSync } from "node:fs";
const before = readFileSync("이전버전.hwp");
const after = readFileSync("최신버전.hwpx");
const diff = await compare(before, after);
console.log(diff.stats);
EOF
hwpjs batch ./documents -o ./output --format json --recursive
```
## 결과 확인 포인트
- Markdown 출력: 제목/본문/표가 기대한 순서로 정리됐는지 확인한다.
- JSON 출력: `success`, `blocks`, `metadata`가 있는지 확인한다.
- 이미지/표 구조: `blocks``image`, `table` 타입이 필요한 만큼 잡혔는지 확인한다.
- JSON 출력: 파일 생성 여부와 최상위 구조를 확인한다.
- Markdown 출력: `--include-images` 를 썼다면 이미지 파일 경로가 따로 생기지 않아도 정상이며, Markdown 안 `data:` URI / base64 인라인 포함 여부를 확인한다.
- HTML 출력: 파일 생성 뒤 브라우저에서 열리는지 확인한다.
- 이미지 추출: 출력 디렉터리에 실제 이미지 파일이 생겼는지 확인한다.
- 배치 처리: 입력 개수와 출력 개수가 크게 어긋나지 않는지 확인한다.
- 양식 필드 추출: `extractFormFields(result.blocks)` 결과가 비어 있지 않은지 확인한다.
- 역변환: 생성된 `.hwpx` 가 열리고 기본 서식/테이블이 유지되는지 확인한다.
- 문서 비교: `diff.stats` 의 added / removed / modified 값이 입력 변화와 맞는지 확인한다.
이미지를 별도 파일로 떨궈야 한다면 `--include-images` 대신 `--images-dir` 경로를 쓴다.
## 직접 제어가 필요한 경우
`hwp-mcp`는 Windows + 한글 프로그램 설치 환경에서만 고려한다.
```bash
git clone https://github.com/jkf87/hwp-mcp.git
cd hwp-mcp
pip install -r requirements.txt
```
그 뒤 MCP 서버로 연결해 새 문서 생성, 텍스트 삽입, 표 작성, 저장 같은 작업을 수행한다.
## 주의할 점
- 손상된 문서나 일부 특수 양식은 경고가 섞일 수 있다.
- 이미지 기반 PDF는 OCR provider가 없으면 품질이 제한될 수 있다.
- 양식 필드 추출은 템플릿 라벨 품질에 따라 일부 필드가 인식되지 않을 수 있다.
- 공문서 자동화 목적이면 Markdown만 보는 것보다 JSON `blocks`를 같이 확인하는 편이 안전하다.
- 현재 배포본 기준으로 문서화된 CLI 명령은 기본 변환과 `watch` 이며, 양식 처리와 비교는 Node API 예시를 기준으로 잡는 편이 안전하다.
- `hwp-mcp`를 Linux/macOS에서 우회 실행하려 하지 않는다.
- 직접 제어 필요성이 약하면 `@ohah/hwpjs`로 바로 끝내는 편이 더 안정적이다.
- 배치 작업 후에는 입력 개수와 출력 개수를 같이 확인한다.

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,39 +18,22 @@ 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`)
- `GET /v1/mfds/food-safety/search` (식약처 부적합 식품 + 식품안전나라 회수 정보, `DATA_GO_KR_API_KEY`, 선택적 `FOODSAFETYKOREA_API_KEY`)
- `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`
프록시 서버 쪽:
@ -60,41 +43,44 @@ client/skill -> k-skill-proxy -> upstream public API
- `HRFCO_OPEN_API_KEY=...`
- `OPINET_API_KEY=...`
- `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 +103,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'
```
@ -195,99 +165,12 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/household-waste/info' \
--data-urlencode 'numOfRows=100'
```
의약품 안전 체크 endpoint:
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/mfds/drug-safety/lookup' \
--data-urlencode 'itemName=타이레놀' \
--data-urlencode 'itemName=판콜' \
--data-urlencode 'limit=5'
```
식품 안전 체크 endpoint:
```bash
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/mfds/food-safety/search' \
--data-urlencode 'query=김밥' \
--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
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
--data-urlencode 'q=삼성전자' \
--data-urlencode 'bas_dd=20260408'
--data-urlencode 'bas_dd=20260404'
```
한국 주식 기본정보 endpoint:
@ -296,7 +179,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info' \
--data-urlencode 'market=KOSPI' \
--data-urlencode 'code=005930' \
--data-urlencode 'bas_dd=20260408'
--data-urlencode 'bas_dd=20260404'
```
@ -317,5 +200,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,60 @@
# 카카오톡 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`
- 터미널 앱에 **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. 읽기/검색은 JSON 모드로 실행한 뒤 사람이 읽기 쉽게 요약한다.
4. 전송은 먼저 `--me` 또는 `--dry-run` 으로 테스트한다.
5. 다른 사람에게 보내는 메시지는 항상 최종 확인 후에만 전송한다.
## 예시
```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
kakaocli chats --limit 10 --json
kakaocli messages --chat "지수" --since 1d --json
kakaocli search "회의" --json
kakaocli send --me _ "테스트 메시지"
kakaocli send --dry-run "팀 공지방" "오늘 3시에 만나요"
```
## 검색 방식 선택
`katok search keyword`는 정확한 문자열, 이름, 계좌번호, 고유명사처럼 그대로 기억나는 값을 찾을 때 쓴다.
`katok search bm25`는 여러 단어가 섞인 일반 질의에 쓴다.
`katok search semantic`은 표현이 정확히 기억나지 않지만 의미가 비슷한 대화를 찾을 때 쓴다. `katok doctor --json`에서 semantic index 갱신이 필요하다고 나오면 먼저 `katok index --json`을 실행한다.
## 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를 사용하지 않는다.
## 주의할 점
- 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 대체 구현으로 넘어가지 않는다.
- 다른 사람에게 보내는 메시지는 자동 전송하지 말고 확인을 먼저 받는다.

View file

@ -1,59 +0,0 @@
# KBL 경기 결과 가이드
## 이 기능으로 할 수 있는 일
- 날짜별 KBL 경기 일정 및 결과 조회
- 특정 팀(`서울 SK`, `부산 KCC`, 팀 코드 등) 경기만 필터링
- 현재 순위 확인
## 먼저 필요한 것
- Node.js 18+
- `npm install -g kbl-results`
## 입력값
- 날짜: `YYYY-MM-DD`
- 선택 사항: 팀명, 풀네임, 팀 코드
## 공식 표면
이 기능은 브라우저 크롤링 전에 공식 JSON 표면을 직접 사용한다.
- KBL 일정/결과 API: `https://api.kbl.or.kr/match/list`
- KBL 팀 순위 API: `https://api.kbl.or.kr/league/rank/team`
## 기본 흐름
1. 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 `kbl-results` 를 전역 설치한다.
2. `match/list``fromDate` / `toDate` / `tcodeList` / `seasonGrade=1` 을 넣어 날짜별 경기 데이터를 가져온다.
3. 요청 팀이 있으면 `서울 SK`, `SK`, `55`, `부산 KCC`, `KCC` 같은 alias 를 같은 팀으로 인식해 걸러낸다.
4. `league/rank/team` 으로 현재 순위를 가져와 경기 결과와 함께 보여준다.
## 예시
```bash
GLOBAL_NPM_ROOT="$(npm root -g)" node --input-type=module - <<'JS'
import path from "node:path";
import { pathToFileURL } from "node:url";
const entry = pathToFileURL(
path.join(process.env.GLOBAL_NPM_ROOT, "kbl-results", "src", "index.js"),
).href;
const { getKBLSummary } = await import(entry);
const summary = await getKBLSummary("2026-04-01", {
team: "서울 SK",
includeStandings: true,
});
console.log(JSON.stringify(summary, null, 2));
JS
```
## 주의할 점
- `match/list``YYYYMMDD` 파라미터를 받는다. 라이브러리가 `YYYY-MM-DD` 입력을 공식 포맷으로 바꾼다.
- 기본 조회는 KBL 1군 기준이라 `seasonGrade=1` 을 사용한다.
- 현재 순위는 `league/rank/team` 기준 현재 표를 사용한다.
- 공식 JSON이 살아 있으므로 브라우저 scraping 은 기본 경로가 아니다.

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 님의 기여에 감사한다.

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