> ## 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 설정 가이드

## 시스템 요구사항

| 항목 | 요구사항 |
| - | - |
| 운영체제 | iOS 27 이상 (앱 최소 타깃은 iOS 18) |
| 지원 기기 | iPhone 12 이상 (UWB 칩 탑재 기기 · iPhone SE · iPad · Apple Watch 미지원) |
| 개발 환경 | Xcode 27 이상 |
| 언어 | Swift 5 이상 · Objective-C ([브리지 경유](/developers/sdk/reference/ios#objective-c-브리지)) |
| 패키지 관리 | Swift Package Manager |

측위정보 수신 미지원 기기인 경우 SDK 동작을 중지하며 운영중인 앱에 영향을 미치지 않습니다.

## SDK 설치

**Step 1**

Xcode에서 **\[File] → \[Add Package Dependencies…]** 를 실행합니다.

**Step 2**

왼쪽 상단의 검색창에 아래 주소를 입력합니다.

```text theme={null}
https://github.com/onecheck-inc/OneS1ght-iOS-SDK
```

**Step 3**

**Dependency Rule** 을 `Up to Next Major Version` 으로 두고 값을 `0.2.2` 로 설정합니다.

**Step 4**

**\[Add Package]** 를 눌러 `OneS1ght` 라이브러리를 앱 타깃에 추가합니다.

## iOS 권한 설정

`Info.plist` 에 아래 다섯 가지 권한 문구를 하나도 빠짐없이 설정합니다.

| 키 | 권한 | 용도 |
| - | - | - |
| `NSBluetoothAlwaysUsageDescription` | 블루투스 권한 | 로케이터 BLE 수신용 |
| `NSLocationWhenInUseUsageDescription` | 위치 권한 | 로케이터의 위치 확인용도 |
| `NSLocationAlwaysAndWhenInUseUsageDescription` | 위치 권한 (항상) | 층 찾기용도 (iOS 27.2 이상에서 BLE 층 판정에 필요) |
| `NSLocationTemporaryUsageDescriptionDictionary` | 정밀 위치 권한 | 정확한 실내 위치 계산용도 안의 키는 반드시 `Positioning` |
| `NSNearbyInteractionUsageDescription` | Nearby Interaction | 사용자의 정밀 측위 용도 |

Xcode 에서 앱 타깃의 **\[Info]** 탭 목록에 추가합니다.

**1. 문자열 키 4개**

행에 마우스를 올리면 나타나는 **\[+]** 를 누르고, Key 에 키 이름을 입력한 뒤 Value 에 문구를 넣습니다. Type 은 String 입니다.

**2. Dictionary 키 1개**

`NSLocationTemporaryUsageDescriptionDictionary` 는 두 줄 구조입니다.

1. **\[+]** → Key 에 키 이름 입력 → Type 을 **Dictionary** 로 바꿉니다 (Value 는 비워 둡니다).
2. 행 왼쪽 **▸** 를 펼친 뒤 그 행에서 **\[+]** → Key 에 `Positioning`, Type String, Value 에 문구를 넣습니다.

**3. 확인**

목록에 다섯 항목이 모두 보이면 완료입니다. 섹션 제목이 **Custom macOS Application Target Properties** 로 보여도 괜찮습니다.

Value 에 넣을 문구는 아래 `<string>` 값을 그대로 쓰면 됩니다. Xcode 화면 대신 파일에 직접 쓰는 방법은 [부록](#부록)을 참고하세요.

```xml Info.plist theme={null}
<key>NSBluetoothAlwaysUsageDescription</key>
<string>주변 측위 장비를 찾는 데 사용합니다.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>매장 내 위치 확인을 위해 사용합니다.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>iOS 27.2 이상에서 매장의 층을 찾는 데 사용합니다.</string>
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
    <key>Positioning</key>
    <string>정확한 실내 위치를 계산하는 데 사용합니다.</string>
</dict>
<key>NSNearbyInteractionUsageDescription</key>
<string>UWB 정밀 측위를 위해 사용합니다.</string>
```

## SDK 초기화

SDK 초기화를 해야 SDK 내부 엔진을 실행할 수 있습니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `initialize(sdkKey:)` | 키 검증과 설정 수신. 앱 시작 시 1회 | 없음 |
| `isInitialized` | 초기화 성공 여부 | `Bool` |
| `sdkVersion` | SDK 버전 | `String` (예: `"0.2.3"`) |

Objective-C 브리지는 같은 이름의 메서드에서 결과를 completion 블록으로 돌려줍니다. \

브리지의 콜백(`onPosition` · `onZoneEnter` · `onFloorDetected` · `onConfigChanged` · `onStopped` 등)은 `initialize` 가 성공한 뒤에 등록하세요. 그 전에 등록하면 아무 일도 일어나지 않습니다(`onDebugLog` 는 예외).

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

#if DEBUG
let sdkKey = "YOUR_DEVELOPMENT_API_KEY" // 개발(테스트) 모드에서 사용 시
#else
let sdkKey = "YOUR_PRODUCTION_API_KEY"  // 운영 모드에서 사용 시
#endif
try await OneS1ght.initialize(sdkKey: sdkKey)
```

```objc Objective-C icon="apple" theme={null}
#import "MyApp-Swift.h"   // MyApp 자리에 Product Module Name. 브리지 파일을 추가하고 1회 빌드하면 생깁니다.

#ifdef DEBUG
NSString *sdkKey = @"YOUR_DEVELOPMENT_API_KEY"; // 개발(테스트) 모드에서 사용 시
#else
NSString *sdkKey = @"YOUR_PRODUCTION_API_KEY";  // 운영 모드에서 사용 시
#endif
[OneS1ghtBridge initializeWithSdkKey:sdkKey completion:^(NSError *error) {
    if (error) { return; }   // 실패 처리
}];
```

## 프로필

사용자(계정 또는 기기) 한 명의 동선은 **프로필** 단위로 관리됩니다. \

프로필 ID 는 고유하며, 분실하면 다시 찾을 수 없으니 보관에 주의해 주세요.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `createProfile(_:)` | 속성으로 프로필 생성 | `String` (프로필 ID) |
| `identify(profileId:)` | 이 기기의 측위를 프로필에 연결.  프로필 해제시 `nil` | 없음 |
| `fetchProfile(_:)` | 프로필 ID 로 속성 조회 | `[String: String]` |
| `replaceProfile(_:attributes:)` | 속성 전체 교체 | 없음 |
| `deleteProfile(_:)` | 프로필 삭제 | 없음 |

### 프로필 생성

새 프로필을 만들면 프로필 ID 를 돌려받습니다. 속성은 레포트 · 지표의 세그먼트로 쓰이며 Key-Value 에 제약이 없습니다.\

발급받은 ID 는 앱(또는 고객사 서버)에 보관해 두고, 매 실행마다 측위 전에 `identify` 로 연결합니다.

> [!WARNING]
>
> 프로필 속성에는 이름 · 전화번호 · 회원 ID 같은 민감한 정보를 넣지 마세요.\\
>
> 연령대(`"20s"`)처럼 식별되지 않는 값만 담습니다.

```swift Swift icon="swift" theme={null}
let profileId = try await OneS1ght.createProfile(["gender": "F", "ageBand": "20s"])   // 새 프로필 생성 → 프로필 ID 반환
save(profileId)                                                                     // 앱 또는 고객사 서버에 보관

OneS1ght.identify(profileId: savedProfileId)   // 매 실행, 측위 전에 연결. 로그아웃 시 nil
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge createProfile:@{ @"gender": @"F", @"ageBand": @"20s" }                 // 새 프로필 생성 → 프로필 ID 반환
                    completion:^(NSString *profileId, NSError *error) {
    if (!error) { [self save:profileId]; }                                          // 앱 또는 고객사 서버에 보관
}];

[OneS1ghtBridge identifyWithProfileId:savedProfileId];   // 매 실행, 측위 전에 연결. 로그아웃 시 nil
```

### 프로필 정보 조회

프로필 ID를 통해 저장된 개인화 속성을 조회합니다.

```swift Swift icon="swift" theme={null}
let attributes = try await OneS1ght.fetchProfile(profileId)   // ["gender": "F", "ageBand": "20s"]
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge fetchProfile:profileId completion:^(NSDictionary *attributes, NSError *error) {
    if (error) { return; }   // 없는 ID
}];
```

### 프로필 정보 수정

개인화 프로필 속성을 **통째로 교체**됩니다. 수정 후 기존 값은 삭제되며 되돌릴 수 없습니다.

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

### 프로필 삭제

개인화 프로필 속성을 삭제합니다. 삭제된 속성은 되돌릴 수 없습니다.

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

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

## 건물, 층 선택

층 지정은 선택입니다. 지정하지 않아도 측위 엔진이 로케이터의 BLE 신호로 층을 찾아 좌표를 보냅니다. \

다만 구역 이벤트를 받으려면 층을 지정해야 합니다([구역 이벤트](#구역-이벤트) 참고).

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `floorSession()` | 측위 세션 | `FloorSession` |
| `session.onFloorDetected` | 엔진이 층을 찾으면 호출.  층을 잃으면 `nil` | `(String?)` (층 ID) |
| `buildings()` | 콘솔에 등록된 건물 목록 | `[Building]`  `id` : 건물 ID  `name` : 건물명  `floorCount` : 층 개수 |
| `floors(buildingId:)` | 건물의 층 목록 (도면 이미지 없음) | `[Floor]` |
| `floor(buildingId:floorId:)` | 층 하나 (도면 이미지 포함) | `Floor` |
| `setFloorMap(_:buildingId:)` | 층 지정  해제 시 `nil` | 없음 |
| `session.floor` | 지금 지정된 층 | `Floor?` |

### BLE 수신으로 선택

SDK 의 측위 엔진이 로케이터의 BLE 신호로 층을 찾으면 `onFloorDetected` 로 층 ID 가 옵니다. \

층을 잃으면 `nil` 이 옵니다.

```swift Swift icon="swift" theme={null}
let session = try OneS1ght.floorSession()   // 측위 세션 획득
session.onFloorDetected = { floorId in      // 엔진이 층을 찾으면 층 ID 전달
    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) {   // 엔진이 층을 찾으면 층 ID 전달
    if (!floorId) { return; }
    [OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floorId completion:^(NSError *error) { }];   // 층 지정
}];
```

### 수동으로 선택

사람이 층을 고르게 하거나 측위 전에 지도를 먼저 그릴 때는 건물과 층을 직접 지정합니다.

```swift Swift icon="swift" theme={null}
let buildings = try await OneS1ght.buildings()                            // 콘솔에 등록된 건물 목록
let floors    = try await OneS1ght.floors(buildingId: buildings[0].id)   // 건물에 속한 층 목록

try await OneS1ght.setFloorMap(floors[0], buildingId: buildings[0].id)   // 층 지정
try await OneS1ght.setFloorMap(nil)                                      // 층 지정 해제
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge buildings:^(NSArray<NSDictionary *> *buildings, NSError *error) {                 // 콘솔에 등록된 건물 목록
    NSString *buildingId = buildings.firstObject[@"id"];
    [OneS1ghtBridge floorsWithBuildingId:buildingId completion:^(NSArray<NSDictionary *> *floors, NSError *error) {   // 층 목록
        [OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floors.firstObject[@"id"]
                                       completion:^(NSError *error) { }];   // 층 지정
    }];
}];
[OneS1ghtBridge setFloorMapWithBuildingId:nil floorId:nil completion:^(NSError *error) { }];   // 층 지정 해제
```

## 측위정보 실시간 수신

`floorSession()` 으로 측위 세션을 얻고, `begin()` 으로 시작해 `end()` 로 종료합니다. \

위치는 `onPosition` 으로 받으며, 좌표(미터)는 엔진이 내는 속도 그대로 옵니다. \

서버로 보내는 좌표만 콘솔 설정 주기(기본 초당 4회)로 솎아 냅니다.

| 상황 | 할 일 |
| - | - |
| 앱이 백그라운드로 감 | 없음 — SDK 가 측위를 멈추고, 포그라운드로 돌아오면 자동으로 다시 시작합니다. |
| 화면을 잠시 벗어날 때 | `pause()` 로 중지, `resume()` 으로 재개합니다. 엔진은 켜져 있어 바로 이어집니다. |
| 측위를 완전히 끝낼 때 | `end()`. 엔진까지 꺼지므로 다음 `begin()` 은 로케이터를 처음부터 다시 찾습니다. |

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `session.onPosition` | 실시간 좌표 | `(Coordinates)`  `x` · `y` : 도면 기준 미터  `z` : 높이 |
| `session.begin()` | 측위 시작 | 없음 |
| `session.end()` | 측위 종료 + 남은 좌표 전송 | 없음 |
| `session.pause()` / `resume()` | 일시정지 / 재개 | 없음 |
| `session.isRunning` / `isPaused` | 측위 가동 / 일시정지 여부 | `Bool` |

```swift Swift icon="swift" theme={null}
let session = try OneS1ght.floorSession()
session.onPosition = { coord in updateMyLocationMarker(x: coord.x, y: coord.y) }   // coord.z: 높이

try await session.begin()   // 측위 시작
await session.end()         // 측위 종료 + 남은 좌표 전송
```

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

[OneS1ghtBridge beginWithCompletion:^(NSError *error) { }];   // 측위 시작
[OneS1ghtBridge endWithCompletion:^{ }];                       // 측위 종료 + 남은 좌표 전송
```

## 구역 이벤트

콘솔 [공간 관리](/console/areas)에서 그린 구역의 진입 · 이탈 · 체류 이벤트를 받습니다. 판정은 기기 안에서 이루어져 바로 옵니다. \

구역에 걸린 시책(쿠폰 · 사이니지 등)은 서버가 매칭해 `onTriggers` 로 전달됩니다. \

구역 목록은 `setFloorMap` 으로 지정한 층에서 받습니다. 층을 지정하지 않으면 구역이 비어 `onZoneEnter` · `onZoneExit` 가 오지 않고 `E3009` 가 남습니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `session.onZoneEnter` / `onZoneExit` | 구역 진입 / 이탈 | `(Zone)`  `id` : 구역 ID  `name` : 구역명  `polygon` : 구역 도형(꼭짓점 목록)  `dwellSeconds` : 체류 판정 기준(초) |
| `session.onZoneDwell` | 구역 체류시  `dwellSeconds` 도달 시 방문당 1회 | `(Zone, TimeInterval)`  구역, 체류 시간(초) |
| `session.onTriggers` | 서버가 매칭한 개인화 액션 | `(String, [Trigger])`  구역 ID, 트리거 목록  (`triggerId` : 트리거 ID  `type` : 유형 — `signage` · `coupon` · `tracking` · `merch` · `generic`  `payload` : 유형별 부가 정보) |
| `session.onConfigChanged` | 콘솔의 구역 설정 변경 시 | `(ConfigChange)`  `.zonesChanged` : 구역 변경  `.planChanged` : 도면 변경  `.rulesChanged` : 시책 변경  `.sdkConfigChanged` : SDK 원격 설정 변경  `.resyncNeeded` : 전체 재조회 (연결이 다시 맺어졌거나 이벤트를 놓칠 경우) |
| `refreshZones()` | 구역정보 최신화 | `[Zone]` |

```swift Swift icon="swift" theme={null}
session.onZoneEnter = { zone in showBanner("진입: \(zone.name)") }
session.onZoneExit  = { zone in hideBanner() }
session.onZoneDwell = { zone, seconds in showBanner("체류: \(zone.name) — \(Int(seconds))초") }
session.onTriggers  = { zoneId, triggers in triggers.forEach(handle) }   // type: signage · coupon · tracking · merch · generic
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge onZoneEnter:^(NSDictionary *zone) { [self showBanner:zone[@"name"]]; }];
[OneS1ghtBridge onZoneExit:^(NSDictionary *zone)  { [self hideBanner]; }];
[OneS1ghtBridge onZoneDwell:^(NSDictionary *zone, double seconds) { [self showDwell:zone[@"name"] seconds:seconds]; }];
[OneS1ghtBridge onTriggers:^(NSString *zoneId, NSArray<NSDictionary *> *triggers) {
    for (NSDictionary *trigger in triggers) { [self handle:trigger]; }   // triggerId · type · payload
}];
```

### 구역정보 업데이트

콘솔에서 구역 정보가 바뀔 때 `onConfigChanged` 가 호출됩니다. SDK 는 호출 횟수를 줄이지 않습니다. \

구역을 다시 받을 때마다 진입 · 이탈 판정이 처음부터 시작되므로, 연달아 오는 신호는 앱에서 1초쯤 모아 한 번만 처리하세요.

```swift Swift icon="swift" theme={null}
var refreshTask: Task<Void, Never>?
session.onConfigChanged = { change in
    switch change {
    case .zonesChanged, .resyncNeeded:
        refreshTask?.cancel()                             // 연달아 오면 마지막 것만
        refreshTask = Task {
            try? await Task.sleep(for: .seconds(1))
            guard !Task.isCancelled else { return }
            _ = await OneS1ght.refreshZones()
        }
    case .planChanged:                 reloadFloorPlan()   // 도면이 바뀜
    default:                           break
    }
}
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge onConfigChanged:^(NSString *kind, NSString *targetId) {
    if ([kind isEqualToString:@"zonesChanged"] || [kind isEqualToString:@"resyncNeeded"]) {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(reloadZones) object:nil];
        [self performSelector:@selector(reloadZones) withObject:nil afterDelay:1.0];   // 1초 모아 한 번만 — reloadZones 에서 refreshZones: 호출
    } else if ([kind isEqualToString:@"planChanged"]) {
        [self reloadFloorPlan];   // 도면이 바뀜
    }
}];
```

## 도면 표시

로케이터와 관련된 도면과 사용자의 현재 위치를 표시할 수 있습니다. \

도면 설정을 하지 않을 경우 배경은 격자로 표시됩니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `floor(buildingId:floorId:)` | 층 상세 정보 | `Floor` `image: Data?`: 도면 이미지 `hasPlan` : 도면 등록 여부 `originX` : 도면 원점 X (미터) `originY` : 도면 원점 Y (미터) `widthM` : 가로 길이 (미터) `heightM` : 세로 길이 (미터)  `minX` : 최소 X값  `minY` : 최소 Y값 `maxX` : 최대 X값 `maxY` : 최대 Y값 |
| `locators(buildingId:floorId:)` | 층의 로케이터 정보 | `FloorLocators` `locators` : 로케이터 정보 (`address`: UWB MAC 뒤 2바이트(`Int`),  `x` · `y` · `z` : 로케이터 좌표) `sessionId` : UWB 세션 ID `positioningReady` : 측위 가능 여부 |

```swift Swift icon="swift" theme={null}
let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
planView.image = floor.image.flatMap(UIImage.init(data:))
setBounds(minX: floor.minX, minY: floor.minY, maxX: floor.maxX, maxY: floor.maxY)
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge floorWithBuildingId:buildingId floorId:floorId completion:^(NSDictionary *floor, NSError *error) {
    self.planView.image = floor[@"image"] ? [UIImage imageWithData:floor[@"image"]] : nil;
    // 범위: originX · originY · widthM · heightM (미터)
}];
```

## 기타 옵션

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `uploadPendingPositions()` | 앱 내부 저장 좌표 즉시 전송 | 없음 |
| `discardPendingPositions()` | 앱 내부 저장 좌표 버림 | 없음 |
| `reset()` | 세션 폐기 (이후 `initialize` 를 다시 불러야 합니다) | 없음 |
| `session.onStopped` | 세션 중단 | `(StopReason)`: 세션 중단 사유  `.ended` : 세션 종료(정상)  `.engineFailed`: 엔진 이상(실패) |
| `OneS1ght.onDebugLog` | SDK 디버깅 도구 | `(LogLevel, String)` 로그 반환 |

### 데이터 전송 제어

좌표는 **300건이 쌓일 때**, 그리고 `begin()` 부터 **60초마다** 자동 전송됩니다(300건 전송으로 60초 주기가 다시 시작되지는 않습니다). \

백그라운드 진입과 `end()` 때도 남은 좌표를 보냅니다. 단 앱이 강제 종료되면 좌표는 폐기됩니다.

```swift Swift icon="swift" theme={null}
await OneS1ght.uploadPendingPositions()   // 쌓인 좌표를 지금 전송
OneS1ght.discardPendingPositions()        // 보내지 않고 버림
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge uploadPendingPositions:^{ }];   // 쌓인 좌표를 지금 전송
[OneS1ghtBridge discardPendingPositions];       // 보내지 않고 버림
```

### 세션 재설정

측위를 멈추고 세션을 버립니다. 저장된 키도 지워지므로, 이후 API 는 `initialize` 를 다시 부를 때까지 `notInitialized` 오류를 냅니다. \

앱을 다시 빌드하지 않고 다른 키로 바꿀 때 씁니다.

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

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

### 측위 종료 처리

측위 엔진이 종료된 경우 `onStopped` 이벤트가 실행되며 측위 가능 상태로 전환 가능합니다.

```swift Swift icon="swift" theme={null}
session.onStopped = { reason in
    if case .engineFailed = reason { showRetrySearch() }   // 원인을 풀고 begin() 다시
}
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge onStopped:^(NSString *reason) {
    if ([reason isEqualToString:@"engineFailed"]) { [self showRetrySearch]; }   // 원인을 풀고 begin 다시
}];
```

### 지원 기기 확인

`deviceAvailability` 는 네트워크(인터넷) 연결 없이 바로 확인 가능하며\

측위를 지원하지 않는 기기라도 앱의 동작에 영향을 미치지 않습니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `deviceAvailability` | 기기 지원 여부 (상세) | `DeviceAvailability` `.available` : 기기 지원 `.osVersionTooLow` : OS 미호환  `.deviceNotSupported` : 측위정보 수신 불가능 기기 |
| `isDeviceAvailable` | 기기 지원 여부 (요약) | `Bool` |

```swift Swift icon="swift" theme={null}
switch OneS1ght.deviceAvailability {
case .available:          break
case .osVersionTooLow:    showNotice("iOS 27 이상이 필요합니다")
case .deviceNotSupported: hidePositioningFeatures()     // UWB 미지원 기기
@unknown default:         break
}
```

```objc Objective-C icon="apple" theme={null}
NSString *availability = OneS1ghtBridge.deviceAvailability;   // @"available" · @"osVersionTooLow" · @"deviceNotSupported"
if (![availability isEqualToString:@"available"]) {
    [self hidePositioningFeatures];
}
```

## 부록

### Info.plist 파일에 직접 쓰기

Xcode 의 **\[Info]** 탭에 보이는 목록은 실제로는 `Info.plist` 라는 파일의 내용입니다. 최근 Xcode 로 만든 프로젝트는 이 파일을
따로 보여 주지 않고 Xcode 가 빌드할 때 자동으로 만들기 때문에, 파일에 직접 쓰려면 먼저 파일을 만들어야 합니다.
Dictionary 키를 넣다가 Xcode 가 종료되는 경우에도 이 방법으로 넣을 수 있습니다.

**1. 파일이 있는지 확인**

왼쪽 프로젝트 네비게이터에 `Info.plist` 파일이 보이면 3단계로 갑니다.

**2. 파일 만들기**

**\[File] → \[New] → \[File from Template…]** 에서 **Property List** 를 고르고, 이름을 `Info.plist` 로, 저장 위치를 앱 타깃 폴더로 합니다.
그다음 **\[Build Settings]** 탭에서 `Info.plist File` 을 검색해 값에 `타깃이름/Info.plist` 를 넣습니다.
`Generate Info.plist File` 은 **Yes** 그대로 둡니다 — Xcode 가 자동 생성 항목과 이 파일을 합쳐 줍니다.

**3. XML 붙여넣기**

`Info.plist` 를 우클릭 → **Open As → Source Code** 로 열고, [iOS 권한 설정](#ios-권한-설정)의 XML 을 `<dict>` 와 `</dict>` 사이에 붙여 넣습니다.

### 권한 요청 (Nearby Interaction)

로케이터의 측위 정보는 애플의 Nearby Interaction을 사용합니다. `requestPermission()` 은 시스템 권한 창을 띄우고, 허용 여부는 사용자가 정합니다. \

한 번 거부되면 앱에서 다시 물을 수 없으므로 설정 앱에서 권한을 켜도록 안내해야 합니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `requestPermission()` | Nearby Interaction 권한 요청 | `PermissionStatus` `.authorized` : 권한 승인 `.denied` : 권한 거부 `.unsupported` : 미지원 기기 |

```swift Swift icon="swift" theme={null}
switch await OneS1ght.requestPermission() {
case .authorized:  break
case .denied:      showSettingsGuide()        // 앱에서 재요청 불가 — 설정 앱으로
case .unsupported: showUnsupportedNotice()    // 측위 미지원 기기
@unknown default:  break
}
```

```objc Objective-C icon="apple" theme={null}
[OneS1ghtBridge requestPermission:^(NSString *status) {
    if ([status isEqualToString:@"denied"])      { [self showSettingsGuide]; }       // 앱에서 재요청 불가 — 설정 앱으로
    if ([status isEqualToString:@"unsupported"]) { [self showUnsupportedNotice]; }   // 측위 미지원 기기
}];
```

### 위치 항상 권한

iOS 27.2 이상에서는 위치 접근 권한이 **'항상 사용'** 이어야 층을 찾을 수 있습니다(BLE 비콘 영역 감지). iOS 27.1 이하는 '앱 사용중에만 허용' 으로 충분합니다. \

SDK 는 '항상' 권한을 요청하지 않으며, 권한이 없어도 SDK 는 시작되지만 층을 찾지 못합니다. 사용자가 거절했다면 설정 앱으로 안내해 주세요.

```swift Swift icon="swift" theme={null}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    if manager.authorizationStatus == .authorizedWhenInUse {
        manager.requestAlwaysAuthorization()   // iOS 는 이 창을 앱마다 한 번만 띄웁니다
    }
}
```

```objc Objective-C icon="apple" theme={null}
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
    if (manager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) {
        [manager requestAlwaysAuthorization];   // iOS 는 이 창을 앱마다 한 번만 띄웁니다
    }
}
```

### 디버그 로그

디버그 로그 확인시 SDK 초기화문 선언(`initialize`) **직전에** 등록해 주세요. \

아래처럼 `print` 로 찍은 로그는 Xcode 디버그 콘솔에서 확인할 수 있어요.

```swift Swift icon="swift" theme={null}
#if DEBUG
OneS1ght.onDebugLog = { level, line in print("[OneS1ght][\(level)] \(line)") }
#endif
```

```objc Objective-C icon="apple" theme={null}
#ifdef DEBUG
[OneS1ghtBridge onDebugLog:^(NSString *level, NSString *line) { NSLog(@"[OneS1ght][%@] %@", level, line); }];
#endif
```

## 문제 해결하기

> **[에러 코드](/developers/sdk/troubleshooting/error-codes)**
>
> 개발 중 발생하는 오류 내용을 안내합니다.


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