> ## 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 SDK

> SDK Version 0.2.3

## 패키지 선언

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  import OneS1ght
  ```

  ```objc Objective-C icon="apple" theme={null}
  #import "MyApp-Swift.h"   // MyApp = Product Module Name
  ```
</CodeGroup>

## 초기화

### .initialize

OneS1ght API 키로 측위엔진을 초기화합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  try await OneS1ght.initialize(sdkKey: "YOUR_SDK_KEY")
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge initializeWithSdkKey:@"YOUR_SDK_KEY" completion:^(NSError *error) { }];
  ```
</CodeGroup>

### .requestPermission

측위엔진 사용을 위한 앱 권한을 요청합니다.

| 결과 | 뜻 |
| - | - |
| `authorized` | 측위 가능 |
| `denied` | 권한 거부됨 |
| `unsupported` | 측위 미지원 기기 |

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let status = await OneS1ght.requestPermission()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge requestPermission:^(NSString *status) { }];   // @"authorized" · @"denied" · @"unsupported"
  ```
</CodeGroup>

### .setLanguage

SDK 로그 문구 언어를 설정합니다.<br />
`ko` : 한국어,  `ja` : 일본어,  `en` : 영어, `nil` : 시스템 설정 언어

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  OneS1ght.setLanguage("ko")
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge setLanguage:@"ko"];
  ```
</CodeGroup>

### .onDebugLog

SDK 로그를 수신합니다.<br />
`ko` : 로그 레벨, `message` : 로그 메세지

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  OneS1ght.onDebugLog = { level, message in
      if level >= .warn { print("[OneS1ght] \(message)") }
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onDebugLog:^(NSString *level, NSString *message) { }];
  ```
</CodeGroup>

### .reset

로케이터의 세션을 초기화합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  await OneS1ght.reset()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge reset:^{ }];
  ```
</CodeGroup>

## 상태 조회

### .isInitialized

초기화 성공 확인 여부를 반환합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let ready = OneS1ght.isInitialized
  ```

  ```objc Objective-C icon="apple" theme={null}
  BOOL ready = OneS1ghtBridge.isInitialized;
  ```
</CodeGroup>

### .deviceAvailability

측위 가능 기기를 판별하고 미지원시 사유를 반환합니다.

| 값 | 뜻 |
| - | - |
| `available` | 측위 가능 |
| `osVersionTooLow` | iOS 27 미만 |
| `deviceNotSupported` | UWB 미지원 기기 |

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  switch OneS1ght.deviceAvailability {
  case .available:          break
  case .osVersionTooLow:    break
  case .deviceNotSupported: break
  @unknown default:         break
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  NSString *availability = OneS1ghtBridge.deviceAvailability;   // @"available" · @"osVersionTooLow" · @"deviceNotSupported"
  ```
</CodeGroup>

### .googleMapKey

도면을 표시할 Google Maps API 키를 반환합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let mapKey = OneS1ght.googleMapKey   // String?
  ```

  ```objc Objective-C icon="apple" theme={null}
  NSString *mapKey = OneS1ghtBridge.googleMapKey;
  ```
</CodeGroup>

### .sdkVersion

사용중인 SDK 버전을 반환합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let version = OneS1ght.sdkVersion   // "0.2.3"
  ```

  ```objc Objective-C icon="apple" theme={null}
  NSString *version = OneS1ghtBridge.sdkVersion;
  ```
</CodeGroup>

## 프로필

### .createProfile

새로운 개인화 프로필을 생성합니다. 성공적으로 생성하면 고유 프로필 ID를 반환합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let profileId = try await OneS1ght.createProfile(["gender": "F", "ageBand": "20s"])
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge createProfile:@{ @"gender": @"F", @"ageBand": @"20s" }
                      completion:^(NSString *profileId, NSError *error) { }];
  ```
</CodeGroup>

### .identify

수집할 측위 데이터를 묶을 프로필을 연결합니다. 연결 해제시 `nil`으로 설정하세요.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  OneS1ght.identify(profileId: profileId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge identifyWithProfileId:profileId];
  ```
</CodeGroup>

### .fetchProfile

프로필의 속성을 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let attributes = try await OneS1ght.fetchProfile(profileId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge fetchProfile:profileId
                     completion:^(NSDictionary<NSString *, NSString *> *attributes, NSError *error) { }];
  ```
</CodeGroup>

### .replaceProfile

프로필 속성을 교체하며 이전 데이터는 삭제됩니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  try await OneS1ght.replaceProfile(profileId, attributes: ["gender": "F", "ageBand": "30s"])
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge replaceProfile:profileId
                      attributes:@{ @"gender": @"F", @"ageBand": @"30s" }
                      completion:^(NSError *error) { }];
  ```
</CodeGroup>

### .deleteProfile

프로필 정보를 영구 삭제합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  try await OneS1ght.deleteProfile(profileId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge deleteProfile:profileId completion:^(NSError *error) { }];
  ```
</CodeGroup>

## 공간 조회

### .buildings

테넌트와 연관된 모든 건물 목록을 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let buildings = try await OneS1ght.buildings()
  let building = try await OneS1ght.building(id: buildingId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge buildings:^(NSArray<NSDictionary *> *buildings, NSError *error) { }];   // id · name · floorCount
  ```
</CodeGroup>

### .floors

건물의 층 목록을 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let floors = try await OneS1ght.floors(buildingId: buildingId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge floorsWithBuildingId:buildingId
                           completion:^(NSArray<NSDictionary *> *floors, NSError *error) { }];
  ```
</CodeGroup>

### .floor

선택한 층의 상세 정보를 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
  let png = floor.image   // Data?
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge floorWithBuildingId:buildingId floorId:floorId
                          completion:^(NSDictionary *floor, NSError *error) {
      NSData *png = floor[@"image"];
  }];
  ```
</CodeGroup>

### .zones

선택한 층의 모든 구역을 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let zones = try await OneS1ght.zones(buildingId: buildingId, floorId: floorId)
  let zone = try await OneS1ght.zone(buildingId: buildingId, floorId: floorId, zoneId: zoneId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge zonesWithBuildingId:buildingId floorId:floorId
                          completion:^(NSArray<NSDictionary *> *zones, NSError *error) { }];   // id · name · polygon · dwellSeconds
  ```
</CodeGroup>

### .locators

선택한 층의 로케이터 정보를 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let floorLocators = try await OneS1ght.locators(buildingId: buildingId, floorId: floorId)
  let ready = floorLocators.positioningReady
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge locatorsWithBuildingId:buildingId floorId:floorId
                             completion:^(NSDictionary *floorLocators, NSError *error) { }];   // locators · sessionId
  ```
</CodeGroup>

## 층 지정

### .setFloorMap

측위를 시작할 층을 지정합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
  try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floorId
                                 completion:^(NSError *error) { }];   // floorId nil = 해제
  ```
</CodeGroup>

### .refreshZones

최신화된 구역 정보를 조회합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let zones = await OneS1ght.refreshZones()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge refreshZones:^(NSArray<NSDictionary *> *zones) { }];
  ```
</CodeGroup>

## 측위 세션

### .floorSession

선택한 층의 로케이터 세션을 획득합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let session = try OneS1ght.floorSession()
  ```

  ```objc Objective-C icon="apple" theme={null}
  // 브리지가 세션을 감쌉니다 — 따로 가져오지 않습니다.
  ```
</CodeGroup>

### .begin

사용자 측위를 시작합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  try await session.begin()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge beginWithCompletion:^(NSError *error) { }];
  ```
</CodeGroup>

### .end

사용자 측위를 종료합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  await session.end()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge endWithCompletion:^{ }];
  ```
</CodeGroup>

### .pause · .resume

사용자 측위를 중지, 재개합니다. 종료와 달리 SDK 내 측위 엔진은 종료하지 않습니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.pause()
  session.resume()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge pause];
  [OneS1ghtBridge resume];
  ```
</CodeGroup>

### .isRunning · .isPaused

측위 상태여부와 중지여부를 반환합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  let running = session.isRunning
  let paused = session.isPaused
  let current = session.floor   // Floor?
  ```

  ```objc Objective-C icon="apple" theme={null}
  BOOL running = OneS1ghtBridge.isRunning;
  BOOL paused = OneS1ghtBridge.isPaused;
  ```
</CodeGroup>

## 세션 이벤트

### .onPosition

실시간으로 사용자 좌표 반환 이벤트를 발생합니다. (기본값 1초당 4회)

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onPosition = { coordinates in print(coordinates.x, coordinates.y) }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onPosition:^(double x, double y, double z) { }];
  ```
</CodeGroup>

### .onZoneEnter · .onZoneExit

구역 진입, 이탈시 이벤트가 동작합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onZoneEnter = { zone in print("IN \(zone.name)") }
  session.onZoneExit  = { zone in print("OUT \(zone.name)") }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onZoneEnter:^(NSDictionary *zone) { }];
  [OneS1ghtBridge onZoneExit:^(NSDictionary *zone) { }];
  ```
</CodeGroup>

### .onZoneDwell

특정 구역의 체류 시간이 초과할 경우(`dwellSeconds` 기준) 이벤트가 발생합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onZoneDwell = { zone, seconds in print("DWELL \(zone.name) \(seconds)s") }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onZoneDwell:^(NSDictionary *zone, double seconds) { }];
  ```
</CodeGroup>

### .onTriggers

공간 내 이벤트(또는 시책) 동작시 이벤트가 발생합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onTriggers = { zoneId, triggers in
      for trigger in triggers { print(trigger.type, trigger.payload ?? [:]) }
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onTriggers:^(NSString *zoneId, NSArray<NSDictionary *> *triggers) { }];   // triggerId · type · payload
  ```
</CodeGroup>

### .onFloorDetected

로케이터의 BLE 신호를 수신한 경우 이벤트가 발생합니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onFloorDetected = { floorId in
      guard let floorId else { return }
      Task {
          let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
          try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
      }
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onFloorDetected:^(NSString *floorId) {
      if (!floorId) { return; }
      [OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floorId completion:^(NSError *error) { }];
  }];
  ```
</CodeGroup>

### .onStopped

사용자 측위가 중지된 경우 이벤트가 발생합니다.

| 이유 | 뜻 |
| - | - |
| `ended` | `end()` · `reset` · 키 교체 |
| `engineFailed` | 측위엔진 구동 실패시 |

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onStopped = { reason in
      switch reason {
      case .ended:        break
      case .engineFailed: break
      @unknown default:   break
      }
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onStopped:^(NSString *reason) { }];   // @"ended" · @"engineFailed"
  ```
</CodeGroup>

### .onConfigChanged

콘솔 페이지에서 설정이 변경되었을 경우 이벤트가 발생합니다.

| 신호 | 의미 |
| - | - |
| `zonesChanged(floorId)` | 구역 변경 |
| `planChanged(floorId)` | 도면 변경 |
| `rulesChanged(zoneId)` | 시책 변경 |
| `sdkConfigChanged` | 원격 설정 변경 |
| `resyncNeeded` | 재연결 · 이벤트 누락 |

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  session.onConfigChanged = { change in
      switch change {
      case .zonesChanged, .resyncNeeded: Task { _ = await OneS1ght.refreshZones() }
      case .planChanged(let floorId):    print("redraw", floorId ?? "")
      default:                           break
      }
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge onConfigChanged:^(NSString *kind, NSString *targetId) {
      if ([kind isEqualToString:@"zonesChanged"]) {
          [OneS1ghtBridge refreshZones:^(NSArray<NSDictionary *> *zones) { }];
      }
  }];
  ```
</CodeGroup>

## 데이터 전송

좌표는 300건 또는 60초마다, 그리고 백그라운드 진입과 `end()` 때 자동 전송됩니다.

### .uploadPendingPositions

쌓인 좌표 즉시 전송.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  await OneS1ght.uploadPendingPositions()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge uploadPendingPositions:^{ }];
  ```
</CodeGroup>

### .discardPendingPositions

쌓인 좌표 폐기.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  OneS1ght.discardPendingPositions()
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge discardPendingPositions];
  ```
</CodeGroup>

## 오류 처리

실패는 `SdkError`(SDK 상태) 와 `ApiError`(통신) 로 옵니다. 코드의 뜻은 [에러 코드](/sdk/troubleshooting/error-codes)를 참고하세요. Objective-C 는 `NSError.userInfo[@"sdkCode"]` 에 담깁니다.

<CodeGroup>
  ```swift Swift icon="swift" theme={null}
  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" · 사유
  }
  ```

  ```objc Objective-C icon="apple" theme={null}
  [OneS1ghtBridge initializeWithSdkKey:key completion:^(NSError *error) {
      NSString *code = error.userInfo[@"sdkCode"];   // @"E1003"
  }];
  ```
</CodeGroup>

## 모델

목록은 `[T]`, 바이트는 `Data` 입니다.

### Coordinates

| 필드 | 타입 | 설명 |
| - | - | - |
| `x` · `y` | `Double` | 도면 로컬 미터 |
| `z` | `Double` | 높이 |

### Building · Floor

| 타입 | 필드 | 설명 |
| - | - | - |
| `Building` | `id` · `name` | 건물 ID · 이름 |
| | `floorCount: Int?` | 층 수 |
| `Floor` | `id` · `name` | 층 ID · 이름 |
| | `image: Data?` | 도면 이미지 (BASE64) |
| | `hasPlan: Bool` | 도면 등록 여부 |
| | `originX` · `originY` | 도면 원점(미터) |
| | `widthM` · `heightM` | 도면 크기(미터) |
| | `minX` · `minY` · `maxX` · `maxY` | 배치 범위 |

### Locator · FloorLocators

| 타입 | 필드 | 설명 |
| - | - | - |
| `Locator` | `address: Int` | UWB MAC 뒤 2바이트 |
| | `x` · `y` · `z` | 도면 로컬 미터 |
| `FloorLocators` | `locators` | 층의 로케이터 목록 |
| | `sessionId: Int?` | UWB 세션 — 층마다 다름 |
| | `positioningReady: Bool` | 로케이터와 세션이 모두 있는가 |

### Zone

| 필드 | 타입 | 설명 |
| - | - | - |
| `id` · `name` | `String` | 구역 ID · 이름 |
| `polygon` | `[Position]` | 꼭짓점(`x`, `y` 미터) |
| `dwellSeconds` | `Int?` | 체류 이벤트 기준(초) |

### Trigger

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

## Objective-C 브리지

아래 파일을 앱 타깃에 추가하고 한 번 빌드하면 `<Product Module Name>-Swift.h` 가 생깁니다.

```swift OneS1ghtBridge.swift theme={null}
// OneS1ghtBridge.swift: Objective-C 에서 OneS1ght 를 부르기 위한 브리지
import Foundation
import OneS1ght

@MainActor @objc public final class OneS1ghtBridge: NSObject {

    // MARK: 초기화

    @objc public static func initialize(sdkKey: String, completion: @escaping (NSError?) -> Void) {
        Task {
            do { try await OneS1ght.initialize(sdkKey: sdkKey); completion(nil) }
            catch { completion(nsError(error)) }
        }
    }

    /// status: "authorized" | "denied" | "unsupported"
    @objc public static func requestPermission(_ completion: @escaping (String) -> Void) {
        Task { completion(String(describing: await OneS1ght.requestPermission())) }
    }

    @objc public static func setLanguage(_ code: String?) {
        OneS1ght.setLanguage(code)
    }

    @objc public static func reset(_ completion: @escaping () -> Void) {
        Task { await OneS1ght.reset(); completion() }
    }

    /// level: "log" | "info" | "warn" | "error"
    @objc public static func onDebugLog(_ handler: @escaping (String, String) -> Void) {
        OneS1ght.onDebugLog = { level, message in handler(String(describing: level), message) }
    }

    // MARK: 상태

    @objc public static var isInitialized: Bool { OneS1ght.isInitialized }

    /// "available" | "osVersionTooLow" | "deviceNotSupported"
    @objc public static var deviceAvailability: String { String(describing: OneS1ght.deviceAvailability) }

    @objc public static var isDeviceAvailable: Bool { OneS1ght.isDeviceAvailable }

    @objc public static var googleMapKey: String? { OneS1ght.googleMapKey }

    @objc public static var sdkVersion: String { OneS1ght.sdkVersion }

    // MARK: 프로필

    @objc public static func createProfile(_ attributes: [String: String],
                                           completion: @escaping (String?, NSError?) -> Void) {
        Task {
            do { completion(try await OneS1ght.createProfile(attributes), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func fetchProfile(_ profileId: String,
                                          completion: @escaping ([String: String]?, NSError?) -> Void) {
        Task {
            do { completion(try await OneS1ght.fetchProfile(profileId), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func replaceProfile(_ profileId: String, attributes: [String: String],
                                            completion: @escaping (NSError?) -> Void) {
        Task {
            do { try await OneS1ght.replaceProfile(profileId, attributes: attributes); completion(nil) }
            catch { completion(nsError(error)) }
        }
    }

    @objc public static func deleteProfile(_ profileId: String, completion: @escaping (NSError?) -> Void) {
        Task {
            do { try await OneS1ght.deleteProfile(profileId); completion(nil) }
            catch { completion(nsError(error)) }
        }
    }

    @objc public static func identify(profileId: String?) {
        OneS1ght.identify(profileId: profileId)
    }

    // MARK: 공간 조회 — 모델은 NSDictionary 로 넘긴다

    @objc public static func buildings(_ completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
        Task {
            do { completion(try await OneS1ght.buildings().map(dict), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func building(id: String, completion: @escaping ([String: Any]?, NSError?) -> Void) {
        Task {
            do { completion(dict(try await OneS1ght.building(id: id)), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func floors(buildingId: String,
                                    completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
        Task {
            do { completion(try await OneS1ght.floors(buildingId: buildingId).map(dict), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func floor(buildingId: String, floorId: String,
                                   completion: @escaping ([String: Any]?, NSError?) -> Void) {
        Task {
            do { completion(dict(try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func zones(buildingId: String, floorId: String,
                                   completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
        Task {
            do { completion(try await OneS1ght.zones(buildingId: buildingId, floorId: floorId).map(dict), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func zone(buildingId: String, floorId: String, zoneId: String,
                                  completion: @escaping ([String: Any]?, NSError?) -> Void) {
        Task {
            do {
                let zone = try await OneS1ght.zone(buildingId: buildingId, floorId: floorId, zoneId: zoneId)
                completion(dict(zone), nil)
            } catch { completion(nil, nsError(error)) }
        }
    }

    @objc public static func locators(buildingId: String, floorId: String,
                                      completion: @escaping ([String: Any]?, NSError?) -> Void) {
        Task {
            do { completion(dict(try await OneS1ght.locators(buildingId: buildingId, floorId: floorId)), nil) }
            catch { completion(nil, nsError(error)) }
        }
    }

    // MARK: 층 지정

    /// floorId 가 nil 이면 층 지정을 비운다
    @objc public static func setFloorMap(buildingId: String?, floorId: String?,
                                         completion: @escaping (NSError?) -> Void) {
        Task {
            do {
                var floor: Floor?
                if let buildingId, let floorId {
                    floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
                }
                try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
                completion(nil)
            } catch { completion(nsError(error)) }
        }
    }

    @objc public static func refreshZones(_ completion: @escaping ([[String: Any]]) -> Void) {
        Task { completion(await OneS1ght.refreshZones().map(dict)) }
    }

    // MARK: 측위 세션

    @objc public static func begin(completion: @escaping (NSError?) -> Void) {
        Task {
            do { try await OneS1ght.floorSession().begin(); completion(nil) }
            catch { completion(nsError(error)) }
        }
    }

    @objc public static func end(completion: @escaping () -> Void) {
        Task {
            if let session = try? OneS1ght.floorSession() { await session.end() }
            completion()
        }
    }

    @objc public static func pause() { (try? OneS1ght.floorSession())?.pause() }

    @objc public static func resume() { (try? OneS1ght.floorSession())?.resume() }

    @objc public static var isRunning: Bool { (try? OneS1ght.floorSession())?.isRunning ?? false }

    @objc public static var isPaused: Bool { (try? OneS1ght.floorSession())?.isPaused ?? false }

    // MARK: 세션 이벤트

    /// 도면 로컬 미터
    @objc public static func onPosition(_ handler: @escaping (Double, Double, Double) -> Void) {
        (try? OneS1ght.floorSession())?.onPosition = { c in handler(c.x, c.y, c.z) }
    }

    @objc public static func onZoneEnter(_ handler: @escaping ([String: Any]) -> Void) {
        (try? OneS1ght.floorSession())?.onZoneEnter = { zone in handler(dict(zone)) }
    }

    @objc public static func onZoneExit(_ handler: @escaping ([String: Any]) -> Void) {
        (try? OneS1ght.floorSession())?.onZoneExit = { zone in handler(dict(zone)) }
    }

    @objc public static func onZoneDwell(_ handler: @escaping ([String: Any], Double) -> Void) {
        (try? OneS1ght.floorSession())?.onZoneDwell = { zone, seconds in handler(dict(zone), seconds) }
    }

    /// triggers: [{ triggerId, type, payload }]
    @objc public static func onTriggers(_ handler: @escaping (String, [[String: Any]]) -> Void) {
        (try? OneS1ght.floorSession())?.onTriggers = { zoneId, triggers in
            handler(zoneId, triggers.map { t in
                var d: [String: Any] = ["triggerId": t.triggerId, "type": t.type]
                if let payload = t.payload { d["payload"] = payload }
                return d
            })
        }
    }

    @objc public static func onFloorDetected(_ handler: @escaping (String?) -> Void) {
        (try? OneS1ght.floorSession())?.onFloorDetected = { floorId in handler(floorId) }
    }

    /// reason: "ended" | "engineFailed"
    @objc public static func onStopped(_ handler: @escaping (String) -> Void) {
        (try? OneS1ght.floorSession())?.onStopped = { reason in handler(String(describing: reason)) }
    }

    /// kind: "zonesChanged" | "planChanged" | "rulesChanged" | "sdkConfigChanged" | "resyncNeeded"
    /// targetId: zonesChanged·planChanged 는 층 ID, rulesChanged 는 구역 ID
    @objc public static func onConfigChanged(_ handler: @escaping (String, String?) -> Void) {
        (try? OneS1ght.floorSession())?.onConfigChanged = { change in
            switch change {
            case .zonesChanged(let floorId): handler("zonesChanged", floorId)
            case .planChanged(let floorId):  handler("planChanged", floorId)
            case .rulesChanged(let zoneId):  handler("rulesChanged", zoneId)
            case .sdkConfigChanged:          handler("sdkConfigChanged", nil)
            case .resyncNeeded:              handler("resyncNeeded", nil)
            @unknown default:                handler("unknown", nil)
            }
        }
    }

    // MARK: 데이터 전송

    @objc public static func uploadPendingPositions(_ completion: @escaping () -> Void) {
        Task { await OneS1ght.uploadPendingPositions(); completion() }
    }

    @objc public static func discardPendingPositions() {
        OneS1ght.discardPendingPositions()
    }

    // MARK: 변환

    private static func dict(_ b: Building) -> [String: Any] {
        var d: [String: Any] = ["id": b.id, "name": b.name]
        if let count = b.floorCount { d["floorCount"] = count }
        return d
    }

    private static func dict(_ f: Floor) -> [String: Any] {
        var d: [String: Any] = ["id": f.id, "name": f.name, "hasPlan": f.hasPlan,
                                "originX": f.originX, "originY": f.originY,
                                "widthM": f.widthM, "heightM": f.heightM]
        if let image = f.image { d["image"] = image }
        return d
    }

    private static func dict(_ z: Zone) -> [String: Any] {
        var d: [String: Any] = ["id": z.id, "name": z.name,
                                "polygon": z.polygon.map { [$0.x, $0.y] }]
        if let dwell = z.dwellSeconds { d["dwellSeconds"] = dwell }
        return d
    }

    private static func dict(_ l: FloorLocators) -> [String: Any] {
        var d: [String: Any] = ["locators": l.locators.map {
            ["address": $0.address, "x": $0.x, "y": $0.y, "z": $0.z] as [String: Any]
        }]
        if let sessionId = l.sessionId { d["sessionId"] = sessionId }
        return d
    }

    /// Swift 오류 → NSError. domain "OneS1ght", code 3001, userInfo["sdkCode"] = "E3001" 형태
    private static func nsError(_ error: Error) -> NSError {
        let code: SdkErrorCode
        let message: String
        switch error {
        case let e as SdkError: code = e.code; message = e.code.summary
        case let e as ApiError: code = e.code; message = e.description
        default: return error as NSError
        }
        return NSError(domain: "OneS1ght",
                       code: Int(code.rawValue.dropFirst()) ?? 0,
                       userInfo: [NSLocalizedDescriptionKey: message,
                                  "sdkCode": code.rawValue])
    }
}
```

## 제약

* UWB 측위는 포그라운드 전용 — 백그라운드에서 멈추고 돌아오면 측위동작이 재개됩니다.
* 좌표 버퍼는 메모리 — 강제 종료 시 미전송 좌표는 사라집니다.
* 시뮬레이터에서는 측위할 수 없습니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.