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

# Android 설정 가이드

## 시스템 요구사항

| 항목 | 요구사항 |
| - | - |
| 운영체제 | Android 17 (API 37) 이상 (앱 최소 타깃은 Android 8.0 · API 26) |
| 지원 기기 | UWB DL-TDoA 지원 기기 · Bluetooth LE |
| 개발 환경 | `compileSdk` · `targetSdk` 37 · JVM target 1.8 이상 |
| 언어 | Kotlin 1.9 이상 · Java 8 이상 |
| 패키지 관리 | Gradle (Maven Central) |

측위정보 수신 미지원 기기에서는 SDK 측위만 동작하지 않고 앱은 그대로 동작합니다. \

단 `initialize` 는 키 · 통신 오류를, `session.begin()` 은 미지원 기기 오류를 던지므로 아래 예제처럼 `try/catch`(Java 는 `onError`)로 받아 주세요.

## SDK 설치

**Step 1**

`settings.gradle(.kts)` 에 Maven Central 저장소를 추가해 주세요.

```kotlin settings.gradle.kts icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
```

**Step 2**

앱 모듈의 빌드 파일에 OneS1ght SDK를 추가합니다.

```kotlin build.gradle.kts icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
dependencies {
    implementation("com.ones1ght.sdk:android:0.0.11")
}
```

```groovy build.gradle theme={null}
dependencies {
    implementation 'com.ones1ght.sdk:android:0.0.11'
}
```

**Step 3**

JVM target 버전과 `BuildConfig` 옵션을 확인합니다. JVM 은 1.8 이상을 권장합니다.

```kotlin build.gradle.kts icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
android {
    compileSdk = 37
    defaultConfig {
        minSdk = 26
        targetSdk = 37
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8   // 1.8 이상 — 11 · 17 도 됩니다
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    buildFeatures { buildConfig = true }
}
```

```groovy build.gradle theme={null}
android {
    compileSdk = 37
    defaultConfig {
        minSdk = 26
        targetSdk = 37
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8   // 1.8 이상 — 11 · 17 도 됩니다
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    buildFeatures { buildConfig = true }
}
```

## Android 권한 설정

측위에 필요한 권한은 SDK 매니페스트가 **앱에 자동으로 병합**해 줍니다.

| 권한 | 용도 |
| - | - |
| `android.permission.RANGING` | UWB 측위 권한 |
| `android.permission.ACCESS_FINE_LOCATION` | 정밀 위치 권한 (UWB/BLE) |
| `android.permission.ACCESS_COARSE_LOCATION` | 대략적 위치 권한 |
| `android.permission.BLUETOOTH_SCAN` | 근처 BLE 로케이터 수신 권한 |
| `android.permission.INTERNET` | 외부망(인터넷) 통신 권한 |
| `android.permission.ACCESS_NETWORK_STATE` | 네트워크 상태 확인 (측위 엔진 요구) |
| `android.permission.CHANGE_NETWORK_STATE` | 네트워크 상태 변경 (측위 엔진 요구) |

런타임 권한은 `requestPermission(activity)` 한 번으로 함께 요청합니다. \

이미 허용된 권한이면 창 없이 바로 돌아오며, `initialize` 전에도 부를 수 있습니다. \

사용자가 거부하거나 30초 안에 답하지 않으면 `DENIED` 입니다. 시스템이 권한 창을 더 이상 띄우지 않으면 설정 앱으로 안내하세요.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `requestPermission(activity)` | `ComponentActivity`를 파라미터로 전달하여 SDK가 필요한 권한을 한번에 요청합니다. | `PermissionStatus`  `AUTHORIZED` : 권한 승인  `DENIED` : 거부됨 (30초 무응답 포함)  `UNSUPPORTED` : 기기 미지원 |

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
lifecycleScope.launch {                                               // Activity(ComponentActivity) 안에서
    when (OneS1ght.requestPermission(this@MainActivity)) {
        PermissionStatus.AUTHORIZED  -> Unit
        PermissionStatus.DENIED      -> showSettingsGuide()           // 거부 · 무응답 — 창이 더 안 뜨면 설정 앱으로
        PermissionStatus.UNSUPPORTED -> showUnsupportedNotice()       // 측위 미지원 기기
        else                         -> Unit
    }
}
```

```java Java icon="java" theme={null}
OneS1ght.requestPermission(activity, new Callback<PermissionStatus>() {   // Activity(ComponentActivity) 안에서
    @Override public void onSuccess(PermissionStatus status) {
        if (status == PermissionStatus.DENIED)      { showSettingsGuide(); }       // 거부 · 무응답 — 창이 더 안 뜨면 설정 앱으로
        if (status == PermissionStatus.UNSUPPORTED) { showUnsupportedNotice(); }   // 측위 미지원 기기
    }
    @Override public void onError(Throwable error) { }
});
```

## SDK 초기화

측위 엔진을 가동하기 위한 초기화 작업을 시작합니다.

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `initialize(context, sdkKey)` | 키 검증과 설정 수신. 앱 시작 시 1회  자체 서버를 구축한 경우에만 `baseUrl` 인자 추가 | 없음 |
| `isInitialized` | 초기화 성공 여부 | `Boolean` |
| `SDK_VERSION` | SDK 버전 | `String` (예: `"0.0.11"`) |

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
import co.onecheck.ones1ght.android.*
import co.onecheck.ones1ght.android.model.*
import co.onecheck.ones1ght.android.network.ApiError

val sdkKey = if (BuildConfig.DEBUG) "YOUR_DEVELOPMENT_API_KEY"   // 개발(테스트) 모드에서 사용 시
             else                   "YOUR_PRODUCTION_API_KEY"    // 운영 모드에서 사용 시
try {
    OneS1ght.initialize(context = applicationContext, sdkKey = sdkKey)   // 코루틴 안에서
} catch (e: ApiError) {
    // 키 무효(E1002) · 네트워크(E5001) 등 — 다시 부르면 재시도
} catch (e: SdkError) {
    // 이 고객사의 측위가 꺼져 있음(PositioningDisabled, E1003)
}
```

```java Java icon="java" theme={null}
import co.onecheck.ones1ght.android.*;
import co.onecheck.ones1ght.android.model.*;

String sdkKey = BuildConfig.DEBUG ? "YOUR_DEVELOPMENT_API_KEY"   // 개발(테스트) 모드에서 사용 시
                                  : "YOUR_PRODUCTION_API_KEY";   // 운영 모드에서 사용 시
OneS1ght.initialize(getApplicationContext(), sdkKey, new Callback<Void>() {
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }   // 실패 처리
});
```

## 프로필

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

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

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

### 프로필 생성

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

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

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val profileId = OneS1ght.createProfile(mapOf("gender" to "F", "ageBand" to "20s"))   // 새 프로필 생성 → 프로필 ID 반환
save(profileId)                                                                     // 앱 또는 고객사 서버에 보관

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

```java Java icon="java" theme={null}
Map<String, String> attributes = new HashMap<>();
attributes.put("gender", "F");
attributes.put("ageBand", "20s");
OneS1ght.createProfile(attributes, new Callback<String>() {                  // 새 프로필 생성 → 프로필 ID 반환
    @Override public void onSuccess(String profileId) { save(profileId); }   // 앱 또는 고객사 서버에 보관
    @Override public void onError(Throwable error) { }
});

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

### 프로필 정보 조회

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val attributes = OneS1ght.fetchProfile(profileId)   // {gender=F, ageBand=20s}
```

```java Java icon="java" theme={null}
OneS1ght.fetchProfile(profileId, new Callback<Map<String, String>>() {
    @Override public void onSuccess(Map<String, String> attributes) { }
    @Override public void onError(Throwable error) { }   // 없는 ID
});
```

### 프로필 정보 수정

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
OneS1ght.replaceProfile(profileId, mapOf("gender" to "F", "ageBand" to "30s"))   // 속성 전체 교체
```

```java Java icon="java" theme={null}
Map<String, String> attributes = new HashMap<>();
attributes.put("gender", "F");
attributes.put("ageBand", "30s");
OneS1ght.replaceProfile(profileId, attributes, new Callback<Void>() {   // 속성 전체 교체
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }
});
```

### 프로필 삭제

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
OneS1ght.deleteProfile(profileId)
```

```java Java icon="java" theme={null}
OneS1ght.deleteProfile(profileId, new Callback<Void>() {
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }
});
```

## 건물, 층 선택

측위좌표를 수신하려면 로케이터가 있는 건물과 층을 선택해야 합니다.

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

### BLE 수신으로 선택

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

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val session = OneS1ght.floorSession()                        // 측위 세션 획득
session.onFloorDetected = SessionFloorListener { floorId ->   // 엔진이 층을 찾으면 층 ID 전달
    if (floorId == null) return@SessionFloorListener
    lifecycleScope.launch {
        val floor = OneS1ght.floor(buildingId, floorId)        // 층 정보 조회
        OneS1ght.setFloorMap(floor, buildingId)                // 층 지정
    }
}
```

```java Java icon="java" theme={null}
FloorSession session = OneS1ght.floorSession();              // 측위 세션 획득
session.setOnFloorDetected(floorId -> {                      // 엔진이 층을 찾으면 층 ID 전달
    if (floorId == null) return;
    OneS1ght.floor(buildingId, floorId, new Callback<Floor>() {                           // 층 정보 조회
        @Override public void onSuccess(Floor floor) {
            OneS1ght.setFloorMap(floor, buildingId, new Callback<Void>() {                // 층 지정
                @Override public void onSuccess(Void result) { }
                @Override public void onError(Throwable error) { }
            });
        }
        @Override public void onError(Throwable error) { }
    });
});
```

### 수동으로 선택

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val buildings = OneS1ght.buildings()                   // 콘솔에 등록된 건물 목록
val floors    = OneS1ght.floors(buildings[0].id)       // 건물에 속한 층 목록

OneS1ght.setFloorMap(floors[0], buildings[0].id)       // 층 지정
OneS1ght.setFloorMap(null)                             // 층 지정 해제
```

```java Java icon="java" theme={null}
OneS1ght.buildings(new Callback<List<Building>>() {                                   // 콘솔에 등록된 건물 목록
    @Override public void onSuccess(List<Building> buildings) {
        String buildingId = buildings.get(0).getId();
        OneS1ght.floors(buildingId, new Callback<List<Floor>>() {                     // 건물에 속한 층 목록
            @Override public void onSuccess(List<Floor> floors) {
                OneS1ght.setFloorMap(floors.get(0), buildingId, new Callback<Void>() {   // 층 지정
                    @Override public void onSuccess(Void result) { }
                    @Override public void onError(Throwable error) { }
                });
            }
            @Override public void onError(Throwable error) { }
        });
    }
    @Override public void onError(Throwable error) { }
});
OneS1ght.setFloorMap(null, new Callback<Void>() {                                     // 층 지정 해제
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }
});
```

## 측위정보 실시간 수신

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

위치는 `onPosition` 으로 받으며, 좌표(미터)는 엔진이 내는 속도 그대로 옵니다. 서버로는 콘솔 설정값(기본 초당 4회)으로 줄여 전송됩니다.

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

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

> [!NOTE]
>
> `begin()` 은 권한을 받은 뒤에 부르세요. 권한 없이 부르면 오류 없이 시작되지만 좌표가 나오지 않고 `E2003` 이 남습니다. \\
>
> 측위 중에 다시 부른 `begin()` 은 무시(WARN 로그)되므로, 사용자가 권한을 허용한 뒤에는 `end()` 를 부르고 `begin()` 을 다시 부르세요.

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val session = OneS1ght.floorSession()
session.onPosition = PositionListener { coord -> updateMyLocationMarker(coord.x, coord.y) }   // coord.z: 높이

try {
    session.begin()   // 측위 시작 (suspend)
} catch (e: SdkError) {
    // OsVersionTooLow · DeviceNotSupported · NotInitialized · NotIdentified
    hidePositioningFeatures()
}
session.end()     // 측위 종료 + 남은 좌표 전송 (suspend)
```

```java Java icon="java" theme={null}
FloorSession session = OneS1ght.floorSession();
session.setOnPosition(coord -> updateMyLocationMarker(coord.getX(), coord.getY()));   // coord.getZ(): 높이

session.begin(new Callback<Void>() {   // 측위 시작
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { hidePositioningFeatures(); }   // SdkError — 미지원 기기 · 초기화 전 등
});
session.end(new Callback<Void>() {     // 측위 종료 + 남은 좌표 전송
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }
});
```

## 구역 이벤트

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

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

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
session.onZoneEnter = ZoneListener { zone -> showBanner("진입: ${zone.name}") }
session.onZoneExit  = ZoneListener { zone -> hideBanner() }
session.onZoneDwell = DwellListener { zone, seconds -> showBanner("체류: ${zone.name} — ${seconds.toInt()}초") }
session.onTriggers  = TriggersListener { zoneId, triggers -> triggers.forEach(::handle) }   // type: signage · coupon · tracking · merch · generic
```

```java Java icon="java" theme={null}
session.setOnZoneEnter(zone -> showBanner("진입: " + zone.getName()));
session.setOnZoneExit(zone -> hideBanner());
session.setOnZoneDwell((zone, seconds) -> showBanner("체류: " + zone.getName() + " — " + (int) seconds + "초"));
session.setOnTriggers((zoneId, triggers) -> {
    for (Trigger trigger : triggers) { handle(trigger); }   // triggerId · type · payload
});
```

### 구역정보 업데이트

콘솔에서 구역 정보가 바뀔 때 `onConfigChanged` 가 호출됩니다. 연달아 오면 1초쯤 모아서 `refreshZones()` 를 한 번만 부릅니다.

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
var refreshJob: Job? = null
session.onConfigChanged = ConfigChangeListener { change ->
    when (change) {
        is ConfigChange.ZonesChanged, is ConfigChange.ResyncNeeded -> {
            refreshJob?.cancel()                    // 연달아 오면 앞의 예약을 취소
            refreshJob = lifecycleScope.launch {
                delay(1_000)                        // 1초 모아서
                OneS1ght.refreshZones()             // 한 번만 재조회
            }
        }
        is ConfigChange.PlanChanged -> reloadFloorPlan()   // 도면이 바뀜
        else -> Unit
    }
}
```

```java Java icon="java" theme={null}
Handler handler = new Handler(Looper.getMainLooper());
Runnable refresh = () -> OneS1ght.refreshZones(new Callback<List<Zone>>() {
    @Override public void onSuccess(List<Zone> zones) { }
    @Override public void onError(Throwable error) { }
});
session.setOnConfigChanged(change -> {
    if (change instanceof ConfigChange.ZonesChanged || change instanceof ConfigChange.ResyncNeeded) {
        handler.removeCallbacks(refresh);           // 연달아 오면 앞의 예약을 취소
        handler.postDelayed(refresh, 1000);         // 1초 모아서 한 번만 재조회
    } else if (change instanceof ConfigChange.PlanChanged) {
        reloadFloorPlan();   // 도면이 바뀜
    }
});
```

## 도면 표시

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

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

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `floor(buildingId, floorId)` | 층 상세 정보 | `Floor` `image: ByteArray?` : 도면 이미지 `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` : 측위 가능 여부 |

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
val floor = OneS1ght.floor(buildingId, floorId)
planView.setImageBitmap(floor.image?.let { BitmapFactory.decodeByteArray(it, 0, it.size) })
setBounds(floor.minX, floor.minY, floor.maxX, floor.maxY)
```

```java Java icon="java" theme={null}
OneS1ght.floor(buildingId, floorId, new Callback<Floor>() {
    @Override public void onSuccess(Floor floor) {
        byte[] png = floor.getImage();
        planView.setImageBitmap(png == null ? null : BitmapFactory.decodeByteArray(png, 0, png.length));
        setBounds(floor.getMinX(), floor.getMinY(), floor.getMaxX(), floor.getMaxY());
    }
    @Override public void onError(Throwable error) { }
});
```

## 기타 옵션

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `uploadPendingPositions()` | 앱 내부 저장 좌표 즉시 전송 | 없음 |
| `discardPendingPositions()` | 앱 내부 저장 좌표 버림 | 없음 |
| `reset()` | 초기화 리셋 — 세션 폐기 · 측위 정지 · 저장된 키와 건물 정보 삭제  이후 `initialize` 를 다시 불러야 합니다 | 없음 |
| `session.onStopped` | 세션 중단 | `SessionStoppedListener` — `(StopReason)` : 세션 중단 사유  `ENDED` : 세션 종료(정상)  `ENGINE_FAILED` : 엔진 이상(실패) |
| `deviceAvailability` | 측위 가능 확인(상세) — `initialize()` 뒤에 읽기 | `DeviceAvailability` `AVAILABLE` : 측위 가능  `OS_VERSION_TOO_LOW` : 미호환 안드로이드 OS 버전 `DEVICE_NOT_SUPPORTED` : 미지원 기기 |
| `isDeviceAvailable` | 측위 가능 확인(요약) | `Boolean` |
| `OneS1ght.onDebugLog` | SDK 디버깅 도구 | `DebugLogListener` — `(LogLevel, String)` 로그 반환 |

### 데이터 전송 제어

좌표는 **300건이 채워지거나 전송 후 60초 경과 시** 자동 전송됩니다. 단 앱이 강제 종료되면 좌표는 폐기됩니다.

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
OneS1ght.uploadPendingPositions()   // 쌓인 좌표를 지금 전송 (suspend)
OneS1ght.discardPendingPositions()  // 보내지 않고 버림
```

```java Java icon="java" theme={null}
OneS1ght.uploadPendingPositions(new Callback<Void>() {   // 쌓인 좌표를 지금 전송
    @Override public void onSuccess(Void result) { }
    @Override public void onError(Throwable error) { }
});
OneS1ght.discardPendingPositions();                      // 보내지 않고 버림
```

### 세션 재설정

세션을 버리고 측위를 멈추며, 저장된 SDK 키와 건물 정보(`setFloorMap` 의 건물)를 지웁니다. 새 세션을 만들지 않습니다. \

이후 대부분의 API 는 `SdkError.NotInitialized`(`E1001`)를 던지므로 `initialize` 를 다시 불러야 합니다. 런타임에 키를 바꿀 때 씁니다. \

`identify` 로 연결한 프로필은 남습니다.

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
OneS1ght.reset()                                                          // 세션 · 키 · 건물 정보 폐기
OneS1ght.initialize(context = applicationContext, sdkKey = newSdkKey)     // 다시 초기화해야 API 사용 가능
```

```java Java icon="java" theme={null}
OneS1ght.reset(new Callback<Void>() {
    @Override public void onSuccess(Void result) { }   // 이후 initialize 를 다시 호출
    @Override public void onError(Throwable error) { }
});
```

### 측위 종료 처리

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

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
session.onStopped = SessionStoppedListener { reason ->
    if (reason == FloorSession.StopReason.ENGINE_FAILED) showRetrySearch()   // 원인을 풀고 begin() 다시
}
```

```java Java icon="java" theme={null}
session.setOnStopped(reason -> {
    if (reason == FloorSession.StopReason.ENGINE_FAILED) { showRetrySearch(); }   // 원인을 풀고 begin() 다시
});
```

### 지원 기기 확인

`deviceAvailability` 는 네트워크(인터넷) 연결 없이 바로 확인할 수 있습니다. \

단 기기 칩 조회에 앱 `Context` 가 필요해 **`initialize()`(또는 `requestPermission(activity)`) 뒤에 읽어야** 합니다. \

그 전에 읽으면 Android 17 이상 기기도 `DEVICE_NOT_SUPPORTED` 가 오고 `onDebugLog` 에 WARN 이 남습니다(Android 17 미만은 항상 `OS_VERSION_TOO_LOW`).

| 함수 · 속성 | 설명 | 반환값 |
| - | - | - |
| `deviceAvailability` | 측위 가능 확인(상세) — `initialize()` 뒤에 읽기 | `DeviceAvailability` `AVAILABLE` : 측위 가능  `OS_VERSION_TOO_LOW` : 미호환 안드로이드 OS 버전 `DEVICE_NOT_SUPPORTED` : 미지원 기기 |
| `isDeviceAvailable` | 측위 가능 확인(요약) | `Boolean` |

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
// OneS1ght.initialize(...) 를 부른 뒤 (실패했어도 됨)
when (OneS1ght.deviceAvailability) {
    DeviceAvailability.AVAILABLE            -> Unit
    DeviceAvailability.OS_VERSION_TOO_LOW   -> showNotice("Android 17 이상이 필요합니다")
    DeviceAvailability.DEVICE_NOT_SUPPORTED -> hidePositioningFeatures()   // UWB 미지원 기기
    else                                    -> Unit
}
```

```java Java icon="java" theme={null}
OneS1ght.initialize(getApplicationContext(), sdkKey, new Callback<Void>() {
    @Override public void onSuccess(Void result) { checkDevice(); }
    @Override public void onError(Throwable error) { checkDevice(); }   // 실패했어도 기기 판정은 가능
});

void checkDevice() {   // initialize 뒤에 읽기
    if (OneS1ght.getDeviceAvailability() != DeviceAvailability.AVAILABLE) {   // OS_VERSION_TOO_LOW · DEVICE_NOT_SUPPORTED
        hidePositioningFeatures();
    }
}
```

## 부록

### Java 에서 사용하기

모든 비동기 API 는 **같은 이름으로 두 가지 형태**를 제공합니다.

| 언어 | 형태 | 예 |
| - | - | - |
| Kotlin | `suspend fun` — 코루틴에서 호출 | `OneS1ght.buildings()` |
| Java | 마지막 인자로 `Callback<T>` — 결과는 메인 스레드로 전달 | `OneS1ght.buildings(callback)` |

* 반환값이 없는 API 는 `Callback<Void>` 를 씁니다.
* 이벤트 콜백(`ZoneListener` · `PositionListener` 등)은 `fun interface` 라 Java 람다로 바로 등록합니다 — `session.setOnZoneEnter(zone -> …)`.
* 상태 값은 getter 로 읽습니다 — `OneS1ght.isInitialized()`, `OneS1ght.getDeviceAvailability()`, `session.isRunning()`.
* 콜백 속성은 setter 로 등록합니다 — `session.setOnStopped(…)`, `OneS1ght.setOnDebugLog(…)`.
* Java 앱은 `compileOptions` 를 1.8 이상으로 두면 되고 Kotlin 설정이 필요 없습니다.

### 디버그 로그

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

로그 확인은 Android Studio 상 **Logcat**(필터 `tag:OneS1ght`) 또는 `adb` 로 확인할 수 있어요.

```kotlin Kotlin icon="https://mintcdn.com/onecheck/wv66PbaB8dD4xd9I/images/icons/kotlin.svg?fit=max&auto=format&n=wv66PbaB8dD4xd9I&q=85&s=3721cc48a6f44aef50ea7a0151e369f2" theme={null}
if (BuildConfig.DEBUG) {
    OneS1ght.onDebugLog = DebugLogListener { level, message -> Log.d("OneS1ght", "[$level] $message") }
}
```

```java Java icon="java" theme={null}
if (BuildConfig.DEBUG) {
    OneS1ght.setOnDebugLog((level, message) -> Log.d("OneS1ght", "[" + level + "] " + message));
}
```

`adb` 로 조회 시

```bash theme={null}
adb logcat -c
adb logcat -s OneS1ght > onesight-log.txt
```

## 문제 해결하기

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


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