---
title: 버그와 개선 의견 제출
description: BugReport와 FeedbackReport를 사용해 보고서를 보내고 재시도와 오류를 처리하세요.
---

# 버그와 개선 의견 제출 {#submit-bugs-and-feedback}

로그인과 앱 참여 승인이 완료되면 `submitBug()` 또는 `submitFeedback()`을 호출합니다. 두 함수 모두 coroutine에서 실행하는 `suspend` 함수이며 결과로 `SubmittedIssue`를 반환합니다.

SDK는 호스트가 전달한 텍스트만 전송합니다. 화면, 로그, 입력값, 기기 정보, 충돌 정보를 자동으로 수집하지 않습니다. 전송 전에 보고서 내용과 포함할 기기 정보를 사용자에게 보여주세요.

## 버그 제출 {#submit-a-bug}

```kotlin
import dev.opentester.sdk.BugReport

val report = BugReport(
    title = "저장 버튼이 동작하지 않아요",
    description = "새 프로젝트에서 제목을 입력한 뒤 저장을 눌렀습니다.",
    steps = "1. 새 프로젝트 열기\n2. 제목 입력\n3. 저장 누르기",
    expected = "프로젝트가 저장되고 목록으로 이동",
    actual = "화면 변화 없이 입력한 내용이 남음",
    appVersion = "1.0.0-test",
    deviceInfo = "Pixel; Android 16",
)

// 인증된 tester를 coroutine에서 호출합니다.
val submitted = tester.submitBug(report)
```

## 개선 의견 제출 {#submit-feedback}

```kotlin
import dev.opentester.sdk.FeedbackReport

val report = FeedbackReport(
    title = "검색창 옆에 필터를 배치해 주세요",
    description = "현재는 목록 끝까지 내려가야 필터를 찾을 수 있습니다.",
    appVersion = "1.0.0-test",
)
val submitted = tester.submitFeedback(report)
```

개선 의견은 `kind: "feedback"`으로 저장됩니다. `FeedbackReport.deviceInfo`는 생략할 수 있습니다. `BugReport.deviceInfo`는 생성자 인자이지만 수집할 정보가 없다면 빈 문자열을 지정할 수 있습니다.

## 입력 제한 {#input-limits}

서버는 문자열의 앞뒤 공백을 제거한 뒤 다음 제한을 검사합니다. 호스트 폼에도 같은 제한을 적용하면 제출 실패를 줄일 수 있습니다.

| 필드              | 필수 여부            | 제한          |
| ----------------- | -------------------- | ------------- |
| `title`           | 필수                 | 1–160자       |
| `description`     | 필수                 | 1–10,000자    |
| `steps`           | 버그에서 선택        | 최대 10,000자 |
| `expected`        | 버그에서 선택        | 최대 5,000자  |
| `actual`          | 버그에서 선택        | 최대 5,000자  |
| `appVersion`      | 필수                 | 1–100자       |
| `deviceInfo`      | 값은 선택            | 최대 500자    |
| `clientRequestId` | 필수, 기본 자동 생성 | UUID          |

`submitBug()`와 `submitFeedback()`은 제목·설명이 비어 있으면 네트워크 요청 전에 `IllegalArgumentException`을 던집니다. 다른 입력 제한은 서버가 검사하며 위반 시 `OpenTesterException.Api`의 상태 코드가 `400`입니다.

## 중복 제출과 재시도 {#retry}

보고서 객체는 생성될 때 `clientRequestId`를 만듭니다. 네트워크가 끊겨 결과를 받지 못했다면 **같은 보고서 객체 또는 같은 UUID**로 재시도합니다. 서버는 같은 앱·테스터·`clientRequestId`의 요청에 기존 보고서를 반환합니다.

```kotlin
// 폼을 확정할 때 한 번 생성해 ViewModel 등 유지되는 객체에 보관합니다.
val pending = BugReport(
    title = "목록이 새로 고쳐지지 않음",
    description = "새 항목을 추가했지만 목록에 나타나지 않습니다.",
    appVersion = "1.0.0-test",
    deviceInfo = "",
)

// 첫 시도와 재시도 모두 pending을 전달합니다.
val result = tester.submitBug(pending)
```

같은 UUID에 다른 내용을 보내도 기존 보고서가 반환되며 내용이 수정되지 않습니다. 사용자가 내용을 바꾸어 새 보고서를 제출한다면 새 UUID를 사용하세요. SDK가 자동으로 재시도하거나 오프라인에서 요청을 저장하지는 않습니다.

## 오류 처리 {#handle-errors}

다음은 인증된 `tester`와 `report`를 coroutine에서 사용하는 처리 예입니다. `statusText`를 앱의 상태 UI에 연결합니다.

```kotlin
import dev.opentester.sdk.OpenTesterException
import kotlinx.coroutines.CancellationException

val statusText = try {
    val issue = tester.submitBug(report)
    "제출 완료: ${issue.id}"
} catch (e: CancellationException) {
    throw e
} catch (e: OpenTesterException.NotAuthenticated) {
    "테스터 로그인이 필요합니다."
} catch (e: OpenTesterException.Network) {
    "연결을 확인한 뒤 같은 보고서로 다시 시도해 주세요."
} catch (e: OpenTesterException.Api) {
    when (e.statusCode) {
        400 -> "보고서의 필수 항목과 길이를 확인해 주세요."
        401 -> "세션이 만료되었습니다. 다시 로그인해 주세요."
        403 -> "이 앱의 참여 승인 상태를 확인해 주세요."
        429 -> "요청이 많습니다. 잠시 후 다시 시도해 주세요."
        else -> e.message ?: "요청을 처리하지 못했습니다."
    }
} catch (e: OpenTesterException) {
    e.message ?: "응답을 처리하지 못했습니다."
} catch (e: IllegalArgumentException) {
    "제목과 설명을 입력해 주세요."
}
```

서버가 `401` 또는 `403`을 반환하면 SDK는 메모리의 인증 세션을 제거합니다. 참여 상태를 확인하고 다시 로그인한 뒤 제출하세요. coroutine 취소를 일반 오류로 삼키지 않도록 `CancellationException`은 다시 던집니다.

## 제출 후 흐름 {#after-submission}

새 보고서의 상태는 `open`입니다. 개발자는 콘솔에서 내용을 확인하고 `confirmed`, `fixed`, `closed` 등으로 상태를 변경할 수 있습니다. 제출 성공만으로 XP나 보상이 지급되지는 않으며 기여 심사가 별도로 진행됩니다.

SDK는 제출 결과의 `id`, `title`, `status`를 반환합니다. 테스터의 기여·보상 내역은 웹 로그인 후 확인하며, SDK 토큰으로 개발자용 API나 보상함에 접근할 수 없습니다.
