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

## 패키지 선언

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

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

## 초기화

### .initialize

OneS1ght API 키를 검증하고 고객사 설정을 받아 옵니다. 측위 엔진을 켜거나 기기 지원 여부를 확인하지는 않습니다.

```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) { }];
```

### .requestPermission

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

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

```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"
```

### .setLanguage

SDK 로그 문구 언어를 설정합니다.\

`ko` : 한국어,  `ja` : 일본어,  `en` : 영어, `nil` : 시스템 설정 언어

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

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

### .onDebugLog

SDK 로그를 수신합니다.\

`level` : 로그 레벨, `message` : 로그 메세지

```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) { }];
```

### .reset

세션을 버립니다. 이후 API 는 `initialize` 를 다시 부를 때까지 `notInitialized` 오류를 냅니다(다른 키로도 가능).

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

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

## 상태 조회

### .isInitialized

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

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

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

### .deviceAvailability

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

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

```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"
```

### .googleMapKey

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

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

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

### .sdkVersion

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

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

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

## 프로필

### .createProfile

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

```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) { }];
```

### .identify

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

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

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

### .fetchProfile

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

```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) { }];
```

### .replaceProfile

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

```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) { }];
```

### .deleteProfile

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

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

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

## 공간 조회

### .buildings

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

```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
```

### .floors

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

```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) { }];
```

### .floor

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

```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"];
}];
```

### .zones

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

```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
```

### .locators

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

```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
```

## 층 지정

### .setFloorMap

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

```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 = 해제
```

### .refreshZones

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

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

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

## 측위 세션

### .floorSession

측위 세션을 얻습니다. 층과 무관하게 앱 전체에서 같은 인스턴스(싱글턴)입니다.

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

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

### .begin

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

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

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

### .end

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

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

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

### .pause · .resume

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

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

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

### .isRunning · .isPaused

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

```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;
```

## 세션 이벤트

### .onPosition

실시간으로 사용자 좌표 반환 이벤트를 발생합니다. 엔진이 내는 속도 그대로 오며, 초당 4회(기본값)는 서버 전송분에만 적용됩니다.

```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) { }];
```

### .onZoneEnter · .onZoneExit

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

```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) { }];
```

### .onZoneDwell

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

```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) { }];
```

### .onTriggers

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

```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
```

### .onFloorDetected

로케이터의 BLE 신호로 층을 찾으면 층 ID 로, 층을 잃으면 `nil` 로 이벤트가 발생합니다.

```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) { }];
}];
```

### .onStopped

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

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

```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"
```

### .onConfigChanged

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

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

```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) { }];
    }
}];
```

## 데이터 전송

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

### .uploadPendingPositions

쌓인 좌표 즉시 전송.

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

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

### .discardPendingPositions

쌓인 좌표 폐기.

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

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

## 오류 처리

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

```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"
}];
```

## 모델

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

### Coordinates

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

### Building · Floor

| 타입 | 필드 | 설명 |
| - | - | - |
| `Building` | `id` · `name` | 건물 ID · 이름 |
| | `floorCount: Int?` | 층 수 |
| `Floor` | `id` · `name` | 층 ID · 이름 |
| | `image: Data?` | 도면 PNG 바이트 |
| | `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.