
Jobkorea Talent Search
- 1 installs
- Updated June 17, 2026
- ez-edu/korea-talent-search-skills
Searches JobKorea corporate talent listings in a logged-in browser session, reads masked resumes, and builds a scored shortlist of candidates before any paid unlock.
About
Searches JobKorea's corporate talent pool using a company-member browser session, reading only masked resume data to score and compare candidates. Recruiters use it to build a shortlist of who is worth paying to unlock, without spending credits automatically.
- Reads only masked listings; never spends credits without approval
- Produces a scored recruiting shortlist from public/masked data
Jobkorea Talent Search by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ez-edu/korea-talent-search-skills --skill jobkorea-talent-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 17, 2026 |
| Repository | ez-edu/korea-talent-search-skills ↗ |
What it does
Searches JobKorea corporate talent listings in a logged-in browser session, reads masked resumes, and builds a scored shortlist of candidates before any paid unlock.
Files
jobkorea-talent-search
잡코리아 기업 인재검색에서 회사가 원하는 인재를 찾는 Vercel Agent Skill이다. 목표는 유료 마스킹 해제/포지션 제안 전에, 기업회원 로그인 상태에서 볼 수 있는 마스킹된 이력서와 공개 목록 정보를 최대한 잘 읽고 비교하여 “돈을 써서 열람할 후보”만 추천하는 것이다.
When to use
Use this skill when the user asks to:
- 잡코리아에서 우리 회사에 맞는 인재를 찾아달라고 요청한다.
- 잡코리아 기업계정으로 로그인해 마스킹된 이력서만 보고 후보를 추려달라고 요청한다.
- 유료 열람/마스킹 해제 전에 후보를 비교·점수화하고 싶어 한다.
- 특정 직무, 기술스택, 경력, 지역, 포트폴리오, 프로젝트 경험 조건으로 잡코리아 인재검색을 수행하려 한다.
Core safety boundary
Never spend money or create recruiting side effects automatically.
Allowed:
- 사용자가 직접 로그인한 잡코리아 기업회원 브라우저 세션에서 검색 조건 입력
- 마스킹된 목록과 마스킹된 이력서 본문 읽기
- 후보 적합도 분석, 점수화, shortlist 작성
- 유료 열람이 필요해 보이는 후보 추천
Not allowed without explicit user confirmation:
- 유료 이력서 열람 / 마스킹 해제
- 포지션 제안 발송
- 스크랩, 메모 저장, 후보 상태 변경
- 연락처 수집 또는 외부 저장
- 잡코리아 계정/비밀번호/세션 쿠키를 저장소나 로그에 기록
- 후보자 데이터를 장기 DB화하거나 대량 수집
If a button or action may consume paid credits, reveal contact information, send a proposal, or mutate account state, stop and ask the user to perform/approve it manually.
Required mode selection
Prefer mode A for the user’s stated goal.
Mode A — logged-in browser mode, preferred
Use when the user has a JobKorea corporate account. The user may not know that login is required before using the skill. Therefore, when a search request comes in, start by opening the JobKorea corporate talent-search page in a browser and checking the session state. If no valid corporate login session exists, pause the search and instruct the user to log in manually in that browser window/tab. After the user confirms login is complete, continue the search in the same browser session. Never ask for the password and never store cookies.
Login bootstrap message
If no logged-in corporate session is detected, say this in Korean:
잡코리아 인재검색은 경력 상세/포트폴리오/마스킹 이력서 확인을 위해 기업회원 로그인이 필요합니다.
제가 브라우저로 잡코리아 기업 인재검색 페이지를 열어둘게요.
열린 브라우저에서 직접 로그인해 주세요. 비밀번호나 인증정보는 저에게 알려주지 마세요.
로그인이 끝나면 “로그인했어”라고 알려주시면, 같은 브라우저 세션에서 검색을 이어가겠습니다.Then open https://www.jobkorea.co.kr/corp/person/find, wait for the user to log in, and resume from the same page. Do not fall back to no-login mode unless the user explicitly says they cannot or do not want to log in.
Mode B — no-login public list fallback
Use only for rough scouting when login is unavailable. This mode can search public/masked list summaries, but it cannot reliably inspect career details, portfolio, project content, or full resume sections. Clearly label results as low-confidence.
Helper script for mode B:
python3 jobkorea-talent-search/scripts/jobkorea_talent_search.py \
--keyword "React TypeScript" \
--job-category "AI·개발·데이터" \
--work-area "서울" \
--career-min 3 \
--career-max 8 \
--limit 20Inputs to collect
Before searching, normalize the hiring requirements into this structure:
role_title: "백엔드 엔지니어"
must_have:
- Python
- FastAPI
- AWS
nice_to_have:
- Kubernetes
- MLOps
- startup experience
negative_keywords:
- QA only
- SI only
career:
min_years: 3
max_years: 8
location:
work_area:
- 서울
- 경기 성남
remote_or_hybrid: optional
portfolio_signals:
- GitHub
- production service
- traffic/scale metrics
company_context:
team: "AI SaaS startup"
selling_points:
- small team ownership
- global product
limit: 20If the user only provides a rough request, make a reasonable first search rather than blocking. Ask follow-up questions only when the role is genuinely ambiguous.
Logged-in browser workflow
1. Open https://www.jobkorea.co.kr/corp/person/find in a browser immediately when a JobKorea talent-search request starts. 2. Check whether the page is in a corporate logged-in state.
- If logged in, continue with the search.
- If not logged in, show the Login bootstrap message above and keep the browser/page available for the user.
- Wait for the user to say login is complete, then re-check the same page/session.
- Do not ask for or store the password, one-time code, cookies, or session data.
3. Enter/search the normalized conditions:
- 통합검색: core role and must-have technologies
- AND keywords: strict must-have terms
- OR keywords: nice-to-have terms
- 제외 keywords: irrelevant/screen-out terms
- 직무/스킬: select the closest JobKorea category, commonly
AI·개발·데이터for engineering roles - 지역: preferred work areas
- 경력: min/max years
- 업데이트/최근 활동: prefer recent activity when too many results appear
4. Collect a broad candidate pool from the result list, usually 20–100 candidates depending on user’s requested limit. 5. For each promising candidate, open the resume detail page in a new tab only if it does not trigger paid unlock. 6. Read masked/free sections only:
- career timeline and total years
- project/responsibility descriptions
- skill keywords
- education/certifications/languages
- portfolio/GitHub/blog URLs if visible
- desired location/salary if visible
- recent activity/update signal
7. Do not click paid unlock, contact reveal, proposal send, scrap, or memo buttons unless the user explicitly instructs and confirms the side effect. 8. Score and rank candidates. 9. Return a shortlist with evidence and “paid unlock recommendation” reasoning.
Browser extraction helper snippets
When using a browser-capable agent, after a search result page loads, run this in the page console to extract candidate links and visible list text:
Array.from(document.querySelectorAll('a[href*="/corp/person/find/resume/view?rNo="]'))
.map((a) => {
const card = a.closest('.booth, tr, li, .personList, .list, .row') || a.parentElement;
const text = (card?.innerText || a.innerText || '')
.replace(/\s+/g, ' ')
.trim();
return {
url: new URL(a.getAttribute('href'), location.origin).href,
rNo: new URL(a.getAttribute('href'), location.origin).searchParams.get('rNo'),
text,
};
})
.filter((item, index, arr) => item.rNo && arr.findIndex((x) => x.rNo === item.rNo) === index)
.slice(0, 100);On a masked resume detail page, run this to capture only visible text and avoid hidden/script noise:
(() => {
const blocked = ['script', 'style', 'noscript', 'svg'];
const clone = document.body.cloneNode(true);
clone.querySelectorAll(blocked.join(',')).forEach((el) => el.remove());
const text = clone.innerText.replace(/\n{3,}/g, '\n\n').trim();
return {
url: location.href,
title: document.title,
text: text.slice(0, 20000),
paidActionHints: Array.from(document.querySelectorAll('button,a'))
.map((el) => el.innerText.trim())
.filter((t) => /열람|결제|구매|제안|연락|마스킹|스크랩|메모/.test(t))
.slice(0, 50),
};
})();If paidActionHints suggests paid unlock/contact reveal/proposal send, do not click those controls. Read only currently visible text.
Scoring rubric
Score each candidate out of 100. Adjust weights if the user provides a different priority.
Role-adaptive scoring notes
Do not use developer-only criteria for every role. Adapt evidence to the target job:
- Sales/customer success: revenue, pipeline, outbound/inbound, contract negotiation, CRM, industry/customer segment fit.
- Marketing: ROAS/CAC/LTV, ad channels, GA4/CRM, campaign ownership, funnel/conversion improvement.
- Design: portfolio, launched work, brand consistency, Figma/Adobe, UX process, collaboration with product/marketing.
- PM/PO/planning: problem definition, roadmap, requirements, stakeholder alignment, metric improvement, launch ownership.
- HR/recruiting: direct sourcing, hiring volume, interview process, offer/compensation, ATS, employer branding.
- Finance/accounting: closing, tax, audit, cashflow, ERP, reporting, responsibility scope.
- Operations/MD/SCM: sales/margin/inventory improvement, vendor negotiation, fulfillment/logistics KPIs, process improvement.
- Developer/data/AI: implementation/project evidence, production operation, relevant stack, portfolio/GitHub/blog, scale/automation impact.
- Must-have fit: 35
- direct evidence of required skills, stack, or domain
- Relevant career depth: 20
- total years and role seniority match the requested range
- Project/portfolio evidence: 20
- concrete project descriptions, production impact, portfolio/GitHub/blog, measurable outcomes
- Location/availability fit: 10
- desired work area, recent activity, job-seeking status
- Nice-to-have signals: 10
- bonus stack, industry, language, leadership, startup/enterprise experience
- Risk penalty: -15 max
- keyword stuffing without project evidence, too senior/junior, unrelated domain, stale resume, unclear role ownership
Use labels:
- 85–100: Strong paid-unlock candidate
- 70–84: Worth HR review
- 55–69: Backup / only if pool is weak
- below 55: Do not pay yet
Output format
Return Korean output in this shape:
잡코리아 인재 shortlist
검색 조건
- 포지션: ...
- 필수 조건: ...
- 우대 조건: ...
- 제외 조건: ...
- 경력/지역: ...
- 모드: 로그인 마스킹 이력서 / 비로그인 목록 fallback
유료 열람 추천 Top N
1. 후보 A (마스킹 이름 또는 rNo)
- 점수: 88/100 — Strong paid-unlock candidate
- 근거: ...
- 보이는 경력/프로젝트: ...
- 기술스택: ...
- 포트폴리오/링크 신호: ...
- 리스크: ...
- 추천 액션: 인사팀이 유료 열람 검토
- URL: ...
보류 후보
- ...
검색 한계
- 마스킹 정보만 분석했음
- 연락처/실명/비공개 정보는 열람하지 않음
- 유료 액션은 실행하지 않음No-login fallback workflow
If corporate login is not available:
1. Use the helper script to fetch public/masked result-list summaries. 2. Score at lower confidence because career/portfolio/detail sections are unavailable. 3. Tell the user that accurate filtering requires logged-in masked resume inspection. 4. Recommend logging in before spending paid unlock credits.
Failure modes
- Login required: open the JobKorea corporate talent-search page in a browser, show the Login bootstrap message, and ask the user to log in manually with a corporate account.
- Browser or Chrome permission unavailable: do not silently switch to no-login fallback. Explain that accurate screening requires a browser session that the agent can read/control, and ask the user to enable the agent's browser/computer-use capability or macOS permissions. If the user launched the agent from Terminal/iTerm/VS Code, they may need to grant Accessibility, Screen Recording, and Automation permissions to that host app. If the agent uses an isolated Playwright/Chromium browser, the user must log in inside the browser window opened by the agent; a normal Chrome login may not be shared.
- Paid unlock wall: stop at the wall and do not click. Mark candidate as “needs HR paid review”.
- Empty/too broad results: relax/tighten keyword, career, region, recent activity filters.
- Bot/rate limit/403: pause, reduce request rate, or switch to manual browser workflow.
- Upstream UI changed: rediscover the search form and visible AJAX/data flow before editing scripts.
Notes for maintainers
This skill follows the Vercel Agent Skills format: a directory containing SKILL.md with YAML frontmatter name and description, plus optional supporting scripts. It can be installed with the skills CLI from a repo or specific skill path, e.g.:
npx skills add <owner/repo> --skill jobkorea-talent-searchThe included Python helper is only the no-login fallback. The primary workflow is browser/login based because accurate JobKorea resume screening requires corporate login visibility before paid unlock.
잡코리아 인재검색 스킬 직원용 사용 가이드
이 문서는 회사 구성원이 jobkorea-talent-search Vercel Agent Skill을 설치하고 Claude Code에서 잡코리아 인재검색을 안전하게 수행하는 방법을 설명한다.
배포 저장소:
https://github.com/ez-edu/jobkorea-talent-search직원용 기본 설치 명령:
npx skills add ez-edu/jobkorea-talent-search --agent claude-code1. 이 스킬로 할 수 있는 일
이 스킬은 잡코리아 기업회원 로그인 상태에서 유료 열람/마스킹 해제 전에 볼 수 있는 마스킹된 인재 이력서를 읽고, 회사 채용 조건에 맞는 후보를 점수화해 shortlist를 만든다.
주요 목적:
- 잡코리아 인재검색 조건 입력 보조
- 마스킹된 후보 목록/이력서 본문 검토
- 기술스택, 경력, 프로젝트, 포트폴리오 신호 기준 점수화
- 인사팀이 유료 열람할 후보 추천
이 스킬이 자동으로 하지 않는 일:
- 유료 이력서 열람
- 마스킹 해제
- 연락처 확인
- 포지션 제안 발송
- 스크랩/메모/후보 상태 변경
- 결제
- 잡코리아 계정/비밀번호/세션 쿠키 저장
2. 준비물
직원 PC에 아래가 필요하다.
- macOS 또는 Windows/Linux 개발 환경
- Node.js 18 이상
- Claude Code 설치 및 로그인
- 잡코리아 기업회원 계정
- 브라우저 자동화/화면 접근 권한
Node.js 확인:
node -v
npm -vNode.js가 없으면 설치:
# macOS + Homebrew
brew install nodeClaude Code가 설치되어 있는지 확인:
claude --versionClaude Code가 없다면 회사 표준 설치 가이드에 따라 설치하거나 Anthropic Claude Code 문서를 참고한다.
3. 스킬 설치
3.1 GitHub 저장소에서 설치하는 경우
이 스킬은 아래 GitHub 저장소에서 배포한다.
https://github.com/ez-edu/jobkorea-talent-searchClaude Code에 설치:
npx skills add ez-edu/jobkorea-talent-search --agent claude-code저장소가 private이면 먼저 GitHub 인증이 되어 있어야 한다. 설치가 실패하면 아래 중 하나를 확인한다.
gh auth status
git ls-remote https://github.com/ez-edu/jobkorea-talent-search.git여러 에이전트에 모두 설치하려면:
npx skills add ez-edu/jobkorea-talent-search --all3.2 로컬 폴더에서 설치하는 경우
개발/테스트 중에는 로컬 폴더에서 설치할 수 있다.
npx skills add /Users/wbjung/jobkorea-talent-search --agent claude-code또는 해당 폴더에서:
cd /Users/wbjung/jobkorea-talent-search
npx skills add . --agent claude-code3.3 설치 확인
npx skills list --agent claude-code목록에 아래 스킬이 보이면 정상이다.
jobkorea-talent-search4. macOS 권한 설정
잡코리아 로그인된 화면을 Claude Code가 읽거나 브라우저를 조작하려면 macOS 권한이 필요할 수 있다.
Claude Code를 실행하는 앱에 권한을 준다.
- Terminal에서 실행하면
Terminal - iTerm에서 실행하면
iTerm - VS Code 터미널에서 실행하면
Visual Studio Code
설정 위치:
System Settings
→ Privacy & Security
→ Accessibility
→ Screen Recording
→ Automation권한을 켠 뒤 Claude Code 또는 터미널을 완전히 종료했다가 다시 실행한다.
주의:
- Claude Code가 별도 Chromium/Playwright 브라우저를 열면 평소 Chrome 로그인 상태가 공유되지 않을 수 있다.
- 이 경우 Claude Code가 연 브라우저 창에서 잡코리아 기업회원 로그인을 다시 해야 한다.
- 비밀번호나 2FA 코드는 Claude에게 알려주지 말고, 사용자가 직접 브라우저에 입력한다.
5. Claude Code에서 사용하기
터미널에서 Claude Code를 실행한다.
claude처음 요청은 아래처럼 한다.
jobkorea-talent-search 스킬을 사용해서 잡코리아에서 백엔드 엔지니어 후보를 찾아줘.
비로그인 fallback으로 진행하지 말고, 먼저 브라우저를 열어서 잡코리아 기업회원 로그인 세션을 확인해줘.
로그인이 안 되어 있으면 내가 직접 로그인할 수 있게 브라우저를 띄워줘.
조건:
- 경력: 3~8년
- 지역: 서울 또는 경기 성남
- 필수: Python, FastAPI, AWS
- 우대: Kubernetes, MLOps, SaaS 운영 경험
- 제외: QA 전담, SI 유지보수 위주
- 포트폴리오/GitHub/운영 서비스 경험이 보이면 높게 평가
- 유료 열람, 마스킹 해제, 포지션 제안, 스크랩, 메모는 하지 마
- 마스킹된 정보만 보고 유료 열람 추천 후보 Top 10을 뽑아줘6. 로그인 흐름
정상 동작하면 Claude가 잡코리아 기업 인재검색 페이지를 브라우저로 열고 로그인 여부를 확인한다.
로그인 세션이 없으면 Claude가 다음처럼 안내해야 한다.
잡코리아 인재검색은 경력 상세/포트폴리오/마스킹 이력서 확인을 위해 기업회원 로그인이 필요합니다.
제가 브라우저로 잡코리아 기업 인재검색 페이지를 열어둘게요.
열린 브라우저에서 직접 로그인해 주세요. 비밀번호나 인증정보는 저에게 알려주지 마세요.
로그인이 끝나면 “로그인했어”라고 알려주시면, 같은 브라우저 세션에서 검색을 이어가겠습니다.직원이 해야 할 일:
1. 열린 브라우저에서 잡코리아 기업회원으로 직접 로그인한다. 2. 로그인 완료 후 Claude에게 아래처럼 말한다.
로그인했어. 같은 브라우저 세션에서 계속 진행해줘.7. 좋은 검색 요청 작성법
검색 품질은 입력 조건에 크게 좌우된다. 아래 정보를 최대한 넣는다.
필수 정보:
- 포지션명
- 경력 범위
- 근무 지역
- 필수 기술
- 제외할 후보 유형
있으면 좋은 정보:
- 우대 기술
- 산업/도메인 경험
- 프로젝트 유형
- 포트폴리오/GitHub/블로그 선호 여부
- 학력/자격증/외국어 조건
- 현재 팀 소개와 후보에게 어필할 포인트
템플릿:
jobkorea-talent-search 스킬로 잡코리아 인재를 찾아줘.
포지션: <포지션명>
경력: <최소>~<최대>년
지역: <선호 지역>
필수 기술: <기술1>, <기술2>, <기술3>
우대 기술: <기술1>, <기술2>
제외 조건: <제외할 키워드/경력 유형>
높게 평가할 신호: <포트폴리오, 운영 경험, 특정 도메인 등>
결과 수: Top <N>
유료 열람/마스킹 해제/포지션 제안/스크랩/메모는 하지 말고,
마스킹된 정보만 보고 유료 열람할 후보를 추천해줘.8. 검색 요청 예시
프론트엔드 엔지니어
jobkorea-talent-search 스킬을 사용해서 잡코리아에서 프론트엔드 엔지니어 후보를 찾아줘.
조건:
- 경력: 3~7년
- 지역: 서울
- 필수: React, TypeScript, Next.js
- 우대: 디자인 시스템, 성능 최적화, B2B SaaS 경험
- 제외: 퍼블리싱만 담당, jQuery 위주, 단순 운영 업무 위주
- 포트폴리오/GitHub/실서비스 운영 경험이 보이면 높게 평가
- 유료 열람 추천 Top 10
유료 열람, 마스킹 해제, 포지션 제안, 스크랩, 메모는 하지 마.AI/ML 엔지니어
jobkorea-talent-search 스킬로 AI/ML 엔지니어 후보를 찾아줘.
조건:
- 경력: 4~10년
- 지역: 서울 또는 원격 가능
- 필수: Python, PyTorch 또는 TensorFlow, LLM/RAG 경험
- 우대: MLOps, Kubernetes, 벡터DB, 검색/추천 시스템
- 제외: 데이터 라벨링/운영만 한 이력, 연구만 하고 제품화 경험이 없는 이력
- 논문보다 실제 제품 적용/운영 경험을 높게 평가
- 유료 열람 추천 Top 8
마스킹된 정보만 사용하고 유료 액션은 하지 마.백엔드 엔지니어
jobkorea-talent-search 스킬을 사용해서 백엔드 엔지니어 후보를 찾아줘.
조건:
- 경력: 5~12년
- 지역: 서울/성남
- 필수: Java 또는 Kotlin, Spring Boot, MySQL, Redis
- 우대: 대용량 트래픽, Kafka, MSA, AWS
- 제외: 사내 시스템 유지보수만 오래 한 후보, PM 역할만 한 후보
- 실서비스 장애 대응/성능 개선/아키텍처 설계 경험을 높게 평가
- 유료 열람 추천 Top 10
연락처 확인이나 포지션 제안은 하지 말고 후보 평가표만 만들어줘.9. 기대 결과 형식
Claude의 결과는 대략 아래 형태여야 한다.
잡코리아 인재 shortlist
검색 조건
- 포지션: ...
- 필수 조건: ...
- 우대 조건: ...
- 제외 조건: ...
- 경력/지역: ...
- 모드: 로그인 마스킹 이력서
유료 열람 추천 Top N
1. 후보 A
- 점수: 88/100 — Strong paid-unlock candidate
- 근거: ...
- 보이는 경력/프로젝트: ...
- 기술스택: ...
- 포트폴리오/링크 신호: ...
- 리스크: ...
- 추천 액션: 인사팀 유료 열람 검토
- URL: ...
보류 후보
- ...
검색 한계
- 마스킹된 정보만 분석했음
- 연락처/실명/비공개 정보는 열람하지 않음
- 유료 액션은 실행하지 않음10. 결과를 해석하는 법
점수 기준:
- 85~100: 유료 열람 강력 추천
- 70~84: 인사팀 검토 가치 있음
- 55~69: 후보 풀이 약할 때만 보류 검토
- 55 미만: 유료 열람 비추천
항상 확인할 것:
- 점수보다 근거가 더 중요하다.
- “기술 키워드만 있음”과 “프로젝트에서 실제 사용함”은 다르다.
- 포트폴리오/GitHub/운영 경험이 보이는 후보를 우선 검토한다.
- 마스킹된 정보 기반이므로 최종 판단 전 유료 열람 후 재확인이 필요하다.
11. 문제 해결
설치 후 Claude가 스킬을 모르는 경우
설치 목록 확인:
npx skills list --agent claude-code없으면 다시 설치:
npx skills add ez-edu/jobkorea-talent-search --agent claude-codeClaude Code를 재시작한다.
# Claude Code 종료 후 재실행
claudeClaude가 비로그인 목록만 가져오는 경우
Claude에게 아래처럼 다시 지시한다.
비로그인 fallback으로 진행하지 마.
브라우저를 열어서 잡코리아 기업회원 로그인 세션을 확인하고,
로그인이 없으면 내가 직접 로그인할 수 있게 안내해줘.
로그인된 마스킹 이력서 본문을 읽을 수 없으면 검색 결과를 확정하지 말고 원인을 알려줘.macOS/Chrome 권한 문제가 나오는 경우
1. Claude Code를 실행한 앱을 확인한다.
- Terminal
- iTerm
- Visual Studio Code
2. 아래 권한을 켠다.
System Settings
→ Privacy & Security
→ Accessibility
→ Screen Recording
→ Automation3. 터미널/Claude Code/브라우저를 종료 후 다시 실행한다. 4. Claude가 별도 브라우저를 열면 그 브라우저 안에서 잡코리아에 다시 로그인한다.
브라우저는 열렸지만 로그인 상태가 공유되지 않는 경우
정상일 수 있다. Claude가 평소 Chrome이 아니라 별도 Chromium 프로필을 쓰면 쿠키가 공유되지 않는다.
해결:
Claude가 연 브라우저 창에서 잡코리아 기업회원 로그인을 직접 한다.유료 열람 버튼이 보이는 경우
Claude가 클릭하면 안 된다. 직원이 직접 판단해야 한다.
Claude에게 이렇게 말한다.
유료 열람 버튼은 누르지 말고, 현재 보이는 마스킹 정보만 기준으로 후보를 평가해줘.12. 보안 및 개인정보 주의사항
반드시 지킬 것:
- 잡코리아 비밀번호를 Claude에게 말하지 않는다.
- 2FA/인증번호를 Claude에게 말하지 않는다.
- 세션 쿠키를 복사하거나 공유하지 않는다.
- 후보자 연락처/실명/개인정보를 외부 문서나 공개 채널에 저장하지 않는다.
- 유료 열람 여부는 인사 담당자가 직접 판단한다.
- 포지션 제안 발송은 반드시 담당자가 직접 확인 후 진행한다.
권장:
- 결과는 사내 접근 제한 문서에만 저장한다.
- 후보 URL, 마스킹 이름, 평가 근거만 최소한으로 기록한다.
- 불필요한 후보 데이터는 보관하지 않는다.
13. 관리자용 업데이트 방법
스킬 저장소가 업데이트되면 직원은 아래 명령으로 갱신한다.
npx skills update jobkorea-talent-search또는 재설치:
npx skills add ez-edu/jobkorea-talent-search --agent claude-code로컬 개발 버전은:
cd /Users/wbjung/jobkorea-talent-search
npx skills add . --agent claude-code14. 빠른 시작 요약
직원이 처음부터 끝까지 실행할 최소 절차:
# 1. 스킬 설치
npx skills add ez-edu/jobkorea-talent-search --agent claude-code
# 2. Claude Code 실행
claudeClaude에 입력:
jobkorea-talent-search 스킬을 사용해서 잡코리아에서 우리 회사에 맞는 후보를 찾아줘.
비로그인 fallback으로 진행하지 말고 먼저 브라우저를 열어 기업회원 로그인 세션을 확인해줘.
로그인이 안 되어 있으면 내가 직접 로그인할 수 있게 안내해줘.
포지션: <포지션명>
경력: <연차>
지역: <지역>
필수 기술: <기술>
우대 조건: <우대 조건>
제외 조건: <제외 조건>
결과: 유료 열람 추천 Top 10
유료 열람/마스킹 해제/포지션 제안/스크랩/메모는 하지 마.브라우저가 열리면 직접 로그인하고 Claude에 입력:
로그인했어. 같은 브라우저 세션에서 계속 진행해줘.jobkorea-talent-search
Vercel Agent Skill for screening JobKorea talent search results with a corporate logged-in browser session.
이 스킬은 잡코리아 기업회원 로그인 상태에서 유료 열람/마스킹 해제 전에 볼 수 있는 마스킹된 인재 이력서를 검색·비교해, 인사팀이 돈을 써서 열람할 후보만 고르게 돕는다.
GitHub: https://github.com/ez-edu/jobkorea-talent-search
직원 설치 명령:
npx skills add ez-edu/jobkorea-talent-search --agent claude-codeStructure
jobkorea-talent-search/
README.md
SKILL.md
docs/
employee-claude-guide.md
scripts/
jobkorea_talent_search.pyEmployee guide
직원용 Claude Code 설치/사용 가이드는 아래 문서를 본다.
docs/employee-claude-guide.mdInstall with Vercel Skills CLI
For Claude Code users:
npx skills add ez-edu/jobkorea-talent-search --agent claude-codeFor all supported agents on the machine:
npx skills add ez-edu/jobkorea-talent-search --allIf the repository is private, authenticate GitHub first and verify access:
gh auth status
git ls-remote https://github.com/ez-edu/jobkorea-talent-search.gitPrimary workflow
1. 에이전트가 잡코리아 기업 인재검색 페이지를 브라우저로 연다. 2. 로그인 세션이 없으면 사용자에게 열린 브라우저에서 직접 로그인하라고 안내한다. 3. 사용자가 “로그인했어”라고 알려주면 같은 브라우저 세션에서 인재검색을 수행한다. 4. 유료 열람/마스킹 해제 전에 보이는 마스킹된 이력서 정보만 읽는다. 5. 후보를 점수화하고 shortlist를 만든다. 6. 유료 열람할 후보를 추천하되, 유료 열람/제안/스크랩/메모/연락처 확인은 자동 실행하지 않는다.
No-login fallback
비로그인 상태에서는 경력 상세/포트폴리오/이력서 본문을 정확히 볼 수 없다. 그래도 rough scouting이 필요할 때만 보조 스크립트를 사용한다.
python3 scripts/jobkorea_talent_search.py \
--keyword "React TypeScript" \
--job-category "AI·개발·데이터" \
--work-area "서울" \
--career-min 3 \
--career-max 8 \
--limit 20Safety boundary
Do not automate:
- paid resume unlock
- mask removal
- contact reveal
- proposal sending
- scrap/memo/status changes
- payment
- saving account passwords, session cookies, or personal candidate data
#!/usr/bin/env python3
"""Search public JobKorea talent summaries.
This helper uses JobKorea's browser-visible corporate talent search page and its
same AJAX endpoint. It only reads public/obfuscated list summaries. Full resume
view, contact details, scraping at scale, scrap/bookmark, and position proposal
flows are intentionally out of scope because they require an employer account,
paid entitlements, or user confirmation.
"""
from __future__ import annotations
import argparse
import html
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, asdict
from typing import Any
BASE_URL = "https://www.jobkorea.co.kr"
FIND_PATH = "/corp/person/find"
AJAX_PATH = "/corp/person/detailsearchajax"
DEFAULT_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36"
)
@dataclass
class Candidate:
rno: str
url: str
name: str = ""
meta: str = ""
career: str = ""
education: str = ""
locations: str = ""
salary: str = ""
skills: str = ""
badges: str = ""
raw_summary: str = ""
def fetch(url: str, *, data: bytes | None = None, headers: dict[str, str] | None = None) -> str:
req_headers = {"User-Agent": DEFAULT_UA, "Referer": BASE_URL + FIND_PATH}
if headers:
req_headers.update(headers)
req = urllib.request.Request(url, data=data, headers=req_headers, method="POST" if data else "GET")
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read().decode("utf-8", "ignore")
def extract_json_object(source: str, marker: str) -> dict[str, Any]:
idx = source.find(marker)
if idx < 0:
raise RuntimeError(f"cannot find marker: {marker}")
start = source.find("{", idx)
if start < 0:
raise RuntimeError("cannot find JSON object start")
depth = 0
in_string = False
escape = False
for pos in range(start, len(source)):
ch = source[pos]
if in_string:
if escape:
escape = False
elif ch == "\\":
escape = True
elif ch == '"':
in_string = False
continue
if ch == '"':
in_string = True
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return json.loads(source[start : pos + 1])
raise RuntimeError("unterminated JSON object")
def iter_nodes(node: Any):
if isinstance(node, dict):
yield node
for value in node.values():
yield from iter_nodes(value)
elif isinstance(node, list):
for item in node:
yield from iter_nodes(item)
def mark_matching_nodes(sc: dict[str, Any], top_key: str, labels: list[str]) -> list[str]:
if not labels:
return []
section = sc.get(top_key)
if section is None:
return []
wanted = [x.strip().lower() for x in labels if x.strip()]
matched: list[str] = []
for node in iter_nodes(section):
title = str(node.get("t", ""))
code = str(node.get("v", ""))
title_l = title.lower()
code_l = code.lower()
if any(w == title_l or w == code_l or w in title_l for w in wanted):
for k in ("s", "c", "use"):
if k in node:
node[k] = 1
matched.append(title or code)
return matched
def build_search_condition(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, Any]]:
first = fetch(BASE_URL + FIND_PATH)
sc = extract_json_object(first, "var searchcondition =")
sc["p"] = args.page
sc["ps"] = args.limit
sc["saveno"] = 0
sc["ff"] = 0
sc["sf"] = args.sort
terms: list[dict[str, Any]] = []
for kw in args.keyword:
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 0})
for kw in args.and_keyword:
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 1})
for kw in args.or_keyword:
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 3})
for kw in args.exclude_keyword:
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 2})
sc["totalkeywordlist"] = terms
if terms:
first_kw = terms[0]["t"]
sc.setdefault("pfr", {}).setdefault("ck", {})["Keyword"] = first_kw
sc["pfr"]["ck"]["KeywordType"] = 1
sc["pfr"]["n"] = 1
if args.career_min is not None:
sc.setdefault("career", {})["s"] = str(args.career_min)
if args.career_max is not None:
sc.setdefault("career", {})["e"] = str(args.career_max)
matched = {
"job_category": mark_matching_nodes(sc, "jobtype", args.job_category),
"work_area": mark_matching_nodes(sc, "workarea", args.work_area),
"residential_area": mark_matching_nodes(sc, "residentialarea", args.residential_area),
}
return sc, matched
def post_search(sc: dict[str, Any]) -> str:
body = urllib.parse.urlencode({"searchCondition": json.dumps(sc, ensure_ascii=False)}).encode()
return fetch(
BASE_URL + AJAX_PATH,
data=body,
headers={
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
)
def clean_text(value: str) -> str:
value = html.unescape(value)
value = re.sub(r"<script[\s\S]*?</script>", " ", value, flags=re.I)
value = re.sub(r"<style[\s\S]*?</style>", " ", value, flags=re.I)
value = re.sub(r"<[^>]+>", " ", value)
value = re.sub(r"[ \t\r\f\v]+", " ", value)
value = re.sub(r"\n\s*\n+", "\n", value)
return value.strip()
def parse_with_bs4(markup: str, limit: int) -> list[Candidate] | None:
try:
from bs4 import BeautifulSoup # type: ignore
except Exception:
return None
soup = BeautifulSoup(markup, "html.parser")
candidates: list[Candidate] = []
seen: set[str] = set()
for link in soup.select('a[href*="/corp/person/find/resume/view?rNo="]'):
href = link.get("href", "")
m = re.search(r"rNo=(\d+)", href)
if not m:
continue
rno = m.group(1)
if rno in seen:
continue
seen.add(rno)
container = link.find_parent(class_=re.compile(r"booth|list|row|person", re.I)) or link.parent
raw = clean_text(str(container)) if container else clean_text(str(link))
texts = [x.get_text(" ", strip=True) for x in (container.find_all(["dt", "dd", "p", "span", "button"]) if container else [])]
text_join = " | ".join(t for t in texts if t)
name = ""
meta = ""
dt = container.find("dt") if container else None
if dt:
name = dt.get_text(" ", strip=True)
dd = dt.find_next("dd")
if dd:
meta = dd.get_text(" ", strip=True)
if not name:
m_name = re.search(r"([가-힣A-Za-z]OO)\s*\(([^)]*)\)", raw)
if m_name:
name = m_name.group(1)
meta = "(" + m_name.group(2) + ")"
skills = []
for btn in container.select("button") if container else []:
label = btn.get_text(" ", strip=True)
if label and label not in {"스크랩", "포지션 제안", "메모하기", "프로필 확인", "이력서 확인"}:
skills.append(label)
candidates.append(
Candidate(
rno=rno,
url=urllib.parse.urljoin(BASE_URL, href),
name=name,
meta=meta,
career=(container.select_one(".career").get_text(" ", strip=True) if container and container.select_one(".career") else ""),
skills=", ".join(skills[:25]),
raw_summary=text_join[:1000] or raw[:1000],
)
)
if len(candidates) >= limit:
break
return candidates
def parse_with_regex(markup: str, limit: int) -> list[Candidate]:
candidates: list[Candidate] = []
seen: set[str] = set()
for m in re.finditer(r'href="(?P<href>/corp/person/find/resume/view\?rNo=(?P<rno>\d+))"', markup):
rno = m.group("rno")
if rno in seen:
continue
seen.add(rno)
start = max(0, m.start() - 1000)
end = min(len(markup), m.end() + 2500)
raw = clean_text(markup[start:end])
name = ""
meta = ""
nm = re.search(r"([가-힣A-Za-z]OO)\s*\(([^)]*)\)", raw)
if nm:
name = nm.group(1)
meta = "(" + nm.group(2) + ")"
candidates.append(
Candidate(
rno=rno,
url=urllib.parse.urljoin(BASE_URL, m.group("href")),
name=name,
meta=meta,
raw_summary=raw[:1000],
)
)
if len(candidates) >= limit:
break
return candidates
def parse_candidates(markup: str, limit: int) -> list[Candidate]:
parsed = parse_with_bs4(markup, limit)
if parsed is not None:
return parsed
return parse_with_regex(markup, limit)
def print_markdown(candidates: list[Candidate], matched: dict[str, Any], args: argparse.Namespace) -> None:
print(f"# 잡코리아 인재검색 결과\n")
print(f"- 검색어: {', '.join(args.keyword + args.and_keyword + args.or_keyword) or '(없음)'}")
print(f"- 제외어: {', '.join(args.exclude_keyword) or '(없음)'}")
if any(matched.values()):
print(f"- 매칭된 필터: {json.dumps(matched, ensure_ascii=False)}")
print(f"- 결과 수: {len(candidates)}")
print("- 주의: 이름/회사명은 잡코리아 공개 화면 기준으로 마스킹되어 있으며, 상세 이력서 확인·포지션 제안은 기업회원 로그인/권한/사용자 확인이 필요합니다.\n")
for idx, c in enumerate(candidates, 1):
bits = [c.name, c.meta, c.career]
title = " ".join(x for x in bits if x).strip() or f"rNo={c.rno}"
print(f"## {idx}. {title}")
print(f"- URL: {c.url}")
if c.skills:
print(f"- 키워드/스킬: {c.skills}")
summary = c.raw_summary.replace("\n", " ")
if summary:
print(f"- 요약: {summary[:500]}")
print()
def main() -> int:
parser = argparse.ArgumentParser(description="Search public JobKorea talent summaries")
parser.add_argument("--keyword", "-k", action="append", default=[], help="통합검색 키워드. 여러 번 지정 가능")
parser.add_argument("--and-keyword", action="append", default=[], help="AND 키워드")
parser.add_argument("--or-keyword", action="append", default=[], help="OR 키워드")
parser.add_argument("--exclude-keyword", action="append", default=[], help="제외 키워드")
parser.add_argument("--job-category", action="append", default=[], help="직무 대분류명 예: AI·개발·데이터")
parser.add_argument("--work-area", action="append", default=[], help="희망 근무지역 예: 서울, 강남구, 경기")
parser.add_argument("--residential-area", action="append", default=[], help="거주지역 예: 서울, 성남시 분당구")
parser.add_argument("--career-min", type=int, help="최소 경력 연수")
parser.add_argument("--career-max", type=int, help="최대 경력 연수")
parser.add_argument("--page", type=int, default=1)
parser.add_argument("--limit", type=int, default=20, choices=[10, 20, 30, 50, 100])
parser.add_argument("--sort", default="0", help="잡코리아 sf 정렬 코드. 기본 0")
parser.add_argument("--json", action="store_true", help="JSON으로 출력")
args = parser.parse_args()
if not (args.keyword or args.and_keyword or args.or_keyword or args.job_category or args.work_area or args.residential_area):
parser.error("최소 하나 이상의 --keyword, --job-category, --work-area 등을 지정하세요")
sc, matched = build_search_condition(args)
markup = post_search(sc)
if "로그인" in clean_text(markup)[:500] and "인재" not in clean_text(markup)[:2000]:
raise RuntimeError("잡코리아가 로그인/차단 화면을 반환했습니다")
candidates = parse_candidates(markup, args.limit)
if args.json:
print(json.dumps({"matched_filters": matched, "candidates": [asdict(c) for c in candidates]}, ensure_ascii=False, indent=2))
else:
print_markdown(candidates, matched, args)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except urllib.error.HTTPError as exc:
print(f"HTTP error: {exc.code} {exc.reason}", file=sys.stderr)
raise SystemExit(2)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1)