mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Compare commits
1 commit
main
...
feature/#4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57fc78401 |
673 changed files with 2881 additions and 104175 deletions
5
.changeset/blue-ribbon-nearby-skill.md
Normal file
5
.changeset/blue-ribbon-nearby-skill.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"blue-ribbon-nearby": minor
|
||||
---
|
||||
|
||||
Add the first reusable Blue Ribbon nearby restaurant client and skill docs.
|
||||
5
.changeset/bright-dodos-wave.md
Normal file
5
.changeset/bright-dodos-wave.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"kleague-results": minor
|
||||
---
|
||||
|
||||
Add the first official K League results and standings client package.
|
||||
5
.changeset/calm-melons-smoke.md
Normal file
5
.changeset/calm-melons-smoke.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"k-lotto": minor
|
||||
---
|
||||
|
||||
Add the initial official dhlottery-backed k-lotto package.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"changelog": false,
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
|
|
|||
5
.changeset/coupang-product-search-skill.md
Normal file
5
.changeset/coupang-product-search-skill.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"coupang-product-search": minor
|
||||
---
|
||||
|
||||
Add the first Coupang shopper URL/parser package with anti-bot probe guidance and skill docs.
|
||||
5
.changeset/fair-eagles-drum.md
Normal file
5
.changeset/fair-eagles-drum.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"toss-securities": minor
|
||||
---
|
||||
|
||||
Add the first safe read-only Toss Securities wrapper package and skill docs.
|
||||
5
.changeset/mean-seas-collect.md
Normal file
5
.changeset/mean-seas-collect.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"kakao-bar-nearby": minor
|
||||
---
|
||||
|
||||
Add the first Kakao Map nearby bar lookup package and skill docs.
|
||||
5
.changeset/tiny-oranges-smell.md
Normal file
5
.changeset/tiny-oranges-smell.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"daiso-product-search": minor
|
||||
---
|
||||
|
||||
Publish the official Daiso Mall store and pickup-stock lookup package.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "k-skill",
|
||||
"owner": {
|
||||
"name": "NomaDamas"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "k-skill",
|
||||
"source": "./",
|
||||
"description": "한국인을 위한 90+ Agent Skill 번들 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
147
.github/workflows/deploy-k-skill-proxy.yml
vendored
147
.github/workflows/deploy-k-skill-proxy.yml
vendored
|
|
@ -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
|
||||
91
.github/workflows/manus-bundle.yml
vendored
91
.github/workflows/manus-bundle.yml
vendored
|
|
@ -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
|
||||
40
.github/workflows/release-npm.yml
vendored
40
.github/workflows/release-npm.yml
vendored
|
|
@ -1,7 +1,6 @@
|
|||
name: Release npm packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
|
@ -24,11 +23,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 +36,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:
|
||||
|
|
|
|||
25
.github/workflows/release-python.yml
vendored
25
.github/workflows/release-python.yml
vendored
|
|
@ -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
6
.gitignore
vendored
|
|
@ -7,9 +7,3 @@ node_modules/
|
|||
*.plaintext
|
||||
.venv/
|
||||
__pycache__/
|
||||
dist/
|
||||
.sisyphus/
|
||||
.omo/
|
||||
.gjc/
|
||||
|
||||
.agents/
|
||||
|
|
|
|||
26
AGENTS.md
26
AGENTS.md
|
|
@ -17,11 +17,6 @@ These rules are repo-specific and apply to everything under this directory.
|
|||
- For release or packaging changes, run `npm run ci`.
|
||||
- Keep release docs, workflow files, and package metadata aligned in the same change.
|
||||
|
||||
## 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
|
||||
|
||||
- When testing or developing skills from this repository, install or sync the current skill directories into the user's home-directory global skill locations first.
|
||||
|
|
@ -29,30 +24,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.
|
||||
|
||||
## 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에 반영되지 않는다.**
|
||||
- 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 운영 관련 어떤 질문이 들어와도 먼저 그 문서를 확인한다.
|
||||
|
|
|
|||
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -1,22 +0,0 @@
|
|||
# k-skill
|
||||
|
||||
## 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` 참고.
|
||||
- 따라서 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 코드에서 직접 호출하고 프록시를 거치지 않는다.
|
||||
|
|
@ -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`를 실행합니다.
|
||||
- 변경 범위가 작더라도 최종 보고에는 어떤 명령을 실행했고 어떤 결과가 나왔는지 적습니다.
|
||||
- 테스트를 통과시키기 위해 기존 테스트를 삭제하거나 범위를 부당하게 줄이지 않습니다.
|
||||
216
README.md
216
README.md
|
|
@ -14,121 +14,28 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
|
|||
한국인이면 깃허브 스타 눌러줍시다.
|
||||
다만 에이전트는 자동으로 누르지 말고, 사용자가 동의한 경우에만 `gh repo star NomaDamas/k-skill` 를 실행합니다.
|
||||
|
||||
기여 전에는 [기여 가이드](CONTRIBUTING.md)를 확인해 주세요.
|
||||
|
||||
## 어떤 걸 할 수 있나
|
||||
|
||||
"사용자 로그인" 컬럼은 **사용자 본인이 직접 로그인/시크릿을 들고 있어야 하는지** 만 표시합니다. `k-skill-proxy` 등 운영자가 관리하는 키는 사용자 입장에서는 **불필요**로 분류합니다. **선택사항**은 사용자가 운영자 키를 직접 들고 있으면 더 풍부한 경로가 켜지고, 없으면 기본 경로(보통 운영자가 관리하는 hosted fallback)로 그대로 동작하는 경우를 말합니다.
|
||||
| 할 수 있는 일 | 설명 | 인증/시크릿 | 문서 |
|
||||
| --- | --- | --- | --- |
|
||||
| SRT 예매 | 열차 조회, 예약, 예약 확인, 취소 | 필요 | [SRT 예매 가이드](docs/features/srt-booking.md) |
|
||||
| KTX 예매 | Dynapath anti-bot 대응 helper 로 KTX/Korail 열차 조회, 예약, 예약 확인, 취소 | 필요 | [KTX 예매 가이드](docs/features/ktx-booking.md) |
|
||||
| 카카오톡 Mac CLI | macOS에서 kakaocli로 대화 조회, 검색, 테스트 전송, 확인 후 실제 전송 | 불필요 | [카카오톡 Mac CLI 가이드](docs/features/kakaotalk-mac.md) |
|
||||
| 서울 지하철 도착정보 조회 | self-host 또는 배포 확인이 끝난 `k-skill-proxy` 경유로 역 기준 실시간 도착 예정 열차 확인 | 프록시 URL 필요 | [서울 지하철 도착정보 가이드](docs/features/seoul-subway-arrival.md) |
|
||||
| 사용자 위치 미세먼지 조회 | `k-skill-proxy` 로 현재 위치 또는 지역 fallback 기준 PM10/PM2.5 확인 | 불필요 | [사용자 위치 미세먼지 조회 가이드](docs/features/fine-dust-location.md) |
|
||||
| 한국 법령 검색 | `korean-law-mcp` 우선 + 장애 시 `법망` fallback으로 법령/조문/판례/유권해석 조회 | 로컬 CLI/MCP면 `LAW_OC` 필요, remote endpoint/법망 fallback은 불필요 | [한국 법령 검색 가이드](docs/features/korean-law-search.md) |
|
||||
| KBO 경기 결과 조회 | 날짜별 경기 일정, 결과, 팀별 필터링 | 불필요 | [KBO 결과 가이드](docs/features/kbo-results.md) |
|
||||
| K리그 경기 결과 조회 | 날짜별 K리그1/K리그2 경기 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [K리그 결과 가이드](docs/features/kleague-results.md) |
|
||||
| 토스증권 조회 | `tossctl` 기반 계좌 요약, 포트폴리오, 시세, 주문내역, 관심종목 조회 | 필요 | [토스증권 조회 가이드](docs/features/toss-securities.md) |
|
||||
| 로또 당첨 확인 | 최신 회차, 특정 회차, 번호 대조 | 불필요 | [로또 결과 가이드](docs/features/lotto-results.md) |
|
||||
| HWP 문서 처리 | `.hwp` → JSON/Markdown/HTML 변환, 이미지 추출, 배치 처리, Windows 직접 제어 선택 | 불필요 | [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) |
|
||||
| 택배 배송조회 | CJ대한통운·우체국 공식 표면으로 배송 상태를 조회하고, carrier adapter 규칙으로 추가 택배사 확장을 준비 | 불필요 | [택배 배송조회 가이드](docs/features/delivery-tracking.md) |
|
||||
| 쿠팡 상품 가격 조회 | 공식 쿠팡 URL + 브라우저 캡처 HTML 기준으로 상품 후보/가격/리뷰를 정리하고 anti-bot 차단 여부를 probe | 브라우저 세션/HTML 캡처 권장 | [쿠팡 상품 가격 조회 가이드](docs/features/coupang-product-search.md) |
|
||||
|
||||
| 할 수 있는 일 | 스킬 이름 | 설명 | 사용자 로그인 | 문서 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 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) |
|
||||
|
||||
## 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)을 참고하세요.
|
||||
|
||||
## 처음 시작하는 순서
|
||||
|
||||
|
|
@ -143,8 +50,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 +61,21 @@ 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/features/coupang-product-search.md)
|
||||
- [릴리스/배포 가이드](docs/releasing.md)
|
||||
|
||||
설치 기본 흐름은 "전체 스킬 설치 → `k-skill-setup` 실행 → 개별 기능 사용" 입니다.
|
||||
|
|
|
|||
|
|
@ -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`)의 공식 출처를 따른다.
|
||||
|
|
@ -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())
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: blue-ribbon-nearby
|
||||
description: Use when the user asks for nearby restaurants or 근처 맛집 and wants 블루리본 picks. Always ask the user's current location first, then search official Blue Ribbon nearby restaurants via k-skill-proxy.
|
||||
description: Use when the user asks for nearby restaurants or 근처 맛집. Always ask the user's current location first, then search official 블루리본 Blue Ribbon Survey ribbon restaurants near that location.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: food
|
||||
|
|
@ -12,13 +12,12 @@ metadata:
|
|||
|
||||
## What this skill does
|
||||
|
||||
유저가 알려준 현재 위치를 기준으로 블루리본 서베이 공식 zone 을 찾고, k-skill-proxy 를 경유해 **근처 블루리본 맛집**을 보여준다.
|
||||
유저가 알려준 현재 위치를 기준으로 블루리본 서베이 공식 검색 표면에서 **근처 블루리본 맛집**만 추려서 보여준다.
|
||||
|
||||
- 위치는 자동으로 추정하지 않는다.
|
||||
- **반드시 먼저 현재 위치를 질문**한다.
|
||||
- 위치 문자열은 공식 `zone` 목록으로 매칭하고, 주변 JSON endpoint 로 좁혀서 찾는다.
|
||||
- 위치 문자열은 공식 `zone` 목록으로 매칭하고, 가능하면 주변 JSON endpoint 로 좁혀서 찾는다.
|
||||
- 좌표를 직접 받으면 더 정확한 nearby 검색을 할 수 있다.
|
||||
- nearby 검색은 기본적으로 k-skill-proxy (`/v1/blue-ribbon/nearby`) 를 경유한다. 프록시에 `BLUE_RIBBON_SESSION_ID` 가 설정되어 있어야 한다.
|
||||
|
||||
## When to use
|
||||
|
||||
|
|
@ -88,7 +87,7 @@ metadata:
|
|||
|
||||
### 3. Query the nearby Blue Ribbon endpoint
|
||||
|
||||
기본적으로 k-skill-proxy 를 경유해 nearby 결과를 가져온다.
|
||||
공식 JSON endpoint 에 nearby 조건을 붙여 호출한다.
|
||||
|
||||
```js
|
||||
const { searchNearbyByLocationQuery } = require("blue-ribbon-nearby");
|
||||
|
|
@ -102,9 +101,7 @@ console.log(result.anchor);
|
|||
console.log(result.items);
|
||||
```
|
||||
|
||||
내부적으로는 zone 매칭 후 프록시의 `/v1/blue-ribbon/nearby` 에 좌표와 거리를 넘긴다. 프록시가 프리미엄 세션으로 Blue Ribbon upstream 을 호출한다.
|
||||
|
||||
직접 호출이 필요하면 `useDirectApi: true` 옵션을 쓸 수 있지만, 프리미엄 세션 없이는 `premium_required` 에러가 난다.
|
||||
내부적으로는 `ribbon=true`, `ribbonType=RIBBON_THREE,RIBBON_TWO,RIBBON_ONE`, `isAround=true`, `sort=distance`, `zone2Lat`, `zone2Lng` 같은 파라미터를 사용한다.
|
||||
|
||||
### 4. Respond with a short restaurant summary
|
||||
|
||||
|
|
@ -119,35 +116,14 @@ console.log(result.items);
|
|||
## Done when
|
||||
|
||||
- 유저의 현재 위치를 먼저 확인했다.
|
||||
- 공식 Blue Ribbon nearby 결과를 최소 1개 이상 찾았거나, 프록시 미설정 등의 이유로 결과를 가져올 수 없다는 이유와 다음 질문을 제시했다.
|
||||
- 공식 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
|
||||
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
---
|
||||
name: bunjang-search
|
||||
description: 번개장터 검색, 상세조회, 찜, 채팅, 대량 수집, AI TOON export를 bunjang-cli로 안내한다.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: marketplace
|
||||
locale: ko-KR
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# Bunjang Search
|
||||
|
||||
## What this skill does
|
||||
|
||||
upstream [`bunjang-cli`](https://www.npmjs.com/package/bunjang-cli) / [`pinion05/bunjangcli`](https://github.com/pinion05/bunjangcli) 를 사용해 번개장터에서 아래 흐름을 처리한다.
|
||||
|
||||
- 상품 검색
|
||||
- 상품 상세조회
|
||||
- 선택적 찜/채팅
|
||||
- 다페이지 대량 수집
|
||||
- AI 분석용 TOON chunk export
|
||||
|
||||
## Core policy
|
||||
|
||||
- 기본 경로는 **항상 CLI first** 다.
|
||||
- 기본 명령은 `npx --yes bunjang-cli ...` 형식을 쓴다.
|
||||
- `auth login` 은 headful 브라우저 + **TTY / interactive 터미널**이 필요하다.
|
||||
- 로그인 전에는 검색/상세조회/대량 수집 위주로 답하고, `favorite` / `chat` / `purchase` 는 **선택적 로그인 플로우**로만 안내한다.
|
||||
- 대량 수집은 `--start-page`, `--pages`, `--max-items`, `--with-detail`, `--output` 조합을 우선 쓴다.
|
||||
- AI 분석용 export 는 `--ai --output <directory>` 로 `.toon` chunk 를 만든다.
|
||||
- 찜/채팅은 명시적으로 요청받지 않으면 실행하지 않는다.
|
||||
|
||||
## When to use
|
||||
|
||||
- "번개장터에서 아이폰 검색해줘"
|
||||
- "번장에서 이 상품 상세 봐줘"
|
||||
- "여러 페이지 모아서 JSON으로 저장해줘"
|
||||
- "AI 평가용으로 번개장터 결과를 chunk 로 만들어줘"
|
||||
|
||||
## When not to use
|
||||
|
||||
- 계정 로그인 없이 바로 찜/채팅을 강행해야 하는 경우
|
||||
- 구매 확정/결제 자동화를 기대하는 경우
|
||||
- 번개장터 외 다른 중고거래 플랫폼을 동시에 다뤄야 하는 경우
|
||||
|
||||
## Quick smoke test
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli --help
|
||||
npx --yes bunjang-cli --json auth status
|
||||
npx --yes bunjang-cli --json search "아이폰" --max-items 3 --sort date
|
||||
npx --yes bunjang-cli --json item get 354957625
|
||||
```
|
||||
|
||||
## Login flow
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli auth login
|
||||
npx --yes bunjang-cli auth logout
|
||||
npx --yes bunjang-cli --json auth status
|
||||
```
|
||||
|
||||
- `auth login` 은 브라우저에서 로그인한 뒤 **터미널로 돌아와 Enter 를 눌러야** 완료된다.
|
||||
- 그래서 비-TTY 실행 대신 interactive 세션에서만 진행한다.
|
||||
|
||||
## Search flow
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰"
|
||||
npx --yes bunjang-cli search "아이폰" --price-min 500000 --price-max 1200000
|
||||
npx --yes bunjang-cli search "아이폰" --sort date
|
||||
npx --yes bunjang-cli --json search "아이폰" --max-items 5
|
||||
```
|
||||
|
||||
검색 결과는 광고/매입글/악세서리 노이즈가 섞이고, search summary 의 `location` 이 noisy 하거나 `description` / `status` 가 비어 있을 수 있다. 그래서 **검색 단계는 제목/가격 중심 1차 triage** 로만 쓴다.
|
||||
|
||||
- 기기명/용량 키워드 일치 여부
|
||||
- 가격대 범위
|
||||
- 판매 링크/썸네일 중복 여부
|
||||
|
||||
`description`, `status`, 깔끔한 `location` 이 필요하면 **반드시 `item get` 또는 `--with-detail` 이후** 에만 판단한다.
|
||||
|
||||
## Detail flow
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli item get 354957625
|
||||
npx --yes bunjang-cli --json item get 354957625
|
||||
npx --yes bunjang-cli --json item list --ids 354957625,354801707
|
||||
```
|
||||
|
||||
상세조회에서는 아래 필드를 먼저 읽는다.
|
||||
|
||||
- `price`
|
||||
- `description`
|
||||
- `location`
|
||||
- `category`
|
||||
- `status`
|
||||
- `sellerName`
|
||||
- `sellerItemCount`
|
||||
- `sellerFollowerCount`
|
||||
- `sellerReviewCount`
|
||||
- `favoriteCount`
|
||||
- `transportUsed`
|
||||
|
||||
## Bulk collection
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰" \
|
||||
--start-page 1 \
|
||||
--pages 5 \
|
||||
--max-items 50 \
|
||||
--sort date \
|
||||
--with-detail \
|
||||
--output artifacts/bunjang-iphone.json
|
||||
```
|
||||
|
||||
검증할 때는 export 파일 생성 여부와 top-level `items[]` 안의 `summary` / `detail` / optional `error` 구조, 그리고 각 item 의 `sourcePage` 또는 `summary.raw.page` 를 같이 확인한다.
|
||||
|
||||
## AI export
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰" \
|
||||
--start-page 1 \
|
||||
--pages 5 \
|
||||
--max-items 50 \
|
||||
--with-detail \
|
||||
--ai \
|
||||
--output artifacts/bunjang-iphone-ai
|
||||
```
|
||||
|
||||
- `--ai` 에서는 `--output` 이 **파일이 아니라 디렉토리** 여야 한다.
|
||||
- 결과는 `items-1.toon` 형태 chunk 로 저장된다.
|
||||
- AI 평가용으로 여러 서브에이전트에 분산 읽기시키기 좋다.
|
||||
|
||||
## Optional favorite/chat flow
|
||||
|
||||
로그인된 interactive 세션에서만 아래 액션을 진행한다.
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli --json favorite list
|
||||
npx --yes bunjang-cli --json favorite add 354957625
|
||||
npx --yes bunjang-cli --json favorite remove 354957625
|
||||
npx --yes bunjang-cli --json chat list
|
||||
npx --yes bunjang-cli --json chat start 354957625 --message "안녕하세요"
|
||||
npx --yes bunjang-cli --json chat send 84191651 --message "상품 상태 괜찮을까요?"
|
||||
```
|
||||
|
||||
- 찜/채팅은 **로그인이 필요한 선택적 기능**이다.
|
||||
- 검증 목적이면 `favorite list` 로 세션을 먼저 확인하고, 같은 상품에 대해 `favorite add` / `favorite remove` 를 왕복 실행한다.
|
||||
- `chat start` 는 상품 페이지에서 새 대화를 열 때, `chat send` 는 기존 thread 에 메시지를 보낼 때 쓴다.
|
||||
|
||||
## Recommended response format
|
||||
|
||||
1. 검색어가 넓으면 예산/모델/지역을 먼저 좁힌다.
|
||||
2. 검색 결과 상위 3~5개는 제목/가격 중심 1차 요약만 한다.
|
||||
3. `description` / `status` / `location` 판단이 필요하면 `item get` 또는 `--with-detail` 로 상세를 먼저 읽는다.
|
||||
4. 로그인 액션이 필요하면 "지금은 로그인 세션이 없으니 interactive TTY 에서 `auth login` 후 다시 진행" 이라고 분명히 말한다.
|
||||
5. 대량 분석이면 JSON export 또는 TOON chunk 생성 경로를 제안한다.
|
||||
|
||||
## Done when
|
||||
|
||||
- 검색/상세조회/대량 수집/AI export 중 필요한 경로가 안내되었다.
|
||||
- 찜/채팅은 로그인 필요성과 선택적 성격이 명확히 고지되었다.
|
||||
- 자동 구매/결제는 범위 밖이라고 분명히 말했다.
|
||||
|
|
@ -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초를 유지합니다 (서버 부하 방지).
|
||||
- 캐치테이블 이용약관을 준수하는 범위에서 사용하세요.
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
---
|
||||
name: cheap-gas-nearby
|
||||
description: Use when the user asks for nearby cheapest gas stations or 근처 가장 싼 주유소. Always ask the user's current location first, then use Kakao Map anchor resolution plus official Opinet fuel-price APIs.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: transport
|
||||
locale: ko-KR
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# Cheap Gas Nearby
|
||||
|
||||
## What this skill does
|
||||
|
||||
유저가 알려준 현재 위치를 기준으로 **근처에서 가장 싼 주유소**를 찾아준다.
|
||||
|
||||
- 위치는 자동으로 추정하지 않는다.
|
||||
- **반드시 먼저 현재 위치를 질문**한다.
|
||||
- 가격 데이터는 한국석유공사 **Opinet 공식 API**를 우선 사용한다.
|
||||
- 동네/역명/랜드마크 입력은 Kakao Map anchor 검색으로 좌표를 잡은 뒤 Opinet nearby 검색으로 연결한다.
|
||||
- 기본 제품은 **휘발유(B027)** 이고, 유저가 경유라고 명시하면 **경유(D047)** 로 바꾼다.
|
||||
|
||||
## When to use
|
||||
|
||||
- "근처 가장 싼 주유소 찾아줘"
|
||||
- "서울역 근처 휘발유 제일 싼 데 어디야?"
|
||||
- "강남에서 경유 싼 주유소 몇 군데만 보여줘"
|
||||
- "지금 여기 근처 셀프주유소 중 싼 순으로 알려줘"
|
||||
|
||||
## Mandatory first question
|
||||
|
||||
위치 정보 없이 바로 검색하지 말고 반드시 먼저 물어본다.
|
||||
|
||||
- 권장 질문: `현재 위치를 알려주세요. 동네/역명/랜드마크/위도·경도 중 편한 형식으로 보내주시면 근처에서 가장 싼 주유소를 찾아볼게요.`
|
||||
- 제품이 불명확하면: `휘발유 기준으로 볼까요, 경유 기준으로 볼까요? 따로 말씀 없으면 휘발유로 찾을게요.`
|
||||
- 위치가 애매하면: `가까운 역명이나 동 이름으로 한 번만 더 알려주세요.`
|
||||
|
||||
## Default path
|
||||
|
||||
기본적으로 `https://k-skill-proxy.nomadamas.org/v1/opinet/around` 와 `/v1/opinet/detail` 을 경유해 조회한다. 사용자 쪽에서 별도 `OPINET_API_KEY` 를 준비할 필요가 없다.
|
||||
|
||||
## Official Opinet surfaces
|
||||
|
||||
- 오픈 API 안내: `https://www.opinet.co.kr/user/custapi/openApiInfo.do`
|
||||
- 반경 내 주유소: `https://www.opinet.co.kr/api/aroundAll.do`
|
||||
- 주유소 상세정보(ID): `https://www.opinet.co.kr/api/detailById.do`
|
||||
- 지역코드: `https://www.opinet.co.kr/api/areaCode.do`
|
||||
|
||||
반경 검색 핵심 파라미터:
|
||||
|
||||
- `x`, `y`: 기준 위치 **KATEC** 좌표
|
||||
- `radius`: 반경(m, 최대 5000)
|
||||
- `prodcd`: `B027`(휘발유), `D047`(경유), `B034`(고급휘발유), `C004`(등유), `K015`(LPG)
|
||||
- `sort=1`: 가격순
|
||||
|
||||
## Location resolution surface
|
||||
|
||||
- Kakao Map 모바일 검색: `https://m.map.kakao.com/actions/searchView?q=<query>`
|
||||
- Kakao Map 장소 패널 JSON: `https://place-api.map.kakao.com/places/panel3/<confirmId>`
|
||||
|
||||
위치 문자열은 Kakao Map으로 **anchor 좌표(WGS84)** 를 구한 뒤, 내부적으로 **WGS84 → KATEC** 변환을 적용해 Opinet `aroundAll.do` 에 넘긴다.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. 유저에게 반드시 현재 위치를 묻는다.
|
||||
2. 위치 문자열을 받으면 Kakao Map anchor 검색으로 좌표를 찾는다.
|
||||
- 위도/경도를 직접 받으면 anchor 검색을 생략한다.
|
||||
3. 좌표를 KATEC으로 변환한다.
|
||||
4. Opinet `aroundAll.do` 를 `sort=1` 가격순으로 조회한다.
|
||||
5. 상위 후보에 대해 `detailById.do` 를 호출해 도로명주소, 전화번호, 셀프 여부, 세차장, 경정비, 품질인증 여부를 보강한다.
|
||||
6. 보통 3~5개만 짧게 정리한다.
|
||||
|
||||
## Responding
|
||||
|
||||
결과는 보통 아래 필드를 포함해 짧게 정리한다.
|
||||
|
||||
- 주유소명
|
||||
- 가격(휘발유/경유 중 요청한 제품)
|
||||
- 거리
|
||||
- 주소
|
||||
- 셀프 여부
|
||||
- 세차장/경정비/품질인증 여부(있으면)
|
||||
|
||||
## Node.js example
|
||||
|
||||
```js
|
||||
const { searchCheapGasStationsByLocationQuery } = require("cheap-gas-nearby");
|
||||
|
||||
async function main() {
|
||||
const result = await searchCheapGasStationsByLocationQuery("서울역", {
|
||||
productCode: "B027",
|
||||
radius: 1000,
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(result.anchor);
|
||||
console.log(result.items);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
## Done when
|
||||
|
||||
- 유저의 현재 위치를 먼저 확인했다.
|
||||
- 기본 proxy 경유로 Opinet 데이터를 조회했다.
|
||||
- 공식 Opinet nearby 결과를 최소 1개 이상 찾았거나, 못 찾은 이유와 다음 질문을 제시했다.
|
||||
- 가격순 상위 결과를 3~5개 이내로 정리했다.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- 프록시 서버가 내려가 있거나 `OPINET_API_KEY` 가 서버에 설정되지 않은 경우.
|
||||
- Kakao Map anchor가 애매하면 좌표가 잘못 잡힐 수 있어 추가 위치 확인이 필요하다.
|
||||
- Opinet Open API 응답이 일시적으로 비거나 갱신 중일 수 있다.
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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 재배포본."
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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}}
|
||||
|
|
@ -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`의 셀 매핑을 사용한다.
|
||||
- 공식 양식은 표와 칸이 많으므로 자동 치환 후 반드시 사람이 한컴오피스/호환 뷰어로 열어 누락 셀, 줄바꿈, 날인란, 첨부서류 목록을 확인한다.
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,86 +1,61 @@
|
|||
---
|
||||
name: coupang-product-search
|
||||
description: retention-corp/coupang_partners의 로컬 Coupang MCP 호환 레이어로 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 상품 비교, 베스트 상품, 골드박스 특가를 조회한다.
|
||||
description: 공식 쿠팡 쇼핑 URL과 브라우저 캡처 HTML을 이용해 상품 후보, 가격, 상세, 리뷰를 정리한다. 먼저 Open API 제한과 anti-bot 차단 여부를 확인하고, 가능하면 검색→상세→리뷰 순으로 답한다.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: retail
|
||||
locale: ko-KR
|
||||
phase: v2
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# Coupang Product Search
|
||||
|
||||
## 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` 계약을 호출한다.
|
||||
쿠팡에서 사용자의 니즈에 맞는 상품을 찾기 위해 다음을 지원한다.
|
||||
|
||||
- 키워드 상품 검색
|
||||
- 로켓배송 전용 필터 검색
|
||||
- 가격대 범위 검색
|
||||
- 상품 비교표 생성
|
||||
- 카테고리별 베스트 상품
|
||||
- 골드박스 당일 특가
|
||||
- 인기 검색어/계절 상품 추천
|
||||
- 공식 쿠팡 검색 URL 생성
|
||||
- 브라우저 세션에서 캡처한 검색 결과 HTML 파싱
|
||||
- 상품 상세/가격/판매자/배지/필수 표기 정보 파싱
|
||||
- 상품 리뷰 요약/개별 리뷰 파싱
|
||||
- direct fetch / headless browser 차단 여부 probe
|
||||
|
||||
## How it works
|
||||
## Important limitation first
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
2026-03-31 기준으로 확인한 사실:
|
||||
|
||||
Hard rules:
|
||||
- 쿠팡 개발자 Open API는 **판매자/WING 중심** 문서만 확인되었다.
|
||||
- 일반 소비자용 상품 검색·리뷰 조회 Open API는 확인하지 못했다.
|
||||
- 이 저장소 환경에서 desktop direct HTTP 는 `403 Access Denied` 로 차단되었다.
|
||||
- mobile direct HTTP 도 차단되었지만, rerun 마다 `200 challenge-html` 또는 `403 access-denied-html` 처럼 **차단 응답이 달라질 수 있었다.**
|
||||
- headless Playwright-core probe 역시 차단되었고, **blocked shape 도 edge/challenge 상태에 따라 달라질 수 있었다.**
|
||||
|
||||
- `COUPANG_MCP_ENDPOINT`는 호환성 knob로만 유지한다. 기본값은 `local://coupang-mcp`다.
|
||||
- 구형 HF Space hosted MCP 엔드포인트를 사용하거나 새로 지어내지 않는다.
|
||||
- upstream 저장소는 `https://github.com/retention-corp/coupang_partners.git`만 사용한다.
|
||||
- `tools`와 `init`은 로컬 MCP 계약 확인용으로 먼저 실행한다.
|
||||
따라서 이 스킬은 **anti-bot 우회**를 시도하지 않는다. 대신:
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
local://coupang-mcp
|
||||
```
|
||||
|
||||
프로토콜 호환 버전: MCP `2025-03-26`. 네트워크로 붙는 Streamable HTTP 서버가 아니라, upstream 저장소의 로컬 MCP 호환 CLI가 같은 도구 이름과 JSON-RPC 모양의 payload를 반환한다.
|
||||
1. 공식 URL을 만든다.
|
||||
2. 브라우저 세션에서 확보한 HTML 이 있으면 파싱한다.
|
||||
3. HTML 확보가 막히면 probe 결과와 함께 제한을 설명한다.
|
||||
|
||||
## When to use
|
||||
|
||||
- "쿠팡에서 생수 가격 좀 찾아줘"
|
||||
- "로켓배송 에어팟 찾아줘"
|
||||
- "20만원 이하 키보드 추천해줘"
|
||||
- "아이패드 vs 갤럭시탭 비교"
|
||||
- "오늘 쿠팡 특가 뭐 있어?"
|
||||
- "전자제품 베스트 보여줘"
|
||||
- "이 쿠팡 상품 상세/리뷰 요약해줘"
|
||||
- "쿠팡 headless 자동화가 가능한지 먼저 테스트해줘"
|
||||
- "쿠팡 검색 URL부터 만들고 상품 후보 정리해줘"
|
||||
|
||||
## When not to use
|
||||
|
||||
- 로그인, 장바구니, 결제 자동화가 필요한 경우
|
||||
- 쿠팡 계정/session 접근이 필요한 경우
|
||||
- 실시간 재고/품절 여부를 100% 보장해야 하는 경우 (hosted fallback과 Partners API 모두 캐시·지연이 있을 수 있다)
|
||||
- anti-bot 우회나 계정/session 탈취가 필요한 경우
|
||||
- 공식 URL이나 브라우저 HTML 없이 결과를 단정해야 하는 경우
|
||||
|
||||
## Official surfaces checked
|
||||
|
||||
- seller Open API docs: `https://developers.coupangcorp.com/hc/ko/sections/360004260614-상품-API`
|
||||
- desktop search URL: `https://www.coupang.com/np/search?q=<query>`
|
||||
- mobile search URL: `https://m.coupang.com/nm/search?q=<query>`
|
||||
- product URL pattern: `https://www.coupang.com/vp/products/<productId>?itemId=<itemId>&vendorItemId=<vendorItemId>`
|
||||
- review section anchor: `#sdpReview`
|
||||
|
||||
## Workflow
|
||||
|
||||
|
|
@ -88,127 +63,88 @@ local://coupang-mcp
|
|||
|
||||
검색어가 너무 넓으면 먼저 의도를 좁힌다.
|
||||
|
||||
- 권장 질문: `어떤 용도/예산/브랜드/용량을 우선할까요?`
|
||||
- 권장 질문: `어떤 용도/예산/브랜드/용량을 우선할까요? 예: 생수 2L / 무라벨 / 로켓배송`
|
||||
- URL이 이미 있으면 바로 상세/리뷰 단계로 간다.
|
||||
|
||||
### 2. Bootstrap and check the tool contract
|
||||
### 2. Probe the environment
|
||||
|
||||
래퍼는 기본적으로 `~/.cache/k-skill/coupang_partners`에 upstream 저장소를 clone한다. 이미 clone되어 있으면 그대로 사용하고, 최신화가 필요할 때만 `--update`를 붙인다.
|
||||
가능 여부를 먼저 확인한다.
|
||||
|
||||
```bash
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py tools
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py init
|
||||
```js
|
||||
const { probeAutomation } = require("coupang-product-search")
|
||||
|
||||
const probe = await probeAutomation("생수")
|
||||
console.log(probe)
|
||||
```
|
||||
|
||||
기존 checkout을 명시하거나 CI/검증에서 네트워크 clone을 막으려면:
|
||||
- `blocked: true` 면 direct fetch / headless browser 가 막힌 것이다.
|
||||
- `browserFetchHtml` 을 주입하지 않은 clean checkout 에서는 `browser === null` 이고, `browser` 값은 수동/외부 Playwright-core runner 같은 `browserFetchHtml` 주입 경로를 붙였을 때만 채워진다.
|
||||
- 이 경우 **브라우저 세션에서 캡처한 HTML** 또는 사용자가 제공한 쿠팡 상품 URL/HTML 이 필요하다고 설명한다.
|
||||
|
||||
```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
|
||||
### 3. Search products when browser HTML is available
|
||||
|
||||
```js
|
||||
const { searchProducts } = require("coupang-product-search")
|
||||
|
||||
const search = await searchProducts("생수", {
|
||||
fetchHtml: browserCapture
|
||||
})
|
||||
|
||||
console.log(search.items)
|
||||
```
|
||||
|
||||
### 3. Call tools
|
||||
정리 우선순위:
|
||||
|
||||
구체적인 사용자 요청에 맞춰 upstream CLI 명령을 호출한다. 결과는 `ok`, `data.tool`, `data.payload`, `data.result`를 포함하는 JSON으로 반환된다.
|
||||
- 제목 정확도
|
||||
- 가격
|
||||
- 로켓배송/무료배송/와우가 등 배지
|
||||
- 평점/리뷰 수
|
||||
- 판매자명
|
||||
|
||||
```bash
|
||||
# 일반 검색 (키 없이도 hosted fallback으로 작동)
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "32인치 4K 모니터"
|
||||
### 4. Read product detail
|
||||
|
||||
# 로켓배송 필터
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py rocket "에어팟"
|
||||
```js
|
||||
const { getProductDetail } = require("coupang-product-search")
|
||||
|
||||
# 가격대 검색
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py budget "키보드" --max-price 100000
|
||||
const detail = await getProductDetail(search.items[0].productUrl, {
|
||||
fetchHtml: browserCapture
|
||||
})
|
||||
|
||||
# 비교
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py compare "아이패드 vs 갤럭시탭"
|
||||
|
||||
# 골드박스 (운영자 키가 필요한 upstream 경로)
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py goldbox
|
||||
console.log(detail)
|
||||
```
|
||||
|
||||
### 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 "에어팟"
|
||||
```
|
||||
### 5. Read reviews
|
||||
|
||||
## Available tools
|
||||
```js
|
||||
const { getProductReviews } = require("coupang-product-search")
|
||||
|
||||
| 도구명 | 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` |
|
||||
const reviews = await getProductReviews(detail.productUrl, {
|
||||
fetchHtml: browserCapture
|
||||
})
|
||||
|
||||
주의: `get_coupang_goldbox`와 `get_coupang_best_products`는 upstream 기준 Coupang Partners API 권한이 필요한 경로이므로, 키가 없는 환경에서는 실패할 수 있다. 이런 경우 에러 메시지를 그대로 전달하고 hosted fallback이 커버하는 `search`/`rocket`/`budget`/`compare` 경로로 우회 제안한다.
|
||||
|
||||
## 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 (상위 후보)
|
||||
|
||||
1) LG전자 4K UHD 모니터
|
||||
가격: 397,750원 (참고용)
|
||||
보러가기: https://a.retn.kr/s/... # hosted fallback shortlink
|
||||
또는: https://link.coupang.com/a/... # operator HMAC 경로 딥링크
|
||||
|
||||
## normal (상위 후보)
|
||||
|
||||
1) 삼성전자 QHD 오디세이 G5 게이밍 모니터
|
||||
가격: 283,000원 (참고용)
|
||||
보러가기: https://a.retn.kr/s/...
|
||||
console.log(reviews.summary)
|
||||
console.log(reviews.items.slice(0, 3))
|
||||
```
|
||||
|
||||
## 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` 문자열(`"파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"`)이 있으면 그대로 노출하고, 없으면 같은 취지의 고지를 답변 말미에 덧붙인다.
|
||||
- probe 가 막혔으면 **막혔다고 먼저 말한다.**
|
||||
- 브라우저 HTML 없이 live 결과를 단정하지 않는다.
|
||||
- 후보가 여러 개면 상위 3개만 짧게 비교한다.
|
||||
- 리뷰는 전체 평균/개수 + 대표 리뷰 2~3개 정도만 요약한다.
|
||||
|
||||
## Done when
|
||||
|
||||
- `tools`와 `init` 또는 실제 명령으로 retention-corp/coupang_partners 로컬 MCP 계약을 확인했다.
|
||||
- 검색 결과가 로켓배송/일반배송으로 구분되어 정리되었다.
|
||||
- 사용자 니즈에 맞는 추천 TOP 3이 제시되었다.
|
||||
- 가격/배송 정보와 변동 가능성 안내가 포함되었다.
|
||||
- affiliate 고지(disclosure)가 답변에 포함되었다.
|
||||
- 검색어 또는 상품 URL이 확보되었다.
|
||||
- probe 결과를 확인했다.
|
||||
- 가능하면 검색 → 상세 → 리뷰를 순서대로 정리했다.
|
||||
- 차단되면 차단 사실과 필요한 다음 입력(브라우저 HTML / 상품 URL)을 분명히 설명했다.
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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 이 남아있는지 사용자에게 알려서 추가 호출 여지를 명시했다.
|
||||
|
|
@ -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을 포함했다.
|
||||
- 인증/거래성 액션은 수행하지 않았다.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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을 포함했다.
|
||||
- 인증/거래성 액션은 수행하지 않았다.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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을 포함했다.
|
||||
- 인증/거래성 액션은 수행하지 않았다.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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을 포함했다.
|
||||
- 인증/거래성 액션은 수행하지 않았다.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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` 픽업 가능 여부.
|
||||
|
|
|
|||
|
|
@ -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개 이상 반환하거나, 반환 실패 이유를 설명했다.
|
||||
- 쇼핑몰별 상품가, 배송비, 실구매가, 카드 할인가, 무이자 문구를 조회 시점 기준으로 정리했다.
|
||||
- 사용자 응답은 표 형식으로 제공했다.
|
||||
- 로그인/구매/차단 우회 범위를 벗어나지 않았다.
|
||||
|
|
@ -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())
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
# 새 스킬 추가 가이드
|
||||
|
||||
새 스킬을 k-skill에 추가하는 방법과 스킬이 동작하는 구조를 설명한다.
|
||||
|
||||
---
|
||||
|
||||
## 스킬이란
|
||||
|
||||
스킬은 AI 에이전트(Claude Code 등)가 특정 작업을 수행하는 방법을 정의한 문서+코드 묶음이다. 에이전트는 `SKILL.md`를 읽고 거기 적힌 워크플로우를 따라 실행한다.
|
||||
|
||||
스킬에는 네 가지 구현 유형이 있다.
|
||||
|
||||
| 유형 | 설명 | 예시 |
|
||||
|------|------|------|
|
||||
| **SKILL.md 전용** | 문서만으로 동작 (에이전트가 bash/python 직접 실행) | `kakaotalk-mac`, `srt-booking` |
|
||||
| **npm 패키지** | `packages/` 아래 Node.js 라이브러리로 구현 | `k-lotto`, `daiso-product-search` |
|
||||
| **프록시 경유** | `k-skill-proxy`가 upstream API 키를 보관하고 HTTP로 중계 | `seoul-subway-arrival`, `fine-dust-location` |
|
||||
| **Python 스크립트** | `scripts/`의 Python 파일 직접 실행 | `korean-spell-check`, `sillok-search` |
|
||||
|
||||
---
|
||||
|
||||
## 스킬의 구조
|
||||
|
||||
모든 스킬은 **저장소 루트에 디렉토리 하나**를 갖는다.
|
||||
|
||||
```
|
||||
k-skill/
|
||||
├── my-new-skill/ ← 스킬 디렉토리 (이름 = 스킬 이름)
|
||||
│ ├── SKILL.md ← 필수. 에이전트가 읽는 핵심 파일
|
||||
│ └── (지원 파일들) ← 선택. 스크립트, 데이터 등
|
||||
├── packages/ ← npm 패키지 유형일 때만
|
||||
│ └── my-new-skill/
|
||||
│ ├── package.json
|
||||
│ ├── src/
|
||||
│ └── test/
|
||||
└── scripts/ ← Python 스크립트 유형일 때만
|
||||
└── my_new_skill.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md 형식
|
||||
|
||||
`SKILL.md`는 YAML frontmatter + Markdown 본문으로 구성된다.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-new-skill
|
||||
description: 한 문장으로 이 스킬이 무엇을 하는지 설명한다. 에이전트 UI에 표시된다.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: utility
|
||||
locale: ko-KR
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# My New Skill
|
||||
|
||||
## What this skill does
|
||||
|
||||
이 스킬이 무엇을 하는지 한두 문단으로 설명한다.
|
||||
|
||||
## When to use
|
||||
|
||||
- "사용자가 이런 말을 할 때"
|
||||
- "또는 이런 상황일 때"
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ (필요하면)
|
||||
- 패키지 설치 명령
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. 첫 번째 단계
|
||||
|
||||
설명과 실행할 코드를 적는다.
|
||||
|
||||
```bash
|
||||
# 실행할 명령어
|
||||
```
|
||||
|
||||
### 2. 두 번째 단계
|
||||
|
||||
...
|
||||
|
||||
## Done when
|
||||
|
||||
- 이런 조건이 만족되면 완료다
|
||||
|
||||
## Failure modes
|
||||
|
||||
- 예상 가능한 실패 상황
|
||||
|
||||
## Notes
|
||||
|
||||
- 특이사항, 보안 정책 등
|
||||
```
|
||||
|
||||
### frontmatter 필드
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
|------|------|------|
|
||||
| `name` | ✅ | **디렉토리 이름과 정확히 일치**해야 한다 |
|
||||
| `description` | ✅ | 에이전트 UI 표시용 한 줄 설명 |
|
||||
| `license` | ✅ | 항상 `MIT` |
|
||||
| `metadata.category` | ✅ | `utility` / `transit` / `travel` / `messaging` / `legal` / `setup` 등 |
|
||||
| `metadata.locale` | ✅ | `ko-KR` |
|
||||
| `metadata.phase` | ✅ | `v1` (안정) / `v1.5` (기능 추가 중) |
|
||||
|
||||
---
|
||||
|
||||
## 유형별 구현 방법
|
||||
|
||||
### A. SKILL.md 전용 스킬
|
||||
|
||||
에이전트가 `SKILL.md` 안의 bash/python 코드를 직접 실행한다.
|
||||
|
||||
1. 디렉토리 생성: `mkdir my-new-skill`
|
||||
2. `my-new-skill/SKILL.md` 작성
|
||||
3. Workflow 섹션에 에이전트가 따를 단계별 명령어를 적는다
|
||||
|
||||
외부 라이브러리나 서버 없이 동작해야 한다.
|
||||
|
||||
### B. npm 패키지 스킬
|
||||
|
||||
`packages/my-new-skill/`에 Node.js 구현체를 만들고, 루트 디렉토리 `my-new-skill/SKILL.md`에서 `require('my-new-skill')`로 호출한다.
|
||||
|
||||
```
|
||||
packages/my-new-skill/
|
||||
├── package.json # name, version, main, exports 필수
|
||||
├── README.md
|
||||
├── src/
|
||||
│ └── index.js
|
||||
└── test/
|
||||
└── index.test.js
|
||||
```
|
||||
|
||||
`package.json`에 `"name": "my-new-skill"` 설정 후 루트 `package.json`의 `workspaces`에 등록한다.
|
||||
|
||||
npm에 배포하려면 `.changeset/` 파일을 추가한다 (`docs/releasing.md` 참고).
|
||||
|
||||
### C. 프록시 경유 스킬
|
||||
|
||||
upstream API 키를 사용자에게 노출하지 않으려면 `k-skill-proxy`를 경유한다.
|
||||
|
||||
1. `packages/k-skill-proxy/src/server.js`에 새 route 추가
|
||||
2. `SKILL.md` Workflow에 `curl $KSKILL_PROXY_BASE_URL/v1/...` 형태로 호출 작성
|
||||
3. upstream API 키는 서버의 `~/.config/k-skill/secrets.env`에만 보관
|
||||
|
||||
프록시 서버는 main 브랜치에 merge되어야 프로덕션에 반영된다 (`CLAUDE.md` 참고).
|
||||
|
||||
### D. Python 스크립트 스킬
|
||||
|
||||
`scripts/my_skill.py`를 만들고 `SKILL.md`에서 `python3 scripts/my_skill.py`로 호출한다.
|
||||
|
||||
---
|
||||
|
||||
## 크롤링/검색 스킬을 만들 때: 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로 해결할 수 있는지 먼저 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## 스킬 등록 & 검증
|
||||
|
||||
스킬은 **별도 레지스트리 없이 디렉토리 스캔으로 자동 발견**된다.
|
||||
|
||||
추가 후 검증:
|
||||
|
||||
```bash
|
||||
npm run ci
|
||||
```
|
||||
|
||||
이 명령은 `scripts/validate-skills.sh`를 실행해 다음을 확인한다.
|
||||
|
||||
- 루트 하위 모든 디렉토리에 `SKILL.md`가 있는지
|
||||
- frontmatter가 `---`로 시작하는지
|
||||
- `name` 필드가 있는지
|
||||
- `description` 필드가 있는지
|
||||
- `name` 필드 값이 디렉토리 이름과 일치하는지
|
||||
|
||||
---
|
||||
|
||||
## 시크릿이 필요한 스킬
|
||||
|
||||
인증이 필요한 스킬은 아래 우선순위로 credential을 확보한다.
|
||||
|
||||
1. 이미 환경변수에 있으면 → 그대로 사용
|
||||
2. 에이전트 vault(1Password, Bitwarden, macOS Keychain) → 주입
|
||||
3. `~/.config/k-skill/secrets.env` → 파일에서 읽기
|
||||
4. 아무것도 없으면 → 사용자에게 물어보고 3번에 저장
|
||||
|
||||
시크릿 변수 이름 규칙: `KSKILL_<서비스명>_<항목>` (예: `KSKILL_SRT_ID`)
|
||||
|
||||
절대 하지 말 것:
|
||||
- 시크릿을 저장소에 커밋
|
||||
- 프록시 upstream 키를 클라이언트에 노출
|
||||
- 사용자 확인 없이 side-effect가 있는 작업 실행
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
새 스킬을 PR 올리기 전에 확인한다.
|
||||
|
||||
- [ ] `my-new-skill/SKILL.md` 작성 완료
|
||||
- [ ] frontmatter `name`이 디렉토리 이름과 일치
|
||||
- [ ] `npm run ci` 통과 (`./scripts/validate-skills.sh` 포함)
|
||||
- [ ] 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` 작성 (선택, 상세 가이드)
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [공통 설정 가이드](setup.md) — 시크릿 설정 방법
|
||||
- [릴리스와 자동 배포](releasing.md) — npm 패키지 배포 흐름
|
||||
- [보안/시크릿 정책](security-and-secrets.md) — 인증 정보 취급 원칙
|
||||
|
|
@ -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가 같은 일을 하기 때문입니다.
|
||||
|
|
@ -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)의 "사업자 실사" 항목 참조.
|
||||
|
|
@ -2,14 +2,11 @@
|
|||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 유저가 알려준 현재 위치를 공식 Blue Ribbon zone 으로 매칭
|
||||
- 유저가 알려준 현재 위치 근처의 블루리본 맛집 검색
|
||||
- 공식 Blue Ribbon zone 목록으로 동네/역명 매칭
|
||||
- k-skill-proxy 경유 nearby 블루리본 맛집 검색
|
||||
- 좌표 기반 nearby 검색
|
||||
- 거리순 상위 결과 정리
|
||||
|
||||
> [!NOTE]
|
||||
> Blue Ribbon의 `/restaurants/map` 은 프리미엄 전용입니다. 이 기능은 k-skill-proxy 에 설정된 프리미엄 세션(`BLUE_RIBBON_SESSION_ID`)을 경유해 nearby 검색을 수행합니다.
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- 인터넷 연결
|
||||
|
|
@ -61,7 +58,7 @@
|
|||
2. 동네/역명/랜드마크를 받으면 공식 `search/zone` 목록에서 가장 가까운 zone 후보를 찾습니다.
|
||||
- 공식 zone 이름이 아닌 대표 랜드마크는 먼저 nearest zone alias 로 확장합니다. 예: `코엑스` → `삼성동/대치동`
|
||||
3. 좌표를 받으면 nearby bounding box 를 계산합니다.
|
||||
4. k-skill-proxy 의 `/v1/blue-ribbon/nearby` 에 좌표와 거리를 넘겨 nearby 검색을 수행합니다. 프록시가 프리미엄 세션으로 `/restaurants/map` 을 호출합니다.
|
||||
4. 공식 `/restaurants/map` endpoint 를 `isAround=true`, `ribbon=true`, `ribbonType=RIBBON_THREE,RIBBON_TWO,RIBBON_ONE`, `sort=distance` 로 조회합니다.
|
||||
5. 거리순 상위 결과를 3~5개 정리합니다.
|
||||
|
||||
## Node.js 예시
|
||||
|
|
@ -85,9 +82,7 @@ main().catch((error) => {
|
|||
});
|
||||
```
|
||||
|
||||
기본적으로 k-skill-proxy 를 경유합니다. 직접 호출이 필요하면 `useDirectApi: true` 옵션을 사용하세요 (프리미엄 세션 없이는 `premium_required` 에러).
|
||||
|
||||
## Live smoke 예시
|
||||
## 검증된 live smoke 예시
|
||||
|
||||
아래 값은 **2026-03-27** 에 `광화문`, `distanceMeters=1000`, `limit=5` 로 실제 호출해 확인한 결과 일부입니다.
|
||||
|
||||
|
|
@ -128,6 +123,6 @@ main().catch((error) => {
|
|||
|
||||
## 주의할 점
|
||||
|
||||
- Blue Ribbon `/restaurants/map` 은 프리미엄 전용입니다. k-skill-proxy 에 `BLUE_RIBBON_SESSION_ID` 가 설정되어 있어야 합니다 (30일마다 갱신).
|
||||
- Blue Ribbon 사이트는 browser-like 요청 헤더가 없으면 403 이 나올 수 있습니다.
|
||||
- 검색 페이지의 zone 목록이 바뀌면 매칭 결과도 바뀔 수 있습니다.
|
||||
- 좌표 없이 너무 넓은 지역명만 받으면 상권 후보가 많아질 수 있습니다.
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
# 번개장터 검색 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
upstream [`bunjang-cli`](https://www.npmjs.com/package/bunjang-cli) / [`pinion05/bunjangcli`](https://github.com/pinion05/bunjangcli) 를 사용해 번개장터 **검색, 상세조회, 선택적 찜/채팅, 대량 수집, AI TOON export** 를 처리한다.
|
||||
|
||||
- `search` 로 검색
|
||||
- `item get` / `item list` 로 상세조회
|
||||
- `favorite add` / `favorite remove` / `favorite list` 로 선택적 찜 관리
|
||||
- `chat list` / `chat start` / `chat send` 로 선택적 채팅
|
||||
- `--start-page`, `--pages`, `--max-items`, `--with-detail`, `--output` 으로 대량 수집
|
||||
- `--ai --output <directory>` 로 TOON chunk 저장
|
||||
|
||||
## 가장 중요한 규칙
|
||||
|
||||
이 기능은 upstream 원본을 그대로 쓴다.
|
||||
`k-skill` 안에 번개장터 수집기를 새로 넣지 않고, **CLI first** 문서/스킬만 유지한다.
|
||||
즉 기본 경로는 아래다.
|
||||
|
||||
1. `npx --yes bunjang-cli ...`
|
||||
2. 반복 사용이면 `npm install -g bunjang-cli`
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- 인터넷 연결
|
||||
- `node` 22+
|
||||
- `npx` 또는 `npm`
|
||||
- 선택적으로 interactive TTY 터미널
|
||||
|
||||
2026-04-06 기준 `npm view bunjang-cli version` 은 `0.2.1` 이고, README 요구 사항은 Node.js 22+ 다.
|
||||
|
||||
## 가장 빠른 시작: npx CLI
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli --help
|
||||
npx --yes bunjang-cli --json auth status
|
||||
npx --yes bunjang-cli --json search "아이폰" --max-items 3 --sort date
|
||||
npx --yes bunjang-cli --json item get 354957625
|
||||
```
|
||||
|
||||
반복 사용이면 전역 설치도 가능하다.
|
||||
|
||||
```bash
|
||||
npm install -g bunjang-cli
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
bunjang-cli --help
|
||||
```
|
||||
|
||||
## 로그인은 선택적 interactive 플로우
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli auth login
|
||||
npx --yes bunjang-cli auth logout
|
||||
npx --yes bunjang-cli --json auth status
|
||||
```
|
||||
|
||||
- `auth login` 은 브라우저에서 로그인 후 **터미널로 돌아와 Enter 를 눌러야** 완료된다.
|
||||
- 즉 **TTY / interactive 세션** 이 아닌 환경에서는 로그인 완료 처리가 멈출 수 있다.
|
||||
- 그래서 기본 문서는 검색/상세조회/대량 수집을 먼저 안내하고, 찜/채팅은 로그인된 경우에만 선택적으로 진행한다.
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
### 1. 검색
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰"
|
||||
npx --yes bunjang-cli search "아이폰" --price-min 500000 --price-max 1200000
|
||||
npx --yes bunjang-cli search "아이폰" --sort date
|
||||
npx --yes bunjang-cli --json search "아이폰" --max-items 5
|
||||
```
|
||||
|
||||
검색 결과에는 매입글/광고/악세서리 글이 섞이고, live `search` payload 에서는 `location` 이 noisy 하거나 `description` / `status` 가 빠질 수 있다. 그래서 **검색 단계는 제목/가격 중심 1차 triage** 로만 쓰고, 세부 판단은 상세조회 이후로 미룬다.
|
||||
|
||||
### 2. 상세조회
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli item get 354957625
|
||||
npx --yes bunjang-cli --json item get 354957625
|
||||
npx --yes bunjang-cli --json item list --ids 354957625,354801707
|
||||
```
|
||||
|
||||
상세에서는 `price`, `description`, `location`, `category`, `status`, `sellerName`, `sellerReviewCount`, `favoriteCount`, `transportUsed` 를 우선 본다. 즉 `description` / `status` / 믿을 만한 `location` 이 필요하면 **반드시 `item get` 또는 `--with-detail` 이후** 에만 판정한다.
|
||||
|
||||
### 3. 대량 수집
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰" \
|
||||
--start-page 1 \
|
||||
--pages 5 \
|
||||
--max-items 50 \
|
||||
--sort date \
|
||||
--with-detail \
|
||||
--output artifacts/bunjang-iphone.json
|
||||
```
|
||||
|
||||
export 검증 시에는 결과 파일 존재 여부와 top-level `items[]` 안의 `summary` / `detail` / optional `error` 구조, item 별 `sourcePage` 또는 `summary.raw.page` 를 함께 확인한다.
|
||||
|
||||
### 4. AI TOON chunk
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli search "아이폰" \
|
||||
--start-page 1 \
|
||||
--pages 5 \
|
||||
--max-items 50 \
|
||||
--with-detail \
|
||||
--ai \
|
||||
--output artifacts/bunjang-iphone-ai
|
||||
```
|
||||
|
||||
- `--ai` 에서는 `--output` 이 **디렉토리** 여야 한다.
|
||||
- 결과는 `items-1.toon` 같은 chunk 로 나뉜다.
|
||||
- 대량 후보를 여러 에이전트에게 분산 평가시키기 좋다.
|
||||
|
||||
### 5. 찜/채팅은 로그인 후에만
|
||||
|
||||
```bash
|
||||
npx --yes bunjang-cli --json favorite list
|
||||
npx --yes bunjang-cli --json favorite add 354957625
|
||||
npx --yes bunjang-cli --json favorite remove 354957625
|
||||
npx --yes bunjang-cli --json chat list
|
||||
npx --yes bunjang-cli --json chat start 354957625 --message "안녕하세요"
|
||||
npx --yes bunjang-cli --json chat send 84191651 --message "상품 상태 괜찮을까요?"
|
||||
```
|
||||
|
||||
- `favorite` 와 `chat` 은 **로그인이 필요한 선택적 기능**이다.
|
||||
- 기본 검색/분석 요청이라면 실행하지 않고 명령만 안내한다.
|
||||
- 검증이 필요하면 `favorite list` 로 세션을 먼저 확인한 뒤 add/remove 를 왕복 실행한다.
|
||||
|
||||
## 라이브 확인 메모
|
||||
|
||||
2026-04-06 기준 아래 흐름을 실제로 실행해 응답을 확인했다.
|
||||
|
||||
- `npx --yes bunjang-cli --help` → top-level commands (`auth`, `search`, `item`, `chat`, `favorite`, `purchase`) 확인
|
||||
- `npx --yes bunjang-cli --json auth status` → `authenticated: false`, `headfulLoginRequired: true` 확인
|
||||
- `npx --yes bunjang-cli --json search "아이폰" --max-items 1` → `items[0].id = 354957625`, `transportUsed = browser` 확인
|
||||
- `npx --yes bunjang-cli --json item get 354957625` → `description`, `location`, `sellerReviewCount`, `favoriteCount`, `transportUsed = api` 확인
|
||||
|
||||
같은 날짜에 `search` summary 는 title/price 중심 triage 용으로만 안전했고, `status` / `description` 은 summary 에서 안정적으로 오지 않았다. 그래서 문서에서는 상세 필드 의존 판단을 `item get` / `--with-detail` 뒤로 고정한다.
|
||||
|
||||
같은 날짜에 `favorite list` 는 로그인 브라우저를 띄운 뒤 터미널 Enter 를 기다렸다. 그래서 문서에서는 찜/채팅을 **로그인 후 선택적으로만 실행** 하도록 고정한다.
|
||||
|
||||
## 제한사항
|
||||
|
||||
- 로그인 없는 환경에서는 찜/채팅/구매 흐름을 바로 검증하기 어렵다.
|
||||
- 검색 결과에는 매입글/광고/악세서리 노이즈가 섞일 수 있다.
|
||||
- DOM/API 변경에 따라 browser transport 동작이 깨질 수 있다.
|
||||
- 구매 자동 확정/결제는 다루지 않는다.
|
||||
|
||||
## 참고 링크
|
||||
|
||||
- npm package: `https://www.npmjs.com/package/bunjang-cli`
|
||||
- upstream repo: `https://github.com/pinion05/bunjangcli`
|
||||
|
|
@ -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 문서에 추가하지 않는다.
|
||||
- 사용자가 이미 로그인해 둔 브라우저 세션만 재사용한다.
|
||||
- 결제나 취소 수수료가 얽힌 단계에서는 사용자 확인을 우선한다.
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
# 근처 가장 싼 주유소 찾기 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 현재 위치 기준 근처 최저가 주유소 검색
|
||||
- 휘발유/경유 기준 nearby 가격 비교
|
||||
- Opinet 공식 API(`aroundAll.do`, `detailById.do`) 기반 요약
|
||||
- 셀프 여부, 세차장, 경정비, 품질인증 여부까지 함께 정리
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- 인터넷 연결
|
||||
- `node` 18+
|
||||
- `cheap-gas-nearby` package 또는 이 저장소 전체 설치
|
||||
|
||||
사용자 쪽에서 별도 `OPINET_API_KEY` 를 준비할 필요가 없다. 기본적으로 `https://k-skill-proxy.nomadamas.org` 프록시를 경유하며, upstream key는 proxy 서버에서만 주입한다.
|
||||
|
||||
## 가장 먼저 할 일
|
||||
|
||||
이 기능은 **반드시 현재 위치를 먼저 물어본 뒤** 실행합니다.
|
||||
|
||||
권장 질문 예시:
|
||||
|
||||
```text
|
||||
현재 위치를 알려주세요. 동네/역명/랜드마크/위도·경도 중 편한 형식으로 보내주시면 근처에서 가장 싼 주유소를 찾아볼게요.
|
||||
```
|
||||
|
||||
제품을 안 알려주면 보통 **휘발유(B027)** 기준으로 시작하고, 경유가 필요하면 `D047` 로 바꿉니다.
|
||||
|
||||
## 입력값
|
||||
|
||||
- 동네/상권: `강남`, `성수동`, `판교`
|
||||
- 역명/랜드마크: `서울역`, `강남역`, `코엑스`
|
||||
- 좌표: `37.55472, 126.97068`
|
||||
- 제품코드: `B027`(휘발유), `D047`(경유)
|
||||
|
||||
위치 문자열은 Kakao Map anchor 검색으로 **WGS84 좌표**를 잡고, 내부적으로 **KATEC** 으로 변환해 Opinet nearby 검색에 사용합니다.
|
||||
|
||||
## 공식 표면
|
||||
|
||||
- Opinet 오픈 API 안내: `https://www.opinet.co.kr/user/custapi/openApiInfo.do`
|
||||
- 반경 내 주유소: `https://www.opinet.co.kr/api/aroundAll.do`
|
||||
- 주유소 상세정보(ID): `https://www.opinet.co.kr/api/detailById.do`
|
||||
- 지역코드: `https://www.opinet.co.kr/api/areaCode.do`
|
||||
- Kakao Map 모바일 검색: `https://m.map.kakao.com/actions/searchView`
|
||||
- Kakao Map 장소 패널 JSON: `https://place-api.map.kakao.com/places/panel3/<confirmId>`
|
||||
|
||||
Opinet nearby 검색의 핵심 파라미터:
|
||||
|
||||
- `x`, `y`: 기준 위치 **KATEC** 좌표
|
||||
- `radius`: 반경(m, 최대 5000)
|
||||
- `prodcd`: `B027`, `D047`, `B034`, `C004`, `K015`
|
||||
- `sort=1`: 가격순
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
1. 유저에게 현재 위치를 먼저 묻습니다.
|
||||
2. 위치 문자열을 받으면 Kakao Map으로 anchor 후보를 고르고 좌표를 확보합니다.
|
||||
3. 좌표를 **WGS84 → KATEC** 으로 변환합니다.
|
||||
4. Opinet `aroundAll.do` 를 `sort=1` 가격순으로 조회합니다.
|
||||
5. 상위 후보는 `detailById.do` 로 재조회해 주소/전화번호/편의시설을 보강합니다.
|
||||
6. 가격순 상위 3~5개만 짧게 응답합니다.
|
||||
|
||||
## Node.js 예시
|
||||
|
||||
```js
|
||||
const { searchCheapGasStationsByLocationQuery } = require("cheap-gas-nearby");
|
||||
|
||||
async function main() {
|
||||
const result = await searchCheapGasStationsByLocationQuery("서울역", {
|
||||
productCode: "B027",
|
||||
radius: 1000,
|
||||
limit: 3
|
||||
});
|
||||
|
||||
for (const item of result.items) {
|
||||
console.log(`${item.name}: ${item.price}원/L, ${item.distanceMeters}m, ${item.roadAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
## Offline smoke example
|
||||
|
||||
실제 키가 없는 환경에서도 패키지 동작은 fixture 기반으로 검증할 수 있습니다.
|
||||
|
||||
```bash
|
||||
node --test packages/cheap-gas-nearby/test/index.test.js
|
||||
```
|
||||
|
||||
## 운영 팁
|
||||
|
||||
- 프록시 서버가 내려가 있거나 upstream key가 없으면 503 을 반환하므로 상태를 안내합니다.
|
||||
- 서울역/강남처럼 넓은 질의는 anchor 위치가 흔들릴 수 있으니 필요하면 더 구체적인 역 출구/동 이름을 한 번 더 받습니다.
|
||||
- 동일 가격이면 거리순으로 다시 정렬해 보여주는 편이 좋습니다.
|
||||
- 결과가 너무 많으면 반경을 `1000m` 또는 `2000m` 정도로 유지하는 편이 읽기 쉽습니다.
|
||||
|
||||
## 주의할 점
|
||||
|
||||
- Opinet Open API 인증키는 proxy 서버에서만 관리합니다. 사용자/client 쪽 secrets 파일에는 넣지 않습니다.
|
||||
- Kakao Map anchor 검색은 위치 기준점만 잡기 위한 보조 단계이고, 최종 가격/순위 데이터는 Opinet을 기준으로 합니다.
|
||||
- Opinet 응답의 좌표는 KATEC 이므로 WGS84와 혼동하면 안 됩니다.
|
||||
|
|
@ -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조 등 감면/중과 제외 가능성을 체크하되 확정하지 않는다.
|
||||
|
||||
## 면책
|
||||
|
||||
이 기능은 참고용이며 법률·세무 자문, 법무사 대행이 아니다. 에이전트는 인터넷등기소/위택스 로그인, 전자서명, 세금 납부, 등기 제출, 사용자 사칭, 최종 법률 판단, 최종 세무 판단을 지원하지 않는다. 실제 제출은 사용자가 직접 수행하고 제출 전 관할 등기소·위택스/지자체·법무사·변호사·세무사 확인을 권한다.
|
||||
|
|
@ -1,166 +1,108 @@
|
|||
# 쿠팡 상품 검색 가이드
|
||||
# 쿠팡 상품 가격/리뷰 조회 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
[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` 계약으로 호출한다.
|
||||
- 쿠팡 공식 검색 URL 만들기
|
||||
- 브라우저에서 캡처한 쿠팡 검색 결과 HTML 파싱
|
||||
- 상품 상세/가격/판매자/배송 배지/필수 표기 정보 파싱
|
||||
- 상품 리뷰 요약과 개별 리뷰 파싱
|
||||
- direct fetch / headless browser 가 차단되는지 probe
|
||||
|
||||
- 키워드 상품 검색
|
||||
- 로켓배송 전용 필터 검색
|
||||
- 가격대 범위 검색
|
||||
- 상품 비교표 생성
|
||||
- 카테고리별 베스트 상품, 골드박스 당일 특가
|
||||
- 인기 검색어/계절 상품 추천
|
||||
## 먼저 알아둘 점
|
||||
|
||||
## 동작 방식
|
||||
2026-03-31 기준 확인 내용:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
- 쿠팡 개발자 Open API 문서는 **판매자/WING 중심**이다.
|
||||
- 일반 소비자용 상품 검색·상품평 조회 Open API는 확인하지 못했다.
|
||||
- 이 저장소 환경에서 `www.coupang.com` direct fetch 는 `403 Access Denied` 였다.
|
||||
- `m.coupang.com` direct fetch 도 차단되었지만, rerun 마다 `200 challenge-html` 또는 `403 access-denied-html` 처럼 **차단 응답 status/reason 이 달라질 수 있었다.**
|
||||
- headless Playwright-core probe 도 막혔고, 여기서도 **exact blocked shape 는 edge/challenge 상태에 따라 달라질 수 있었다.**
|
||||
|
||||
- **구형 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 형태를 먼저 확인한다.
|
||||
즉, 이 기능은 **anti-bot 우회**가 아니라 다음 흐름을 기준으로 동작한다.
|
||||
|
||||
## MCP 계약
|
||||
1. 공식 쿠팡 URL을 만든다.
|
||||
2. 가능하면 브라우저 세션에서 확보한 HTML 을 파싱한다.
|
||||
3. 막히면 probe 결과를 그대로 보여주고, 브라우저 HTML 또는 상품 URL을 추가 입력으로 받는다.
|
||||
|
||||
```
|
||||
local://coupang-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한 값이 최종 결정을 가져간다.
|
||||
|
||||
## 사용 가능한 도구
|
||||
|
||||
| 도구명 | 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`로 우회 제안한다.
|
||||
- seller Open API docs: `https://developers.coupangcorp.com/hc/ko/sections/360004260614-상품-API`
|
||||
- desktop search: `https://www.coupang.com/np/search?q=<query>`
|
||||
- mobile search: `https://m.coupang.com/nm/search?q=<query>`
|
||||
- product detail: `https://www.coupang.com/vp/products/<productId>?itemId=<itemId>&vendorItemId=<vendorItemId>`
|
||||
- review anchor: `#sdpReview`
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
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 고지를 제공한다.
|
||||
1. 검색어 또는 상품 URL을 받는다.
|
||||
2. `probeAutomation()` 으로 이 환경에서 direct/headless 접근이 가능한지 본다.
|
||||
3. 브라우저 HTML 이 있으면 `searchProducts()` 로 후보를 정리한다.
|
||||
4. 같은 방식으로 `getProductDetail()` 과 `getProductReviews()` 를 호출한다.
|
||||
5. 차단되면 `현재 환경에서는 쿠팡 anti-bot 에 막혀 브라우저 HTML 이 필요하다`고 답한다.
|
||||
|
||||
## 호출 예시
|
||||
## Node.js 예시
|
||||
|
||||
```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
|
||||
```js
|
||||
const {
|
||||
getProductDetail,
|
||||
getProductReviews,
|
||||
probeAutomation,
|
||||
searchProducts
|
||||
} = require("coupang-product-search")
|
||||
|
||||
# 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
|
||||
async function browserCapture(url) {
|
||||
// 호출 환경에서 구현
|
||||
throw new Error(`Implement browser capture for ${url}`)
|
||||
}
|
||||
|
||||
# 3. 기존 checkout을 fast-forward로 최신화한 뒤 계약 확인
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py \
|
||||
--repo-dir ~/.cache/k-skill/coupang_partners \
|
||||
--update \
|
||||
tools
|
||||
async function main() {
|
||||
const probe = await probeAutomation("생수")
|
||||
console.log(probe)
|
||||
// browser 결과는 browserFetchHtml 을 주입하지 않으면 null 이다.
|
||||
|
||||
# 4. 상품 검색 (키 없이도 hosted fallback으로 동작)
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "생수"
|
||||
const search = await searchProducts("생수", { fetchHtml: browserCapture })
|
||||
console.log(search.items.slice(0, 3))
|
||||
|
||||
# 5. 로켓배송 필터
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py rocket "에어팟"
|
||||
const detail = await getProductDetail(search.items[0].productUrl, { fetchHtml: browserCapture })
|
||||
console.log(detail)
|
||||
|
||||
# 6. hosted fallback 강제 (upstream 기본 allowlist client-id 유지)
|
||||
OPENCLAW_SHOPPING_FORCE_HOSTED=1 \
|
||||
python3 coupang-product-search/scripts/coupang_partners_mcp.py search "무선청소기"
|
||||
const reviews = await getProductReviews(detail.productUrl, { fetchHtml: browserCapture })
|
||||
console.log(reviews.summary)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
```
|
||||
|
||||
## 결과 형식
|
||||
## Live probe 메모
|
||||
|
||||
upstream CLI는 다음과 같은 JSON envelope를 반환한다.
|
||||
아래 값은 **2026-03-31** 기준 `query=생수` 로 확인한 blocked outcome 범위다.
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"session_id": "session-...",
|
||||
"tool": "search_coupang_products",
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": "[...]"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"result": []
|
||||
"directDesktop": {
|
||||
"blocked": true,
|
||||
"observed": ["403/access-denied-html"]
|
||||
},
|
||||
"directMobile": {
|
||||
"blocked": true,
|
||||
"observed": ["200/challenge-html", "403/access-denied-html"]
|
||||
},
|
||||
"browser": {
|
||||
"blocked": true,
|
||||
"observed": ["access-denied-html"],
|
||||
"notes": "browserFetchHtml 을 주입한 수동/외부 Playwright-core 검증 기준"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- hosted fallback 경로는 각 상품의 `productUrl`에 `https://a.retn.kr/s/...` 형태의 short deeplink를 붙여 돌려준다.
|
||||
- operator (로컬 HMAC) 경로는 `https://link.coupang.com/...?lptag=AF...` 형태의 직접 딥링크를 돌려준다.
|
||||
- 두 경로 모두 Retention Corp의 쿠팡 파트너스 채널로 트래킹된다. affiliate 고지를 반드시 포함한다.
|
||||
즉, **차단 자체는 확인되지만 exact status/reason 은 달라질 수 있고, clean checkout 의 `probeAutomation(\"생수\")` 는 browser 값을 자동으로 채우지 않는다.** 공식 URL 구조 자체는 안정적으로 만들 수 있었지만, live HTML 수집은 환경 의존적이다.
|
||||
|
||||
사용자 답변은 짧은 비교표 형태로 정리한다.
|
||||
## 운영 팁
|
||||
|
||||
```
|
||||
## rocket (상위 후보)
|
||||
|
||||
1) LG전자 4K UHD 모니터
|
||||
가격: 397,750원 (참고용)
|
||||
보러가기: https://a.retn.kr/s/...
|
||||
|
||||
## normal (상위 후보)
|
||||
|
||||
1) 삼성전자 QHD 오디세이 G5 게이밍 모니터
|
||||
가격: 283,000원 (참고용)
|
||||
보러가기: https://a.retn.kr/s/...
|
||||
```
|
||||
|
||||
## Affiliate 고지 (필수)
|
||||
|
||||
hosted fallback이 반환하는 응답에는 `disclosure` 필드가 포함될 수 있다. 예시 문구: `"파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"`. 이 문구가 오면 **답변에 그대로 노출**한다. 응답에 disclosure가 없더라도 `a.retn.kr/s/` shortlink나 `lptag=AF` 딥링크가 포함되는 이상 같은 취지의 문구를 반드시 포함한다.
|
||||
|
||||
## 제한사항
|
||||
|
||||
- 가격/품절/배송 정보는 실시간으로 바뀔 수 있다.
|
||||
- 로그인, 장바구니, 결제 자동화는 지원하지 않는다.
|
||||
- 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을 지정해야 한다.
|
||||
|
||||
## 출처
|
||||
|
||||
- [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`
|
||||
- 검색어가 넓으면 용도/예산/브랜드/용량을 먼저 물어본다.
|
||||
- 리뷰는 평균 평점, 총 리뷰 수, 대표 리뷰 2~3개만 먼저 요약한다.
|
||||
- 상품 URL이 이미 있으면 검색 단계를 건너뛰고 상세/리뷰 파싱으로 바로 들어간다.
|
||||
- headless probe 가 막히면 우회 시도를 늘리지 말고 브라우저 캡처 HTML 필요 사실을 분명히 말한다.
|
||||
|
|
|
|||
|
|
@ -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 이슈로 분리되어 추적됩니다.
|
||||
|
|
@ -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을 함께 제시합니다.
|
||||
|
|
@ -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가 바뀌면 실패할 수 있습니다.
|
||||
- 마감·삭제·비공개 전환된 공고는 상세 조회가 실패할 수 있습니다.
|
||||
- 지원, 채팅, 문의, 개인정보 제출 자동화는 범위 밖입니다.
|
||||
|
|
@ -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 구조 변경에 영향을 받습니다.
|
||||
- 문의, 방문 예약, 계약, 결제, 채팅은 실행하지 않습니다.
|
||||
- 공고 내용은 실시간 상태와 달라질 수 있어 최종 판단 전 원문 확인이 필요합니다.
|
||||
|
|
@ -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, 로그인벽, 봇 차단이 나오면 실패 모드로 보고하고 우회하지 않습니다.
|
||||
- 상대방에게 영향을 주는 채팅, 찜, 거래 제안, 구매 자동화는 범위 밖입니다.
|
||||
|
|
@ -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`개까지 원문을 읽어 매칭하므로 너무 낮게 잡으면 결과가 누락될 수 있다.
|
||||
|
|
@ -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` 으로 응답했습니다. 즉, **공식 경로가 실제로 동작함은 확인했지만 당시 해당 매장 재고는 없었습니다.**
|
||||
|
|
|
|||
|
|
@ -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위로 올라올 수 있고, 라벨이 없으면 카드 결제 사용자에게 적용 불가능한 가격을 일반 최저가로 안내하게 됩니다.
|
||||
|
|
@ -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 확인 보조 링크와 후보 공식 홈페이지를 함께 제시하며, 후보별 등록 검증이 이미 완료됐다고 표현하지 않는다.
|
||||
|
|
@ -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>`
|
||||
|
|
@ -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를 유지하는 흐름이 안정적이다.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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으로 보정하지 않는다.
|
||||
|
|
@ -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`
|
||||
|
|
@ -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`
|
||||
|
|
@ -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 페이지는 실패 모드로 처리합니다.
|
||||
- 의료 판단이나 병원 선택 보증을 대신하지 않습니다.
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# 긱뉴스 조회 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- GeekNews 공개 RSS/Atom 피드에서 최신 글 목록 조회
|
||||
- 제목/요약/작성자 기준 키워드 검색
|
||||
- 특정 항목의 RSS 기반 요약/링크/작성자/게시 시각 확인
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- [공통 설정 가이드](../setup.md) 완료
|
||||
- `python3` 사용 가능 환경
|
||||
- 인터넷 연결
|
||||
|
||||
## v1 범위
|
||||
|
||||
이 기능은 **RSS-first / 읽기 전용** 범위로 제공된다.
|
||||
|
||||
- 공개 피드(`https://feeds.feedburner.com/geeknews-feed`)만 사용한다.
|
||||
- 최신 글/검색/상세 조회까지만 다룬다.
|
||||
- 댓글, 투표, 로그인, 개인화 상태는 다루지 않는다.
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
1. 최신 글을 훑을 때는 목록 조회부터 실행한다.
|
||||
2. 원하는 주제가 있으면 제목/요약/작성자 기준 검색으로 좁힌다.
|
||||
3. 특정 글을 확인할 때는 링크/id/토픽 번호 일부로 상세 조회한다.
|
||||
|
||||
## 예시
|
||||
|
||||
최신 글 목록:
|
||||
|
||||
```bash
|
||||
python3 scripts/geeknews_search.py list --limit 5
|
||||
```
|
||||
|
||||
검색:
|
||||
|
||||
```bash
|
||||
python3 scripts/geeknews_search.py search --query Claude --limit 5
|
||||
```
|
||||
|
||||
상세:
|
||||
|
||||
```bash
|
||||
python3 scripts/geeknews_search.py detail --id 28439
|
||||
```
|
||||
|
||||
오프라인 fixture 또는 저장된 feed로 검증할 때:
|
||||
|
||||
```bash
|
||||
python3 scripts/geeknews_search.py list \
|
||||
--feed-file scripts/fixtures/geeknews-feed.xml \
|
||||
--limit 3
|
||||
```
|
||||
|
||||
## 출력에서 확인할 점
|
||||
|
||||
- `source.feed_url` 이 GeekNews RSS feed를 가리키는지
|
||||
- `items[].title`, `items[].link`, `items[].author_name`, `items[].summary` 가 함께 내려오는지
|
||||
- 상세 조회에서 `item.content_html` 과 `item.summary` 가 모두 포함되는지
|
||||
- 검색 결과가 제목/요약/작성자 기준으로 보수적으로 매칭되는지
|
||||
|
||||
## 주의할 점
|
||||
|
||||
- RSS 피드 기반이라 원문 전체/댓글/메타데이터는 제한적일 수 있다.
|
||||
- FeedBurner 응답이 느리거나 실패하면 재시도하거나 직접 링크를 여는 fallback이 필요하다.
|
||||
- 상세 조회는 feed에 포함된 `content` 범위까지만 보장한다.
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# 한강 수위 정보 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 한강홍수통제소(HRFCO) 관측소명/관측소코드 기준 현재 수위 확인
|
||||
- 현재 유량(`FW`) 같이 확인
|
||||
- 관심/주의/경보/심각 기준수위 같이 확인
|
||||
- 별도 사용자 `ServiceKey` 없이 `k-skill-proxy` 로 조회
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- [공통 설정 가이드](../setup.md) 확인
|
||||
|
||||
## 기본 경로
|
||||
|
||||
기본적으로 `https://k-skill-proxy.nomadamas.org/v1/han-river/water-level` 로 요청한다.
|
||||
|
||||
사용자는 별도 HRFCO `ServiceKey` 를 준비하지 않는다. upstream key는 proxy 서버에서만 `HRFCO_OPEN_API_KEY` 로 관리한다.
|
||||
|
||||
`KSKILL_PROXY_BASE_URL` 환경변수가 있으면 그 값을 사용하고, 없으면 기본 hosted path를 사용한다.
|
||||
|
||||
## 입력값
|
||||
|
||||
- 기본: 관측소명/교량명 (`stationName`)
|
||||
- 대체: 관측소코드 (`stationCode`)
|
||||
|
||||
예: `한강대교`, `잠수교`, `1018683`
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
1. client/skill 은 기본 hosted path 또는 `KSKILL_PROXY_BASE_URL` 아래 `/v1/han-river/water-level` endpoint 를 호출한다.
|
||||
2. proxy 는 HRFCO `waterlevel/info.json` 을 읽어 관측소명 → `WLOBSCD` 를 해석한다.
|
||||
3. 해석된 `WLOBSCD` 로 `waterlevel/list/10M/{WLOBSCD}.json` 최신 10분 자료를 조회한다.
|
||||
4. 관측시각, 수위(`WL`), 유량(`FW`), 기준수위 메타데이터를 요약해서 반환한다.
|
||||
5. 관측소명이 여러 개에 걸리면 `ambiguous_station` + `candidate_stations` 를 반환한다.
|
||||
|
||||
## 예시
|
||||
|
||||
proxy URL 이 준비된 뒤 조회:
|
||||
|
||||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
||||
--data-urlencode 'stationName=한강대교'
|
||||
```
|
||||
|
||||
관측소코드 직접 조회:
|
||||
|
||||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
||||
--data-urlencode 'stationCode=1018683'
|
||||
```
|
||||
|
||||
애매한 관측소명 예시:
|
||||
|
||||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
||||
--data-urlencode 'stationName=한강'
|
||||
```
|
||||
|
||||
예상 응답 예시:
|
||||
|
||||
```json
|
||||
{
|
||||
"station_code": "1018683",
|
||||
"station_name": "한강대교",
|
||||
"agency_name": "한강홍수통제소",
|
||||
"address": "서울특별시 용산구 한강대교",
|
||||
"observed_at": "2026-04-05T19:00:00+09:00",
|
||||
"water_level": {
|
||||
"value_m": 0.66,
|
||||
"unit": "m"
|
||||
},
|
||||
"flow_rate": {
|
||||
"value_cms": 208.58,
|
||||
"unit": "m^3/s"
|
||||
},
|
||||
"thresholds": {
|
||||
"interest_level_m": 5.5,
|
||||
"warning_level_m": 8,
|
||||
"alarm_level_m": 10,
|
||||
"serious_level_m": 11,
|
||||
"plan_flood_level_m": 13
|
||||
},
|
||||
"special_report_station": true
|
||||
}
|
||||
```
|
||||
|
||||
## fallback / 대체 흐름
|
||||
|
||||
- `KSKILL_PROXY_BASE_URL` 을 별도로 넣으면 해당 proxy를 우선 사용한다.
|
||||
- 기본 hosted path는 `https://k-skill-proxy.nomadamas.org/v1/han-river/water-level` 이다.
|
||||
- self-host 운영자는 서버 쪽에만 `HRFCO_OPEN_API_KEY` 를 넣는다.
|
||||
- 사용자/client 쪽 secrets 파일에는 HRFCO key 를 넣지 않는다.
|
||||
|
||||
## 주의할 점
|
||||
|
||||
- HRFCO 레퍼런스는 이 데이터를 원시자료로 설명하므로 조회 시각을 함께 적는다.
|
||||
- 기본 endpoint 는 현재값 중심이라 기간별 시계열은 직접 노출하지 않는다.
|
||||
- 관측소명이 너무 넓으면 `candidate_stations` 로 좁힌 뒤 다시 조회한다.
|
||||
- 최신 자료는 보통 10분 단위지만 관측소별 수집 지연이 있을 수 있다.
|
||||
- 별도 proxy를 쓸 때만 `KSKILL_PROXY_BASE_URL` 을 명시한다.
|
||||
|
||||
## 참고 표면
|
||||
|
||||
- 공식 레퍼런스: `https://www.hrfco.go.kr/web/openapiPage/reference.do`
|
||||
- 인증키 안내: `https://www.hrfco.go.kr/web/openapiPage/certifyKey.do`
|
||||
- 정책: `https://www.hrfco.go.kr/web/openapi/policy.do`
|
||||
- proxy 운영 안내: [k-skill 프록시 서버 가이드](k-skill-proxy.md)
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# 하이패스 영수증 발급 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 공식 하이패스 홈페이지에서 로그인된 Chrome 세션 재사용
|
||||
- 사용내역 조회
|
||||
- 특정 거래의 영수증 팝업/출력 화면 진입
|
||||
- 세션 만료 감지 후 재로그인 안내
|
||||
|
||||
## 먼저 알아둘 점
|
||||
|
||||
- 이 기능은 **로그인된 브라우저 세션에서만 동작**한다.
|
||||
- 하이패스 ID/PW/인증코드 입력을 자동화하지 않는다.
|
||||
- 공개 페이지 기준으로 세션 타이머는 **20분(`session_time=1200`)** 이고, 세션 연장/종료는 `/comm/sessionCheck.do`, `/comm//sessionout.do` 경로를 사용한다.
|
||||
- 보호 페이지는 `mgs_type 11/12` 와 `/comm/lginpg.do` 이동으로 세션 종료를 알린다.
|
||||
- 따라서 쿠키 파일만 오래 보관해 재사용하는 완전 자동 로그인 유지 봇은 v1 범위 밖이다.
|
||||
|
||||
## 설치
|
||||
|
||||
```bash
|
||||
npm install hipass-receipt
|
||||
```
|
||||
|
||||
배포 패키지에는 CDP 연결용 `playwright-core` 가 함께 들어 있다. 별도 Playwright 브라우저를 내려받지 않고, 사용자가 직접 연 Chrome/Chromium 세션에 붙는다.
|
||||
|
||||
이 레포를 clone 한 유지보수자라면 루트에서 `npm install` 로 workspace 패키지까지 함께 설치해도 된다.
|
||||
|
||||
## 로그인 브라우저 준비
|
||||
|
||||
전용 Chrome 프로필 + CDP 포트로 브라우저를 띄운다.
|
||||
|
||||
```bash
|
||||
hipass-receipt chrome-command --profile-dir "$HOME/.cache/k-skill/hipass-chrome" --debugging-port 9222
|
||||
```
|
||||
|
||||
그 다음 사용자가 직접 `https://www.hipass.co.kr/comm/lginpg.do` 에 로그인한다.
|
||||
|
||||
## 사용내역 조회
|
||||
|
||||
```bash
|
||||
hipass-receipt list \
|
||||
--cdp-url http://127.0.0.1:9222 \
|
||||
--start-date 2026-04-01 \
|
||||
--end-date 2026-04-07 \
|
||||
--page-size 30 \
|
||||
--encrypted-card-number BASE64_OR_SITE_VALUE
|
||||
```
|
||||
|
||||
내부적으로 다음 흐름을 사용한다.
|
||||
|
||||
1. `/usepculr/InitUsePculrTabSearch.do` 진입
|
||||
2. `hpForm` 에 검색조건 주입
|
||||
3. `/usepculr/UsePculrTabSearchList.do` 로 submit
|
||||
4. iframe HTML 파싱 후 정규화 JSON 반환
|
||||
|
||||
`--encrypted-card-number` 는 기존 `--ecd-no` 별칭과 같다.
|
||||
|
||||
## 영수증 팝업 열기
|
||||
|
||||
먼저 `list` 결과에서 원하는 행의 `rowIndex` 를 확인한다.
|
||||
|
||||
```bash
|
||||
hipass-receipt receipt \
|
||||
--cdp-url http://127.0.0.1:9222 \
|
||||
--start-date 2026-04-01 \
|
||||
--end-date 2026-04-07 \
|
||||
--row-index 1
|
||||
```
|
||||
|
||||
이 명령은 선택한 행의 `영수증`/`출력` control 을 클릭하고, 팝업이 열리면 URL/title 을 반환한다. 공식 안내 기준으로 사용내역 화면에서는 `영수증선택출력` 또는 `영수증전체출력` 으로 이어진다.
|
||||
|
||||
## 세션 만료 처리
|
||||
|
||||
다음 신호 중 하나가 보이면 즉시 실패시키고 재로그인을 요구한다.
|
||||
|
||||
- `/comm/lginpg.do` redirect
|
||||
- `mgs_type = 11`
|
||||
- `mgs_type = 12`
|
||||
- 권한체크(CommonAuthCheck.jsp) 응답
|
||||
|
||||
## 검증 전략
|
||||
|
||||
### 자동 검증
|
||||
|
||||
- query builder 테스트
|
||||
- 사용내역 목록 HTML parser 테스트
|
||||
- 상세/영수증 submit field 보존 테스트
|
||||
- 로그인/권한체크 페이지 감지 테스트
|
||||
|
||||
### 로그인 없이 가능한 smoke
|
||||
|
||||
```bash
|
||||
node packages/hipass-receipt/src/cli.js fixture-demo \
|
||||
--fixture packages/hipass-receipt/test/fixtures/usage-history-list.html
|
||||
```
|
||||
|
||||
### 로그인 세션이 필요한 최종 확인
|
||||
|
||||
- 실제 계정으로 로그인된 전용 Chrome 프로필 준비
|
||||
- `hipass-receipt list` 실행
|
||||
- `hipass-receipt receipt --row-index <n>` 실행
|
||||
- 세션 만료 후 다시 실행해 재로그인 요구 메시지 확인
|
||||
|
||||
## 보안 원칙
|
||||
|
||||
- 하이패스 비밀번호를 새 env var나 repo 문서에 추가하지 않는다.
|
||||
- 로그인은 반드시 사용자가 브라우저 안에서 직접 수행한다.
|
||||
- 이 기능은 조회/영수증 보조까지만 다루며 장기 무인 자동화는 약속하지 않는다.
|
||||
|
|
@ -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`
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# 생활쓰레기 배출정보 조회 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 시군구 기준 생활쓰레기 배출요일/시간 조회
|
||||
- 음식물쓰레기/재활용품 배출방법 조회
|
||||
- 배출장소, 미수거일, 관리부서 연락처 확인
|
||||
- 공공데이터 원본 API를 k-skill-proxy `/v1/household-waste/info` 라우트로 감싸서 조회 + API 키는 프록시 주입
|
||||
|
||||
## 가장 중요한 규칙
|
||||
|
||||
스킬은 항상 `k-skill-proxy`의 `/v1/household-waste/info` 라우트를 호출한다.
|
||||
사용자는 `DATA_GO_KR_API_KEY`를 직접 들고 있지 않고, `serviceKey`는 proxy 서버에서만 원본 API(`https://apis.data.go.kr/1741000/household_waste_info/info`)로 주입한다.
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- 인터넷 연결
|
||||
- 원본 API 접근 가능 환경
|
||||
- API 키 주입용 proxy 접근 가능 환경
|
||||
|
||||
## 기본 조회 예시
|
||||
|
||||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/household-waste/info' \
|
||||
--data-urlencode 'cond[SGG_NM::LIKE]=강남구' \
|
||||
--data-urlencode 'pageNo=1' \
|
||||
--data-urlencode 'numOfRows=100'
|
||||
```
|
||||
|
||||
클라이언트는 **`cond[SGG_NM::LIKE]`** 와 **`pageNo` / `numOfRows`**(또는 `page_no` / `num_of_rows`)를 **함께** 넘긴다. `pageNo` / `numOfRows` 값은 **반드시 `1` / `100`** 이어야 하고, 그 외 값이나 숫자만으로 표현되지 않는 문자열이면 proxy가 **`400`** 을 반환하고 upstream을 호출하지 않는다. upstream에는 항상 `pageNo=1`, `numOfRows=100`만 전달된다. `returnType`은 항상 `json`으로 강제된다. 원본 API의 `cond[DAT_CRTR_YMD::*]`, `cond[DAT_UPDT_PNT::*]` 같은 부가 필터는 현재 지원하지 않는다 — 일반 사용자 질의("강남구 쓰레기 배출 요일")는 시군구 검색만으로 충분하기 때문이다.
|
||||
|
||||
## 조회 흐름 권장 순서
|
||||
|
||||
1. 사용자에게 시/군/구를 먼저 확인한다.
|
||||
2. 입력이 모호하면 상위 행정구역을 포함해 재질문한다.
|
||||
3. proxy `/v1/household-waste/info` 라우트로 조회한다 (`serviceKey`는 proxy가 주입).
|
||||
4. 배출장소/요일/시간/미수거일/문의처를 3~6개 포인트로 요약한다.
|
||||
5. 결과가 여러 건이면 응답 항목을 `DAT_UPDT_PNT` 기준으로 클라이언트에서 정렬해 최신 항목을 우선 보여준다.
|
||||
|
||||
## 자주 보는 필드
|
||||
|
||||
- `SGG_NM`: 시군구명
|
||||
- `MNG_ZONE_NM`, `MNG_ZONE_TRGT_RGN_NM`: 관리구역/대상지역
|
||||
- `EMSN_PLC`, `EMSN_PLC_TYPE`: 배출장소/유형
|
||||
- `LF_WST_EMSN_MTHD`, `FOD_WST_EMSN_MTHD`, `RCYCL_EMSN_MTHD`: 배출방법
|
||||
- `LF_WST_EMSN_DOW`, `FOD_WST_EMSN_DOW`, `RCYCL_EMSN_DOW`: 배출요일
|
||||
- `LF_WST_EMSN_BGNG_TM`, `LF_WST_EMSN_END_TM`: 생활쓰레기 배출시간
|
||||
- `UNCLLT_DAY`: 미수거일
|
||||
- `MNG_DEPT_NM`, `MNG_DEPT_TELNO`: 담당부서/연락처
|
||||
|
||||
## 참고 링크
|
||||
|
||||
- 공식 데이터 출처: 공공데이터포털 (`https://www.data.go.kr`)
|
||||
- upstream API: `https://apis.data.go.kr/1741000/household_waste_info/info`
|
||||
- 프록시 역할: 인증키(`DATA_GO_KR_API_KEY`) 서버 측 주입/보호
|
||||
|
|
@ -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`로 바로 끝내는 편이 더 안정적이다.
|
||||
- 배치 작업 후에는 입력 개수와 출력 개수를 같이 확인한다.
|
||||
|
|
|
|||
|
|
@ -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`가 포함된 일반 오류 페이지가 반환될 수 있다.
|
||||
|
|
@ -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
|
||||
|
|
@ -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를 조정해야 할 수 있다.
|
||||
- 계정 권한, 유료 상품 상태, 마스킹 정책에 따라 보이는 정보가 다르다.
|
||||
- 후보 개인정보를 장기 저장하거나 대량 수집하지 않는다.
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# 조선왕조실록 검색 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- 조선왕조실록 공식 사이트에서 키워드 검색
|
||||
- 검색 결과를 왕별로 좁혀 보기
|
||||
- 서기 연도(`1443`처럼 Gregorian year) 기준으로 결과 필터링
|
||||
- 기사 상세에서 국역/원문 excerpt 가져오기
|
||||
- JSON 형태로 후속 자동화에 넘기기
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- 인터넷 연결
|
||||
- `python3`
|
||||
- 설치된 `joseon-sillok-search` skill 안에 `scripts/sillok_search.py` helper 포함
|
||||
|
||||
별도 API 키나 로그인은 필요 없다.
|
||||
|
||||
## 공식 표면
|
||||
|
||||
- 메인: `https://sillok.history.go.kr`
|
||||
- 검색 결과: `https://sillok.history.go.kr/search/searchResultList.do`
|
||||
- 기사 상세: `https://sillok.history.go.kr/id/<article_id>`
|
||||
|
||||
## 기본 흐름
|
||||
|
||||
1. 검색어를 받는다.
|
||||
2. 공식 검색 결과 HTML을 POST로 가져온다.
|
||||
3. 검색 결과에서 기사 링크, 왕별 분류, 요약을 파싱한다.
|
||||
4. `--king`, `--year`가 있으면 결과를 좁힌다.
|
||||
5. 선택된 기사 상세 페이지를 열어 국역/원문 excerpt를 붙인다.
|
||||
6. 최종 결과를 JSON으로 출력한다.
|
||||
|
||||
## CLI 예시
|
||||
|
||||
### 기본 검색
|
||||
|
||||
```bash
|
||||
python3 scripts/sillok_search.py --query "훈민정음"
|
||||
```
|
||||
|
||||
### 왕 + 연도 필터
|
||||
|
||||
```bash
|
||||
python3 scripts/sillok_search.py --query "훈민정음" --king "세종" --year 1443 --limit 3
|
||||
```
|
||||
|
||||
### 원문 검색
|
||||
|
||||
```bash
|
||||
python3 scripts/sillok_search.py --query "훈민정음" --type w --limit 3
|
||||
```
|
||||
|
||||
## 응답 예시 포맷
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "훈민정음",
|
||||
"type": "k",
|
||||
"filters": {
|
||||
"king": "세종",
|
||||
"year": 1443,
|
||||
"limit": 3
|
||||
},
|
||||
"total_results": 21,
|
||||
"type_count": 11,
|
||||
"returned_count": 1,
|
||||
"items": [
|
||||
{
|
||||
"article_id": "kda_12512030_002",
|
||||
"url": "https://sillok.history.go.kr/id/kda_12512030_002",
|
||||
"title": "세종실록 102권, 세종 25년 12월 30일 경술 2번째기사 / 훈민정음을 창제하다",
|
||||
"king": "세종",
|
||||
"gregorian_year": 1443,
|
||||
"excerpt": "이달에 임금이 친히 언문(諺文) 28자(字)를 지었는데..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 구현 메모
|
||||
|
||||
- v1 은 검색 결과 HTML과 기사 상세 HTML만 읽는다.
|
||||
- `--year` 는 실록 title metadata의 왕대 연차를 accession year와 합쳐 서기 연도로 변환한 뒤 필터링한다.
|
||||
- `--king` 는 `세종`, `세종실록` 같은 입력을 canonical king label로 정규화한다.
|
||||
- 결과가 적으면 detail page를 바로 열어 excerpt를 붙이는 편이 가장 단순하고 안정적이다.
|
||||
|
||||
## 라이브 검증 메모
|
||||
|
||||
2026-04-03 기준 live smoke run에서 `훈민정음` 검색으로 실결과가 반환되었고, `--king 세종 --year 1443` 필터로 `kda_12512030_002` 기사(「훈민정음을 창제하다」)를 안정적으로 다시 찾을 수 있었다.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue