운전면허증 확인 API 연동: 일치 결과와 판정 불가를 구분하는 방법

운전면허증 확인 화면에서 외부 조회가 늦어졌다는 이유로 “면허 정보가 일치하지 않습니다”라고 표시하면 어떤 일이 생길까요? 이용자는 정확한 정보를 반복 입력하고, 운영자는 정상적인 요청을 불일치 건으로 분류하게 됩니다. APICK의 텍스트 기반 운전면허증 확인 API는 일치·불일치 외에 판정 확인 실패와 처리 시간 초과를 구분하므로, 연동 단계에서 이 차이를 살리는 것이 중요합니다.
이 글은 인적사항과 면허번호의 일치 확인이 필요한 업무를 준비하는 개발자와 도입 담당자를 위한 안내입니다. API의 결과를 최종 서비스 승인과 분리하고, 확인을 끝내지 못한 이용자가 정상적인 재확인 경로로 돌아갈 수 있도록 설계합니다. 설명은 공개 명세를 기준으로 하며 실제 면허정보를 전송하는 시험 호출은 수행하지 않았습니다.
텍스트 방식이 맡을 수 있는 확인 범위
APICK의 /rest/identi_card/2는 생년월일, 성명, 면허번호를 입력받는 텍스트 방식입니다. 이미 적법하게 수집한 텍스트 정보의 일치 여부를 업무에 연결할 때 검토할 수 있습니다. 이미지 업로드나 OCR 단계를 이 요청에 필수로 덧붙일 필요는 없습니다. 이미지를 따로 수집한다면 그 목적과 처리 조건도 별도로 설계해야 합니다.
공식 상품명에 진위 확인이 포함되어 있어도, 응답 하나로 실물 카드의 위변조, 제출자의 신원이나 현존성, 현재의 운전 자격 전체가 입증된다고 확대 해석하면 안 됩니다. 차량 관련 서비스라면 이 확인 결과와 최종 이용 승인을 서로 다른 단계로 두고, 업무에 필요한 추가 확인 범위를 담당자가 정하는 편이 좋습니다.
수집 전에는 개인정보 처리 근거, 공급자의 이용 자격과 허용 목적, 전송·접근 통제, 보유·파기 기준을 확인하세요. 선택 필드가 있다는 사실이 수집할 이유를 만들어 주지는 않습니다. 특히 주민등록번호는 동의만으로 처리할 수 있다고 전제하지 말고 개인정보 보호법 제24조의2의 처리 제한과 개별 처리 근거를 검토해야 합니다. 이 글의 구현은 주민등록번호를 수집하거나 보내지 않습니다.
필수 입력만 모아 요청 만들기
필수 필드는 출생연도 birth_y, 월 birth_m, 일 birth_d, 성명 name, 면허번호의 네 구획 licen_no0부터 licen_no3까지 총 여덟 개입니다. 모두 문자열로 전달하고 월·일 및 번호 구획의 앞자리 0이 사라지지 않도록 합니다. 앱 내부에서 숫자형으로 변환한 뒤 다시 문자열로 만드는 흐름은 피하세요.
ghost_num은 선택 항목이며 공식 문서에 전달값이 판정에 사용되지 않는다고 명시되어 있습니다. 따라서 이 연동을 위해 수집하지 않습니다. 선택 항목인 rrn1과 rrn2도 아래 요청에서 제외합니다. 서버가 받은 전체 객체를 그대로 외부로 보내지 않고 필요한 키만 고르는 것이 좋습니다.
결과 코드와 HTTP 상태를 함께 읽기
data.result === 1: 일치 결과입니다. 이 확인 단계를 완료로 처리합니다.data.result === 0: 불일치 결과입니다. 입력 내용을 확인하도록 안내합니다.data.result === 2: 판정 확인 실패이며, 문서상 HTTP 424와 무과금으로 안내됩니다.data.result === 3: 처리 시간 초과이며, 문서상 HTTP 408과 무과금으로 안내됩니다.
HTTP 오류를 만나자마자 예외를 던지면 424·408 본문에 담긴 업무 결과를 놓치기 쉽습니다. 먼저 JSON을 안전하게 읽고, 상태 코드와 결과 숫자를 확인한 뒤 내부 상태로 바꾸세요. 반대로 HTTP 200이라는 이유만으로 통과시켜서도 안 됩니다. api.success는 서버의 정상 응답 여부, data.success는 과금 관련 필드이며 실제 일치 판정과 구별해야 합니다.
curl 요청 예시
요청은 POST, Bearer 인증, multipart/form-data 조합입니다. 아래 값은 실제 신분정보가 아닌 명시적인 자리표시자입니다. 실제 값을 셸 이력·협업 문서·CI 로그에 남기지 않도록, 운영 처리는 보호된 서버 경로에서 진행하세요. 이 예시는 응답 본문을 출력하지 않습니다.
# 요청 구조 설명용입니다. 실제 개인정보로 치환해 실행하지 마세요.
curl --fail-with-body --silent --show-error \
--request POST 'https://apick.app/rest/identi_card/2' \
--header "Authorization: Bearer $APICK_API_KEY" \
--form-string 'birth_y=<보안_서버_요청의_출생연도>' \
--form-string 'birth_m=<보안_서버_요청의_출생월>' \
--form-string 'birth_d=<보안_서버_요청의_출생일>' \
--form-string 'name=<보안_서버_요청의_성명>' \
--form-string 'licen_no0=<보안_서버_요청의_면허번호_1구획>' \
--form-string 'licen_no1=<보안_서버_요청의_면허번호_2구획>' \
--form-string 'licen_no2=<보안_서버_요청의_면허번호_3구획>' \
--form-string 'licen_no3=<보안_서버_요청의_면허번호_4구획>' \
--output /dev/null --write-out 'HTTP %{http_code}\n'
multipart 경계값은 클라이언트가 설정하도록 Content-Type 헤더를 직접 고정하지 않았습니다. API 키 역시 서버에서만 사용해야 합니다. 화면에서 APICK으로 직접 요청하면 키와 입력값의 통제를 어렵게 만들 수 있으므로, 사용자 화면과 공급자 API 사이에 권한을 검증하는 서버를 둡니다.
Node.js에서 네 가지 업무 결과 분리하기
다음은 기본 fetch와 FormData를 지원하는 Node.js 서버용 예시입니다. 함수 바깥에서 요청자 인증, 업무상 호출 권한, 생년월일의 실제 달력 유효성과 각 필드의 공식 입력 규격을 검증해야 합니다. 예제의 비어 있지 않은 문자열 검사는 완성된 입력 검증기를 대신하지 않습니다.
// Node.js 서버 ESM. API 키와 입력값은 브라우저로 전달하지 않습니다.
/** @typedef {{birth_y:string, birth_m:string, birth_d:string, name:string,
* licen_no0:string, licen_no1:string, licen_no2:string, licen_no3:string}} SecureInput */
/** @typedef {'match'|'mismatch'|'unconfirmed'|'provider_timeout'|
* 'http_error'|'invalid_response'|'transport_unknown'|'input_error'|'config_error'} State */
/** @typedef {{state:State, httpStatus:number|null, costPoints:number|null}} Outcome */
const fields = ["birth_y", "birth_m", "birth_d", "name",
"licen_no0", "licen_no1", "licen_no2", "licen_no3"];
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
/** @param {SecureInput} secureInput
* 보호된 서버 요청에서 받은 값입니다. 처리 권한과 입력 규격을 먼저 검증하세요.
* @returns {Promise<Outcome>} */
export async function verifyDriverLicense(secureInput) {
const done = (state, httpStatus = null, costPoints = null) =>
({ state, httpStatus, costPoints });
const key = process.env.APICK_API_KEY;
if (!key) return done("config_error");
if (!isObject(secureInput) || fields.some((field) =>
typeof secureInput[field] !== "string" || !secureInput[field].trim())) {
return done("input_error");
}
const form = new FormData();
// 허용 목록만 전송: 선택 주민번호와 ghost_num은 포함하지 않습니다.
for (const field of fields) form.append(field, secureInput[field]);
let response;
try {
response = await fetch("https://apick.app/rest/identi_card/2", {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
signal: AbortSignal.timeout(120_000),
});
} catch {
// 로컬 타임아웃은 공급자가 반환한 result=3과 구분합니다.
return done("transport_unknown");
}
// !response.ok 검사보다 먼저 JSON을 읽어 424/408의 결과를 보존합니다.
// 원문 본문과 예외 객체는 로그나 이용자 화면에 출력하지 않습니다.
const body = await response.json().catch(() => null);
const data = isObject(body?.data) ? body.data : null;
const api = isObject(body?.api) ? body.api : null;
const cost = Number.isSafeInteger(api?.cost) && api.cost >= 0 ? api.cost : null;
if (response.status === 424 && data?.result === 2) {
return done("unconfirmed", response.status, cost);
}
if (response.status === 408 && data?.result === 3) {
return done("provider_timeout", response.status, cost);
}
if (!response.ok) return done("http_error", response.status, cost);
if (api?.success !== true || data?.type !== 2 || !Number.isInteger(data?.result)) {
return done("invalid_response", response.status, cost);
}
if (data.result === 1) return done("match", response.status, cost);
if (data.result === 0) return done("mismatch", response.status, cost);
// 문서와 다른 HTTP/결과 조합도 통과시키지 않고 별도 확인합니다.
return done("invalid_response", response.status, cost);
}
424와 결과 2, 408과 결과 3의 조합은 각각 확인 대기와 공급자 시간 초과로 반환합니다. 그 밖의 비정상 HTTP 상태나 예상하지 못한 결과 조합은 통과시키지 않습니다. 숫자 1을 문자열 “1”과 섞어 처리하지 않고, 정상 결과에서는 문서의 신분증 종류 값 2도 확인합니다. 이용자 화면은 공급자의 메시지 원문 대신 서비스가 정의한 안내 문구를 사용합니다.
클라이언트가 120초 뒤 요청을 중단한 경우는 공급자가 HTTP 408과 결과 3을 돌려준 경우와 다릅니다. 응답을 받지 못하면 실제 완료·과금 상태를 확정할 수 없으므로 transport_unknown으로 분리했습니다. 이 상태를 문서의 무과금 시간 초과로 단정해서 비용을 집계하면 안 됩니다.
입력 정정과 일시 장애의 사용자 경험 나누기
입력 불일치에는 성명, 생년월일, 번호 구획을 다시 확인할 수 있는 경로를 제공합니다. 확인 불가에는 “현재 확인을 완료하지 못했습니다. 잠시 후 다시 시도해 주세요”처럼 상태를 설명합니다. 결과 2·3을 불일치 횟수에 포함해 계정을 자동 제한하거나 고객을 부정 이용자로 분류하지 않도록 주의하세요.
업무 상태는 처리 중, 일치, 입력 확인, 재확인 대기 정도로 나누면 운영자가 원인을 찾기 쉽습니다. 중복 클릭은 무작위 작업 ID를 기준으로 서버에서 합치고, 확인 대기 건의 재시도는 횟수·간격·최대 대기시간을 제한합니다. APICK이 이 엔드포인트의 중복 호출을 자동으로 합쳐 준다고 가정하지 마세요. 인증 설정이나 요청 형식 문제는 재시도 전에 원인을 수정해야 합니다.
상담용 로그에는 면허번호와 생년월일을 남기지 않고 작업 ID, 정규화한 결과 상태, HTTP 상태, 확인 시간과 필요한 결제 메타데이터만 남깁니다. 요청·응답 전체를 저장하는 디버그 코드와 APM 본문 수집도 점검하세요. 재확인용 정보를 별도로 유지해야 한다면 접근 가능한 사람, 보유기간, 파기 시점과 보호 방식을 먼저 정해야 합니다.
40P 요금과 재조회 비용 계산하기
2026년 10월 11일 공개 요금표 기준 텍스트 운전면허증 진위 확인은 건당 40P입니다. 설명용으로 월 과금 대상 호출 1,500건을 예상하면 60,000P입니다. 이후 과금 대상 재조회 50건이 발생한다면 추가 2,000P를 반영해 62,000P로 계산합니다. 수치는 예산 산식 설명을 위한 가정이며 실제 고객 이용량과 무관합니다.
문서에서 무과금이라고 명시한 결과 2·HTTP 424, 결과 3·HTTP 408은 별도 집계하되, 모든 실패가 무과금이라고 확대하지 마세요. api.cost와 결제 내역을 대조하고 불일치 건도 표시 요금과 실제 과금 상태를 확인해야 합니다. 원화 환산·부가세·계약별 조건은 이 글에서 확정하지 않았습니다.
개인정보 없이 먼저 확인할 테스트
모의 응답으로 일치, 불일치, 424와 결과 2, 408과 결과 3을 준비하면 핵심 화면 분기를 검증할 수 있습니다. JSON 대신 HTML이 온 경우, 결과가 문자열인 경우, 신분증 종류가 다른 경우, 응답 없이 연결이 끊긴 경우도 포함하세요. 테스트 데이터에는 실제 또는 실제처럼 보이는 면허번호를 쓰지 않고 자리표시자를 사용합니다. 화면 테스트와 실제 API 검증은 구분하고, 운영 전 공급자와 허용된 시험 방법을 확인하세요.
자주 묻는 질문
판정 확인 실패가 나오면 면허증이 잘못된 건가요?
그렇게 판단할 수 없습니다. 결과 2는 확인을 끝내지 못한 상태이므로 불일치와 분리해 재확인 대기로 처리합니다. 공급자 오류가 이용자의 불이익으로 바로 이어지지 않게 설계하세요.
식별번호나 주민등록번호를 더 보내면 판정이 좋아지나요?
ghost_num은 공식 문서상 판정에 사용되지 않습니다. 선택 주민등록번호를 추가로 보내 정확도가 높아진다는 근거도 이 안내에서는 확인하지 않았습니다. 이 예제는 필수 여덟 필드만 사용합니다.
일치 결과를 받으면 바로 운전 가능한 사람으로 승인해도 되나요?
이 API의 텍스트 일치 결과만으로 현재 운전 자격 전체나 제출자 본인 여부를 확정하면 안 됩니다. 서비스가 요구하는 추가 확인과 승인 기준을 별도 절차로 마련해야 합니다.
APICK 운전면허증 확인 API 도입 검토하기
입력 필드를 최소화하고 판정 불가를 별도 상태로 두면, 확인 결과와 장애 대응을 같은 화면에서 명확하게 운영할 수 있습니다. APICK 운전면허증 확인 개발 가이드에서 최신 규격을 확인하고, 공식 요금표에서 예상 호출량에 맞는 예산을 검토해 보세요.
기준일: 2026년 10월 11일. 공개 기술 문서와 요금표를 바탕으로 작성한 정적 예제입니다. 실제 호출, 공급자 계약 및 개별 사업자의 처리 자격 검증은 수행하지 않았습니다.
댓글
댓글 쓰기