diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7de257c..bd34298 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -79,7 +79,6 @@ dependencies { // Coil implementation(libs.coil.compose) - implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a240caa..5be8679 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + + + val cityName = backStackEntry.arguments?.getString("cityName") - if(cityName != null) { - WeatherDetailScreen(cityName = cityName) + if (cityName != null) { + WeatherDetailScreen( + cityName = cityName, + navController = navController + ) } else { - // Handle the case where cityName is null, maybe show an error or a default screen + // 예외 처리 화면 } } composable("search") { CitySearchScreen() } diff --git a/app/src/main/java/com/ben/simpleweather/data/ForecastItem.kt b/app/src/main/java/com/ben/simpleweather/data/ForecastItem.kt new file mode 100644 index 0000000..e5a1155 --- /dev/null +++ b/app/src/main/java/com/ben/simpleweather/data/ForecastItem.kt @@ -0,0 +1,7 @@ +package com.ben.simpleweather.data + +data class ForecastItem( + val time: String, // "12PM" + val temperature: Int, + val iconCode: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/ben/simpleweather/data/WeatherDetail.kt b/app/src/main/java/com/ben/simpleweather/data/WeatherDetail.kt new file mode 100644 index 0000000..9b8dfc1 --- /dev/null +++ b/app/src/main/java/com/ben/simpleweather/data/WeatherDetail.kt @@ -0,0 +1,19 @@ +package com.ben.simpleweather.data + +data class WeatherDetail( + val temperature: Int, + val feelsLike: Int, + val description: String, + val iconCode: String, + val humidity: Int, + val windSpeed: Int, + val precipitationChance: Float, + val visibility: Int, // meters + val cloudiness: Int, // % + val windDegree: Int, // 0 ~ 360 + val pressure: Int, // hPa + val sunrise: Long, // Unix time + val sunset: Long, // Unix time + val rainAmount: Double?, // mm + val snowAmount: Double?, // mm +) \ No newline at end of file diff --git a/app/src/main/java/com/ben/simpleweather/data/WeatherItem.kt b/app/src/main/java/com/ben/simpleweather/data/WeatherItem.kt index 24ca6f0..89ed0ee 100644 --- a/app/src/main/java/com/ben/simpleweather/data/WeatherItem.kt +++ b/app/src/main/java/com/ben/simpleweather/data/WeatherItem.kt @@ -3,5 +3,5 @@ package com.ben.simpleweather.data data class WeatherItem( val cityName: String, val temperature: Int, - val weatherType: String + val weatherType: String, ) \ No newline at end of file diff --git a/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailScreen.kt b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailScreen.kt index 33271f8..1a5b59e 100644 --- a/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailScreen.kt +++ b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailScreen.kt @@ -1,12 +1,309 @@ package com.ben.simpleweather.features.detail +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import coil.compose.AsyncImage +import com.ben.simpleweather.R +import com.ben.simpleweather.data.ForecastItem +import com.ben.simpleweather.data.WeatherDetail +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +const val WEATHER_ICON_BASE_URL: String = "https://openweathermap.org/img/wn/" +val directions = listOf( + "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" +) + +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun WeatherDetailScreen(cityName: String) { - Text("Weather Detail Screen" - + "\nCity: $cityName" - + "\nHere you can display detailed weather information for $cityName." +fun WeatherDetailScreen( + cityName: String, + navController: NavController, + viewModel: WeatherDetailViewModel = androidx.hilt.navigation.compose.hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(cityName) { + viewModel.loadWeather() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(cityName) }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + when (val state = uiState) { + is WeatherUiState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + + is WeatherUiState.Success -> { + WeatherDetailContent( + modifier = Modifier.fillMaxSize(), + weather = state.weather, + forecast = state.forecast + ) + } + + is WeatherUiState.Error -> { + Text( + text = state.message, + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} + +@Composable +fun WeatherDetailContent( + modifier: Modifier = Modifier, + weather: WeatherDetail, + forecast: List +) { + val scrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + WeatherIcon(iconCode = weather.iconCode, size = 60.dp) + Column { + Text( + text = "${weather.temperature}°C", + style = MaterialTheme.typography.headlineLarge + ) + Text(text = weather.description, style = MaterialTheme.typography.bodyMedium) + Text( + text = stringResource(id = R.string.feels_like, weather.feelsLike), + style = MaterialTheme.typography.bodySmall + ) + } + } + + Text( + text = stringResource(R.string.hourly_forecast), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold) + ) + LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + items(forecast) { item -> + ForecastHourCard(item) + } + } + + Text( + text = stringResource(R.string.details), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold) + ) + DetailRow( + iconResId = R.drawable.outline_rainy_24, + label = stringResource(R.string.chance_of_rain), + value = "${(weather.precipitationChance * 100).toInt()}%" + ) + DetailRow( + iconResId = R.drawable.outline_water_drop_24, + label = stringResource(R.string.humidity), + value = "${weather.humidity}%" + ) + DetailRow( + iconResId = R.drawable.outline_air_24, + label = stringResource(R.string.wind), + value = "${weather.windSpeed} km/h" + ) + DetailRow( + iconResId = R.drawable.outline_visibility_24, + label = stringResource(R.string.visibility), + value = "${weather.visibility / 1000.0} km" + ) + + DetailRow( + iconResId = R.drawable.outline_cloud_24, + label = stringResource(R.string.cloudiness), + value = "${weather.cloudiness}%" + ) + + DetailRow( + iconResId = R.drawable.outline_explore_24, + label = stringResource(R.string.wind_direction), + value = degToCompass(weather.windDegree) + ) + + DetailRow( + iconResId = R.drawable.outline_wb_twilight_24, + label = stringResource(R.string.sunrise), + value = formatTime(weather.sunrise) + ) + + DetailRow( + iconResId = R.drawable.outline_nights_stay_24, + label = stringResource(R.string.sunset), + value = formatTime(weather.sunset) + ) + + weather.rainAmount?.let { + DetailRow( + iconResId = R.drawable.outline_grain_24, + label = stringResource(R.string.rain_amount), + value = "$it mm" + ) + } + + weather.snowAmount?.let { + DetailRow( + iconResId = R.drawable.outline_ac_unit_24, + label = stringResource(R.string.snow_amount), + value = "$it mm" + ) + } + } +} + +@Composable +fun ForecastHourCard(item: ForecastItem) { + Column( + modifier = Modifier + .size(width = 80.dp, height = 140.dp) + .padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + WeatherIcon( + iconCode = item.iconCode, + size = 46.dp + ) + + Spacer(modifier = Modifier.height(8.dp)) // 아이콘과 텍스트 사이 간격 + + Text( + text = "${item.temperature}°", + style = MaterialTheme.typography.bodyLarge + ) + + Text( + text = item.time, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + + +@Composable +fun DetailRow( + @DrawableRes iconResId: Int, + label: String, + value: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = label, + modifier = Modifier + .size(40.dp) + .padding(end = 12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Column { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) + ) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + + +@Composable +fun WeatherIcon(iconCode: String, size: Dp = 60.dp, modifier: Modifier = Modifier) { + val iconUrl = "${WEATHER_ICON_BASE_URL}${iconCode}@2x.png" + AsyncImage( + model = iconUrl, + contentDescription = null, + modifier = modifier.size(size) ) } + +fun degToCompass(deg: Int): String { + val index = ((deg / 22.5) + 0.5).toInt() % 16 + return directions[index] +} + +fun formatTime(unixTime: Long): String { + val instant = Instant.ofEpochSecond(unixTime) + val localTime = instant.atZone(ZoneId.systemDefault()).toLocalTime() + return localTime.format(DateTimeFormatter.ofPattern("hh:mm a")) +} \ No newline at end of file diff --git a/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailViewModel.kt b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailViewModel.kt new file mode 100644 index 0000000..5b77aa5 --- /dev/null +++ b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherDetailViewModel.kt @@ -0,0 +1,56 @@ +package com.ben.simpleweather.features.detail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ben.simpleweather.data.ForecastItem +import com.ben.simpleweather.data.WeatherDetail +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class WeatherDetailViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(WeatherUiState.Loading) + val uiState: StateFlow = _uiState + + fun loadWeather() { + viewModelScope.launch { + _uiState.value = WeatherUiState.Loading + + try { + // 더미 데이터 + val weather = WeatherDetail( + temperature = 27, + feelsLike = 25, + description = "Sunny", + iconCode = "01d", + humidity = 60, + windSpeed = 10, + precipitationChance = 0.1f, + visibility = 9800, + cloudiness = 78, + windDegree = 135, + pressure = 1013, + sunrise = 1726636384, + sunset = 1726680975, + rainAmount = 1.2, + snowAmount = null + ) + + val forecast = listOf( + ForecastItem(time = "12:00", temperature = 27, iconCode = "01d"), + ForecastItem(time = "15:00", temperature = 28, iconCode = "02d"), + ForecastItem(time = "18:00", temperature = 26, iconCode = "03d"), + ForecastItem(time = "21:00", temperature = 24, iconCode = "01n") + ) + + _uiState.value = WeatherUiState.Success(weather, forecast) + } catch (_: Exception) { + _uiState.value = WeatherUiState.Error("날씨 데이터를 불러오는 데 실패했습니다.") + } + } + } +} diff --git a/app/src/main/java/com/ben/simpleweather/features/detail/WeatherUiState.kt b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherUiState.kt new file mode 100644 index 0000000..bea7bb4 --- /dev/null +++ b/app/src/main/java/com/ben/simpleweather/features/detail/WeatherUiState.kt @@ -0,0 +1,13 @@ +package com.ben.simpleweather.features.detail + +import com.ben.simpleweather.data.ForecastItem +import com.ben.simpleweather.data.WeatherDetail + +sealed class WeatherUiState { + object Loading : WeatherUiState() + data class Success( + val weather: WeatherDetail, + val forecast: List + ) : WeatherUiState() + data class Error(val message: String) : WeatherUiState() +} diff --git a/app/src/main/res/drawable/outline_ac_unit_24.xml b/app/src/main/res/drawable/outline_ac_unit_24.xml new file mode 100644 index 0000000..08f4179 --- /dev/null +++ b/app/src/main/res/drawable/outline_ac_unit_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_air_24.xml b/app/src/main/res/drawable/outline_air_24.xml new file mode 100644 index 0000000..6f8dd38 --- /dev/null +++ b/app/src/main/res/drawable/outline_air_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_cloud_24.xml b/app/src/main/res/drawable/outline_cloud_24.xml new file mode 100644 index 0000000..52685dd --- /dev/null +++ b/app/src/main/res/drawable/outline_cloud_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_explore_24.xml b/app/src/main/res/drawable/outline_explore_24.xml new file mode 100644 index 0000000..435c41a --- /dev/null +++ b/app/src/main/res/drawable/outline_explore_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_grain_24.xml b/app/src/main/res/drawable/outline_grain_24.xml new file mode 100644 index 0000000..1cb3e4b --- /dev/null +++ b/app/src/main/res/drawable/outline_grain_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_nights_stay_24.xml b/app/src/main/res/drawable/outline_nights_stay_24.xml new file mode 100644 index 0000000..13d13c6 --- /dev/null +++ b/app/src/main/res/drawable/outline_nights_stay_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_rainy_24.xml b/app/src/main/res/drawable/outline_rainy_24.xml new file mode 100644 index 0000000..d8374a3 --- /dev/null +++ b/app/src/main/res/drawable/outline_rainy_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_visibility_24.xml b/app/src/main/res/drawable/outline_visibility_24.xml new file mode 100644 index 0000000..ee9e562 --- /dev/null +++ b/app/src/main/res/drawable/outline_visibility_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_water_drop_24.xml b/app/src/main/res/drawable/outline_water_drop_24.xml new file mode 100644 index 0000000..f0b6048 --- /dev/null +++ b/app/src/main/res/drawable/outline_water_drop_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_wb_twilight_24.xml b/app/src/main/res/drawable/outline_wb_twilight_24.xml new file mode 100644 index 0000000..34cc91d --- /dev/null +++ b/app/src/main/res/drawable/outline_wb_twilight_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 87c253c..9f8fe28 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -8,4 +8,18 @@ 취소 삭제 모드 진입 도시 추가 + 뒤로가기 + 시간대별 예보 + 상세 정보 + 강수 확률 + 습도 + 풍속 + 가시거리 + 구름량 + 풍향 + 일출 + 일몰 + 강수량 (3시간) + 적설량 (3시간) + 체감온도 %1$s°C \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92924b8..5967fa3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,18 @@ Cancel Enter delete mode Add City + Back + Hourly Forecast + Details + Chance of Rain + Humidity + Wind + Visibility + Cloudiness + Wind Direction + Sunrise + Sunset + Rain (3h) + Snow (3h) + Feels like %1$s°C \ No newline at end of file