본문으로 건너뛰기

채용 게시판 미러링

Tahoe 공고를 내 사이트에 보여 주고 최신으로 유지하세요.

채용 게시판 미러링은 고객이 Tahoe에 게시 중인 채용 공고를 채용 사이트, 채용 공고 게시판, 사내 인트라넷 같은 다른 곳에 보여 주고, 그 사본을 최신 상태로 유지하는 연동입니다. 가장 흔한 연동이면서, 실수가 가장 눈에 잘 띄는 연동이기도 합니다. 초안이 공개되거나, 채용이 끝난 포지션이 계속 걸려 있는 경우입니다. 방법은 스코프 두 개, 백필(전체 복사) 한 번, 그다음은 이벤트입니다.

필요한 것

  • jobs:read: 채용 공고 자체를 읽습니다.
  • events:read: 모든 공고를 주기적으로 다시 읽는 대신 변경 사항을 전달받습니다.

이 두 스코프만 있으면 변경 피드에는 채용 공고 이벤트와 삭제 통지만 들어옵니다. 키를 받는 방법은 API 키와 스코프를 참고하세요.

미러 만들기

워터마크를 먼저 기록하고 백필하기

GET /jobs를 limit=100으로 한 번 끝까지 넘기며 읽으세요. 게시판에는 게시 중인 공고만 보여 주므로 status=published를 지정하세요. 기본 필터는 published,closed이고, 초안은 include_unpublished=true를 넘기지 않는 한 반환되지 않습니다. 따라서 파라미터를 빠뜨려서 초안이 게시되는 일은 생기지 않습니다.

콜드 스타트
import os

import requests

BASE = "https://tahoe.workonward.com/api/partner/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TAHOE_API_KEY']}"


def current_sequence():
    """The sequence of the newest event this key can see, or None if none."""
    after, last = None, None
    while True:
        params = {"limit": 100}
        if after is not None:
            params["after"] = after
        response = session.get(f"{BASE}/events", params=params, timeout=30)
        response.raise_for_status()
        body = response.json()
        if body["data"]:
            last = body["data"][-1]["sequence"]
        if not body["has_more"]:
            return last
        after = body["next_after"]


def backfill_jobs(store):
    cursor = None
    while True:
        # Send the same filters on every page: a cursor is tied to them.
        params = {"status": "published", "limit": 100}
        if cursor:
            params["cursor"] = cursor
        response = session.get(f"{BASE}/jobs", params=params, timeout=30)
        response.raise_for_status()
        body = response.json()

        for job in body["data"]:
            store.upsert_job(job)

        # next_cursor is the only correct way to stop. A short page can still
        # have more rows behind it.
        cursor = body["next_cursor"]
        if not cursor:
            return


def cold_start(store):
    watermark = current_sequence()  # 1. BEFORE the backfill
    backfill_jobs(store)            # 2. copy every published job
    store.save_sequence(watermark)  # 3. saved only once the copy is complete

보여 줄 공고의 본문 가져오기

목록의 행에는 summary는 있지만 전체 설명은 없습니다. 단건 조회는 설명, 담당 업무, 자격 요건, 기술, 복리후생을 담은 content를 반환합니다. 공고 페이지를 렌더링할 때 가져오거나, 미리 렌더링한다면 동기화할 때 공고마다 한 번 가져오세요. 목록에만 보여 주는 공고는 가져오지 마세요.

요청
curl https://tahoe.workonward.com/api/partner/v1/jobs/job_3Hn6tWq9Lc2v \
  -H "Authorization: Bearer $TAHOE_API_KEY"
응답 (일부 생략)
{
  "object": "job",
  "id": "job_3Hn6tWq9Lc2v",
  "workspace_id": "wsp_4Kd8sPm2Qx7L",
  "slug": "field-coordinator-columbus-7f3a",
  "status": "published",
  "title": "Field Coordinator",
  "department": "Operations",
  "employment_type": "full_time",
  "location_type": "onsite",
  "locations": ["Columbus, Ohio"],
  "experience_level": "mid",
  "compensation": {
    "salary_min": 52000,
    "salary_max": 64000,
    "currency": "USD",
    "interval": "annual",
    "unit": "major",
    "equity": false,
    "commission": false
  },
  "accept_applications": true,
  "external_apply_url": null,
  "published_at": "2026-09-02T14:00:00.000Z",
  "updated_at": "2026-09-08T11:20:45.112Z",
  "content": {
    "summary": "Schedule and support field crews across central Ohio job sites.",
    "description_md": "## About the role\nYou will plan daily crew schedules ...",
    "responsibilities": ["Plan daily crew schedules", "Track equipment across sites"],
    "requirements": ["Two years coordinating field or site teams"],
    "nice_to_have": ["A valid driver's license"],
    "skills_required": ["Scheduling", "Spreadsheets"],
    "skills_preferred": [],
    "benefits": ["Health insurance", "Company vehicle"]
  },
  "links": {
    "self": "/api/partner/v1/jobs/job_3Hn6tWq9Lc2v",
    "applications": "/api/partner/v1/jobs/job_3Hn6tWq9Lc2v/applications"
  }
}

content.description_md는 고객이 작성한 Markdown입니다. Markdown으로 렌더링하고, 페이지에 넣기 전에 HTML을 정제(sanitize)하세요.

지원자를 올바른 곳으로 안내하기

지원 버튼을 그리기 전에 세 필드를 읽으세요.

  • accept_applications: false이면 지원 버튼을 보여 주지 마세요. 포지션은 계속 보이지만 새 지원은 받지 않습니다.
  • external_apply_url: 값이 있으면 지원은 그 주소로 보내야 합니다. Tahoe는 신뢰하는 호스트의 HTTPS 주소일 때만 이 값을 반환하므로 그대로 링크해도 안전합니다. null인 경우가 많습니다.
  • slug: 외부 URL이 없으면 Tahoe가 호스팅하는 공고 페이지로 링크하세요. 주소는 https://tahoe.workonward.com/jobs/ 뒤에 slug를 붙인 것입니다.

채용 공고 이벤트로 최신 상태 유지하기

워터마크부터 변경 피드를 읽거나 웹훅을 받아, 채용 공고 이벤트 일곱 가지를 처리하세요. 그중 여섯 가지는 규칙 하나로 충분합니다. 공고를 다시 읽고 저장하면 됩니다.

채용 공고 이벤트 핸들러
def handle_job_event(store, event):
    if not event["type"].startswith("job."):
        return  # erasure notices name people, and a job board holds none

    handle = event["data"]["object"]["id"]
    if event["type"] == "job.deleted":
        store.delete_job(handle)
        return

    # Every other job event: read the job again. The payload is thin on
    # purpose, and a fresh read is always the current state.
    response = session.get(f"{BASE}/jobs/{handle}", timeout=30)
    if response.status_code == 404:
        store.delete_job(handle)  # deleted after the event was written
        return
    response.raise_for_status()
    store.upsert_job(response.json())

    # The board shows a job only while its stored status is "published".
    # job.closed, job.unpublished and job.reopened all come down to that.
이벤트내 게시판에서 일어나는 일
job.published공고가 나타납니다.
job.updated, job.sections_updated공고가 갱신됩니다.
job.closed공고가 내려갑니다. 마감된 포지션도 남겨 두는 게시판이라면 마감으로 표시됩니다.
job.reopened마감된 공고가 다시 게시됩니다.
job.unpublished공고가 초안으로 돌아가 내려갑니다. 다시 게시되면 job.published를 받습니다.
job.deleted공고가 내 테이블에서 삭제됩니다.

흔히 생기는 네 가지 실수

  • 초안 게시. include_unpublished=true를 넘길 때만 생깁니다. 공개 게시판에서는 절대 쓰지 마세요.
  • 마감된 포지션을 그대로 두기. job.closed와 job.unpublished를 처리하고, accept_applications: false는 “지원 버튼 없음”으로 처리하세요.
  • slug를 키로 쓰기. 제목이 처음 바뀔 때 중복이 생깁니다.
  • 이벤트를 받지 않고 폴링하기. 15분마다 모든 공고를 다시 읽으면 얻는 것 없이 속도 제한만 소진하고, 그래도 최대 15분까지 늦습니다. 이벤트를 받으세요. 안전망이 필요하다면 하루에 한 번 GET /jobs?updated_after=로 확인하세요.

다음 단계