# UFace 라이브니스 연동 가이드

라이브니스는 사용자가 얼굴 방향을 바꾸거나 눈/입 동작을 수행하도록 안내하고, SDK가 진행 상태와 결과를 제공하는 기능입니다.

자동 캡처만 필요하면 `uface-auto-capture-interface-kr.md`를 먼저 보세요. 이 문서는 얼굴 방향, 눈 깜빡임, 입 벌림 같은 챌린지가 필요한 페이지용입니다.

## 가장 먼저 확인할 것

- SDK 패키지 전체를 같은 정적 경로에 배포했나요?
- 페이지가 HTTPS 또는 신뢰된 WebView origin에서 열리나요?
- `video` 태그에 `autoplay playsinline muted`가 있나요?
- 앱 코드는 `uface-liveness-sdk.js`와 필요한 공개 helper만 import하나요?

## 최소 예제

```html
<video id="faceVideo" autoplay playsinline muted></video>
<p id="instruction">카메라를 준비하는 중입니다.</p>
<progress id="progress" max="100" value="0"></progress>

<script type="module">
  import { createSession } from "./uface-liveness-sdk.js";

  const session = createSession({
    videoEl: document.getElementById("faceVideo"),
    challenge: {
      directions: {
        count: 4,
        diagonal: false
      },
      eyeAction: {
        mode: "close-and-return",
        target: "both"
      },
      mouthAction: {
        mode: "off"
      },
      capture: {
        mode: "single"
      }
    },
    quality: {
      blur: "auto",
      threshold: 0.35,
      faceDistance: {
        mode: "on",
        tooFarRatio: 0.45,
        tooNearRatio: 0.72
      },
      facePosition: {
        mode: "gate",
        targetX: 0.5,
        targetY: 0.5,
        toleranceX: 0.18,
        toleranceY: 0.2
      }
    }
  });

  session.onState((state) => {
    instruction.textContent = state.prompt.text;
    progress.value = state.progress.percent;
  });

  session.onResult(async (result) => {
    await fetch("/api/liveness-result", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(result)
    });
  });

  session.onError((error) => {
    console.error(error.code, error.message);
  });

  window.addEventListener("pagehide", () => session.destroy());

  await session.start();
</script>
```

## import 규칙

보통 앱에서는 아래 파일만 import합니다.

```js
import {
  createSession,
  preloadAssets,
  normalizeSessionOptions,
  validateSessionOptions
} from "./uface-liveness-sdk.js";
import {
  projectVisualFaceToElement,
  projectVisualFeaturePathToElement,
  projectVisualVideoPointToElement,
  visualFeatureLayer
} from "./liveness-visual-output.js";
```

직접 import하지 마세요.

- `liveness-engine.js`
- `front-capture-core.js`
- `uface-liveness-session.js`
- `uface-auto-capture-session.js`
- `blur_module.js`
- `libs/mediapipe/...`
- `models/face_landmarker.task`

앱은 제공된 facade, session API, `state`, `result`, visual helper만 사용하면 됩니다.

## 공개 함수

라이브니스 facade가 제공하는 공개 함수는 아래와 같습니다.

| 함수 | 입력 | 출력 | 설명 |
| --- | --- | --- | --- |
| `createSession(options)` | `SessionOptions` | `SessionApi` | 옵션을 정규화/검증한 뒤 세션 객체를 반환합니다. `options.videoEl`은 필수입니다. |
| `preloadAssets(options?)` | 일부 `SessionOptions` | `Promise<void>` | MediaPipe model/runtime과, blur가 켜진 경우 blur WASM을 미리 로드합니다. 안내 페이지 또는 카메라 시작 직전에 호출하세요. 카메라는 시작하지 않습니다. |
| `normalizeSessionOptions(options?)` | 일부 `SessionOptions` | 정규화된 `SessionOptions` | SDK 기본값을 적용하고 지원 범위로 값을 보정합니다. 브라우저 전용입니다. SDK runtime을 import하므로 Node/build-time validator가 아닙니다. |
| `validateSessionOptions(options?)` | 일부 또는 정규화된 `SessionOptions` | 정규화된 `SessionOptions` | 필수 옵션이 없거나 지원하지 않는 옵션이면 UFace 에러를 throw합니다. throw되는 모든 `error.code`는 `UFace`로 시작합니다. |

preload 예:

```js
const options = {
  videoEl,
  quality: { blur: "auto" }
};

const prewarm = preloadAssets(options).catch((error) => {
  console.warn("UFace preload failed", error.code, error.message);
});

const session = createSession(options);
await prewarm;
await session.start();
```

## 운영 추천 기본값

처음 운영 테스트를 시작할 때는 아래 값으로 시작하세요.

```js
{
  debug: false,
  challenge: {
    directions: {
      count: 4,
      diagonal: false,
      thresholdDeg: 15
    },
    eyeAction: {
      mode: "close-and-return",
      target: "both"
    },
    mouthAction: {
      mode: "off"
    },
    capture: {
      mode: "single"
    }
  },
  quality: {
    blur: "auto",
    threshold: 0.35
  }
}
```

처음에는 위 추천값으로 인증 정책과 화면 흐름을 먼저 확인하세요. SDK의 암묵적 기본값은 눈/입 액션은 직접 설정하지 않으면 `"off"`로 두고, 거리와 얼굴 중앙 위치 품질 gate는 켭니다. 기본 카메라 요청은 HD(`1280x720`)이며, 정면 캡처는 자세, 거리, 중앙 위치 조건이 모두 통과할 때만 진행됩니다. 고급 설정은 인증 난이도와 UX를 검증하기 위한 옵션이며, 기기별 성능 최적화 튜닝 용도로 사용하지 않습니다.

## 옵션 전체 보기

### `debug`

| 값 | 의미 |
| --- | --- |
| `false` | 운영 기본값입니다. |
| `true` | `state.debug`, `result.debug`, capture debug 정보가 추가됩니다. 운영 기본값으로 쓰지 마세요. |

### `challenge.directions`

얼굴 방향 챌린지입니다.

```js
directions: {
  count: 4,
  diagonal: false,
  allowed: null,
  thresholdDeg: 15,
  diagonalAxisThresholdRatio: 0.55,
  diagonalAxisThresholdDeg: null
}
```

| 옵션 | 범위 | 기본값 | 설명 |
| --- | --- | --- | --- |
| `count` | `0`-`12` | `4` | 요청할 방향 동작 수입니다. `0`이면 방향 챌린지를 사용하지 않습니다. |
| `diagonal` | boolean | `false` | 대각선 방향을 포함할지 정합니다. |
| `allowed` | array 또는 `null` | `null` | 사용할 방향 pool을 직접 제한합니다. 예: `["LEFT", "RIGHT"]` |
| `thresholdDeg` | `5`-`45` | `15` | 방향 통과 판정 각도입니다. 낮을수록 더 작은 움직임도 통과합니다. |
| `diagonalAxisThresholdRatio` | `0.3`-`1.2` | `0.55` | 대각선 방향에서 yaw/pitch 축 균형을 조정합니다. |
| `diagonalAxisThresholdDeg` | `3`-`45` 또는 `null` | `null` | 대각선 각도 기준을 직접 지정합니다. `null`이면 SDK가 계산합니다. |

`allowed`에 사용할 수 있는 값:

```js
["UP", "DOWN", "LEFT", "RIGHT", "UP_LEFT", "UP_RIGHT", "DOWN_LEFT", "DOWN_RIGHT"]
```

대각선 방향은 사용자 안내가 어렵습니다. 처음에는 `diagonal: false`로 시작하고, 꼭 필요할 때만 켜세요.

### `challenge.eyeAction`

눈 동작 챌린지입니다.

```js
eyeAction: {
  mode: "close-and-return",
  target: "both"
}
```

| mode | 의미 | 권장 상황 |
| --- | --- | --- |
| `"off"` | 눈 동작 없음 | 방향 챌린지만 사용할 때 |
| `"close-only"` | 눈을 감으면 통과 | 빠른 흐름이 필요할 때 |
| `"close-and-return"` | 눈을 감았다가 다시 떠야 통과 | 기본 권장 |
| `"both-blink"` | 양쪽 눈 깜빡임 | `"close-only"`와 같은 양쪽 눈 동작 |
| `"both-blink-return"` | 양쪽 눈 감기 후 다시 뜨기 | `"close-and-return"`과 같은 양쪽 눈 동작 |
| `"left-wink"` | 왼쪽 눈 윙크 후 다시 뜨기 | 서비스 정책상 한쪽 눈 동작이 필요할 때 |
| `"right-wink"` | 오른쪽 눈 윙크 후 다시 뜨기 | 서비스 정책상 한쪽 눈 동작이 필요할 때 |
| `"either-wink"` | 어느 한쪽 눈 윙크 후 다시 뜨기 | 좌우 구분 없이 윙크를 허용할 때 |

`target`은 `"both"`, `"left"`, `"right"`, `"either"` 중 하나입니다. 대부분은 `target: "both"`를 사용합니다.

`sequence` 모드는 현재 공개 연동에서 사용하지 않습니다. 순차 눈동작이 필요하면 SDK 업데이트 여부를 먼저 확인하세요.

### `challenge.mouthAction`

입 동작 챌린지입니다.

| mode | 의미 |
| --- | --- |
| `"off"` | 입 동작 없음 |
| `"open-close"` | 입을 벌렸다가 닫는 동작 |

입 동작은 사용자 안내가 더 필요합니다. 초기 연동에서는 꺼두고, 정책상 필요할 때만 켜세요.

### `challenge.capture.mode`

| 값 | 의미 |
| --- | --- |
| `"single"` | 최종 이미지 1장 캡처 |
| `"triple"` | 정면/복귀 프레임을 포함해 여러 장 캡처 |

대부분의 페이지는 `"single"`로 시작하면 됩니다. `"triple"`을 쓰면 `result.captures`가 여러 장이 될 수 있으므로 서버 처리도 함께 준비해야 합니다.

### `quality`

```js
quality: {
  blur: "auto",
  threshold: 0.35,
  faceDistance: {
    mode: "on",
    tooFarRatio: 0.45,
    tooNearRatio: 0.72
  },
  facePosition: {
    mode: "gate",
    targetX: 0.5,
    targetY: 0.5,
    toleranceX: 0.18,
    toleranceY: 0.2
  }
}
```

| 옵션 | 값 | 설명 |
| --- | --- | --- |
| `blur` | `"auto"` | 가능한 경우 blur 품질 검사를 사용합니다. 라이브니스 기본 권장입니다. |
| `blur` | `"on"` | blur 품질 검사를 반드시 사용합니다. 품질 검사를 불러오지 못하면 진행이 실패할 수 있습니다. |
| `blur` | `"off"` | blur 품질 검사를 사용하지 않습니다. 서비스 정책상 필요한 경우에만 사용하세요. |
| `threshold` | `0`-`1` | 낮을수록 더 선명한 이미지만 통과합니다. 기본값은 `0.35`입니다. |
| `faceDistance.mode` | `"off"` / `"on"` | 정면 단계에서 얼굴이 너무 멀거나 가까운 상태를 SDK prompt와 state로 분리합니다. 기본값은 `"on"`입니다. |
| `faceDistance.tooFarRatio` | `0.1`-`0.95` | 비디오 짧은 축 대비 얼굴 최대축이 이 값보다 작으면 `too-far`입니다. 권장값은 `0.45`입니다. |
| `faceDistance.tooNearRatio` | `0.2`-`1.5` | 비디오 짧은 축 대비 얼굴 최대축이 이 값보다 크면 `too-near`입니다. 권장값은 `0.72`입니다. |
| `facePosition.mode` | `"off"` / `"guide"` / `"gate"` | 얼굴 중심이 중앙 허용 범위 안에 있는지 안내하거나 정면 캡처 gate에 포함합니다. 기본값은 `"gate"`입니다. |
| `facePosition.targetX/Y` | `0`-`1` | 비디오 좌표 기준 목표 중심입니다. 기본값은 `0.5`입니다. |
| `facePosition.toleranceX/Y` | `0.02`-`0.45` | 목표 중심에서 허용할 normalized 오차입니다. 표준 데모는 `0.18`, `0.2`를 사용합니다. |
| `facePosition.farX/Y` | `0.03`-`1` | 중앙 유도 UI의 보정 강도가 최대가 되는 offset입니다. 기본값은 `0.36`, `0.4`입니다. |

`threshold`를 너무 낮게 잡으면 실내 조명이나 저가형 카메라에서 캡처가 늦어질 수 있습니다.
사진 품질을 위해 `faceDistance.mode`는 기본 `"on"`입니다. 거리 gate를 의도적으로 제외할 때만 `"off"`로 설정하세요.
사진 품질을 위해 `facePosition.mode`는 기본 `"gate"`입니다. 안내만 보여주려면 `"guide"`, 완전히 끄려면 `"off"`로 설정하세요.

### 정면 캡처 통과 조건

초기 정면 캡처와 복귀 정면 캡처는 아래 활성 조건이 모두 true일 때만 통과합니다.

| 조건 | 통과 기준 |
| --- | --- |
| 얼굴 검출 | 얼굴이 감지되고 얼굴 전체가 사용할 수 있는 상태입니다. |
| 얼굴 크기 | 얼굴이 최소 사용 가능 크기보다 작지 않습니다. |
| 얼굴 거리 | `quality.faceDistance.mode`가 `"on"`이면 `state.quality.faceDistance.status`가 `"ok"`여야 합니다. `too-far`, `too-near`는 캡처를 막습니다. |
| 얼굴 중앙 위치 | `quality.facePosition.mode`가 `"gate"`이면 `state.quality.facePosition.pass`가 `true`여야 합니다. |
| 정면 pose | normal profile 기준 `abs(yaw) <= 7`, `abs(pitch) <= 10`이어야 합니다. 두 축이 모두 통과해야 하며 yaw만 맞거나 pitch만 맞으면 통과하지 않습니다. |
| blur/흔들림 | blur가 켜져 있으면 최신 blur 상태가 통과하고 얼굴이 충분히 안정적으로 유지되어야 합니다. |

`FACE_TOO_FAR`, `FACE_TOO_NEAR`, `CENTER_FACE`, pose 보정 prompt는 사용자 안내 상태이며 에러가 아닙니다.

### `performance`

대부분의 연동에서는 직접 설정하지 않습니다. SDK 기본값을 사용하고, 별도 지원 안내를 받은 경우에만 적용하세요.

```js
performance: {
  mode: "auto",
  camera: "auto",
  faceDelegate: "auto",
  gpuNoFaceFallback: false,
  faceMinDetectionConfidence: null,
  faceMinPresenceConfidence: null,
  faceMinTrackingConfidence: null
}
```

| 옵션 | 값 | 설명 |
| --- | --- | --- |
| `mode` | `"auto"` | 기본값입니다. 현재는 `"normal"` 프로필로 시작합니다. |
| `mode` | `"normal"` | 품질과 반응성을 우선합니다. |
| `mode` | `"balanced"` | 추론 주기를 줄이는 지원용 프로필입니다. 기본값으로 쓰지 마세요. |
| `mode` | `"low"` | 처리량을 더 줄이는 지원용 프로필입니다. 기본값으로 쓰지 마세요. |
| `camera` | `"auto"` | 기본 HD 카메라 요청을 사용합니다. |
| `camera` | `"low"` | 낮은 카메라 해상도를 요청합니다. 지원 안내에서 명시한 경우에만 사용하세요. |
| `faceDelegate` | `"auto"` | SDK가 GPU/CPU를 선택합니다. |
| `faceDelegate` | `"gpu"` | GPU를 우선 사용합니다. |
| `faceDelegate` | `"cpu"` | CPU를 사용합니다. 지원 안내를 받은 경우에만 사용하세요. |
| `gpuNoFaceFallback` | boolean | SDK fallback 동작 허용 여부입니다. 기본값은 비활성화이며, 지원 안내가 있을 때만 켜세요. |
| `faceMinDetectionConfidence` | `0`-`1` 또는 `null` | MediaPipe face detector confidence override입니다. 지원 안내가 없으면 `null`로 두세요. |
| `faceMinPresenceConfidence` | `0`-`1` 또는 `null` | MediaPipe landmark presence confidence override입니다. 지원 안내가 없으면 `null`로 두세요. |
| `faceMinTrackingConfidence` | `0`-`1` 또는 `null` | MediaPipe tracking confidence override입니다. 지원 안내가 없으면 `null`로 두세요. |

성능 모드는 카메라 요청 해상도를 자동으로 낮추지 않습니다. 기본 카메라 요청은 HD(`1280x720`)를 유지합니다.

실행 중에도 성능 옵션을 바꿀 수 있지만, 일반 화면 구현에서는 필요하지 않습니다.

```js
session.setPerformanceOptions({
  mode: "balanced"
});
```

기기별 문제를 이 옵션으로 직접 해결하려고 하지 마세요. 재현 정보와 오류 상태를 수집한 뒤 지원 절차에 따라 적용 여부를 결정하세요.

### `rules.eye` 고급 옵션

눈 판정 기준을 직접 조정하는 고급 옵션입니다. 인증 정책상 필요한 동작 난이도를 검증할 때만 사용하세요. 성능 최적화나 기기별 문제 해결 용도로 사용하지 마세요.

```js
rules: {
  eye: {
    blinkCloseRatio: null,
    winkCloseRatio: 0.5,
    returnRatio: 0.65,
    closedScore: null,
    openScore: 0.25,
    landmarkClosedThreshold: null,
    scoreSupportRatio: 1.25,
    holdFrames: 1
  }
}
```

| 옵션 | 범위 | 기본값 | 설명 |
| --- | --- | --- | --- |
| `blinkCloseRatio` | `0.2`-`0.95` 또는 `null` | `null` | 양쪽 눈 감김 판정 비율입니다. `null`이면 성능 프로필 기본값을 사용합니다. |
| `winkCloseRatio` | `0.2`-`0.95` | `0.5` | 한쪽 눈이 다른 쪽 눈보다 얼마나 닫혀야 윙크로 볼지 정합니다. |
| `returnRatio` | `0.2`-`0.95` | `0.65` | 감은 뒤 다시 뜬 상태를 확인하는 비율입니다. |
| `closedScore` | `0.05`-`0.95` 또는 `null` | `null` | blink score 기반 감김 threshold입니다. `null`이면 성능 프로필 기본값을 사용합니다. |
| `openScore` | `0.01`-`0.9` | `0.25` | 다시 뜬 상태로 볼 blink score 기준입니다. |
| `landmarkClosedThreshold` | `0.01`-`0.2` 또는 `null` | `null` | landmark 거리 기반 감김 threshold입니다. `null`이면 성능 프로필 기본값을 사용합니다. |
| `scoreSupportRatio` | `1`-`3` | `1.25` | score와 landmark 기준을 함께 볼 때 보조 허용 폭입니다. |
| `holdFrames` | `1`-`10` | `1` | 눈 상태가 몇 프레임 유지되어야 통과로 볼지 정합니다. |

판정 정책 조정 기준:

- 눈을 감았는데 잘 통과하지 않으면 `blinkCloseRatio` 또는 `closedScore`를 완화합니다.
- 윙크가 너무 쉽게 통과하면 `winkCloseRatio`를 낮춥니다.
- 다시 뜨는 동작이 너무 쉽게 통과하면 `returnRatio`를 높입니다.
- 오검출이 많으면 `holdFrames`를 `2` 이상으로 올립니다.

## session API

`createSession()`은 아래 메서드를 반환합니다.

| API | 설명 |
| --- | --- |
| `start()` | 카메라와 SDK 처리를 시작합니다. |
| `stop({ keepCamera })` | 처리를 멈춥니다. `keepCamera: true`면 카메라 stream을 유지합니다. |
| `retry()` | 현재 세션을 초기화하고 다시 시도합니다. |
| `destroy()` | 카메라와 listener를 정리합니다. 페이지 종료 또는 모달 닫기 시 호출하세요. |
| `on(eventName, handler)` | `"state"`, `"result"`, `"error"`, `"performance"` 이벤트를 구독합니다. 반환값은 구독 해제 함수입니다. |
| `onState(handler)` | `state` 이벤트를 구독합니다. 등록 즉시 현재 state가 한 번 전달됩니다. |
| `onResult(handler)` | 완료 결과를 받습니다. |
| `onError(handler)` | 실패 오류를 받습니다. |
| `onPerformance(handler)` | FPS, 추론 주기 같은 성능 통계를 받습니다. |
| `setPerformanceOptions(options)` | 실행 중 성능 옵션을 바꿉니다. |
| `getState()` | 현재 state snapshot을 반환합니다. |
| `getLastResult()` | 마지막 result snapshot을 반환합니다. 없으면 `null`입니다. |
| `getLastError()` | 마지막 error snapshot을 반환합니다. 없으면 `null`입니다. |

예:

```js
const offState = session.onState(render);

closeButton.onclick = () => {
  offState();
  session.destroy();
};
```

## state 처리

UI에서 가장 중요한 값은 `state.prompt.text`와 `state.progress.percent`입니다.

```js
session.onState((state) => {
  instruction.textContent = state.prompt.text;
  progressBar.value = state.progress.percent;
  captureCount.textContent = `${state.capture.count}/${state.capture.required}`;
});
```

| 필드 | 설명 |
| --- | --- |
| `state.phase` | 현재 단계입니다. `idle`, `initializing`, `camera-ready`, `tracking`, `challenge`, `capturing`, `complete`, `failed`, `stopped` |
| `state.prompt.code` | 안내 코드입니다. |
| `state.prompt.text` | 사용자에게 보여줄 안내 문구입니다. |
| `state.prompt.severity` | `hint`, `warning`, `critical`, `challenge` 중 하나입니다. |
| `state.progress.completed` | 완료된 챌린지 수입니다. |
| `state.progress.total` | 전체 챌린지 수입니다. |
| `state.progress.percent` | 전체 진행률입니다. |
| `state.capture.count` | 캡처된 이미지 수입니다. |
| `state.capture.required` | 필요한 이미지 수입니다. |
| `state.capture.mode` | `"single"` 또는 `"triple"`입니다. |
| `state.quality.tracking` | `unknown`, `ready`, `wait`, `lost` 중 하나입니다. |
| `state.quality.blur` | `unknown`, `ok`, `hold-still`, `disabled`, `unavailable` 중 하나입니다. |
| `state.quality.faceDistance` | `enabled`, `status`, `ratio`, `tooFarRatio`, `tooNearRatio`, `pass`를 포함합니다. `status`는 `unknown`, `too-far`, `too-near`, `ok` 중 하나입니다. |
| `state.quality.facePosition` | `enabled`, `mode`, `status`, `center`, `offset`, `tolerance`, `corrections`, `pass`를 포함합니다. `status`는 `unknown`, `off-center`, `ok` 중 하나입니다. |
| `state.quality.performance` | 현재 성능 프로필입니다. |
| `state.face.eyes` | 눈 열림/감김 상태입니다. 눈 UI를 만들 때 사용합니다. |
| `state.currentAction` | 눈/입 챌린지 중일 때 현재 action 상태입니다. |
| `state.visual` | overlay 렌더링용 좌표와 guide 정보입니다. |
| `state.visualGuide` | 단순 face box/pose hint가 필요한 화면용 보조 값입니다. |
| `state.performance` | 마지막 성능 통계입니다. 없으면 `null`입니다. |
| `state.debug` | `debug: true`일 때만 포함됩니다. |

`state.visual`은 화면 렌더링용입니다. raw 좌표나 각도로 별도 인증 판정을 만들지 마세요.

## prompt code

| 코드 | 사용자 안내 의미 |
| --- | --- |
| `IDLE` | 세션이 준비됐지만 아직 실행 전 |
| `INITIALIZING` | SDK가 카메라/runtime을 준비 중 |
| `PREPARING_TRACKING` | 카메라는 준비됐고 추적을 준비 중 |
| `LOOK_CENTER` | 정면 보기 |
| `HOLD_CENTER` | 정면 상태 유지 |
| `RETURN_CENTER` | 다시 정면으로 돌아오기 |
| `HOLD_STILL` | 잠시 가만히 있기 |
| `TURN_LEFT` | 왼쪽으로 고개 돌리기 |
| `TURN_RIGHT` | 오른쪽으로 고개 돌리기 |
| `LOOK_UP` | 위 보기 |
| `LOOK_DOWN` | 아래 보기 |
| `LOOK_UP_LEFT` | 왼쪽 위 보기 |
| `LOOK_UP_RIGHT` | 오른쪽 위 보기 |
| `LOOK_DOWN_LEFT` | 왼쪽 아래 보기 |
| `LOOK_DOWN_RIGHT` | 오른쪽 아래 보기 |
| `EYE_CLOSE` | 눈 감기 |
| `EYE_OPEN` | 눈 다시 뜨기 |
| `LEFT_EYE_CLOSE` | 왼쪽 눈 감기 |
| `LEFT_EYE_OPEN` | 왼쪽 눈 다시 뜨기 |
| `RIGHT_EYE_CLOSE` | 오른쪽 눈 감기 |
| `RIGHT_EYE_OPEN` | 오른쪽 눈 다시 뜨기 |
| `MOUTH_OPEN` | 입 벌리기 |
| `MOUTH_CLOSE` | 입 닫기 |
| `BLUR_HOLD_STILL` | 이미지가 흐리므로 잠시 고정 |
| `CENTER_FACE` | 얼굴을 화면 중앙에 맞추기 |
| `FACE_TOO_FAR` | 화면 쪽으로 가까이 이동 |
| `FACE_TOO_NEAR` | 화면에서 뒤로 이동 |
| `SHOW_FULL_FACE` | 얼굴 전체를 보여주기 |
| `COMPLETE` | 인증 완료 |
| `STOPPED` | 세션 중지 |
| `FAILED` | 인증 실패 |

## result 처리

완료되면 `result` 이벤트가 발생합니다.

```js
session.onResult(async (result) => {
  await fetch("/api/liveness-result", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(result)
  });
});
```

주요 필드:

| 필드 | 설명 |
| --- | --- |
| `status` | 보통 `"success"`입니다. |
| `images` | 캡처 이미지 배열입니다. |
| `captureMode` | `"single"` 또는 `"triple"`입니다. |
| `captureCount` | 실제 캡처 이미지 수입니다. |
| `requiredCaptureCount` | 필요한 캡처 이미지 수입니다. |
| `blinkMode` | 사용된 눈 동작 모드입니다. |
| `blinkTarget` | 눈 동작 target입니다. |
| `enableDiagonalDirections` | 대각선 방향 사용 여부입니다. |
| `directionsPassed` | 통과한 방향 동작 목록입니다. |
| `actionsPassed` | 통과한 눈/입 동작 목록입니다. |
| `actionVerdicts` | 눈/입 동작 판정 상세입니다. |
| `challengeSteps` | 수행한 챌린지 단계 목록입니다. |
| `captures[]` | 서버로 보낼 이미지와 blur 정보입니다. |
| `debug` | `debug: true`일 때만 포함됩니다. `startedAt`, `endedAt`, `durationMs`가 여기 들어갑니다. |

`captures[]` 예:

```js
{
  sequence: 1,
  captureIndex: 1,
  captureType: "INITIAL_FRONT",
  imageBase64: "data:image/jpeg;base64,...",
  reason: "front-capture",
  ts: 1778760000000,
  blur: {
    state: "accepted",
    mode: "auto",
    enabled: true,
    available: true,
    score: 0.28,
    threshold: 0.35,
    pass: true,
    fresh: true,
    unavailableReason: ""
  }
}
```

## 에러 처리

```js
session.onError((error) => {
  alert(toUserMessage(error));
});

function toUserMessage(error) {
  if (error.code === "UFaceCameraPermissionDenied") return "카메라 권한을 허용해 주세요.";
  if (error.code === "UFaceSessionTimeout") return "인증 시간이 초과되었습니다. 다시 시도해 주세요.";
  return "인증을 진행하지 못했습니다. 다시 시도해 주세요.";
}
```

SDK의 모든 에러 코드는 `UFace`로 시작합니다. 이 prefix로 SDK 실패와 앱 자체 에러를 구분하세요. `FACE_TOO_FAR` 같은 prompt code는 에러가 아니므로 `UFace` prefix를 사용하지 않습니다.

| 에러 코드 | 의미 | 권장 처리 |
| --- | --- | --- |
| `UFaceInvalidOptions` | 필수 옵션이 없거나 잘못됨 | 개발 연동 오류입니다. 로그를 보고 옵션을 수정하세요. |
| `UFaceUnsupportedOption` | 예약 또는 미지원 옵션 사용 | 개발 연동 오류입니다. 옵션을 제거하거나 SDK 버전을 확인하세요. |
| `UFaceInvalidHandler` | 이벤트 handler가 함수가 아님 | 개발 연동 오류입니다. |
| `UFaceSessionDestroyed` | `destroy()` 이후 세션 메서드 호출 | 새 세션을 만들어 다시 시작하세요. |
| `UFaceCameraPermissionDenied` | 브라우저/WebView가 카메라 권한을 거부 | 사용자에게 카메라 권한 허용을 안내하세요. |
| `UFaceCameraUnavailable` | 카메라 장치 또는 stream 시작 실패 | 카메라 사용 가능 여부를 확인하고 재시도하도록 안내하세요. |
| `UFaceInitError` | runtime/model/camera 초기화 중 기타 실패 | 재시도 UI를 보여주고 진단 정보를 수집하세요. |
| `UFaceSessionTimeout` | 전체 세션 제한 시간 초과 | 재시도 UI를 보여주세요. |
| `UFaceStepTimeout` | 챌린지 단계 제한 시간 초과 | 재시도 UI를 보여주세요. |
| `UFaceTrackingError` | SDK retry/fallback 이후 얼굴 추적 실패 | 재시도 UI와 진단 수집을 진행하세요. |
| `UFaceCaptureFailed` | capture-only 흐름 실패 | 주로 auto-capture에서 사용되며 공통 에러 처리에 포함하세요. |
| `UFaceLivenessError` | 일반 라이브니스 실패 fallback | 재시도 UI와 진단 수집을 진행하세요. |
| `UFaceSessionStartFailed` | 데모/앱 wrapper에서 `start()` reject를 받은 fallback | 시작 실패로 처리하세요. |
| `UFaceJsBridgeError` | 데모 native bridge wrapper fallback | native bridge 연동을 점검하세요. |

## 세션 정리

페이지를 떠날 때는 반드시 정리하세요.

```js
window.addEventListener("pagehide", () => {
  session.destroy();
});
```

모달에서 사용하는 경우 닫기 버튼에도 `destroy()` 또는 `stop()`을 연결하세요.

## 하지 말아야 할 것

- 지원 파일을 직접 import하지 마세요.
- raw landmark나 pose angle로 별도 인증 판정을 만들지 마세요.
- `state.visual`을 판정 로직으로 사용하지 마세요. overlay 렌더링용입니다.
- `debug: true`를 운영 기본값으로 사용하지 마세요.
- SDK가 완료되기 전에 앱에서 별도 사진을 저장하지 마세요.
