<|start_filename|>src/main/kotlin/be/zvz/koreanbots/dto/Bot.kt<|end_filename|>
package be.zvz.koreanbots.dto

/**
 * 봇 정보를 나타냅니다.
 * @property id 봇의 아이디
 * @property name 봇의 이름
 * @property tag 봇의 태그
 * @property avatar 봇의 아바타(없는 경우 null)
 * @property owners 봇의 소유자 리스트
 * @property flags 봇의 플래그
 * @property lib 봇이 사용한 라이브러리
 * @property prefix 봇의 접두사
 * @property votes 봇의 투표수
 * @property servers 봇이 참가한 서버 수
 * @property intro 봇의 짧은 설명
 * @property desc 봇의 자세한 설명
 * @property web 봇의 웹사이트(없는 경우 빈 문자열)
 * @property git 봇의 깃허브 닉네임(없는 경우 빈 문자열)
 * @property url 봇 초대 링크(없는 경우 빈 문자열)
 * @property discord 봇의 지원 디스코드 참가 링크(없는 경우 빈 문자열)
 * @property category 봇의 카테고리
 * @property vanity 봇의 VANITY URL(없는 경우 빈 문자열)
 * @property bg 봇의 배경 이미지 URL(없는 경우 빈 문자열)
 * @property banner 봇의 배너 이미지 URL(없는 경우 빈 문자열)
 * @property status 봇의 유저 상태(없는 경우 null)
 * @property state 한국 디스코드봇 리스트에서의 봇의 상태
 */
data class Bot(
    val id: String,
    val name: String,
    val tag: String,
    val avatar: String?,
    val owners: List<BotOwner>,
    val flags: Int,
    val lib: String,
    val prefix: String,
    val votes: Int,
    val servers: Int,
    val intro: String,
    val desc: String,
    val web: String,
    val git: String,
    val url: String,
    val discord: String,
    val category: List<Category>,
    val vanity: String,
    val bg: String,
    val banner: String,
    val status: Status?,
    val state: State
) {
    /**
     * 봇의 카테고리를 나타냅니다.
     */
    enum class Category(private val apiName: String) {
        MODERATION("관리"),
        MUSIC("뮤직"),
        STATS("전적"),
        GAME("게임"),
        GAMBLING("도박"),
        LOGGING("로깅"),
        SLASH_COMMAND("슬래시 명령어"),
        WEB_DASHBOARD("웹 대시보드"),
        MEME("밈"),
        LEVELING("레벨링"),
        UTILITY("유틸리티"),
        TALK("대화"),
        NSFW("NSFW"),
        SEARCH("검색"),
        SCHOOL("학교"),
        COVID("코로나19"),
        TRANSLATE("번역"),
        OVERWATCH("오버워치"),
        LEAGUE_OF_LEGENDS("리그 오브 레전트"),
        BATTLE_GROUND("배틀그라운드"),
        MINECRAFT("마인크래프트");

        override fun toString() = apiName
    }

    /**
     * 봇의 유저 상태를 나타냅니다.
     */
    enum class Status(private val apiName: String) {
        /** 온라인 */
        ONLINE("online"),
        /** 자리 비움 */
        IDLE("idle"),
        /** 다른 용무 중 */
        DO_NOT_DISTURB("dnd"),
        /** 방송중 */
        STREAMING("streaming"),
        /** 오프라인 */
        OFFLINE("offline");

        override fun toString() = apiName
    }

    /**
     * 한국 디스코드봇 리스트에서의 봇의 상태를 나타냅니다.
     * @property OK 정상
     */
    enum class State(private val apiName: String) {
        /** 정상 */
        OK("ok"),
        /** 일시 정지 */
        REPORTED("reported"),
        /** 강제 삭제 */
        BLOCKED("blocked"),
        /** 특수 목적 봇 */
        PRIVATE("private"),
        /** 잠금 처리 (지원 종료) */
        ARCHIVED("archived");

        override fun toString() = apiName
    }

    /**
     * 봇의 플래그를 나타냅니다.
     */
    enum class Flag(private val value: Int) {
        /** 공식 봇 */
        OFFICAL(1 shl 0),
        /** 	한국 디스코드봇 리스트 인증된 봇 */
        KOREANBOTS_VERIFIED(1 shl 2),
        /** 파트너 */
        PARTNER(1 shl 3),
        /** 디스코드 인증된 봇 */
        DISCORD_VERIFIED(1 shl 4),
        /** 프리미엄 */
        PREMIUM(1 shl 5),
        /** 제1회 한국 디스코드봇 리스트 해커톤 우승자 봇 */
        FIRST_HACKATON_WINNER(1 shl 6);
    }

    /**
     * 봇의 소유자를 나타냅니다.
     * [bots] 프로퍼티가 문자열 리스트인것을 제외하면 [User] 클래스와 동일합니다.
     */
    data class BotOwner(
        val id: String,
        val username: String,
        val tag: String,
        val github: String?,
        val flags: Int,
        val bots: List<String>
    )
}


<|start_filename|>resources/KoreanBotsInfo.kt<|end_filename|>
package __PACKAGE

object KoreanBotsInfo {
    const val API_BASE_URL = "__API_BASE_URL"
    const val GITHUB_URL = "__GITHUB_URL"
    const val VERSION = "__VERSION"
}


<|start_filename|>settings.gradle.kts<|end_filename|>
rootProject.name = "Koreanbots"

<|start_filename|>src/main/kotlin/be/zvz/koreanbots/dto/Voted.kt<|end_filename|>
package be.zvz.koreanbots.dto

/**
 * 유저가 투표했는지를 나타냅니다.
 * @property voted 최근 12시간 이내 투표 여부
 * @property lastVote 타임스탬프(한 번도 하지 않은 경우 0)
 */
data class Voted(
    val voted: Boolean,
    val lastVote: Long
)


<|start_filename|>build.gradle.kts<|end_filename|>
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.5.31"
    id("org.jmailen.kotlinter") version "3.6.0"
    `maven-publish`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation(kotlin("reflect"))

    implementation(group = "com.github.kittinunf.fuel", name = "fuel", version = "2.3.1")
    implementation(group = "com.github.kittinunf.fuel", name = "fuel-jackson", version = "2.3.1")

    implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = "2.13.0")
}

tasks.withType<KotlinCompile> {
    sourceCompatibility = JavaVersion.VERSION_1_8.toString()
    targetCompatibility = JavaVersion.VERSION_1_8.toString()
    kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<JavaCompile> {
    sourceCompatibility = JavaVersion.VERSION_1_8.toString()
    targetCompatibility = JavaVersion.VERSION_1_8.toString()
}

val githubRepo = project.property("github.repo") as String

val writeVersion by tasks.registering {
    val apiBaseUrl = project.property("api.base.url") as String
    val githubUrl = "https://github.com/$githubRepo"
    val group = "be.zvz.koreanbots"
    val version = project.version as String

    val template = File(rootDir, "resources/KoreanBotsInfo.kt").readText()
    val compiled = template
        .replace("__PACKAGE", group)
        .replace("__VERSION", version)
        .replace("__API_BASE_URL", apiBaseUrl)
        .replace("__GITHUB_URL", githubUrl)

    val dest = File(rootDir, "src/main/kotlin/${group.replace('.', '/')}/KoreanBotsInfo.kt")
    dest.writeText(compiled)
}

tasks.compileKotlin {
    dependsOn(writeVersion)
}

val sourcesJar by tasks.registering(Jar::class) {
    from(sourceSets["main"].allSource)
    archiveClassifier.set("sources")
}

publishing {
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/$githubRepo")
            credentials {
                username = project.findProperty("gpr.user") as String? ?: System.getenv("GH_USERNAME")
                password = project.findProperty("gpr.key") as String? ?: System.getenv("GH_TOKEN")
            }
        }
    }
    publications {
        register<MavenPublication>("KoreanBots") {
            artifactId = "koreanbots"
            from(components["java"])
            artifact(sourcesJar)
        }
    }
}


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/dto/SearchResult.kt<|end_filename|>
package be.zvz.koreanbots.dto

/**
 * 검색결과를 나타냅니다.
 * @property type 검색 종류(예: `SEARCH`, `VOTE`)
 * @property data 검색 결과
 * @property currentPage 현재 페이지
 * @property totalPage 전체 페이지
 */
data class SearchResult(
    val type: String,
    val data: List<Bot>,
    val currentPage: Int,
    val totalPage: Int
)


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/dto/User.kt<|end_filename|>
package be.zvz.koreanbots.dto

/**
 * 유저 정보를 나타냅니다.
 * @property id 유저의 디스코드 아이디
 * @property username 유저의 디스코드 닉네임
 * @property tag 유저의 디스코드 태그
 * @property github 유저의 깃허브 닉네임(없는 경우 null)
 * @property flags 유저 플래그
 * @property bots 유저가 소유한 봇 리스트
 */
data class User(
    val id: String,
    val username: String,
    val tag: String,
    val github: String?,
    val flags: Int,
    val bots: List<OwnedBot>
) {
    /**
     * 유저가 소유한 봇를 나타냅니다.
     * [owners] 프로퍼티가 문자열 리스트인것을 제외하면 [Bot] 클래스와 동일합니다.
     */
    data class OwnedBot(
        val id: String,
        val name: String,
        val tag: String,
        val avatar: String?,
        val owners: List<String>,
        val flags: Int,
        val lib: String,
        val prefix: String,
        val votes: Int,
        val servers: Int,
        val intro: String,
        val desc: String,
        val web: String,
        val git: String,
        val url: String,
        val discord: String,
        val category: List<Bot.Category>,
        val vanity: String,
        val bg: String,
        val banner: String,
        val status: Bot.Status?,
        val state: Bot.State
    )

    /**
     * 유저의 플래그를 나타냅니다.
     */
    enum class Flag(val value: Int) {
        /** 관리자 */
        ADMIN(1 shl 0),
        /** 버그 헌터 */
        BUG_HUNTER(1 shl 1),
        /** 봇 리뷰어 */
        BOT_REVIEWER(1 shl 2),
        /** 프리미엄 */
        PREMIUM(1 shl 3),
    }
}


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/Widget.kt<|end_filename|>
package be.zvz.koreanbots

fun KoreanBots.getWidgetUrl(
    id: Int,
    type: WidgetType,
    style: WidgetStyle = WidgetStyle.FLAT,
    scale: Double = 1.0,
    icon: Boolean = true
) = "${KoreanBotsInfo.API_BASE_URL}/widget/bots/$type/$id.svg?style=${style.apiName}&scale=$scale&icon=$icon"

enum class WidgetType(val apiName: String) {
    VOTES("votes"),
    SERVERS("servers"),
    STATUS("status");

    override fun toString() = apiName
}

enum class WidgetStyle(val apiName: String) {
    CLASSIC("classic"),
    FLAT("flat");

    override fun toString() = apiName
}


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/KoreanBots.kt<|end_filename|>
/*
 * Copyright 2021 JellyBrick, nkgcp, and all its contributor. All rights reserved.
 *
 * Licensed undr the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions andlimitations under the License.
 */
package be.zvz.koreanbots

import be.zvz.koreanbots.dto.Bot
import be.zvz.koreanbots.dto.ResponseWrapper
import be.zvz.koreanbots.dto.SearchResult
import be.zvz.koreanbots.dto.User
import be.zvz.koreanbots.dto.Voted
import be.zvz.koreanbots.exception.InvalidDataReceivedException
import be.zvz.koreanbots.exception.NoDataReceivedException
import be.zvz.koreanbots.exception.RequestFailedException
import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.core.Headers
import com.github.kittinunf.fuel.jackson.objectBody
import com.github.kittinunf.fuel.jackson.responseObject
import com.github.kittinunf.result.Result
import com.github.kittinunf.result.getOrElse

class KoreanBots @JvmOverloads constructor(
    private val botId: String,
    private val token: String,
    private val mapper: ObjectMapper = JsonMapper()
        .registerKotlinModule()
        .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
        .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING),
    private val fuelManager: FuelManager = FuelManager()
) {
    init {
        fuelManager.basePath = KoreanBotsInfo.API_BASE_URL
        fuelManager.baseHeaders = mapOf(
            Headers.USER_AGENT to "Kotlin SDK(${KoreanBotsInfo.VERSION}, ${KoreanBotsInfo.GITHUB_URL})"
        )
    }

    /**
     * 봇 정보를 받아옵니다.
     * @param targetId 받아올 봇의 아이디
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [Bot] 인스턴스
     */
    @Throws(RequestFailedException::class)
    fun getBotInfo(targetId: String): Bot = handleResponseOrThrow(
        fuelManager
            .get("/v2/bots/$targetId")
            .responseObject<ResponseWrapper<Bot>>(mapper = mapper)
            .third
    )

    /**
     * 봇 정보를 받아옵니다.
     * @param targetId 받아올 봇의 아이디
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun getBotInfo(targetId: String, onSuccess: (Bot) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/bots/$targetId")
            .responseObject<ResponseWrapper<Bot>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    /**
     * 봇을 검색합니다.
     * @param query 검색어
     * @param page 페이지 번호(기본값: 1)
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [SearchResult] 인스턴스
     */
    @JvmOverloads
    @Throws(RequestFailedException::class)
    fun searchBots(query: String, page: Int = 1): SearchResult = handleResponseOrThrow(
        fuelManager
            .get("/v2/search/bots", listOf("query" to query, "page" to page))
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper)
            .third
    )

    /**
     * 봇을 검색합니다.
     * @param query 검색어
     * @param page 페이지 번호(기본값: 1)
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun searchBots(query: String, page: Int = 1, onSuccess: (SearchResult) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/search/bots", listOf("query" to query, "page" to page))
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    /**
     * 하트 랭킹을 받아옵니다.
     * @param page 페이지 번호(기본값: 1)
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [SearchResult] 인스턴스
     */
    @JvmOverloads
    @Throws(RequestFailedException::class)
    fun getHeartRanking(page: Int = 1): SearchResult = handleResponseOrThrow(
        fuelManager
            .get("/v2/list/bots/votes", listOf("page" to page))
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper)
            .third
    )

    /**
     * 하트 랭킹을 받아옵니다.
     * @param page 페이지 번호(기본값: 1)
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun getHeartRanking(page: Int = 1, onSuccess: (SearchResult) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/list/bots/votes", listOf("page" to page))
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    /**
     * 한디리에 새롭게 등록된 봇들을 받아옵니다.
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [SearchResult] 인스턴스
     */
    @Throws(RequestFailedException::class)
    fun getNewBots(): SearchResult = handleResponseOrThrow(
        fuelManager
            .get("/v2/list/bots/new")
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper)
            .third
    )

    /**
     * 한디리에 새롭게 등록된 봇들을 받아옵니다.
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun getNewBots(onSuccess: (SearchResult) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/list/bots/new")
            .responseObject<ResponseWrapper<SearchResult>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    /**
     * 유저가 봇을 투표했는지 확인합니다.
     * @param userId 유저의 아이디
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [Bot] 인스턴스
     */
    @Throws(RequestFailedException::class)
    fun checkUserVote(userId: String): Voted = handleResponseOrThrow(
        fuelManager
            .get("/v2/bots/$botId/vote", listOf("userID" to userId))
            .header(Headers.AUTHORIZATION, token)
            .responseObject<ResponseWrapper<Voted>>(mapper = mapper)
            .third
    )

    /**
     * 유저가 봇을 투표했는지 확인합니다.
     * @param userId 유저의 아이디
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun checkUserVote(userId: String, onSuccess: (Voted) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/bots/$botId/vote", listOf("userID" to userId))
            .header(Headers.AUTHORIZATION, token)
            .responseObject<ResponseWrapper<Voted>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    private fun getUpdateServerCountBody(serverCount: Int, shardCount: Int): Map<String, Int> =
        mutableMapOf<String, Int>().apply {
            if (serverCount != 0) {
                put("servers", serverCount)
            }
            if (shardCount != 0) {
                put("shards", shardCount)
            }
        }

    /**
     * 봇 서버 수를 업데이트합니다.
     * @param serverCount 현재 서버 수 (기본값: 0, 서버 수를 전송하지 않음)
     * @param shardCount 현재 샤드 수 (기본값: 0, 샤드 수를 전송하지 않음)
     * @throws [RequestFailedException] 요청이 실패한 경우
     */
    @Throws(RequestFailedException::class)
    @JvmOverloads
    fun updateServerCount(serverCount: Int = 0, shardCount: Int = 0) {
        handleResponse(
            fuelManager
                .post("/v2/bots/$botId/stats")
                .header(Headers.AUTHORIZATION, token)
                .objectBody(getUpdateServerCountBody(serverCount, shardCount), mapper = mapper)
                .responseObject<ResponseWrapper<Unit>>(mapper = mapper)
                .third
        )
    }

    /**
     * 봇 서버 수를 업데이트합니다.
     * @param serverCount 현재 서버 수 (기본값: 0, 서버 수를 전송하지 않음)
     * @param shardCount 현재 샤드 수 (기본값: 0, 샤드 수를 전송하지 않음)
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수 (기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun updateServerCount(serverCount: Int = 0, shardCount: Int = 0, onSuccess: () -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .post("/v2/bots/$botId/stats")
            .header(Headers.AUTHORIZATION, token)
            .objectBody(getUpdateServerCountBody(serverCount, shardCount), mapper = mapper)
            .responseObject<ResponseWrapper<Unit>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponse(result) }
                    .mapCatching { onSuccess.invoke() }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    /**
     * 유저 정보를 받아옵니다.
     * @param userId 받아올 유저의 아이디
     * @throws [RequestFailedException] 요청이 실패한 경우
     * @return [User] 인스턴스
     */
    @Throws(RequestFailedException::class)
    fun getUserInfo(userId: String): User = handleResponseOrThrow(
        fuelManager
            .get("/v2/users/$userId")
            .responseObject<ResponseWrapper<User>>(mapper = mapper)
            .third
    )

    /**
     * 유저 정보를 받아옵니다.
     * @param userId 받아올 유저의 아이디
     * @param onSuccess 요청이 성공한 경우 호출될 콜백 함수
     * @param onFailure 요청이 실패한 경우 호출될 콜백 함수(기본값: null, 아무 동작도 하지 않음)
     */
    @JvmOverloads
    fun getUserInfo(userId: String, onSuccess: (User) -> Unit, onFailure: ((Throwable) -> Unit)? = null) {
        fuelManager
            .get("/v2/users/$userId")
            .header(Headers.AUTHORIZATION, token)
            .responseObject<ResponseWrapper<User>>(mapper = mapper) { _, _, result ->
                runCatching { handleResponseOrThrow(result) }
                    .mapCatching { onSuccess.invoke(it) }
                    .getOrElse { onFailure?.invoke(it) }
            }
    }

    private fun <T> handleResponse(result: Result<ResponseWrapper<T>, FuelError>): T? =
        result.getOrElse {
            if (it.exception is JacksonException) throw InvalidDataReceivedException(it.exception)
            throw mapper.readValue<RequestFailedException>(it.errorData)
        }
            .data

    private fun <T> handleResponseOrThrow(result: Result<ResponseWrapper<T>, FuelError>): T =
        handleResponse(result) ?: throw NoDataReceivedException()
}


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/exception/RequestFailedException.kt<|end_filename|>
package be.zvz.koreanbots.exception

/**
 * 요청이 실패하였음을 나타냅니다.
 * @property message 간단한 에러 메세지
 * @property errors 오류에 대한 자세한 설명(없는 경우 null)
 * @property code 반환된 HTTP 상태코드
 * @property version 요청을 보낸 API 버전
 */
data class RequestFailedException(
    override val message: String,
    val errors: List<String>?,
    val code: Int,
    val version: Int
) : RuntimeException("Request failed with message: $message")


<|start_filename|>src/main/kotlin/be/zvz/koreanbots/dto/ResponseWrapper.kt<|end_filename|>
package be.zvz.koreanbots.dto

internal data class ResponseWrapper<T> (
    val code: Int,
    val version: Int,
    val data: T?,
    val message: String?
)
