> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ones1ght.com/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS 레퍼런스

> OneS1ght iOS SDK의 전체 API 시그니처, 모델, 에러 정의입니다.

<Info>
  이 문서는 **v0.1.13** 기준입니다. 판별 값은 `OneS1ght.sdkVersion` 으로 확인할 수 있습니다.
</Info>

`OneS1ght` 는 앱 전체에 하나뿐인 정적 진입점입니다 — 인스턴스를 만들지 않으며 모든 API 를 타입에서
직접 호출합니다. 모든 API 는 **메인 액터(@MainActor)** 에서 호출합니다.

```swift theme={null}
import OneS1ght
```

## 생명주기

| API                                                  | 설명                                                                                         |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `initialize(sdkKey:geoSdkKey:baseURL:) async throws` | 앱 시작 시 1회. 기기 게이트 → 키 검증 → 설정 로드. 멱등 — 성공 후 재호출은 무시, 실패 후 재호출은 재시도, **다른 키로 재호출 시 세션 재구성** |
| `permissions() async -> PermissionStatus`            | 측위 권한 확인 — **호출하면 시스템 팝업이 뜹니다**                                                            |
| `reset() async`                                      | 세션 폐기. 이후 다른 키로 `initialize` 가능 (런타임 키 교체용)                                                |
| `setLanguage(_ code: String?)`                       | SDK 로그·안내 문구의 언어 — `"ko"`·`"ja"`·`"en"`. `nil` 이면 기기 언어 (기본값). `0.1.11~`                   |

`initialize` 의 파라미터:

| 파라미터        | 타입                   | 설명                                                           |
| ----------- | -------------------- | ------------------------------------------------------------ |
| `sdkKey`    | `String`             | OneS1ght 콘솔 발급 (`ock_sdk_…`) — 인증·구역·수집·이벤트·도면               |
| `geoSdkKey` | `String?` (기본 `nil`) | **임시 인자** — 로케이터·세션·층 정보 조회용 과도기 값, 담당자 전달. **생략 시 측위만 비활성** |
| `baseURL`   | `URL` (기본 운영 서버)     | 자체 서버를 구축한 고객만. 운영/개발 구분은 이 인자가 아니라 **키**가 가릅니다              |

기본 `baseURL` 은 `https://console.ones1ght.com/api/sdk/v1` 입니다.

<Warning>
  **`initialize` 는 건물·층을 조회하지 않습니다.** 공간 설정은 `setFloorMap` 의 몫입니다 —
  건너뛰면 측위 파이프라인은 돌지만 좌표가 나오지 않습니다(`E3001`).
</Warning>

## 측위 세션 — FloorSession

<Tabs>
  <Tab title="SwiftUI">
    ```swift theme={null}
    .task {
        guard let session = try? OneS1ght.floorSession() else { return }
    }
    ```
  </Tab>

  <Tab title="UIKit">
    ```swift theme={null}
    override func viewDidLoad() {
        super.viewDidLoad()
        guard let session = try? OneS1ght.floorSession() else { return }
    }
    ```
  </Tab>
</Tabs>

| API                                     | 설명                                                       |
| --------------------------------------- | -------------------------------------------------------- |
| `floorSession() throws -> FloorSession` | 세션 획득. **항상 같은 인스턴스** — UWB 라디오·판정 엔진·좌표 버퍼가 기기당 하나뿐     |
| `session.begin() async throws`          | 측위 시작 (매장 진입 시)                                          |
| `session.begin(provider:) async throws` | 커스텀 측위 주입 — 시뮬레이터 테스트(`MockPositioningProvider`) 등 특수 경로 |
| `session.end() async`                   | 측위 정지 + 잔여 좌표 전송. 초기화 상태는 유지 → `begin` 재호출로 재시작 가능       |
| `session.floor: Floor?`                 | 현재 지정된 층                                                 |
| `session.isRunning: Bool`               | 측위 가동 여부                                                 |

## 콜백

세션 콜백은 `FloorSession` 인스턴스에, 디버그 로그는 `OneS1ght` 타입에 답니다.

| 콜백                    | 타입                                | 설명                                                     |
| --------------------- | --------------------------------- | ------------------------------------------------------ |
| `session.onPosition`  | `((Coordinates) -> Void)?`        | 실시간 좌표 (도면 로컬 미터) — 최대 4회/초                            |
| `session.onZoneEnter` | `((Zone) -> Void)?`               | 구역 진입 — 온디바이스 판정 즉시 (서버 왕복 없음)                         |
| `session.onZoneExit`  | `((Zone) -> Void)?`               | 구역 이탈                                                  |
| `session.onZoneDwell` | `((Zone, TimeInterval) -> Void)?` | 체류 발화 — (구역, 체류 초)                                     |
| `session.onTriggers`  | `((String, [Trigger]) -> Void)?`  | (zoneId, 트리거 배열) — **서버가 매칭**한 개인화 액션                  |
| `OneS1ght.onDebugLog` | `((LogLevel, String) -> Void)?`   | SDK 내부 활동 로그. `initialize` 보다 먼저 등록해야 초기화 로그를 놓치지 않습니다 |

<Warning>
  `0.1.12` 부터 `onDebugLog` 가 **등급과 글자를 함께** 줍니다(`(String) -> Void` 에서 바뀜).
  [마이그레이션 가이드](/sdk/integration/ios/migration-guide) 참고.
</Warning>

### LogLevel — 로그 등급

```swift theme={null}
public enum LogLevel { case log, info, warn, error }   // log < info < warn < error
```

`Comparable` 이라 `level >= .warn` 처럼 걸러낼 수 있습니다.

| 등급       | 뜻                                        | 예                           |
| -------- | ---------------------------------------- | --------------------------- |
| `.log`   | 흐름 기록 — 평상시엔 안 봐도 됩니다                    | 좌표 전송, 존 주입                 |
| `.info`  | 알아두면 좋은 것 — 정상이지만 눈에 띄면 도움이 됩니다          | 이 층에 구역이 없음, 실시간 연결됨        |
| `.warn`  | 확인이 필요한 것 — 고장은 아니지만 이대로면 기대한 대로 안 돌아갑니다 | 집계간격이 좌표 주기보다 짧아 IN 발화 불가   |
| `.error` | 고장 — 손대지 않으면 그 기능이 동작하지 않습니다             | 유효 앵커 부족으로 측위 불가, 세션 ID 미주입 |

<Note>
  "이 층에 등록된 구역이 없다"는 콘솔에서 아직 구역을 만들지 않은 **정상 상태**라
  `.info` 입니다. 고장이 아닙니다.
</Note>

<Warning>
  `onTriggers` 만 서버 응답입니다 — 네트워크가 끊기면 진입 판정은 오지만 트리거는 오지 않습니다.
</Warning>

## 공간 조회

엔드포인트 하나당 메서드 하나이며, 목록과 단건이 짝을 이룹니다.

| API                                            | 설명                                               |
| ---------------------------------------------- | ------------------------------------------------ |
| `buildings() async throws -> [Building]`       | 건물 목록. `geoSdkKey` 없이 초기화했으면 빈 배열                |
| `building(_:) async throws -> Building`        | 건물 하나                                            |
| `floors(_:) async throws -> [Floor]`           | 층 목록 — **도면 이미지 없음**(`image == nil`, 목록을 가볍게 유지) |
| `floor(_:_:) async throws -> Floor`            | 층 하나 — 도면 이미지 포함 (캐시에서 나와 왕복이 늘지 않음)             |
| `zones(_:_:) async throws -> [Zone]`           | 층의 구역 목록                                         |
| `zone(_:_:_:) async throws -> Zone`            | 구역 하나                                            |
| `locators(_:_:) async throws -> FloorLocators` | 층의 로케이터 배치와 UWB 세션 ID                            |

## 층 지정

| API                                       | 설명                                                                   |
| ----------------------------------------- | -------------------------------------------------------------------- |
| `setFloorMap(_:buildingID:) async throws` | 층 지정 — 로케이터·세션·구역을 내부 엔진에 주입. 가동 중이면 **즉시 층 전환**(세션 유지). `nil` 이면 해제 |
| `refreshZones() async -> [Zone]`          | 현재 층의 구역만 재조회 (경량 — 도면 재다운로드 없음). 판정 엔진에 즉시 반영                       |

## 프로필

| API                                               | 설명                                              |
| ------------------------------------------------- | ----------------------------------------------- |
| `createProfile(_:) async throws -> String`        | 프로필 생성 — 서버가 발급한 `profileId` 반환. **앱이 보관해 재사용** |
| `getProfile(_:) async throws -> [String: String]` | 속성 조회                                           |
| `putProfile(_:_:) async throws`                   | 속성 **전체 교체** (부분 갱신 아님)                         |
| `deleteProfile(_:) async throws`                  | 프로필 삭제 (쌓인 좌표·이벤트는 보존 정책에 따름)                   |
| `identify(profileId:)`                            | 연결 — 측위 전에 반드시. 로그아웃 시 `nil`                    |

<Note>
  고객사 회원 ID 는 서버로 오지 않습니다 — 매핑은 고객사만 보관합니다.
</Note>

<Warning>
  `savedProfileId ?? (try await OneS1ght.createProfile(…))` 는 **컴파일되지 않습니다.**
  `??` 의 오른쪽은 autoclosure 라서 `try await` 를 담을 수 없습니다 — `if let` 으로 푸세요.
</Warning>

## 데이터 전송

좌표는 **300건 또는 60초** 중 먼저 도달하는 쪽에서 전송됩니다.
백그라운드 진입 시와 `end()` 시에도 잔여분을 보냅니다.

| API            | 설명                    |
| -------------- | --------------------- |
| `send() async` | 버퍼를 지금 즉시 전송          |
| `empty()`      | 버퍼를 **버린다** — 전송하지 않음 |

## 상태 조회

| API                  | 타입                   | 설명                                                    |
| -------------------- | -------------------- | ----------------------------------------------------- |
| `isInitialized`      | `Bool`               | 초기화 성공 여부 (기기 통과 + 키 유효 + 설정 로드)                      |
| `deviceAvailability` | `DeviceAvailability` | 측위 가능 여부 + 불가 사유. `initialize` 전에도 호출 가능 (네트워크 사용 없음) |
| `isDeviceAvailable`  | `Bool`               | 위의 요약형 (`== .available`)                              |
| `sdkVersion`         | `String`             | SDK 버전 (예: `"0.1.13"`)                                |

```swift theme={null}
public enum DeviceAvailability: Equatable {
    case available            // 측위 가능
    case osVersionTooLow      // iOS 27 미만 — "OS 업데이트 후 사용 가능" 안내
    case deviceNotSupported   // UWB 칩 미지원 — "iPhone 12 이상 필요" 안내
}

public enum PermissionStatus: Equatable {
    case authorized           // 측위를 시작할 수 있다
    case denied               // 앱에서 다시 물을 수 없다 — 설정 앱으로 안내
    case unsupported          // 이 기기·OS 에서는 측위 자체가 불가
}
```

## 에러 — SdkError

`SdkError` 는 SDK 자체가 던지는 5가지입니다. 통신 실패는 `ApiError` 로 옵니다.
두 타입 모두 `.code` 로 [에러 코드](/sdk/faq/error-code)를 꺼낼 수 있습니다.

<Tabs>
  <Tab title="SwiftUI">
    ```swift theme={null}
    .task {
        do {
            try await OneS1ght.initialize(sdkKey: key)
        } catch let e as SdkError {
            print(e.code.rawValue)                  // "E1003"
        } catch let e as ApiError {
            print(e.code.rawValue, e.description)   // "E5005" · "응답 해석 실패 — …"
        }
    }
    ```
  </Tab>

  <Tab title="UIKit">
    ```swift theme={null}
    Task { @MainActor in
        do {
            try await OneS1ght.initialize(sdkKey: key)
        } catch let e as SdkError {
            print(e.code.rawValue)                  // "E1003"
        } catch let e as ApiError {
            print(e.code.rawValue, e.description)   // "E5005" · "응답 해석 실패 — …"
        }
    }
    ```
  </Tab>
</Tabs>

| 케이스                            | 코드      | 발생 시점                     | 조치                           |
| ------------------------------ | ------- | ------------------------- | ---------------------------- |
| `SdkError.notInitialized`      | `E1001` | `initialize` 없이 다른 API 호출 | 호출 순서 수정                     |
| `SdkError.notIdentified`       | `E1004` | `identify` 없이 측위 시작       | 인증 직후 `identify(profileId:)` |
| `SdkError.positioningDisabled` | `E1003` | 키는 유효하나 측위 기능 꺼짐          | 콘솔 설정 확인 · 지원팀 문의            |
| `SdkError.osVersionTooLow`     | `E2001` | iOS 27 미만                 | OS 업데이트 안내                   |
| `SdkError.deviceNotSupported`  | `E2002` | UWB 칩 없음                  | `deviceAvailability` 로 사전 분기 |
| `ApiError.invalidKey`          | `E1002` | 키가 잘못됐거나 폐기됨 (401)        | 콘솔에서 키 상태 확인·재발급             |
| `ApiError.forbidden`           | `E5004` | 다른 테넌트 자원 (403)           | 키 스코프 확인                     |
| `ApiError.notFound`            | `E5003` | 대상 없음 (404)               | 대개 계약 불일치                    |
| `ApiError.unprocessable`       | `E5003` | 페이로드 문제 (422)             | SDK·서버 버전 불일치 의심             |
| `ApiError.server`              | `E5002` | 서버 5xx                    | 통합 관리자 확인                    |
| `ApiError.network`             | `E5001` | 오프라인·타임아웃                 | 자동 재시도                       |
| `ApiError.decoding`            | `E5005` | 응답 JSON 형태 불일치            | `description` 에 사유가 담깁니다     |

측위 런타임(`E4001`~~`E4003`)과 공간 설정(`E3001`~~`E3004`)은 throw 되지 않고 **로그로만** 남습니다 —
`onDebugLog` 또는 콘솔 로그 분석기에서 확인하세요. [전체 목록](/sdk/faq/error-code)

## 모델

### Coordinates — 실시간 좌표

```swift theme={null}
public struct Coordinates: Codable, Equatable {
    public let x: Double   // 미터
    public let y: Double   // 미터
    public let z: Double   // 높이 (2D 측위면 0)
}
```

### Building · Floor — 건물·층

| 타입         | 필드                                | 설명                                                         |
| ---------- | --------------------------------- | ---------------------------------------------------------- |
| `Building` | `id` · `name`                     | 건물 ID · 이름                                                 |
|            | `floorCount: Int?`                | 층 수 (서버가 주지 않으면 `nil`)                                     |
| `Floor`    | `id` · `name`                     | 층 ID · 이름                                                  |
|            | `image: Data?`                    | 도면 PNG — `floors(_:)` 로 받으면 `nil`, `floor(_:_:)` 로 받으면 채워짐 |
|            | `hasPlan: Bool`                   | 도면 등록 여부                                                   |
|            | `originX` · `originY`             | 도면 원점 오프셋 (미터)                                             |
|            | `widthM` · `heightM`              | 도면 실제 크기 (미터)                                              |
|            | `minX` · `minY` · `maxX` · `maxY` | 배치 범위 (원점 + 크기에서 파생)                                       |

### Locator · FloorLocators — 로케이터

| 타입              | 필드                       | 설명                                 |
| --------------- | ------------------------ | ---------------------------------- |
| `Locator`       | `address: Int`           | UWB MAC 뒤 2바이트 (예: `0x9DD7`)       |
|                 | `x` · `y` · `z`          | 도면 로컬 미터                           |
| `FloorLocators` | `locators: [Locator]`    | 이 층에 설치된 로케이터                      |
|                 | `sessionId: Int?`        | UWB 세션 — 층마다 다름. `nil` 이면 측위 시작 불가 |
|                 | `positioningReady: Bool` | 로케이터가 있고 세션도 있는가 (파생값)             |

### Zone — 구역

| 필드                            | 타입           | 설명                                        |
| ----------------------------- | ------------ | ----------------------------------------- |
| `id` / `name`                 | `String`     | 구역 식별자 · 이름                               |
| `polygon`                     | `[Position]` | 꼭짓점 (순서대로) — `Position` 은 도면 로컬 미터 (x, y) |
| `inDist`                      | `Double`     | 진입 판단 거리(m) — 기본 3.0                      |
| `inCount` / `inCountInterval` | `Int`        | 진입 확정 감지 횟수 · 카운트 간격(초)                   |
| `outPeriod`                   | `Int`        | 이탈 판정 유예                                  |
| `priority`                    | `Int`        | 영역이 겹칠 때 우선순위                             |
| `callInout`                   | `Bool`       | 진출입 콜백 발행 여부                              |
| `dwellSeconds`                | `Int?`       | 체류 발화 주기(초) — `nil` 이면 기본 5초              |

판정 파라미터는 서버의 구역 메타에서 내려오는 값으로, 콘솔 **공간 관리 → 구역 선택 → SDK 영역판정**에서
구역마다 설정합니다 ([설정 방법](/locator/areas#sdk-영역판정)).
`zone.contains(Position(x:y:))` 로 임의 좌표의 구역 포함 여부를 직접 검사할 수도 있습니다.

<Warning>
  `inCount` 가 `0` 이면 진입이 영원히 확정되지 않아 구역 이벤트가 발생하지 않습니다.
  콘솔에서 등록한 신규 구역은 기본 `1` 이며, 값이 `0` 인 옛 구역이 있다면 콘솔에서 확인하세요.
</Warning>

### Trigger — 개인화 액션

| 필드           | 타입                  | 설명                                              |
| ------------ | ------------------- | ----------------------------------------------- |
| `trigger_id` | `String`            | 트리거 식별자                                         |
| `type`       | `String`            | `signage · coupon · tracking · merch · generic` |
| `payload`    | `[String: String]?` | 타입별 부가 정보                                       |

### MockPositioningProvider — 테스트용

```swift theme={null}
public final class MockPositioningProvider: PositioningProvider {
    public init()
    public func simulateEnter(buildingId: String)
    public func simulatePosition(_ c: Coordinates, floorId: String, at: Date = Date())
    public func simulateZone(_ zoneId: String, status: ZoneEventStatus, floorId: String, at: Date = Date())
}
```

`session.begin(provider:)` 로 주입해 UWB 없이 SDK 파이프라인을 확인합니다.
`ZoneEventStatus` 는 `.enter` · `.dwell` · `.exit` 입니다.

## 스레딩

* `OneS1ght` 의 모든 API 는 `@MainActor` 입니다. SwiftUI 뷰·`.task` 에서는 그대로 호출하면 되고,
  백그라운드 컨텍스트에서는 `await MainActor.run { … }` 으로 감싸세요.
* 콜백(`onPosition` 등)도 메인 액터에서 호출됩니다 — UI 갱신을 바로 해도 안전합니다.

## 알아둘 제약

<Warning>
  iOS 의 UWB 는 **포그라운드 전용**입니다. 앱이 백그라운드로 가면 측위가 멈추고 버퍼를 비운 뒤,
  돌아오면 다시 시작합니다. 플랫폼 제약이라 SDK 가 우회할 수 없습니다.
</Warning>

<Warning>
  좌표 버퍼는 메모리에 있습니다. 앱이 강제 종료되면 아직 올리지 못한 좌표는 사라집니다(`E5006`).
</Warning>

<Warning>
  LICENSE 파일은 아직 없습니다. 사용 조건은 별도 계약을 따릅니다.
</Warning>
