Install
$ agentstack add skill-jinukmoon-agent-skills-snu-tennis-reserve ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Tennis Court Reservation (SNU 스포츠진흥원)
월요일 오전 9시 30분에 열리는 본부테니스코트 예약을, 오픈 순간 자동으로 낚아채는 skill. 사용자는 말로 우선순위만 알려주면 되고, 나머지(timelist 고속 스캔, 폼 채우기, 멤버 인증, 동의 체크, 정각 발사, 코트 순회, 제출, 결과 검증)는 전부 자동이다.
스크립트 경로: 이 skill 폴더의 scripts/ (~/.claude/skills/snu-tennis-reserve/scripts/). 모든 명령은 이 디렉토리에서 실행한다. 세션 파일(state.json), 브라우저 프로필 (browser_profile/), 동반자 정보(members.local.json), 우선순위(targets.txt)도 전부 이 폴더 안에 생긴다 — skill 폴더 밖으로 나가는 개인정보는 없다.
첫 사용 설정 (필수 — 이 컴퓨터에서 처음일 때만)
skill을 처음 쓸 때, 아래 항목들을 사용자에게 직접 물어봐서 설정한다. 한 번 설정하면 파일로 남으므로 다음부터는 묻지 않는다.
- Python 환경 확인 — Playwright가 설치된 Python이 필요하다:
``bash python -c "import playwright" 2>/dev/null && echo OK || echo MISSING ``
- MISSING이면 사용자에게 알리고 설치를 진행한다:
pip install playwright && playwright install chromium
- 사용자가 특정 conda env를 쓰면 그 env의 python 경로를 기억해두고 이후 모든 명령에 사용.
- 동반자 정보 —
scripts/members.local.json이 없으면 묻는다:
"이 컴퓨터에서 처음이네요. 같이 예약할 동반자 한 분의 이름 / 학번 / 전화번호 / 학과를 알려주세요. (본인 정보는 로그인으로 자동 입력돼요. 이 정보는 이 컴퓨터에만 저장되고 공유되지 않아요.)" 받은 정보로 파일을 만든다: ``json [{"name": "홍길동", "userno": "2024-12345", "mobile": "010-1234-5678", "dept": "재료공학부"}] `` 동반자가 여러 명이면 배열에 객체를 추가. 최소 동반자 1명(본인 포함 2명)이어야 예약 가능. 저장 후 "저장했어요. 다음부터는 안 물어봐요."라고 알린다.
핵심 제약 (먼저 이해할 것)
- 세션 수명 ~50분: 로그인은 발사 직전(9:30 기준 20분 이내)에 해야 한다. 일찍 하면 만료됨.
- 재로그인은 휴대폰 모바일 인증(MFA) 필요: 완전 무인 로그인은 불가능. 사용자가 직접 1회 인증.
- 9:30 전에는 폼이 안 열림: 다음 주 날짜는 9:30:00이 되어야 유효. 그래서 정각 발사 + 서버오픈 폴링 구조다.
- reCAPTCHA: 제출 직전 "로봇이 아닙니다"가 뜰 수 있음. 첫 1회만 사용자가 풀면, 이후엔 세션이 신뢰돼서 보통 자동 통과(0.5초)된다. → 발사는 반드시 헤드 모드(창 표시)로 하고, 사용자는 화면 앞에서 첫 캡챠를 대기.
- ★ 1인 1일 1예약 규칙: 한 사람은 하루에 코트 1개만 예약 가능. 같은 날 여러 항목은 1개만 잡히고 나머진 "충돌"로 즉시 다음 우선순위로 넘어간다. → 멀티 예약은 서로 다른 날로 짜야 의미 있다.
예약 동작 규칙 (reserve.py 내부 — 사용자에게 설명할 때 참고)
- 목록을 위에서부터 전부 예약한다. 우선순위 번호 = 처리 순서. 한 항목이 성공해도 멈추지 않고 다음으로 간다 (코트 하나당 1건).
- 다음 우선순위로 넘어가는 경우는 딱 세 가지뿐: (a) 예약 확정 검증됨, (b) 그 날짜·시간 14개 코트 모두 매진, (c) 1일1예약 충돌. 그 외(빈 코트 있는데 실패)면 넘어가지 않고 멈춰서 알린다 (잘못된 건너뜀 방지).
- 빈자리 탐지는 timelist JSON 고속 스캔 (코트당 ~0.4초). 무거운 폼은 빈 코트에만 로드.
- 성공은 결과 페이지 리다이렉트로 검증된 것만 인정.
진행 흐름 (이 순서대로 사용자를 안내한다)
Step 0. 첫 사용 설정
위 "첫 사용 설정" 섹션 참고. scripts/members.local.json 존재 여부로 판단:
ls ~/.claude/skills/snu-tennis-reserve/scripts/members.local.json
있으면 건너뛴다.
Step 1. 시각 확인
지금이 월요일 9:30 직전인지 확인한다 (date 명령).
- 9:10~9:30 사이면 정상. 바로 진행.
- 너무 이르면(예: 9시 이전) 알려준다: "세션이 50분이면 만료돼서 9:10 이후에 로그인하는 게 안전해요. 지금 미리 준비만 해둘까요, 아니면 9:10에 다시 부르실래요?"
- 이미 9:30이 지났으면 알려주고, 그래도 시도할지 묻는다(취소분이 풀렸을 수 있으니
--now로 즉시 시도 가능).
Step 2. 로그인 안내 — 사용자의 "했어" 신호로 진행
세션부터 확인한다:
cd ~/.claude/skills/snu-tennis-reserve/scripts && ./run.sh check
- 이미 유효(OK)하면 로그인 생략하고 Step 3로.
- 만료(FAIL)면 로그인 창을 background로 띄운다:
``bash ./run.sh login `` 그리고 사용자에게: "새로 뜬 창에서 모바일 인증으로 로그인해주세요. (평소 쓰는 브라우저 말고 이 창이어야 해요.) 다 되면 '했어'라고 말씀해주세요."
자동감지에 의존하지 말 것. 사용자가 직접 로그인하므로, 진행 신호는 사용자의 "했어 / 됐어 / 완료" 다. 단, 우리 브라우저(./run.sh login)로 로그인해야 우리 세션(state.json)에 저장된다 — 사용자가 "본인 평소 브라우저에서 이미 로그인했다"고 하면, 그건 우리 세션과 별개임을 알리고 반드시 띄워준 창에서 다시 로그인하게 한다.
- 사용자가 "했어" 하면
./run.sh check로 검증한다: - OK → Step 3.
- 여전히 FAIL → 보통 (a) 띄워준 창이 아닌 다른 브라우저에서 했거나, (b) 인증이 덜 끝난 경우. 어느 쪽인지 짚어주고 그 창에서 마저 로그인하게 한 뒤 다시 "했어"를 기다린다.
(login 잡은 background로 계속 살아있으면서, 사용자가 로그인된 athletics 페이지에 도달하면 state.json을 저장한다. 그래서 "했어" 시점엔 보통 check가 OK가 된다.)
Step 3. 우선순위 물어보기
사용자에게 묻는다: "이번 주 뭐뭐 예약할까요? (날짜+시간으로, 위에서부터 순서대로 알려주세요. 예: 토 20-22, 일 19-21)"
이미 사용자가 메시지에서 우선순위를 말했으면 다시 묻지 말고 그대로 쓴다. 목록의 항목들을 전부 예약한다는 점, 그리고 같은 날 여러 개는 1개만 된다(1일1예약)는 점을 필요하면 알려준다.
Step 4. 말 → 24시간제로 해석하고 반드시 한 번 확인받기
사용자의 자연어를 우선순위 목록으로 옮긴다. 각 줄 형식은 요일 시작-종료(예: 토 20-22) 또는 날짜 직접 지정 MM-DD 시작-종료(예: 06-20 14-16). 같은 요일이 두 번 열리는 주(토/일)나 특정 날짜를 콕 집을 때는 날짜 형식을 쓴다. 변환 규칙은 아래 "시간 해석 규칙" 참고.
이 단계의 핵심은 확인 게이트다. 사용자의 말(특히 "저녁 8시", "8시부터" 같은 표현)은 12/24시간제나 종료시간이 애매할 수 있어서, 내가 이해한 걸 24시간제로 풀어서 보여주고 딱 한 번 확인을 받은 뒤에만 진행한다. 멋대로 발사하지 않는다.
확인 멘트는 이런 식으로 — 내가 어떻게 이해했는지 24시간 기준으로 명시하고, 진행 여부를 묻는다:
> "24시간 기준으로 이렇게 이해했어요: > 1순위 토 20-22시 (토요일 저녁 8시~10시) > 2순위 일 20-22시 > 이렇게 진행할까요?"
- 사용자가 OK 하면
targets.txt를 그 목록으로 덮어쓰고 Step 5로. - 사용자가 고치면 (예: "아니 9시부터") 다시 해석해서 또 한 번 보여주고 확인. 확실해질 때까지 발사하지 않는다.
- 해석이 모호한 항목이 하나라도 있으면, 추측해서 넘어가지 말고 그 항목을 콕 집어 되묻는다.
Step 5. 발사
헤드 모드로 9:30:00 정각 발사를 건다. 백그라운드로 실행하고 출력 로그를 따라가며 사용자에게 중계한다:
./run.sh reserve -- --fire 09:30:00
--fire시각은 서버시간 기준이다. 스크립트가 athletics 서버의 HTTP Date로 로컬-서버 시계 오프셋을 측정해 보정하고, 발사 5초 전부터 카운트다운하다가 서버가 09:30:00 되는 순간 발사한다. (로컬 시계가 몇 초 빠르거나 느려도 정확)- 발사 시각이 이미 지났으면
--now로 바꿔 즉시 시도. - 실행 후 사용자에게: "걸었어요. 9:30:00에 자동 발사됩니다. reCAPTCHA 이미지 챌린지가 뜨면 창에서 풀어주세요."
- 로그를 지켜보다가:
>>> 이미지 챌린지가 떴다면...→ 사용자에게 지금 창에서 reCAPTCHA 풀어달라고 즉시 알림. (첫 1회만, 이후 자동 통과)✅ 예약 성공(검증됨)→ 각 건 성공을 전달. 목록이 여러 개면 다음 항목으로 계속 진행됨.⚠️ 멈춤→ 빈 코트가 있는데 예약 실패가 반복돼 안전상 멈춘 것. 창을 확인하라고 알리고, 원인(인증 실패 등) 파악.- 마지막
===== 결과 요약 =====→ 예약 완료/매진/충돌/스킵 건수를 그대로 사용자에게 전달.
시간 해석 규칙
목표는 사용자의 자연스러운 말을 요일 시작-종료(24시간제)로 정확히 옮기는 것. 예약은 최대 2시간.
- 오후/저녁/밤 + 시간 → 24시간제로 변환. "저녁 8시"=20, "오후 7시"=19, "밤 9시"=21, "오전 10시"=10.
- 종료 시간을 안 말하면 시작+2시간으로 채운다 (최대 2시간이 보통 기본). "토 저녁 8시" →
토 20-22. - 오전/오후가 없는 애매한 시간(예: "8-10시", "7시부터")은 저녁(황금시간대)으로 기본 추정한다. 테니스 인기 시간대가 저녁이고 실제로 저녁 슬롯부터 매진되기 때문. 그래서 "8-10시" →
20-22, "7시부터" →19-21로 보되, 확인 멘트에 "(저녁 8~10시)"처럼 풀어 적고 "혹시 오전이면 말씀해주세요"를 덧붙여 한 번 확인한다. 사용자가 오전/낮을 명시하면 그대로 따른다. - 여러 개를 우선순위로 말하면 말한 순서대로 줄을 쌓는다. 목록의 항목들은 전부 예약된다(폴백 아님). 단 같은 날 여러 개는 1일1예약 규칙상 1개만 된다.
- 요일은 한 글자로: 월 화 수 목 금 토 일. 특정 날짜는
MM-DD(예:06-20 14-16). 같은 요일이 두 번 열리는 주거나 사용자가 날짜를 콕 집으면 날짜 형식을 쓴다.
변환 예시:
Input: "토요일 저녁 8시부터 2시간" Output: 토 20-22
Input: "토 20-22, 안 되면 일 20-22, 그것도 안 되면 월 19-21" Output:
토 20-22
일 20-22
월 19-21
Input: "주말 저녁 8시 위주로, 토 먼저 일 나중" Output:
토 20-22
일 20-22
명령어 빠른 참조
| 목적 | 명령 (scripts/ 디렉토리에서) | |------|------| | 세션 확인 | ./run.sh check | | 로그인 (창 띄움) | ./run.sh login | | 정각 발사 | ./run.sh reserve -- --fire 09:30:00 | | 즉시 시도 (취소분) | ./run.sh reserve -- --now | | 빈자리 확인만 | ./run.sh reserve -- --dry --headless |
발사/로그인은 창이 떠야 하므로 --headless를 붙이지 않는다. 확인/스캔만 할 땐 붙여도 된다. WSL에서는 WSLg가 있어야 창이 뜬다 (Windows 11 기본 포함).
예약 조회 / 취소
- 조회: 마이페이지(
/mypage)를 읽어 예약 목록(시설명/일시/상태)을 보여준다. - 취소 (사용자가 명시적으로 요청할 때만): 마이페이지에서 각 예약의
apply_idx를 찾아
/mypage?mode=cancel&target=facility&apply_idx={idx} 로 이동하면 취소된다(alert "예약을 취소하였습니다"). 취소 전 어느 예약인지 사용자에게 확인하고, 취소 후 마이페이지로 검증한다. 사용자가 시키지 않은 예약은 절대 취소하지 않는다.
- ⚠️ mypage 페이지네이션: 마이페이지는 한 번에 5개만 보여준다. "더보기" 버튼을 끝까지
눌러야(page.locator("text=더보기").click() 반복) 전체 예약이 나온다. 조회·취소 시 이걸 안 하면 뒤쪽 예약을 놓친다. 또 같은 슬롯이 "사용승인"과 "예약취소" 두 행으로 보일 수 있으니 상태가 사용승인인 행만 활성 예약으로 센다.
주의사항
- 동반자 명단은 로컬 파일
scripts/members.local.json에만 있다 (PII는 skill 코드에 없음). 본인(로그인 계정)은 자동 입력.
사용자가 동반자를 바꾸거나 추가하려 하면 그 파일을 수정한다. 최소 예약인원 2명(본인+동반자 1명). 첫 사용이면 Step 0.
- 발사를 백그라운드로 돌릴 때, 9:30까지 대기하는 동안 사용자가 다른 얘기를 해도 발사 잡(job)은 계속 살아있다. 완료 알림이 오면 결과를 전한다.
- 발사 중 사용자가 창을 닫으면 스크립트가 TargetClosedError로 죽는다. reCAPTCHA 대기 중엔 창을 닫지 말라고 미리 안내한다.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: JinukMoon
- Source: JinukMoon/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.