SLA 영업일 계산으로 서비스 수준 계약 위반 타이머 운영하기

영업일 기준 위반 타이머는 계약이 달력, 기산일, 시간대를 명시하지 않으면 양쪽 방향으로 모두 틀립니다. Narwhal은 국가와 연도별 공휴일을 제공하고, 나머지 규칙은 데스크의 코드와 계약이 정합니다.

계약에서 지정할 수 있는 달력으로 타이머 시작하기

공휴일에도 타이머가 계속 돌면 계약상 허용되지 않은 위반이 표시됩니다. 계약이 제외하지 않은 날에 타이머가 멈추면 실제 위반이 가려집니다. 해결책은 양쪽이 가리킬 수 있는 달력입니다. 이 요청은 싱가포르의 2026년 공휴일을 읽고 Deepavali 전후의 두 항목만 남깁니다.

curl, 한 나라와 한 해의 공휴일
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/calendars/holidays/SGP/2026"
응답, 두 개의 항목
{
  "country": "SGP",
  "coverage": [
    {
      "scope": "national_public",
      "subdivision_codes": []
    }
  ],
  "holidays": [
    {
      "date": "2026-11-08",
      "name": "Deepavali",
      "holiday_key": "deepavali",
      "scope": "national_public",
      "subdivision_codes": []
    },
    {
      "date": "2026-11-09",
      "name": "Deepavali (Observed)",
      "holiday_key": "deepavali",
      "scope": "national_public",
      "subdivision_codes": [],
      "observed": true
    }
  ]
}

각 항목에는 date, name, scope, subdivision_codes가 있습니다. Deepavali의 날짜는 11월 8일 일요일입니다. observed: true가 붙은 항목은 11월 9일 월요일의 대체 휴일이며 평일에 해당합니다. coverage 목록은 달력이 가진 scope를 알려 주며, 싱가포르는 national_public입니다. API는 공휴일만 제공하므로 타이머의 나머지는 계약으로 정합니다.

코드를 실행하기 전에 계약에서 정해야 할 것

코드는 표의 각 행에 대한 답이 필요합니다. 계약이 아무 말이 없으면 데스크와 고객은 처음으로 다툼이 생긴 티켓에서 그 사실을 알게 됩니다.

설정계약에 던질 질문누가 제공하는가
캘린더어느 나라이고, 어떤 공휴일 scope인가?계약, 그다음 API의 scope 필터
계산하는 날토요일과 일요일은 제외하는가?계약
첫날접수한 날은 0일째인가 1일째인가?계약
기한기한은 그날이 끝날 때인가?계약
시간대어느 지역의 현지 날짜로 시계를 시작하는가?계약
일시 정지 시간고객이 답하는 동안 시계가 멈추는가?계약과 티켓 시스템
하루의 시간영업일은 업무 시간을 뜻하는가?계약과 지원 일정

각 행에 서면으로 된 답이 있으면 타이머는 판단이 남지 않는 함수가 됩니다. API가 정하는 것은 달력 행뿐이고, 그 밖의 것은 정하지 않습니다.

마감일을 계산하고 위반 여부 확인하기

스크립트는 공휴일 날짜를 집합으로 모으고, 집합에 없는 평일을 영업일로 취급합니다. 접수일이 0일째이므로 계산은 다음 날부터 시작합니다. 첫 번째 블록은 티켓의 타임스탬프를 계약 시간대의 날짜로 변환하고, 두 번째 블록은 해결된 티켓을 검사합니다.

python, 영업일 기준 위반 타이머
import json, os, urllib.request
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo

def get(path):
    req = urllib.request.Request("https://api.narwhalapi.com" + path,
                                 headers={"Authorization": "Bearer " + os.environ["NARWHAL_API_KEY"]})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)

def holiday_dates(country, years):
    return {date.fromisoformat(h["date"]) for y in years for h in get(f"/v1/calendars/holidays/{country}/{y}")["holidays"]}

def is_working_day(d, off):
    return d.weekday() < 5 and d not in off

def due_date(opened, working_days, off):
    d, left = opened, working_days
    while left:
        d += timedelta(1)
        left -= is_working_day(d, off)
    return d

def elapsed_working_days(opened, resolved, off):
    return sum(is_working_day(opened + timedelta(n), off) for n in range(1, (resolved - opened).days + 1))

off = holiday_dates("SGP", (2026,))
sg = ZoneInfo("Asia/Singapore")

# Clock start: the ticket timestamp becomes a date in the contract's time zone.
created = datetime(2026, 11, 5, 20, 0, tzinfo=ZoneInfo("America/New_York"))
opened = created.astimezone(sg).date()
print("created", created.date(), "in New York is", opened, "in Singapore")
print("3 working days from the Singapore date:", due_date(opened, 3, off))
print("3 working days from the New York date: ", due_date(created.date(), 3, off))

# Breach test: compare the resolution date with the due date.
opened, resolved = date(2026, 2, 13), date(2026, 2, 27)
due = due_date(opened, 5, off)
late = elapsed_working_days(opened, resolved, off) - 5
print("5 working days from", opened, "is due", due, "; resolved", resolved, "so", late, "working days late")
생산
created 2026-11-05 in New York is 2026-11-06 in Singapore
3 working days from the Singapore date: 2026-11-12
3 working days from the New York date:  2026-11-11
5 working days from 2026-02-13 is due 2026-02-24 ; resolved 2026-02-27 so 3 working days late

뉴욕의 티켓은 11월 5일 목요일 20:00에 만들어졌고, 싱가포르에서는 이미 11월 6일 금요일입니다. 싱가포르 날짜로 영업일 3일을 세면 기한은 11월 12일 목요일입니다. 11월 9일 월요일이 Deepavali 대체 휴일이기 때문입니다. 뉴욕 날짜로 세면 같은 티켓의 기한은 11월 11일 수요일로 하루 빨라지므로, 시간대 행이 데스크의 위반 여부를 가릅니다. 두 번째 티켓은 2월 13일 금요일에 접수되어 설날 연휴가 2월 17일 화요일과 18일 수요일이므로 기한은 2월 24일 화요일입니다. 2월 27일 금요일에 해결되었으니 영업일 기준 3일 늦은 것입니다.

주말에 겹친 공휴일은 타이머가 어떻게 처리합니까?

평일 판정이 먼저 그날을 제외하므로 두 번 빼지 않습니다. 싱가포르 응답에서 Hari Raya Puasa는 2026년 3월 21일 토요일, Deepavali는 11월 8일 일요일입니다. 둘 다 계산을 바꾸지 않습니다. 대체 휴일은 observed: true가 붙은 별도 항목이므로 추가 코드 없이 시계가 제외합니다.

SLA에는 어느 scope를 써야 합니까?

고객의 사업이 따르는 달력에 맞는 scope를 사용합니다. 싱가포르는 national_public만 돌려줍니다. 인도네시아는 national_public과 collective_leave를 돌려주고, 호주는 subdivision_codes가 있는 지역 항목과 bank scope를 돌려줍니다. 연도 엔드포인트에는 선택 사항인 scope 필터가 있습니다. SLA가 특정 종류의 날을 제외할 때 지정하고, 계산을 다시 재현할 수 있도록 모든 요청에 같은 scope를 쓰세요.

API가 업무 시간을 계산할 수 있습니까?

아니요. 달력 엔드포인트는 공휴일 날짜를 돌려줍니다. 영업 시간, 점심시간, 고객 답변 대기 중의 일시 정지는 지원 일정과 티켓 시스템에서 가져옵니다. 시간 단위로 쓰인 SLA라면 이 코드로 영업일을 구하고, 첫날과 마지막 날에 일정을 적용하세요.

미해결 티켓이 많을 때 요청 수를 줄이려면?

국가와 연도별로 한 번만 가져오고, 날짜 집합을 메모리나 자체 테이블에 보관합니다. 그 국가와 연도의 모든 티켓이 같은 집합을 재사용합니다. 아직 불러오지 않은 해에 걸치는 티켓이 있을 때만 연도 엔드포인트를 다시 호출하세요. 두 날짜 사이의 영업일 계산하기 가이드에 스프레드시트 방식을 포함한 같은 계산이 나와 있습니다.

에 게시되었습니다.

다음 단계