diff --git a/build.gradle.kts b/build.gradle.kts index d9e6c06..87bfe22 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,13 +12,24 @@ application { } dependencies { + // СЕРВЕРНЫЕ ЗАВИСИМОСТИ KTOR implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) implementation(libs.logback.classic) - implementation(libs.ktor.server.core) implementation(libs.ktor.server.config.yaml) + + // HTTP-клиент для Kotlin/JVM (не Android!) + implementation("io.ktor:ktor-client-cio:2.3.9") + implementation("io.ktor:ktor-client-content-negotiation:2.3.9") + implementation("io.ktor:ktor-client-logging:2.3.9") + + // Корутины и сериализация + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + + // Тесты testImplementation(libs.ktor.server.test.host) testImplementation(libs.kotlin.test.junit) -} +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 213dd3b..faab862 100755 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ kotlin = "2.2.20" ktor = "3.3.0" logback = "1.4.14" +lifecycleViewmodelAndroid = "2.10.0" [libraries] ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } @@ -12,6 +13,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" } ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycleViewmodelAndroid" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..2fa1e4a --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Fri Dec 12 08:17:55 YEKT 2025 +sdk.dir=D\:\\androidstudio diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt old mode 100755 new mode 100644 index 378958a..eed7221 --- a/src/main/kotlin/Application.kt +++ b/src/main/kotlin/Application.kt @@ -13,4 +13,4 @@ fun Application.module() { json() } configureRouting() -} +} \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..7895ed5 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,37 @@ +package com.example + +import com.example.di.Dependencies +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay + +fun main() = runBlocking { + val viewModel = Dependencies.createBookingViewModel() + + // Настраиваем колбэки + viewModel.onBookingsUpdated = { bookings -> + println("Бронирования обновлены:") + bookings.forEach { booking -> + println(" Комната: ${booking.room}, Время: ${booking.time}") + } + } + + viewModel.onError = { error -> + if (error != null) { + println("Ошибка: $error") + } + } + + // Загружаем текущие бронирования + println("Загружаем бронирования...") + viewModel.loadBookings() + + delay(1000) // Ждём загрузки + + // Добавляем новое бронирование + println("\nДобавляем новое бронирование...") + viewModel.addBooking("505.6", "14:00 - 16:00") + + delay(1000) // Ждём обработки + + viewModel.dispose() +} \ No newline at end of file diff --git a/src/main/kotlin/Routing.kt b/src/main/kotlin/Routing.kt index 45b01a5..321eeda 100755 --- a/src/main/kotlin/Routing.kt +++ b/src/main/kotlin/Routing.kt @@ -8,6 +8,7 @@ import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.server.application.* import io.ktor.server.request.receiveMultipart +import io.ktor.server.request.receiveParameters import io.ktor.server.response.* import io.ktor.server.routing.* @@ -27,38 +28,29 @@ fun Application.configureRouting() { get("/user") { call.respond(UserDto(booking = booking)) } + + // ЗАМЕНИТЕ ЭТОТ БЛОК (от post("/book") до закрывающей скобки): post("/book") { - var room = "" - var time = "" - val multipartData = call.receiveMultipart() + try { + // Получаем данные в формате x-www-form-urlencoded + val parameters = call.receiveParameters() + val room = parameters["room"]?.trim() ?: "" + val time = parameters["time"]?.trim() ?: "" - multipartData.forEachPart { part -> - try { - if (part !is PartData.FormItem) return@forEachPart - when (part.name) { - "room" -> { - room = part.value.trim() - } - - "time" -> { - time = part.value.trim() - } - - else -> Unit - } - } finally { - part.dispose() + if (room.isEmpty() || time.isEmpty()) { + call.respond( + HttpStatusCode.BadRequest, + ErrorDto(error = "Fields 'room' and 'time' are required") + ) + } else { + booking.add(BookingDto(room = room, time = time)) + call.respond(HttpStatusCode.OK, mapOf("success" to true)) } - } - - if (room.isEmpty() && time.isEmpty()) { + } catch (e: Exception) { call.respond( HttpStatusCode.BadRequest, - ErrorDto(error = "Field is empty") + ErrorDto(error = "Invalid request format") ) - } else { - booking.add(BookingDto(room = room, time = time)) - call.respond(HttpStatusCode.OK) } } } diff --git a/src/main/kotlin/data/mapper/BookingMapper.kt b/src/main/kotlin/data/mapper/BookingMapper.kt new file mode 100644 index 0000000..a9c8b7f --- /dev/null +++ b/src/main/kotlin/data/mapper/BookingMapper.kt @@ -0,0 +1,11 @@ +package com.example.data.mapper + +import com.example.data.remote.dto.BookingResponse +import com.example.domain.models.Booking + +fun BookingResponse.toDomain(): Booking { + return Booking( + room = room, + time = time + ) +} \ No newline at end of file diff --git a/src/main/kotlin/data/remote/BookingApi.kt b/src/main/kotlin/data/remote/BookingApi.kt new file mode 100644 index 0000000..2c4a48a --- /dev/null +++ b/src/main/kotlin/data/remote/BookingApi.kt @@ -0,0 +1,37 @@ +package com.example.data.remote + +import com.example.data.remote.dto.BookingResponse +import com.example.data.remote.dto.UserResponse +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.client.request.forms.* + +interface BookingApi { + suspend fun getUser(): UserResponse + suspend fun bookRoom(room: String, time: String): Boolean +} + +class BookingApiImpl(private val client: HttpClient) : BookingApi { + private val baseUrl = "http://localhost:8080" + + override suspend fun getUser(): UserResponse { + return client.get("$baseUrl/user").body() + } + + override suspend fun bookRoom(room: String, time: String): Boolean { + return try { + val response = client.submitForm( + url = "$baseUrl/book", + formParameters = Parameters.build { + append("room", room) + append("time", time) + } + ) + response.status == HttpStatusCode.OK + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/remote/dto/UserResponse.kt b/src/main/kotlin/data/remote/dto/UserResponse.kt new file mode 100644 index 0000000..7dd2aad --- /dev/null +++ b/src/main/kotlin/data/remote/dto/UserResponse.kt @@ -0,0 +1,20 @@ +package com.example.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserResponse( + @SerialName("name") + val name: String, + @SerialName("booking") + val booking: List +) + +@Serializable +data class BookingResponse( + @SerialName("room") + val room: String, + @SerialName("time") + val time: String +) \ No newline at end of file diff --git a/src/main/kotlin/data/repository/BookingRepositoryImpl.kt b/src/main/kotlin/data/repository/BookingRepositoryImpl.kt new file mode 100644 index 0000000..491573b --- /dev/null +++ b/src/main/kotlin/data/repository/BookingRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.example.data.repository + +import com.example.data.mapper.toDomain +import com.example.data.remote.BookingApi +import com.example.domain.models.Booking +import com.example.domain.repository.BookingRepository + +class BookingRepositoryImpl( + private val api: BookingApi +) : BookingRepository { + + override suspend fun getBookings(): List { + return try { + api.getUser().booking.map { it.toDomain() } + } catch (e: Exception) { + emptyList() + } + } + + override suspend fun addBooking(room: String, time: String): Boolean { + return api.bookRoom(room, time) + } +} \ No newline at end of file diff --git a/src/main/kotlin/di/Dependencies.kt b/src/main/kotlin/di/Dependencies.kt new file mode 100644 index 0000000..f079ba7 --- /dev/null +++ b/src/main/kotlin/di/Dependencies.kt @@ -0,0 +1,41 @@ +package com.example.di + +import com.example.data.remote.BookingApi +import com.example.data.remote.BookingApiImpl +import com.example.data.repository.BookingRepositoryImpl +import com.example.domain.repository.BookingRepository +import com.example.presentation.BookingViewModel +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json + +object Dependencies { + val httpClient: HttpClient by lazy { + HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + isLenient = true + }) + } + install(Logging) { + level = LogLevel.ALL + } + } + } + + val bookingApi: BookingApi by lazy { + BookingApiImpl(httpClient) + } + + val bookingRepository: BookingRepository by lazy { + BookingRepositoryImpl(bookingApi) + } + + fun createBookingViewModel(): BookingViewModel { + return BookingViewModel(bookingRepository) + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/models/Booking.kt b/src/main/kotlin/domain/models/Booking.kt new file mode 100644 index 0000000..f7ee65c --- /dev/null +++ b/src/main/kotlin/domain/models/Booking.kt @@ -0,0 +1,6 @@ +package com.example.domain.models + +data class Booking( + val room: String, + val time: String +) \ No newline at end of file diff --git a/src/main/kotlin/domain/models/User.kt b/src/main/kotlin/domain/models/User.kt new file mode 100644 index 0000000..61b0dfa --- /dev/null +++ b/src/main/kotlin/domain/models/User.kt @@ -0,0 +1,6 @@ +package com.example.domain.models + +data class User( + val name: String, + val bookings: List +) \ No newline at end of file diff --git a/src/main/kotlin/domain/repositories/BookingRepository.kt b/src/main/kotlin/domain/repositories/BookingRepository.kt new file mode 100644 index 0000000..53dfb7d --- /dev/null +++ b/src/main/kotlin/domain/repositories/BookingRepository.kt @@ -0,0 +1,8 @@ +package com.example.domain.repository + +import com.example.domain.models.Booking + +interface BookingRepository { + suspend fun getBookings(): List + suspend fun addBooking(room: String, time: String): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/viewmodels/BookingViewModel.kt b/src/main/kotlin/presentation/viewmodels/BookingViewModel.kt new file mode 100644 index 0000000..c220a78 --- /dev/null +++ b/src/main/kotlin/presentation/viewmodels/BookingViewModel.kt @@ -0,0 +1,63 @@ +package com.example.presentation + +import com.example.domain.models.Booking +import com.example.domain.repository.BookingRepository +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + +class BookingViewModel( + private val repository: BookingRepository, + private val coroutineContext: CoroutineContext = Dispatchers.Default +) { + private var bookings: List = emptyList() + private var error: String? = null + private val scope = CoroutineScope(coroutineContext) + + // Колбэки для обновления UI + var onBookingsUpdated: (List) -> Unit = {} + var onError: (String?) -> Unit = {} + + fun loadBookings() { + scope.launch { + try { + bookings = repository.getBookings() + error = null + onBookingsUpdated(bookings) + onError(null) + } catch (e: Exception) { + error = "Ошибка загрузки: ${e.message}" + onError(error) + } + } + } + + fun addBooking(room: String, time: String) { + if (room.isBlank() || time.isBlank()) { + error = "Заполните все поля" + onError(error) + return + } + + scope.launch { + try { + val success = repository.addBooking(room, time) + if (success) { + loadBookings() // Обновляем список + } else { + error = "Ошибка бронирования" + onError(error) + } + } catch (e: Exception) { + error = "Ошибка: ${e.message}" + onError(error) + } + } + } + + fun getCurrentBookings(): List = bookings + fun getCurrentError(): String? = error + + fun dispose() { + scope.cancel() + } +} \ No newline at end of file