
Foresttrip Vacancy
Check read-only monthly campsite vacancy on Korea’s foresttrip.go.kr without touching booking, payment, or queues.
Overview
foresttrip-vacancy is an agent skill for the Build phase that performs read-only foresttrip.go.kr monthly vacancy lookups via Playwright session bootstrap and JSON availability queries.
Install
npx skills add https://github.com/nomadamas/k-skill --skill foresttrip-vacancyWhat is this skill?
- Playwright login to obtain CSRF token and session cookies for foresttrip.go.kr
- Parses official monthly reservation status page to resolve forest IDs
- Queries read-only monthly availability JSON (POST to selectRsrvtAvailInfoListForMonthRsrvtSmpl.do)
- Explicitly does not click booking, submit forms, handle payment, solve captcha, or bypass queues
- Configurable concurrency (default 4, max 5) and category codes 01/02 for reservation types
- Default concurrency 4 with a hard cap of 5 parallel workers
- Supports reservation category codes 01 and 02
Adoption & trust: 1.1k installs on skills.sh; 5.4k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You need up-to-date forest campsite availability across many forests and dates but the official site expects a logged-in session and scattered page/API calls.
Who is it for?
Solo builders scripting Korea forest-camping monitors or travel bots that only need availability facts.
Skip if: Automated booking, payment, captcha solving, queue jumping, or any use without valid credentials and respect for site terms.
When should I use this skill?
You need foresttrip.go.kr monthly vacancy JSON after a one-time Playwright login and must not perform booking or payment steps.
What do I get? / Deliverables
You get structured monthly availability JSON and resolved forest IDs without triggering booking or payment flows.
- Structured monthly availability JSON per forest and date range
- Resolved forest ID map from the official monthly status page
Recommended Skills
Journey fit
Canonical shelf is Build → integrations because the skill wires Playwright login, CSRF/session handling, and a read-only availability JSON API into an agent workflow. Subphase integrations fits external-site session auth plus HTTP JSON queries rather than app frontend or backend product code.
How it compares
Use as a read-only data bridge skill, not a general Playwright booking framework or an MCP browser server.
Common Questions / FAQ
Who is foresttrip-vacancy for?
Indie developers and agent users who legally monitor foresttrip.go.kr vacancy data for personal or authorized automation, not bulk scalping or checkout bots.
When should I use foresttrip-vacancy?
During Build integrations when you are wiring a CLI or agent job that must log in, list forests from the monthly status page, and poll read-only availability JSON before a human books manually.
Is foresttrip-vacancy safe to install?
Review the Security Audits panel on this Prism page, treat stored login credentials as secrets, and confirm your use stays read-only and compliant with foresttrip.go.kr rules.
SKILL.md
READMESKILL.md - Foresttrip Vacancy
#!/usr/bin/env python3 """Read-only foresttrip.go.kr vacancy lookup helper. The script logs in with Playwright to obtain a CSRF token and session cookies, extracts forest IDs from the official monthly reservation status page, then queries the read-only monthly availability JSON endpoint. It intentionally does not click booking buttons, submit reservation forms, handle payment, solve captcha, or bypass queues. """ from __future__ import annotations import argparse import concurrent.futures import json import os import sys import time import urllib.error import urllib.request from dataclasses import asdict, dataclass from datetime import datetime, timedelta from pathlib import Path from typing import Any LOGIN_URL = "https://www.foresttrip.go.kr/com/login.do" RSRVT_PAGE = "https://www.foresttrip.go.kr/rep/or/sssn/monthRsrvtSmplStatus.do" POST_URL = "https://www.foresttrip.go.kr/rep/or/selectRsrvtAvailInfoListForMonthRsrvtSmpl.do" DEFAULT_CONCURRENCY = 4 MAX_CONCURRENCY = 5 DEFAULT_WEEK_RANGE = 1 CATEGORY_CODES = {"01", "02"} RESERVE_ROOM_MARKER = "예비" @dataclass class Session: cookies: dict[str, str] csrf: str user_agent: str forests: dict[str, str] expires_at: float def parse_csv(value: str) -> list[str]: return [part.strip() for part in value.split(",") if part.strip()] def parse_categories(value: str) -> tuple[str, ...]: categories = parse_csv(value) if not categories: raise argparse.ArgumentTypeError("must include at least one category code") invalid = [category for category in categories if category not in CATEGORY_CODES] if invalid: raise argparse.ArgumentTypeError( "unknown category code(s): " + ", ".join(invalid) + " (allowed: 01=lodging, 02=camping)" ) return tuple(dict.fromkeys(categories)) def parse_dates(value: str) -> tuple[str, ...]: dates = parse_csv(value) if not dates: raise argparse.ArgumentTypeError("must include at least one YYYYMMDD date") today = datetime.now().date() normalized: list[str] = [] for raw_date in dates: try: parsed = datetime.strptime(raw_date, "%Y%m%d").date() except ValueError as exc: raise argparse.ArgumentTypeError(f"invalid YYYYMMDD date: {raw_date}") from exc if parsed.strftime("%Y%m%d") != raw_date: raise argparse.ArgumentTypeError(f"invalid YYYYMMDD date: {raw_date}") if parsed < today: raise argparse.ArgumentTypeError(f"date is in the past: {raw_date}") normalized.append(raw_date) return tuple(sorted(dict.fromkeys(normalized))) def parse_concurrency(value: str) -> int: try: concurrency = int(value) except ValueError as exc: raise argparse.ArgumentTypeError("must be an integer") from exc if not 1 <= concurrency <= MAX_CONCURRENCY: raise argparse.ArgumentTypeError(f"must be between 1 and {MAX_CONCURRENCY}") return concurrency def parse_week_range(value: str) -> int: try: week_range = int(value) except ValueError as exc: raise argparse.ArgumentTypeError("must be an integer") from exc if week_range < 1: raise argparse.ArgumentTypeError("must be at least 1") return week_range def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Read-only foresttrip.go.kr vacancy lookup.", ) target = parser.add_argument_group("target selection") target.add_argument("--all", action="store_true", help="Scan all extracted forest IDs.") target.add_argument( "--forest-id", action="append", help="ForestTrip insttId. Can be passed multiple times or comma-separated.", ) target.add_argument( "--forest-name", action="append", help="Substring to match against official forest names.", ) output = parser.add_mutually_exclusive_group() output.add_argument("--json", action="sto