From ccc13719c2f75fcd908953ebfaba39aa68eab49c Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:37:37 +0900 Subject: [PATCH 01/44] =?UTF-8?q?chore:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=BB=B4=ED=8F=AC=EC=A6=88=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(NavHost=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b950ef..01efa91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,10 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + + // 네비게이션 바를 위한 의존성 추가 + implementation("androidx.navigation:navigation-compose:2.8.3") + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) From 43526f637da0798fa78f03b85763ac33336f6718 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:38:18 +0900 Subject: [PATCH 02/44] =?UTF-8?q?chore:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80=20(ic=5Fconfir?= =?UTF-8?q?m=5Fcheck=20=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/drawable/ic_add_button.png | Bin 0 -> 12716 bytes app/src/main/res/drawable/ic_confirm_check.png | Bin 0 -> 40207 bytes app/src/main/res/drawable/ic_left_arrow.png | Bin 0 -> 5647 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/src/main/res/drawable/ic_add_button.png create mode 100644 app/src/main/res/drawable/ic_confirm_check.png create mode 100644 app/src/main/res/drawable/ic_left_arrow.png diff --git a/app/src/main/res/drawable/ic_add_button.png b/app/src/main/res/drawable/ic_add_button.png new file mode 100644 index 0000000000000000000000000000000000000000..704efd991831037b7d56c0001a0660229d270b06 GIT binary patch literal 12716 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_confirm_check.png b/app/src/main/res/drawable/ic_confirm_check.png new file mode 100644 index 0000000000000000000000000000000000000000..315fadf71bedb3462ac38beac0917a3c51bc533b GIT binary patch literal 40207 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

L)9W;7ju5|{&1Pt;$GfS98ndqoB|2R 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 o% Date: Sat, 8 Nov 2025 15:38:55 +0900 Subject: [PATCH 03/44] =?UTF-8?q?chore:=20MainActivity=EC=97=90=20AppNavGr?= =?UTF-8?q?aph=20=EC=97=B0=EA=B2=B0=ED=95=98=EC=97=AC=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sprinter/studyspot/MainActivity.kt | 44 +++---------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt index c76b347..36f91e8 100644 --- a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt +++ b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt @@ -1,50 +1,18 @@ +// MainActivity.kt package com.sprinter.studyspot import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.sprinter.studyspot.ui.theme.StudySpot_AndroidTheme +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.navigation.AppNavGraph class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() setContent { - StudySpot_AndroidTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding), - ) - } - } + val navController = rememberNavController() + AppNavGraph(navController = navController) } } -} - -@Composable -fun Greeting( - name: String, - modifier: Modifier = Modifier, -) { - Text( - text = "Hello $name!", - modifier = modifier, - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - StudySpot_AndroidTheme { - Greeting("Android") - } -} +} \ No newline at end of file From 9cc2761bc9ecdb582a6bd843f30421b0d52d75ce Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:40:05 +0900 Subject: [PATCH 04/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B0=84=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=84=20=EC=9C=84=ED=95=9C=20AppNavGraph?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt new file mode 100644 index 0000000..194cd6c --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -0,0 +1,78 @@ +// navigation/AppNavGraph.kt +package com.sprinter.studyspot.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.navigation.* +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.sprinter.studyspot.timer.ui.TimerScreen +import com.sprinter.studyspot.timer.ui.TimerSettingScreen + +@Composable +fun AppNavGraph(navController: NavHostController) { + NavHost( + navController = navController, + startDestination = "timer" + ) { + // 타이머 메인 화면 + composable( + route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) + ) { backStackEntry -> + val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" + val colorValue = backStackEntry.arguments?.getString("color") ?: "0" + val hour = backStackEntry.arguments?.getInt("hour") ?: 0 + val minute = backStackEntry.arguments?.getInt("minute") ?: 0 + val second = backStackEntry.arguments?.getInt("second") ?: 0 + + TimerScreen( + navController = navController, + colorValue = colorValue, + title = title, + hour = hour, + minute = minute, + second = second + ) + } + + // 타이머 설정 화면 + composable( + route = "timerSetting?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) + ) { backStackEntry -> + val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" + val colorValue = backStackEntry.arguments?.getString("color") ?: "0" + val hour = backStackEntry.arguments?.getInt("hour") ?: 0 + val minute = backStackEntry.arguments?.getInt("minute") ?: 0 + val second = backStackEntry.arguments?.getInt("second") ?: 0 + + val initialColor = if (colorValue == "0" || colorValue.isBlank()) { + null + } else { + try { Color(colorValue.toULong()) } catch (e: Exception) { null } + } + + TimerSettingScreen( + navController = navController, + initialTitle = title, + initialHour = hour, + initialMinute = minute, + initialSecond = second, + initialColor = initialColor + ) + } + } +} \ No newline at end of file From cd59a797abc5d280b20b95a236616d9c7f78b04d Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:41:30 +0900 Subject: [PATCH 05/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=9A=A9=20StudyItem=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/component/StudyItem.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt new file mode 100644 index 0000000..be00561 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt @@ -0,0 +1,57 @@ +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +// StudyItem (그대로 유지) +@Composable +fun StudyItem(color: Color, title: String, time: String, onClick: () -> Unit) { + val isCompact = LocalConfiguration.current.screenWidthDp < 360 + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 28.dp) + .padding(vertical = if (isCompact) 6.dp else 8.dp) + .clickable { onClick() } + ) { + Box( + modifier = Modifier + .size(if (isCompact) 31.dp else 33.dp) + .background(color, CircleShape) + ) + Spacer(modifier = Modifier.width(if (isCompact) 8.dp else 12.dp)) + Text( + title, + fontWeight = FontWeight.Medium, + fontSize = if (isCompact) 14.sp else 16.sp, + color = Color.Black + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + time, + fontWeight = FontWeight.Medium, + fontSize = if (isCompact) 30.sp else 32.sp, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(34.dp)) + } +} \ No newline at end of file From 45e2b9079db11ea6bfddd56218496e8450c0b8e0 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:41:56 +0900 Subject: [PATCH 06/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=EC=9D=B4=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=EB=90=9C=20TimerScreen=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt new file mode 100644 index 0000000..2f8048d --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -0,0 +1,199 @@ +// timer/ui/TimerScreen.kt +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.R +import com.sprinter.studyspot.timer.ui.component.StudyItem +import kotlinx.coroutines.delay +import java.net.URLEncoder + +@Composable +fun TimerScreen( + navController: NavController, + colorValue: String = "0", + title: String = "기본 타이머", + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + modifier: Modifier = Modifier +) { + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + val circleSize = when { + screenWidth < 360.dp -> 150.dp + screenWidth < 600.dp -> 190.dp + else -> 250.dp + } + + var elapsed by remember { mutableStateOf(0L) } + var selectedTab by remember { mutableStateOf(0) } + + val selectedColor = remember(colorValue) { + try { + if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) + else Color(colorValue.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) + } + } + + val selectedTitle = if (title.isNotBlank()) title else "기본 타이머" + val formattedTime = String.format("%02d:%02d:%02d", hour, minute, second) + val total = (hour * 3600L) + (minute * 60L) + second + + LaunchedEffect(Unit) { + while (elapsed < total) { + delay(1000) + elapsed++ + } + } + + val remainingSeconds = (total - elapsed).coerceAtLeast(0L) + val h = remainingSeconds / 3600 + val m = (remainingSeconds % 3600) / 60 + val s = remainingSeconds % 60 + val timeText = String.format("%02d:%02d:%02d", h, m, s) + + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(48.dp)) + + TimerProgress( + totalSeconds = total, + elapsedSeconds = elapsed, + modifier = Modifier.size(circleSize).aspectRatio(1f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + Text("남은시간", fontSize = 14.sp, color = Color.Gray) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = timeText, + fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Surface(color = Color.White, modifier = Modifier.fillMaxSize()) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 41.dp) + ) { + TabRow( + selectedTabIndex = selectedTab, + containerColor = Color.Transparent, + divider = { }, + modifier = Modifier.fillMaxWidth().height(48.dp), + indicator = { tabPositions -> + Box( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black) + ) + } + ) { + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { + Text( + "타이머", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 0) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { + Text( + "기록", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 1) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn(modifier = Modifier.weight(1f)) { + item { + StudyItem( + color = selectedColor, + title = selectedTitle, + time = formattedTime, + onClick = { + val encodedTitle = URLEncoder.encode(selectedTitle, "UTF-8") + val colorValueStr = selectedColor.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=$hour&minute=$minute&second=$second" + ) + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(start = 28.dp, end = 16.dp, top = 8.dp) + .clip(RoundedCornerShape(24.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_button), + contentDescription = "추가", + tint = Color.Unspecified, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.width(12.dp)) + Text( + "추가하기", + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + color = Color(0xFF999999) + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_4) +@Composable +fun TimerScreenPreview() { + MaterialTheme { + TimerScreen(navController = rememberNavController()) + } +} \ No newline at end of file From 9863bab77fc27a663fb858738ec10a5a9c36c6f3 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:42:19 +0900 Subject: [PATCH 07/44] =?UTF-8?q?feat:=20=EC=9B=90=ED=98=95=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=EC=9C=84=ED=95=9C=20TimerProgress=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerProgress.kt | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt new file mode 100644 index 0000000..07ad204 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt @@ -0,0 +1,114 @@ +// TimerProgress.kt +package com.sprinter.studyspot.timer.ui + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +@Composable +fun TimerProgress( + totalSeconds: Long, + elapsedSeconds: Long, + maxDisplaySeconds: Long = 60 * 60L, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + val remainingSeconds = (totalSeconds - elapsedSeconds).coerceAtLeast(0L) + val remainingRatio = (remainingSeconds.toFloat() / maxDisplaySeconds.toFloat()).coerceIn(0f, 1f) + val animatedProgress by animateFloatAsState( + targetValue = remainingRatio, + animationSpec = tween(300, easing = LinearEasing) + ) + + Canvas(modifier = modifier.aspectRatio(1f)) { + val center = Offset(size.width / 2f, size.height / 2f) + + // 배경 원 + drawCircle(color = Color(0xFFD9D9D9), radius = size.minDimension / 2f, center = center) + + // 진행 원 + drawArc( + color = Color(0xFFB8E9BB), + startAngle = -90f, + sweepAngle = 360f * animatedProgress, + useCenter = true, + style = Fill + ) + + // 물방울 + val dropRadiusPx = (35f / 2f).dp.toPx() + drawCircle(color = Color.White, radius = dropRadiusPx, center = center) + + // 뿔 + val hornWidthPx = 23.dp.toPx() + val hornHeightPx = 25.dp.toPx() + val path = Path().apply { + val tipY = center.y - dropRadiusPx - hornHeightPx / 2 + moveTo(center.x, tipY) + lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + close() + } + drawPath(path, color = Color.White) + } +} + +// 데모 +@Composable +fun TimerProgressDemo() { + var elapsed by remember { mutableStateOf(0L) } + val total = 45 * 60L + + LaunchedEffect(Unit) { + while (true) { + delay(1000) + if (elapsed < total) elapsed++ + } + } + + TimerProgress(totalSeconds = total, elapsedSeconds = elapsed) +} + +@Preview( + showBackground = true, + name = "45분 시작 - 실시간 감소", + widthDp = 500, // 그대로 + heightDp = 500 // 그대로 +) +@Composable +fun Preview45MinInteractive() { + var elapsed by remember { mutableStateOf(0L) } + val total = 45 * 60L + + // Interactive Mode에서만 동작 + LaunchedEffect(Unit) { + while (elapsed < total) { + delay(1000) + elapsed++ + } + } + + TimerProgress( + totalSeconds = total, + elapsedSeconds = elapsed, + modifier = Modifier.size(190.dp) + ) +} \ No newline at end of file From 8dddcfe5b0a991d630a929a06348e4478ab08bd6 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 15:42:45 +0900 Subject: [PATCH 08/44] =?UTF-8?q?feat:=20=EC=8B=9C=C2=B7=EB=B6=84=C2=B7?= =?UTF-8?q?=EC=B4=88=20=EC=A7=81=EC=A0=91=20=EC=9E=85=EB=A0=A5=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=EC=9D=B4=20=ED=8F=AC=ED=95=A8=EB=90=9C=20?= =?UTF-8?q?TimerSettingScreen=20=EB=A6=AC=EB=89=B4=EC=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerSettingScreen.kt | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt new file mode 100644 index 0000000..22e3679 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -0,0 +1,296 @@ +// timer/ui/TimerSettingScreen.kt +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.R +import java.net.URLEncoder + +@Composable +private fun TimeUnitField( + value: String, + onValueChange: (String) -> Unit, + maxLength: Int, + maxValue: Int +) { + var internalValue by remember(value) { mutableStateOf(value) } + + LaunchedEffect(value) { + if (value != internalValue) { + internalValue = value + } + } + + val isError = internalValue.isNotEmpty() && internalValue.toIntOrNull()?.let { it > maxValue } == true + + OutlinedTextField( + value = internalValue, + onValueChange = { raw -> + val digits = raw.filter { it.isDigit() }.take(maxLength) + if (digits != internalValue && digits.length <= maxLength) { + internalValue = digits + onValueChange(digits) + } + }, + modifier = Modifier + .width(64.dp) + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) + .border( + width = if (isError) 2.dp else 0.dp, + color = if (isError) Color.Red else Color.Transparent, + shape = RoundedCornerShape(30.dp) + ), + textStyle = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = if (isError) Color.Red else Color.Black, + textAlign = TextAlign.Center + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = { text -> + val padded = text.text.padStart(2, '0').take(2) + TransformedText( + text = androidx.compose.ui.text.AnnotatedString(padded), + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return when { + offset == 0 -> 0 + offset == 1 -> 1 + else -> 2 + } + } + + override fun transformedToOriginal(offset: Int): Int { + return when { + offset == 0 -> 0 + offset == 1 -> 1 + else -> 2 + }.coerceAtMost(text.text.length) + } + } + ) + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + placeholder = { + Text( + text = "00", + color = Color.Gray, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimerSettingScreen( + navController: NavController? = null, + initialTitle: String = "자바스크립트", + initialHour: Int = 0, + initialMinute: Int = 0, + initialSecond: Int = 0, + initialColor: Color? = null +) { + var selectedColor by remember { mutableStateOf(initialColor) } + var timerTitle by remember { mutableStateOf(initialTitle) } + + var hourStr by remember(initialHour) { mutableStateOf(initialHour.toString().padStart(2, '0').take(2)) } + var minuteStr by remember(initialMinute) { mutableStateOf(initialMinute.toString().padStart(2, '0').take(2)) } + var secondStr by remember(initialSecond) { mutableStateOf(initialSecond.toString().padStart(2, '0').take(2)) } + + val hour = hourStr.toIntOrNull() ?: 0 + val minute = minuteStr.toIntOrNull() ?: 0 + val second = secondStr.toIntOrNull() ?: 0 + + val isValid = hour in 0..99 && minute in 0..59 && second in 0..59 && + hourStr.isNotBlank() && minuteStr.isNotBlank() && secondStr.isNotBlank() + + Surface(modifier = Modifier.fillMaxSize(), color = Color.White) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 11.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_arrow), + contentDescription = "뒤로가기", + modifier = Modifier + .size(28.dp) + .clickable { navController?.popBackStack() } + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "\"$timerTitle\" 타이머를 설정하시는군요!\n오늘 공부 목표 시간을 설정해보세요!", + fontSize = 14.sp, + color = Color.DarkGray, + lineHeight = 20.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(28.dp)) + + Text("타이머 색상", fontWeight = FontWeight.Bold, fontSize = 16.sp) + Spacer(modifier = Modifier.height(12.dp)) + val colors = listOf( + Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), + Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + colors.forEach { color -> + Box( + modifier = Modifier + .size(27.dp, 26.dp) + .background(color, RoundedCornerShape(8.dp)) + .border( + width = if (selectedColor == color) 3.dp else 0.dp, + color = if (selectedColor == color) Color.Black else Color.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable { selectedColor = color } + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text("타이머 이름", fontWeight = FontWeight.Bold, fontSize = 16.sp) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = timerTitle, + onValueChange = { timerTitle = it }, + shape = RoundedCornerShape(30.dp), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), + textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), + placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text("시간 설정", fontWeight = FontWeight.Bold, fontSize = 16.sp) + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + TimeUnitField( + value = hourStr, + onValueChange = { hourStr = it }, + maxLength = 2, + maxValue = 99 + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(":", fontSize = 28.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(8.dp)) + TimeUnitField( + value = minuteStr, + onValueChange = { minuteStr = it }, + maxLength = 2, + maxValue = 59 + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(":", fontSize = 28.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(8.dp)) + TimeUnitField( + value = secondStr, + onValueChange = { secondStr = it }, + maxLength = 2, + maxValue = 59 + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 301.dp, end = 16.dp), + contentAlignment = Alignment.BottomEnd + ) { + Surface( + modifier = Modifier + .size(77.dp, 41.dp) + .clickable(enabled = isValid) { + val safeTitle = timerTitle.ifBlank { "기본 타이머" } + val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") + val colorValue = selectedColor?.value?.toString() ?: "0" + navController?.navigate( + "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" + ) { + popUpTo("timerSetting") { inclusive = true } + } + }, + shape = RoundedCornerShape(8.dp), + color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(id = R.drawable.ic_confirm_check), + contentDescription = "완료", + tint = if (isValid) Color.Unspecified else Color.Gray, + modifier = Modifier.size(48.dp) + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun TimerSettingPreview() { + TimerSettingScreen(navController = rememberNavController()) +} \ No newline at end of file From 58346117aadce4bcd44fe9ad6f1c87e2d53cbd6d Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 17:22:07 +0900 Subject: [PATCH 09/44] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=85=9C=EC=97=90=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91/=EC=82=AD=EC=A0=9C=20=EC=B6=94=EA=B0=80=20(=ED=88=AC?= =?UTF-8?q?=EB=AA=85=20=EC=B9=B4=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 55 ++++-- .../studyspot/timer/ui/TimerSettingScreen.kt | 2 +- .../timer/ui/component/SwipeableStudyItem.kt | 171 ++++++++++++++++++ .../timer/ui/component/TimerItemData.kt | 11 ++ .../timer/viewmodel/TimerViewModel.kt | 23 +++ app/src/main/res/drawable/ic_delete.png | Bin 0 -> 11965 bytes app/src/main/res/drawable/ic_edit.png | Bin 0 -> 8918 bytes 7 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt create mode 100644 app/src/main/res/drawable/ic_delete.png create mode 100644 app/src/main/res/drawable/ic_edit.png diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 2f8048d..b493ba5 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -2,8 +2,10 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset @@ -19,10 +21,13 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.R -import com.sprinter.studyspot.timer.ui.component.StudyItem +import com.sprinter.studyspot.timer.ui.component.SwipeableStudyItem +import com.sprinter.studyspot.timer.ui.component.TimerItemData +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import kotlinx.coroutines.delay import java.net.URLEncoder @@ -36,6 +41,9 @@ fun TimerScreen( second: Int = 0, modifier: Modifier = Modifier ) { + val timerViewModel: TimerViewModel = viewModel() + val timerList by remember { derivedStateOf { timerViewModel.timers } } + val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp @@ -109,7 +117,9 @@ fun TimerScreen( selectedTabIndex = selectedTab, containerColor = Color.Transparent, divider = { }, - modifier = Modifier.fillMaxWidth().height(48.dp), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), indicator = { tabPositions -> Box( modifier = Modifier @@ -143,28 +153,49 @@ fun TimerScreen( Spacer(modifier = Modifier.height(16.dp)) LazyColumn(modifier = Modifier.weight(1f)) { - item { - StudyItem( - color = selectedColor, - title = selectedTitle, - time = formattedTime, + items(timerList) { timer -> + SwipeableStudyItem( + color = timer.color, + title = timer.title, + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), + onEdit = { + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + ) + }, + onDelete = { timerViewModel.deleteTimer(timer.title) }, onClick = { - val encodedTitle = URLEncoder.encode(selectedTitle, "UTF-8") - val colorValueStr = selectedColor.value.toString() + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=$hour&minute=$minute&second=$second" + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" ) } ) Spacer(modifier = Modifier.height(8.dp)) + } + // 추가 버튼 + item { Row( modifier = Modifier .fillMaxWidth() .height(48.dp) .padding(start = 28.dp, end = 16.dp, top = 8.dp) - .clip(RoundedCornerShape(24.dp)), + .clip(RoundedCornerShape(24.dp)) + .clickable { + timerViewModel.addTimer( + TimerItemData("새 타이머", Color(0xFFD9D9D9), 0, 0, 0) + ) + }, verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -196,4 +227,4 @@ fun TimerScreenPreview() { MaterialTheme { TimerScreen(navController = rememberNavController()) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 22e3679..1a14789 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -255,7 +255,7 @@ fun TimerSettingScreen( Box( modifier = Modifier .fillMaxSize() - .padding(bottom = 301.dp, end = 16.dp), + .padding(bottom = 281.dp, end = 16.dp), contentAlignment = Alignment.BottomEnd ) { Surface( diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt new file mode 100644 index 0000000..bd2ec06 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt @@ -0,0 +1,171 @@ +// timer/ui/component/SwipeableStudyItem.kt +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.animation.core.tween +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sprinter.studyspot.R +import kotlin.math.roundToInt + +enum class SwipeState { Closed, Swiped } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SwipeableStudyItem( + color: Color, + title: String, + time: String, + onEdit: () -> Unit, + onDelete: () -> Unit, + onClick: () -> Unit +) { + val density = LocalDensity.current + var widthPx by remember { mutableStateOf(0f) } + + val decay = rememberSplineBasedDecay() + + val state = remember { + AnchoredDraggableState( + initialValue = SwipeState.Closed, + positionalThreshold = { distance -> distance * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = tween(), + decayAnimationSpec = decay, + confirmValueChange = { true } + ) + } + + // 앵커 업데이트: Swiped = 오른쪽으로 40% 이동 + LaunchedEffect(widthPx) { + if (widthPx > 0f) { + state.updateAnchors( + DraggableAnchors { + SwipeState.Closed at 0f + SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 + } + ) + } + } + + // 안전한 offset 계산 + val safeOffsetX by remember { + derivedStateOf { + val current = state.offset + if (current.isNaN()) return@derivedStateOf 0f + + val swipedPos = state.anchors.positionOf(SwipeState.Swiped) ?: 0f + current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .onGloballyPositioned { coordinates -> + widthPx = coordinates.size.width.toFloat() + } + ) { + // 배경 버튼 (왼쪽에 고정) + Row( + modifier = Modifier + .matchParentSize() + .background(Color(0xFFF0F0F0)) + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = "Edit", + tint = Color.Black, + modifier = Modifier + .size(24.dp) + .clickable { onEdit() } + ) + Spacer(modifier = Modifier.width(24.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_delete), + contentDescription = "Delete", + tint = Color.Black, + modifier = Modifier + .size(24.dp) + .clickable { onDelete() } + ) + } + + // 앞쪽 카드: 오른쪽으로 스와이프 + Surface( + color = Color.White, + modifier = Modifier + .offset { + if (widthPx <= 0f) { + IntOffset(0, 0) + } else { + IntOffset(safeOffsetX.roundToInt(), 0) + } + } + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal + ) + .background(Color.White) + .clickable( + enabled = state.currentValue == SwipeState.Closed, + onClick = onClick + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 28.dp, end = 16.dp) + .height(64.dp) + ) { + Box( + modifier = Modifier + .size(33.dp) + .background(color, CircleShape) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = title, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + color = Color.Black + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = time, + fontWeight = FontWeight.Medium, + fontSize = 28.sp, + color = Color.Gray + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt new file mode 100644 index 0000000..78e7599 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt @@ -0,0 +1,11 @@ +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.ui.graphics.Color + +data class TimerItemData( + val title: String, + val color: Color, + val hour: Int, + val minute: Int, + val second: Int +) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt new file mode 100644 index 0000000..bba58e9 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -0,0 +1,23 @@ +package com.sprinter.studyspot.timer.viewmodel + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.ViewModel +import com.sprinter.studyspot.timer.ui.component.TimerItemData + +class TimerViewModel : ViewModel() { + private val _timers = mutableStateListOf() + val timers: List get() = _timers + + fun addTimer(timer: TimerItemData) { + _timers.add(timer) + } + + fun deleteTimer(title: String) { + _timers.removeAll { it.title == title } + } + + fun updateTimer(updated: TimerItemData) { + val idx = _timers.indexOfFirst { it.title == updated.title } + if (idx != -1) _timers[idx] = updated + } +} diff --git a/app/src/main/res/drawable/ic_delete.png b/app/src/main/res/drawable/ic_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..624652a05ae91d54171f291d37d8f85282408f49 GIT binary patch literal 11965 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_edit.png b/app/src/main/res/drawable/ic_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4df36ee0db760b80a817b157cafe77e12954bf GIT binary patch literal 8918 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 XYpqyi+>8Ai3fSPYd2Q9tv}6ARWd;vG literal 0 HcmV?d00001 From 222085c4203d0f94643e7f4dd0fcba8cf6cd317d Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 17:28:39 +0900 Subject: [PATCH 10/44] =?UTF-8?q?chore(ui):=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=98=81=EC=97=AD=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerSettingScreen.kt | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 1a14789..c049d20 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -4,11 +4,31 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -42,7 +62,8 @@ private fun TimeUnitField( } } - val isError = internalValue.isNotEmpty() && internalValue.toIntOrNull()?.let { it > maxValue } == true + val isError = + internalValue.isNotEmpty() && internalValue.toIntOrNull()?.let { it > maxValue } == true OutlinedTextField( value = internalValue, @@ -123,9 +144,21 @@ fun TimerSettingScreen( var selectedColor by remember { mutableStateOf(initialColor) } var timerTitle by remember { mutableStateOf(initialTitle) } - var hourStr by remember(initialHour) { mutableStateOf(initialHour.toString().padStart(2, '0').take(2)) } - var minuteStr by remember(initialMinute) { mutableStateOf(initialMinute.toString().padStart(2, '0').take(2)) } - var secondStr by remember(initialSecond) { mutableStateOf(initialSecond.toString().padStart(2, '0').take(2)) } + var hourStr by remember(initialHour) { + mutableStateOf( + initialHour.toString().padStart(2, '0').take(2) + ) + } + var minuteStr by remember(initialMinute) { + mutableStateOf( + initialMinute.toString().padStart(2, '0').take(2) + ) + } + var secondStr by remember(initialSecond) { + mutableStateOf( + initialSecond.toString().padStart(2, '0').take(2) + ) + } val hour = hourStr.toIntOrNull() ?: 0 val minute = minuteStr.toIntOrNull() ?: 0 @@ -251,11 +284,11 @@ fun TimerSettingScreen( maxValue = 59 ) } - + Spacer(modifier = Modifier.height(30.dp)) Box( modifier = Modifier .fillMaxSize() - .padding(bottom = 281.dp, end = 16.dp), + .padding(bottom = 261.dp, end = 16.dp), contentAlignment = Alignment.BottomEnd ) { Surface( From b1f3c35894ac13dab391631e93f8f2f88cfb3b5c Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 18:22:58 +0900 Subject: [PATCH 11/44] =?UTF-8?q?feat:=20TimerSettingScreen=EC=97=90=20Tim?= =?UTF-8?q?erViewModel=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=98=EC=98=81=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 21 ++++++-- .../studyspot/timer/ui/TimerScreen.kt | 51 +++++++++++++++---- .../studyspot/timer/ui/TimerSettingScreen.kt | 35 +++++++++---- .../timer/ui/component/TimerItemData.kt | 2 + .../timer/viewmodel/TimerViewModel.kt | 3 +- 5 files changed, 86 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 194cd6c..50e16a8 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -1,16 +1,22 @@ -// navigation/AppNavGraph.kt package com.sprinter.studyspot.navigation import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import androidx.navigation.* +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.navArgument import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @Composable fun AppNavGraph(navController: NavHostController) { + // ✅ NavGraph 전역에서 공유되는 ViewModel 인스턴스 + val timerViewModel: TimerViewModel = viewModel() + NavHost( navController = navController, startDestination = "timer" @@ -34,6 +40,7 @@ fun AppNavGraph(navController: NavHostController) { TimerScreen( navController = navController, + timerViewModel = timerViewModel, // ✅ 공유 ViewModel 전달 colorValue = colorValue, title = title, hour = hour, @@ -46,6 +53,7 @@ fun AppNavGraph(navController: NavHostController) { composable( route = "timerSetting?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", arguments = listOf( + navArgument("id") { type = NavType.StringType; defaultValue = "" }, navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, navArgument("color") { type = NavType.StringType; defaultValue = "0" }, navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, @@ -62,11 +70,16 @@ fun AppNavGraph(navController: NavHostController) { val initialColor = if (colorValue == "0" || colorValue.isBlank()) { null } else { - try { Color(colorValue.toULong()) } catch (e: Exception) { null } + try { + Color(colorValue.toULong()) + } catch (e: Exception) { + null + } } TimerSettingScreen( navController = navController, + timerViewModel = timerViewModel, // ✅ 동일 ViewModel 전달 initialTitle = title, initialHour = hour, initialMinute = minute, @@ -75,4 +88,4 @@ fun AppNavGraph(navController: NavHostController) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index b493ba5..4827d57 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,15 +1,35 @@ -// timer/ui/TimerScreen.kt package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -21,7 +41,6 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.R @@ -34,6 +53,7 @@ import java.net.URLEncoder @Composable fun TimerScreen( navController: NavController, + timerViewModel: TimerViewModel, // ✅ 외부에서 ViewModel 전달받음 colorValue: String = "0", title: String = "기본 타이머", hour: Int = 0, @@ -41,7 +61,7 @@ fun TimerScreen( second: Int = 0, modifier: Modifier = Modifier ) { - val timerViewModel: TimerViewModel = viewModel() + // ❌ 기존: val timerViewModel: TimerViewModel = viewModel() → 제거됨 val timerList by remember { derivedStateOf { timerViewModel.timers } } val configuration = LocalConfiguration.current @@ -91,7 +111,9 @@ fun TimerScreen( TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.size(circleSize).aspectRatio(1f) + modifier = Modifier + .size(circleSize) + .aspectRatio(1f) ) Spacer(modifier = Modifier.height(16.dp)) @@ -167,7 +189,7 @@ fun TimerScreen( val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") val colorValueStr = timer.color.value.toString() navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" ) }, onDelete = { timerViewModel.deleteTimer(timer.title) }, @@ -193,8 +215,16 @@ fun TimerScreen( .clip(RoundedCornerShape(24.dp)) .clickable { timerViewModel.addTimer( - TimerItemData("새 타이머", Color(0xFFD9D9D9), 0, 0, 0) + TimerItemData( + id = java.util.UUID.randomUUID().toString(), // ✅ id 명시 + title = "새 타이머", + color = Color(0xFFD9D9D9), + hour = 0, + minute = 0, + second = 0 + ) ) + }, verticalAlignment = Alignment.CenterVertically ) { @@ -224,7 +254,8 @@ fun TimerScreen( @Preview(showBackground = true, device = Devices.PIXEL_4) @Composable fun TimerScreenPreview() { + val vm = TimerViewModel() // ✅ Preview에서 사용할 임시 ViewModel MaterialTheme { - TimerScreen(navController = rememberNavController()) + TimerScreen(navController = rememberNavController(), timerViewModel = vm) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index c049d20..e3d1aa2 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -45,7 +45,10 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.R +import com.sprinter.studyspot.timer.ui.component.TimerItemData +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import java.net.URLEncoder +import java.util.UUID @Composable private fun TimeUnitField( @@ -135,29 +138,25 @@ private fun TimeUnitField( @Composable fun TimerSettingScreen( navController: NavController? = null, + timerViewModel: TimerViewModel, // ✅ ViewModel 주입 initialTitle: String = "자바스크립트", initialHour: Int = 0, initialMinute: Int = 0, initialSecond: Int = 0, - initialColor: Color? = null + initialColor: Color? = null, + timerId: String? = null // ✅ id 전달받을 수 있도록 추가 ) { var selectedColor by remember { mutableStateOf(initialColor) } var timerTitle by remember { mutableStateOf(initialTitle) } var hourStr by remember(initialHour) { - mutableStateOf( - initialHour.toString().padStart(2, '0').take(2) - ) + mutableStateOf(initialHour.toString().padStart(2, '0').take(2)) } var minuteStr by remember(initialMinute) { - mutableStateOf( - initialMinute.toString().padStart(2, '0').take(2) - ) + mutableStateOf(initialMinute.toString().padStart(2, '0').take(2)) } var secondStr by remember(initialSecond) { - mutableStateOf( - initialSecond.toString().padStart(2, '0').take(2) - ) + mutableStateOf(initialSecond.toString().padStart(2, '0').take(2)) } val hour = hourStr.toIntOrNull() ?: 0 @@ -284,6 +283,7 @@ fun TimerSettingScreen( maxValue = 59 ) } + Spacer(modifier = Modifier.height(30.dp)) Box( modifier = Modifier @@ -298,6 +298,18 @@ fun TimerSettingScreen( val safeTitle = timerTitle.ifBlank { "기본 타이머" } val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") val colorValue = selectedColor?.value?.toString() ?: "0" + + val updatedTimer = TimerItemData( + id = timerId ?: UUID.randomUUID().toString(), + title = safeTitle, + color = selectedColor ?: Color(0xFFD9D9D9), + hour = hour, + minute = minute, + second = second + ) + + timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 연결 + navController?.navigate( "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" ) { @@ -325,5 +337,6 @@ fun TimerSettingScreen( @Preview(showBackground = true) @Composable fun TimerSettingPreview() { - TimerSettingScreen(navController = rememberNavController()) + val vm = TimerViewModel() + TimerSettingScreen(navController = rememberNavController(), timerViewModel = vm) } \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt index 78e7599..9d8b944 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt @@ -1,8 +1,10 @@ package com.sprinter.studyspot.timer.ui.component import androidx.compose.ui.graphics.Color +import java.util.UUID data class TimerItemData( + val id: String = UUID.randomUUID().toString(), // ✅ 고유 식별자 추가 val title: String, val color: Color, val hour: Int, diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index bba58e9..3c808cb 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -16,8 +16,9 @@ class TimerViewModel : ViewModel() { _timers.removeAll { it.title == title } } + // ✅ id 기반으로 수정 fun updateTimer(updated: TimerItemData) { - val idx = _timers.indexOfFirst { it.title == updated.title } + val idx = _timers.indexOfFirst { it.id == updated.id } if (idx != -1) _timers[idx] = updated } } From 6f8a9ffe6c26dbdfdd9fbe5241c8a55e66047aa2 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 18:35:01 +0900 Subject: [PATCH 12/44] =?UTF-8?q?fix:=20imerSettingScreen=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80=ED=95=9C=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=A8=B8=EA=B0=80=20TimerScreen=EC=97=90=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(ViewModel=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 16 +- .../studyspot/timer/ui/TimerScreen.kt | 1 - .../studyspot/timer/ui/TimerSettingScreen.kt | 326 ++++++++---------- .../timer/viewmodel/TimerViewModel.kt | 2 +- 4 files changed, 154 insertions(+), 191 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 50e16a8..7df4891 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -40,7 +40,7 @@ fun AppNavGraph(navController: NavHostController) { TimerScreen( navController = navController, - timerViewModel = timerViewModel, // ✅ 공유 ViewModel 전달 + timerViewModel = timerViewModel, // ✅ 동일 ViewModel 공유 colorValue = colorValue, title = title, hour = hour, @@ -51,7 +51,7 @@ fun AppNavGraph(navController: NavHostController) { // 타이머 설정 화면 composable( - route = "timerSetting?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", + route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", arguments = listOf( navArgument("id") { type = NavType.StringType; defaultValue = "" }, navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, @@ -61,6 +61,7 @@ fun AppNavGraph(navController: NavHostController) { navArgument("second") { type = NavType.IntType; defaultValue = 0 } ) ) { backStackEntry -> + val id = backStackEntry.arguments?.getString("id") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val hour = backStackEntry.arguments?.getInt("hour") ?: 0 @@ -70,21 +71,18 @@ fun AppNavGraph(navController: NavHostController) { val initialColor = if (colorValue == "0" || colorValue.isBlank()) { null } else { - try { - Color(colorValue.toULong()) - } catch (e: Exception) { - null - } + try { Color(colorValue.toULong()) } catch (e: Exception) { null } } TimerSettingScreen( navController = navController, - timerViewModel = timerViewModel, // ✅ 동일 ViewModel 전달 + timerViewModel = timerViewModel, // ✅ ViewModel 동일 공유 initialTitle = title, initialHour = hour, initialMinute = minute, initialSecond = second, - initialColor = initialColor + initialColor = initialColor, + timerId = if (id.isNotEmpty()) id else null ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 4827d57..e76c303 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -61,7 +61,6 @@ fun TimerScreen( second: Int = 0, modifier: Modifier = Modifier ) { - // ❌ 기존: val timerViewModel: TimerViewModel = viewModel() → 제거됨 val timerList by remember { derivedStateOf { timerViewModel.timers } } val configuration = LocalConfiguration.current diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index e3d1aa2..151e17f 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -1,4 +1,3 @@ -// timer/ui/TimerSettingScreen.kt package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background @@ -60,9 +59,7 @@ private fun TimeUnitField( var internalValue by remember(value) { mutableStateOf(value) } LaunchedEffect(value) { - if (value != internalValue) { - internalValue = value - } + if (value != internalValue) internalValue = value } val isError = @@ -72,7 +69,7 @@ private fun TimeUnitField( value = internalValue, onValueChange = { raw -> val digits = raw.filter { it.isDigit() }.take(maxLength) - if (digits != internalValue && digits.length <= maxLength) { + if (digits != internalValue) { internalValue = digits onValueChange(digits) } @@ -99,21 +96,8 @@ private fun TimeUnitField( TransformedText( text = androidx.compose.ui.text.AnnotatedString(padded), offsetMapping = object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return when { - offset == 0 -> 0 - offset == 1 -> 1 - else -> 2 - } - } - - override fun transformedToOriginal(offset: Int): Int { - return when { - offset == 0 -> 0 - offset == 1 -> 1 - else -> 2 - }.coerceAtMost(text.text.length) - } + override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) + override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) } ) }, @@ -125,7 +109,7 @@ private fun TimeUnitField( ), placeholder = { Text( - text = "00", + "00", color = Color.Gray, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() @@ -138,195 +122,177 @@ private fun TimeUnitField( @Composable fun TimerSettingScreen( navController: NavController? = null, - timerViewModel: TimerViewModel, // ✅ ViewModel 주입 + timerViewModel: TimerViewModel, initialTitle: String = "자바스크립트", initialHour: Int = 0, initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, - timerId: String? = null // ✅ id 전달받을 수 있도록 추가 + timerId: String? = null ) { var selectedColor by remember { mutableStateOf(initialColor) } var timerTitle by remember { mutableStateOf(initialTitle) } - var hourStr by remember(initialHour) { - mutableStateOf(initialHour.toString().padStart(2, '0').take(2)) - } - var minuteStr by remember(initialMinute) { - mutableStateOf(initialMinute.toString().padStart(2, '0').take(2)) - } - var secondStr by remember(initialSecond) { - mutableStateOf(initialSecond.toString().padStart(2, '0').take(2)) - } + var hourStr by remember { mutableStateOf(initialHour.toString().padStart(2, '0')) } + var minuteStr by remember { mutableStateOf(initialMinute.toString().padStart(2, '0')) } + var secondStr by remember { mutableStateOf(initialSecond.toString().padStart(2, '0')) } val hour = hourStr.toIntOrNull() ?: 0 val minute = minuteStr.toIntOrNull() ?: 0 val second = secondStr.toIntOrNull() ?: 0 - - val isValid = hour in 0..99 && minute in 0..59 && second in 0..59 && - hourStr.isNotBlank() && minuteStr.isNotBlank() && secondStr.isNotBlank() + val isValid = hour in 0..99 && minute in 0..59 && second in 0..59 Surface(modifier = Modifier.fillMaxSize(), color = Color.White) { - Box(modifier = Modifier.fillMaxSize()) { - Column( + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(top = 20.dp, start = 11.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( + Icon( + painter = painterResource(id = R.drawable.ic_left_arrow), + contentDescription = "뒤로가기", modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, start = 11.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_left_arrow), - contentDescription = "뒤로가기", + .size(28.dp) + .clickable { navController?.popBackStack() } + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "\"$timerTitle\" 타이머를 설정하시는군요!\n오늘 공부 목표 시간을 설정해보세요!", + fontSize = 14.sp, + color = Color.DarkGray, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(28.dp)) + + // ✅ 색상 선택 + val colors = listOf( + Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), + Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + colors.forEach { color -> + Box( modifier = Modifier - .size(28.dp) - .clickable { navController?.popBackStack() } + .size(27.dp, 26.dp) + .background(color, RoundedCornerShape(8.dp)) + .border( + width = if (selectedColor == color) 3.dp else 0.dp, + color = if (selectedColor == color) Color.Black else Color.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable { selectedColor = color } ) } + } - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = "\"$timerTitle\" 타이머를 설정하시는군요!\n오늘 공부 목표 시간을 설정해보세요!", - fontSize = 14.sp, - color = Color.DarkGray, - lineHeight = 20.sp, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - fontWeight = FontWeight.Bold + Spacer(modifier = Modifier.height(32.dp)) + Text("타이머 이름", fontWeight = FontWeight.Bold, fontSize = 16.sp) + Spacer(modifier = Modifier.height(14.dp)) + OutlinedTextField( + value = timerTitle, + onValueChange = { timerTitle = it }, + shape = RoundedCornerShape(30.dp), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), + textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), + placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent ) + ) - Spacer(modifier = Modifier.height(28.dp)) - - Text("타이머 색상", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(12.dp)) - val colors = listOf( - Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), - Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) + Spacer(modifier = Modifier.height(32.dp)) + Text("시간 설정", fontWeight = FontWeight.Bold, fontSize = 16.sp) + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + TimeUnitField( + value = hourStr, + onValueChange = { hourStr = it }, + maxLength = 2, + maxValue = 99 ) - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - colors.forEach { color -> - Box( - modifier = Modifier - .size(27.dp, 26.dp) - .background(color, RoundedCornerShape(8.dp)) - .border( - width = if (selectedColor == color) 3.dp else 0.dp, - color = if (selectedColor == color) Color.Black else Color.Transparent, - shape = RoundedCornerShape(8.dp) - ) - .clickable { selectedColor = color } - ) - } - } - - Spacer(modifier = Modifier.height(32.dp)) - - Text("타이머 이름", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = timerTitle, - onValueChange = { timerTitle = it }, - shape = RoundedCornerShape(30.dp), - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), - textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), - placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ) + Spacer(modifier = Modifier.width(8.dp)) + Text(":", fontSize = 28.sp) + Spacer(modifier = Modifier.width(8.dp)) + TimeUnitField( + value = minuteStr, + onValueChange = { minuteStr = it }, + maxLength = 2, + maxValue = 59 ) + Spacer(modifier = Modifier.width(8.dp)) + Text(":", fontSize = 28.sp) + Spacer(modifier = Modifier.width(8.dp)) + TimeUnitField( + value = secondStr, + onValueChange = { secondStr = it }, + maxLength = 2, + maxValue = 59 + ) + } - Spacer(modifier = Modifier.height(32.dp)) - - Text("시간 설정", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - TimeUnitField( - value = hourStr, - onValueChange = { hourStr = it }, - maxLength = 2, - maxValue = 99 - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(":", fontSize = 28.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.width(8.dp)) - TimeUnitField( - value = minuteStr, - onValueChange = { minuteStr = it }, - maxLength = 2, - maxValue = 59 - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(":", fontSize = 28.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.width(8.dp)) - TimeUnitField( - value = secondStr, - onValueChange = { secondStr = it }, - maxLength = 2, - maxValue = 59 - ) - } - - Spacer(modifier = Modifier.height(30.dp)) - Box( + Spacer(modifier = Modifier.height(30.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd + ) { + Surface( modifier = Modifier - .fillMaxSize() - .padding(bottom = 261.dp, end = 16.dp), - contentAlignment = Alignment.BottomEnd - ) { - Surface( - modifier = Modifier - .size(77.dp, 41.dp) - .clickable(enabled = isValid) { - val safeTitle = timerTitle.ifBlank { "기본 타이머" } - val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") - val colorValue = selectedColor?.value?.toString() ?: "0" + .size(77.dp, 41.dp) + .clickable(enabled = isValid) { + val safeTitle = timerTitle.ifBlank { "기본 타이머" } + val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") + val colorValue = selectedColor?.value?.toString() ?: "0" - val updatedTimer = TimerItemData( - id = timerId ?: UUID.randomUUID().toString(), - title = safeTitle, - color = selectedColor ?: Color(0xFFD9D9D9), - hour = hour, - minute = minute, - second = second - ) - - timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 연결 - - navController?.navigate( - "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" - ) { - popUpTo("timerSetting") { inclusive = true } - } - }, - shape = RoundedCornerShape(8.dp), - color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - painter = painterResource(id = R.drawable.ic_confirm_check), - contentDescription = "완료", - tint = if (isValid) Color.Unspecified else Color.Gray, - modifier = Modifier.size(48.dp) + val updatedTimer = TimerItemData( + id = timerId ?: UUID.randomUUID().toString(), + title = safeTitle, + color = selectedColor ?: Color(0xFFD9D9D9), + hour = hour, + minute = minute, + second = second ) - } + timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 + + navController?.navigate( + "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" + ) { + popUpTo("timerSetting") { inclusive = true } + } + }, + shape = RoundedCornerShape(8.dp), + color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(id = R.drawable.ic_confirm_check), + contentDescription = "완료", + tint = if (isValid) Color.Unspecified else Color.Gray, + modifier = Modifier.size(48.dp) + ) } } } @@ -339,4 +305,4 @@ fun TimerSettingScreen( fun TimerSettingPreview() { val vm = TimerViewModel() TimerSettingScreen(navController = rememberNavController(), timerViewModel = vm) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index 3c808cb..c304b87 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -16,9 +16,9 @@ class TimerViewModel : ViewModel() { _timers.removeAll { it.title == title } } - // ✅ id 기반으로 수정 fun updateTimer(updated: TimerItemData) { val idx = _timers.indexOfFirst { it.id == updated.id } if (idx != -1) _timers[idx] = updated + else _timers.add(updated) } } From cba235e5f295108d1ced3a7ea4e54f75a1f941dd Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 22:38:01 +0900 Subject: [PATCH 13/44] =?UTF-8?q?feat:=20=ED=95=98=EB=8B=A8=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 149 ++++++++++-------- .../timer/ui/component/BottomNavBar.kt | 93 +++++++++++ app/src/main/res/drawable/ic_clock.png | Bin 0 -> 25555 bytes app/src/main/res/drawable/ic_home.png | Bin 0 -> 12943 bytes app/src/main/res/drawable/ic_search.png | Bin 0 -> 17910 bytes 5 files changed, 179 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt create mode 100644 app/src/main/res/drawable/ic_clock.png create mode 100644 app/src/main/res/drawable/ic_home.png create mode 100644 app/src/main/res/drawable/ic_search.png diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 7df4891..323bd04 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -1,89 +1,112 @@ package com.sprinter.studyspot.navigation +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen +import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @Composable fun AppNavGraph(navController: NavHostController) { - // ✅ NavGraph 전역에서 공유되는 ViewModel 인스턴스 + // ✅ NavGraph 전역에서 공유되는 ViewModel val timerViewModel: TimerViewModel = viewModel() - NavHost( - navController = navController, - startDestination = "timer" - ) { - // 타이머 메인 화면 - composable( - route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) - ) { backStackEntry -> - val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" - val colorValue = backStackEntry.arguments?.getString("color") ?: "0" - val hour = backStackEntry.arguments?.getInt("hour") ?: 0 - val minute = backStackEntry.arguments?.getInt("minute") ?: 0 - val second = backStackEntry.arguments?.getInt("second") ?: 0 + // ✅ 현재 라우트 감지 (BottomBar 표시 조건에 사용) + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route ?: "" - TimerScreen( - navController = navController, - timerViewModel = timerViewModel, // ✅ 동일 ViewModel 공유 - colorValue = colorValue, - title = title, - hour = hour, - minute = minute, - second = second - ) + Scaffold( + bottomBar = { + // ✅ TimerSetting 화면에서는 하단바 숨기기 + if (!currentRoute.startsWith("timerSetting")) { + BottomNavBar(navController = navController, currentRoute = currentRoute) + } } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = "timer", + modifier = androidx.compose.ui.Modifier.padding(innerPadding) + ) { + // 타이머 메인 화면 + composable( + route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) + ) { backStackEntry -> + val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" + val colorValue = backStackEntry.arguments?.getString("color") ?: "0" + val hour = backStackEntry.arguments?.getInt("hour") ?: 0 + val minute = backStackEntry.arguments?.getInt("minute") ?: 0 + val second = backStackEntry.arguments?.getInt("second") ?: 0 - // 타이머 설정 화면 - composable( - route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("id") { type = NavType.StringType; defaultValue = "" }, - navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) - ) { backStackEntry -> - val id = backStackEntry.arguments?.getString("id") ?: "" - val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" - val colorValue = backStackEntry.arguments?.getString("color") ?: "0" - val hour = backStackEntry.arguments?.getInt("hour") ?: 0 - val minute = backStackEntry.arguments?.getInt("minute") ?: 0 - val second = backStackEntry.arguments?.getInt("second") ?: 0 - - val initialColor = if (colorValue == "0" || colorValue.isBlank()) { - null - } else { - try { Color(colorValue.toULong()) } catch (e: Exception) { null } + TimerScreen( + navController = navController, + timerViewModel = timerViewModel, + colorValue = colorValue, + title = title, + hour = hour, + minute = minute, + second = second + ) } - TimerSettingScreen( - navController = navController, - timerViewModel = timerViewModel, // ✅ ViewModel 동일 공유 - initialTitle = title, - initialHour = hour, - initialMinute = minute, - initialSecond = second, - initialColor = initialColor, - timerId = if (id.isNotEmpty()) id else null - ) + // 타이머 설정 화면 + composable( + route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", + arguments = listOf( + navArgument("id") { type = NavType.StringType; defaultValue = "" }, + navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) + ) { backStackEntry -> + val id = backStackEntry.arguments?.getString("id") ?: "" + val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" + val colorValue = backStackEntry.arguments?.getString("color") ?: "0" + val hour = backStackEntry.arguments?.getInt("hour") ?: 0 + val minute = backStackEntry.arguments?.getInt("minute") ?: 0 + val second = backStackEntry.arguments?.getInt("second") ?: 0 + + val initialColor = if (colorValue == "0" || colorValue.isBlank()) { + null + } else { + try { + Color(colorValue.toULong()) + } catch (e: Exception) { + null + } + } + + TimerSettingScreen( + navController = navController, + timerViewModel = timerViewModel, + initialTitle = title, + initialHour = hour, + initialMinute = minute, + initialSecond = second, + initialColor = initialColor, + timerId = if (id.isNotEmpty()) id else null + ) + } } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt new file mode 100644 index 0000000..3055fa8 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt @@ -0,0 +1,93 @@ +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.R + +@Composable +fun BottomNavBar( + navController: androidx.navigation.NavController, + currentRoute: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(76.dp) + .background(Color.White) + .padding(horizontal = 36.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + BottomNavItem( + icon = R.drawable.ic_clock, + label = "타이머", + isSelected = currentRoute == "timer", + onClick = { navController.navigate("timer") } + ) + + BottomNavItem( + icon = R.drawable.ic_home, + label = "홈", + isSelected = currentRoute == "home", + onClick = { navController.navigate("home") } + ) + + BottomNavItem( + icon = R.drawable.ic_search, + label = "검색", + isSelected = currentRoute == "search", + onClick = { navController.navigate("search") } + ) + } +} + +@Composable +private fun BottomNavItem( + icon: Int, + label: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .width(60.dp) + .clickable(onClick = onClick), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = icon), + contentDescription = label, + modifier = Modifier.size(20.dp), + tint = Color.Black + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = label, + fontSize = 12.sp, + color = Color.Black, + fontWeight = FontWeight.Medium + ) + } +} + +@Preview(showBackground = true) +@Composable +fun BottomNavBarPreview() { + BottomNavBar( + navController = rememberNavController(), + currentRoute = "home" + ) +} diff --git a/app/src/main/res/drawable/ic_clock.png b/app/src/main/res/drawable/ic_clock.png new file mode 100644 index 0000000000000000000000000000000000000000..70bf7cb9ae723a62e831dbfa9eceb1f2c4cee82a GIT binary patch literal 25555 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 L6WwNQm$?4{uHC;1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_home.png b/app/src/main/res/drawable/ic_home.png new file mode 100644 index 0000000000000000000000000000000000000000..aec21a46b3992bd75f9b8e6c99d65740eb6b0ded GIT binary patch literal 12943 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 literal 0 HcmV?d00001 From 2f6491d3ca3b572df8ab36a042f285bde7fef4aa Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 8 Nov 2025 23:14:11 +0900 Subject: [PATCH 14/44] =?UTF-8?q?fix:=20id=20=EA=B0=92=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B0=81=20=ED=83=80=EC=9D=B4=EB=A8=B8=EC=9D=98=20Swipeable?= =?UTF-8?q?StudyItem=EB=A5=BC=20=EA=B5=AC=EB=B6=84=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 310 +++++++++--------- .../studyspot/timer/ui/TimerSettingScreen.kt | 4 +- .../timer/viewmodel/TimerViewModel.kt | 4 +- 3 files changed, 160 insertions(+), 158 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index e76c303..3b684de 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -2,34 +2,13 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -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.aspectRatio -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.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -53,7 +32,7 @@ import java.net.URLEncoder @Composable fun TimerScreen( navController: NavController, - timerViewModel: TimerViewModel, // ✅ 외부에서 ViewModel 전달받음 + timerViewModel: TimerViewModel, colorValue: String = "0", title: String = "기본 타이머", hour: Int = 0, @@ -84,8 +63,6 @@ fun TimerScreen( } } - val selectedTitle = if (title.isNotBlank()) title else "기본 타이머" - val formattedTime = String.format("%02d:%02d:%02d", hour, minute, second) val total = (hour * 3600L) + (minute * 60L) + second LaunchedEffect(Unit) { @@ -101,150 +78,173 @@ fun TimerScreen( val s = remainingSeconds % 60 val timeText = String.format("%02d:%02d:%02d", h, m, s) - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + // ✅ 전체 배경은 흰색 + Surface( + modifier = Modifier.fillMaxSize(), + color = Color.White ) { - Spacer(modifier = Modifier.height(48.dp)) - - TimerProgress( - totalSeconds = total, - elapsedSeconds = elapsed, - modifier = Modifier - .size(circleSize) - .aspectRatio(1f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - Text("남은시간", fontSize = 14.sp, color = Color.Gray) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = timeText, - fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Surface(color = Color.White, modifier = Modifier.fillMaxSize()) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 41.dp) - ) { - TabRow( - selectedTabIndex = selectedTab, - containerColor = Color.Transparent, - divider = { }, + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // ✅ 상단 회색 영역 (타이머 영역) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.45f) // 상단 비율 조정 + .background(Color(0xFFF5F5F5)), // 🔹 살짝 연한 회색 배경 (#F5F5F5 or #D9D9D9) + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( modifier = Modifier - .fillMaxWidth() - .height(48.dp), - indicator = { tabPositions -> - Box( - modifier = Modifier - .tabIndicatorOffset(tabPositions[selectedTab]) - .height(2.dp) - .background(Color.Black) - ) - } + .size(circleSize) + .aspectRatio(1f) + .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center ) { - Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { - Text( - "타이머", - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (selectedTab == 0) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) - ) - } - Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { - Text( - "기록", - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (selectedTab == 1) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) - ) - } + TimerProgress( + totalSeconds = total, + elapsedSeconds = elapsed, + modifier = Modifier.fillMaxSize() + ) } - } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text("남은시간", fontSize = 14.sp, color = Color.Gray) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = timeText, + fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } + } - LazyColumn(modifier = Modifier.weight(1f)) { - items(timerList) { timer -> - SwipeableStudyItem( - color = timer.color, - title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), - onEdit = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + // ✅ 하단 흰색 영역 + Surface( + modifier = Modifier + .fillMaxWidth() + .weight(0.55f), // 하단 비율 + color = Color.White + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 41.dp) + ) { + TabRow( + selectedTabIndex = selectedTab, + containerColor = Color.Transparent, + divider = { }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + indicator = { tabPositions -> + Box( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black) ) - }, - onDelete = { timerViewModel.deleteTimer(timer.title) }, - onClick = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + } + ) { + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { + Text( + "타이머", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 0) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) ) } - ) - - Spacer(modifier = Modifier.height(8.dp)) + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { + Text( + "기록", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 1) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } } - // 추가 버튼 - item { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(start = 28.dp, end = 16.dp, top = 8.dp) - .clip(RoundedCornerShape(24.dp)) - .clickable { - timerViewModel.addTimer( - TimerItemData( - id = java.util.UUID.randomUUID().toString(), // ✅ id 명시 - title = "새 타이머", - color = Color(0xFFD9D9D9), - hour = 0, - minute = 0, - second = 0 - ) - ) + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn(modifier = Modifier.weight(1f)) { + items(timerList) { timer -> + SwipeableStudyItem( + color = timer.color, + title = timer.title, + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), + onEdit = { + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + ) }, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_add_button), - contentDescription = "추가", - tint = Color.Unspecified, - modifier = Modifier.size(32.dp) - ) - Spacer(Modifier.width(12.dp)) - Text( - "추가하기", - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - color = Color(0xFF999999) + onDelete = { timerViewModel.deleteTimer(timer.id) }, + onClick = { + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + ) + } ) + Spacer(modifier = Modifier.height(8.dp)) + } + + // 추가 버튼 + item { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(start = 28.dp, end = 16.dp, top = 8.dp) + .clip(RoundedCornerShape(24.dp)) + .clickable { + timerViewModel.addTimer( + TimerItemData( + id = java.util.UUID.randomUUID().toString(), + title = "새 타이머", + color = Color(0xFFD9D9D9), + hour = 0, + minute = 0, + second = 0 + ) + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_button), + contentDescription = "추가", + tint = Color.Unspecified, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.width(12.dp)) + Text( + "추가하기", + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + color = Color(0xFF999999) + ) + } } } - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) + } } } } @@ -253,7 +253,7 @@ fun TimerScreen( @Preview(showBackground = true, device = Devices.PIXEL_4) @Composable fun TimerScreenPreview() { - val vm = TimerViewModel() // ✅ Preview에서 사용할 임시 ViewModel + val vm = TimerViewModel() MaterialTheme { TimerScreen(navController = rememberNavController(), timerViewModel = vm) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 151e17f..2ce675c 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -210,7 +210,9 @@ fun TimerSettingScreen( .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 + ), colors = TextFieldDefaults.colors( unfocusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index c304b87..711dadf 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -12,8 +12,8 @@ class TimerViewModel : ViewModel() { _timers.add(timer) } - fun deleteTimer(title: String) { - _timers.removeAll { it.title == title } + fun deleteTimer(id: String) { + _timers.removeAll { it.id == id } } fun updateTimer(updated: TimerItemData) { From 208a17fab4237b3fe68426284b9e6691c462ecfc Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 9 Nov 2025 00:20:26 +0900 Subject: [PATCH 15/44] =?UTF-8?q?style:=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 161 +++++++++--------- 1 file changed, 85 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 3b684de..ac015c9 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,17 +1,35 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource @@ -26,7 +44,6 @@ import com.sprinter.studyspot.R import com.sprinter.studyspot.timer.ui.component.SwipeableStudyItem import com.sprinter.studyspot.timer.ui.component.TimerItemData import com.sprinter.studyspot.timer.viewmodel.TimerViewModel -import kotlinx.coroutines.delay import java.net.URLEncoder @Composable @@ -65,13 +82,6 @@ fun TimerScreen( val total = (hour * 3600L) + (minute * 60L) + second - LaunchedEffect(Unit) { - while (elapsed < total) { - delay(1000) - elapsed++ - } - } - val remainingSeconds = (total - elapsed).coerceAtLeast(0L) val h = remainingSeconds / 3600 val m = (remainingSeconds % 3600) / 60 @@ -122,14 +132,15 @@ fun TimerScreen( } } - // ✅ 하단 흰색 영역 +// ✅ 하단 흰색 영역 Surface( modifier = Modifier .fillMaxWidth() - .weight(0.55f), // 하단 비율 + .weight(0.55f), color = Color.White ) { Column { + // 🔹 타이머 / 기록 탭 바 Row( modifier = Modifier .fillMaxWidth() @@ -174,78 +185,76 @@ fun TimerScreen( Spacer(modifier = Modifier.height(16.dp)) - LazyColumn(modifier = Modifier.weight(1f)) { - items(timerList) { timer -> - SwipeableStudyItem( - color = timer.color, - title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), - onEdit = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" - ) - }, - onDelete = { timerViewModel.deleteTimer(timer.id) }, - onClick = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" - ) - } - ) - Spacer(modifier = Modifier.height(8.dp)) - } + // 🔹 리스트 + 아이콘 고정 부분 + Box(modifier = Modifier.fillMaxSize()) { - // 추가 버튼 - item { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(start = 28.dp, end = 16.dp, top = 8.dp) - .clip(RoundedCornerShape(24.dp)) - .clickable { - timerViewModel.addTimer( - TimerItemData( - id = java.util.UUID.randomUUID().toString(), - title = "새 타이머", - color = Color(0xFFD9D9D9), - hour = 0, - minute = 0, - second = 0 - ) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 80.dp) // 아이콘 공간 확보 + ) { + items(timerList) { timer -> + SwipeableStudyItem( + color = timer.color, + title = timer.title, + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), + onEdit = { + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" ) }, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_add_button), - contentDescription = "추가", - tint = Color.Unspecified, - modifier = Modifier.size(32.dp) - ) - Spacer(Modifier.width(12.dp)) - Text( - "추가하기", - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - color = Color(0xFF999999) + onDelete = { timerViewModel.deleteTimer(timer.id) }, + onClick = { + val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + ) + } ) + Spacer(modifier = Modifier.height(8.dp)) } } + + // 🔹 오른쪽 하단 고정 아이콘 + IconButton( + onClick = { + timerViewModel.addTimer( + TimerItemData( + id = java.util.UUID.randomUUID().toString(), + title = "새 타이머", + color = Color(0xFFD9D9D9), + hour = 0, + minute = 0, + second = 0 + ) + ) + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_button), + contentDescription = "추가", + tint = Color.Unspecified, + modifier = Modifier.size(32.dp) + ) + } } Spacer(modifier = Modifier.height(24.dp)) } } + + } } } From ecc2ebe63ee2a4044d972b0c9e3cd29ac1ada82c Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 9 Nov 2025 01:43:24 +0900 Subject: [PATCH 16/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=B0=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sprinter/studyspot/MainActivity.kt | 2 + .../studyspot/navigation/AppNavGraph.kt | 30 +- .../studyspot/timer/ui/TimerProgress.kt | 5 +- .../studyspot/timer/ui/TimerRunningScreen.kt | 197 ++++++++++++ .../studyspot/timer/ui/TimerScreen.kt | 292 +++++++++--------- 5 files changed, 380 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt diff --git a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt index 36f91e8..83e991d 100644 --- a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt +++ b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt @@ -11,6 +11,8 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + window.statusBarColor = android.graphics.Color.WHITE + val navController = rememberNavController() AppNavGraph(navController = navController) } diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 323bd04..330178c 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -12,6 +12,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument +import com.sprinter.studyspot.timer.ui.TimerRunningScreen import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar @@ -29,7 +30,7 @@ fun AppNavGraph(navController: NavHostController) { Scaffold( bottomBar = { // ✅ TimerSetting 화면에서는 하단바 숨기기 - if (!currentRoute.startsWith("timerSetting")) { + if (!currentRoute.startsWith("timerSetting") && !currentRoute.startsWith("timerRunning")) { BottomNavBar(navController = navController, currentRoute = currentRoute) } } @@ -107,6 +108,33 @@ fun AppNavGraph(navController: NavHostController) { timerId = if (id.isNotEmpty()) id else null ) } + + composable( + route = "timerRunning?title={title}&color={color}&total={total}", + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("total") { type = NavType.LongType; defaultValue = 0L } + ) + ) { backStackEntry -> + val title = backStackEntry.arguments?.getString("title") ?: "" + val colorValue = backStackEntry.arguments?.getString("color") ?: "0" + val total = backStackEntry.arguments?.getLong("total") ?: 0L + + val color = try { + if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) + else Color(colorValue.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) + } + + TimerRunningScreen( + navController = navController, + title = title, + color = color, + totalSeconds = total + ) + } } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt index 07ad204..be64014 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt @@ -28,7 +28,8 @@ fun TimerProgress( totalSeconds: Long, elapsedSeconds: Long, maxDisplaySeconds: Long = 60 * 60L, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + progressColor: Color = Color(0xFFB8E9BB) ) { val density = LocalDensity.current val remainingSeconds = (totalSeconds - elapsedSeconds).coerceAtLeast(0L) @@ -46,7 +47,7 @@ fun TimerProgress( // 진행 원 drawArc( - color = Color(0xFFB8E9BB), + color = progressColor, startAngle = -90f, sweepAngle = 360f * animatedProgress, useCenter = true, diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt new file mode 100644 index 0000000..096e092 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -0,0 +1,197 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.aspectRatio +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.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.R +import kotlinx.coroutines.delay + +@Composable +fun TimerRunningScreen( + navController: NavController, + color: Color, + title: String, + totalSeconds: Long, +) { + // 🔹 상태 변수들 + var elapsed by remember { mutableStateOf(0L) } // 경과 시간 + var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 + + // 🔹 남은시간 / 집중시간 계산 + val remainingSeconds = (totalSeconds - elapsed).coerceAtLeast(0L) + val focusSeconds = totalSeconds - remainingSeconds + + // ⏰ 시/분/초 포맷 + val timeText = String.format( + "%02d:%02d:%02d", + remainingSeconds / 3600, + (remainingSeconds % 3600) / 60, + remainingSeconds % 60 + ) + val focusTimeText = String.format( + "%02d:%02d:%02d", + focusSeconds / 3600, + (focusSeconds % 3600) / 60, + focusSeconds % 60 + ) + + // ⏱ 타이머 동작 제어 + LaunchedEffect(isRunning) { + while (isRunning && elapsed < totalSeconds) { + delay(1000) + elapsed++ + } + } + + Surface( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .background(Color.White), + color = Color.White + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 🔹 상단 바 + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_arrow), + contentDescription = "뒤로가기", + modifier = Modifier + .size(28.dp) + .clickable { navController.popBackStack() } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 🔹 타이머명 + 색상 표시 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(16.dp) + .background(color = color, shape = RoundedCornerShape(50)) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(title, fontSize = 18.sp, fontWeight = FontWeight.Bold) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 🔹 원형 타이머 + Box( + modifier = Modifier + .size(220.dp) + .aspectRatio(1f) + .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center + ) { + TimerProgress( + totalSeconds = totalSeconds, + elapsedSeconds = elapsed, + modifier = Modifier.fillMaxSize(), + progressColor = color + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Text("👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!", textAlign = TextAlign.Center, color = Color.Gray) + Spacer(modifier = Modifier.height(24.dp)) + + // 🔹 남은 시간 + Text("남은 시간", fontSize = 14.sp, color = Color.Gray) + Text(timeText, fontSize = 40.sp, fontWeight = FontWeight.Bold) + + Spacer(modifier = Modifier.height(16.dp)) + Text("집중 시간", fontSize = 14.sp, color = Color.Gray) + Text(focusTimeText, fontSize = 24.sp, fontWeight = FontWeight.Medium) + + Spacer(modifier = Modifier.height(32.dp)) + + // 🔹 버튼 2개 + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Button( + onClick = { + // ⏸ 일시정지 / ▶ 재시작 토글 + isRunning = !isRunning + }, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), + shape = RoundedCornerShape(8.dp) + ) { + Text( + if (isRunning) "⏸ 일시 정지" else "▶ 실행", + color = Color.Black + ) + } + + Button( + onClick = { + // 🔄 초기화 + elapsed = 0L + isRunning = false + }, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), + shape = RoundedCornerShape(8.dp) + ) { + Text("🔄 초기화", color = Color.Black) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun TimerRunningPreview() { + TimerRunningScreen( + navController = rememberNavController(), + color = Color(0xFFFF8686), + title = "자바스크립트", + totalSeconds = 60 * 60L + ) +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index ac015c9..f8687fe 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -81,180 +81,186 @@ fun TimerScreen( } val total = (hour * 3600L) + (minute * 60L) + second - val remainingSeconds = (total - elapsed).coerceAtLeast(0L) val h = remainingSeconds / 3600 val m = (remainingSeconds % 3600) / 60 val s = remainingSeconds % 60 val timeText = String.format("%02d:%02d:%02d", h, m, s) - // ✅ 전체 배경은 흰색 + // ✅ 전체 배경 완전 흰색 (상단 연분홍 제거) Surface( modifier = Modifier.fillMaxSize(), - color = Color.White + color = Color.Transparent ) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) // ✅ 상단 StatusBar까지 완전히 흰색으로 덮음 ) { - // ✅ 상단 회색 영역 (타이머 영역) - Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.45f) // 상단 비율 조정 - .background(Color(0xFFF5F5F5)), // 🔹 살짝 연한 회색 배경 (#F5F5F5 or #D9D9D9) - contentAlignment = Alignment.Center + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(circleSize) - .aspectRatio(1f) - .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center - ) { - TimerProgress( - totalSeconds = total, - elapsedSeconds = elapsed, - modifier = Modifier.fillMaxSize() + // ✅ 상단 회색 영역 (타이머 영역) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.45f) + .background(Color(0xFFF5F5F5)), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(circleSize) + .aspectRatio(1f) + .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center + ) { + TimerProgress( + totalSeconds = total, + elapsedSeconds = elapsed, + modifier = Modifier.fillMaxSize() + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Text("남은시간", fontSize = 14.sp, color = Color.Gray) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = timeText, + fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, + fontWeight = FontWeight.Bold, + color = Color.Black ) } - - Spacer(modifier = Modifier.height(16.dp)) - Text("남은시간", fontSize = 14.sp, color = Color.Gray) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = timeText, - fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) } - } -// ✅ 하단 흰색 영역 - Surface( - modifier = Modifier - .fillMaxWidth() - .weight(0.55f), - color = Color.White - ) { - Column { - // 🔹 타이머 / 기록 탭 바 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 41.dp) - ) { - TabRow( - selectedTabIndex = selectedTab, - containerColor = Color.Transparent, - divider = { }, + // ✅ 하단 흰색 영역 + Surface( + modifier = Modifier + .fillMaxWidth() + .weight(0.55f), + color = Color.White + ) { + Column { + // 🔹 타이머 / 기록 탭 바 + Row( modifier = Modifier .fillMaxWidth() - .height(48.dp), - indicator = { tabPositions -> - Box( - modifier = Modifier - .tabIndicatorOffset(tabPositions[selectedTab]) - .height(2.dp) - .background(Color.Black) - ) - } + .padding(horizontal = 41.dp) ) { - Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { - Text( - "타이머", - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (selectedTab == 0) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) - ) - } - Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { - Text( - "기록", - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (selectedTab == 1) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) - ) + TabRow( + selectedTabIndex = selectedTab, + containerColor = Color.Transparent, + divider = { }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + indicator = { tabPositions -> + Box( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black) + ) + } + ) { + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { + Text( + "타이머", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 0) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { + Text( + "기록", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (selectedTab == 1) Color.Black else Color.Gray, + modifier = Modifier.padding(vertical = 12.dp) + ) + } } } - } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - // 🔹 리스트 + 아이콘 고정 부분 - Box(modifier = Modifier.fillMaxSize()) { + // 🔹 리스트 + 아이콘 고정 부분 + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 80.dp) + ) { + items(timerList) { timer -> + SwipeableStudyItem( + color = timer.color, + title = timer.title, + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), + onEdit = { + val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + ) + }, + onDelete = { timerViewModel.deleteTimer(timer.id) }, + onClick = { + val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + val total = + timer.hour * 3600 + timer.minute * 60 + timer.second + navController.navigate( + "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" + ) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 80.dp) // 아이콘 공간 확보 - ) { - items(timerList) { timer -> - SwipeableStudyItem( - color = timer.color, - title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), - onEdit = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" - ) - }, - onDelete = { timerViewModel.deleteTimer(timer.id) }, - onClick = { - val encodedTitle = URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}" + // 🔹 오른쪽 하단 고정 아이콘 + IconButton( + onClick = { + timerViewModel.addTimer( + TimerItemData( + id = java.util.UUID.randomUUID().toString(), + title = "새 타이머", + color = Color(0xFFD9D9D9), + hour = 0, + minute = 0, + second = 0 ) - } + ) + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_button), + contentDescription = "추가", + tint = Color.Unspecified, + modifier = Modifier.size(32.dp) ) - Spacer(modifier = Modifier.height(8.dp)) } } - // 🔹 오른쪽 하단 고정 아이콘 - IconButton( - onClick = { - timerViewModel.addTimer( - TimerItemData( - id = java.util.UUID.randomUUID().toString(), - title = "새 타이머", - color = Color(0xFFD9D9D9), - hour = 0, - minute = 0, - second = 0 - ) - ) - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(24.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_add_button), - contentDescription = "추가", - tint = Color.Unspecified, - modifier = Modifier.size(32.dp) - ) - } + Spacer(modifier = Modifier.height(24.dp)) } - - Spacer(modifier = Modifier.height(24.dp)) } } - - } } } From 6950ca9cb71b14221b22bbca3c3ea44b960d91bb Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Tue, 11 Nov 2025 01:43:40 +0900 Subject: [PATCH 17/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8?= =?UTF-8?q?=EA=B0=80=201=EC=8B=9C=EA=B0=84=20=EB=8B=A8=EC=9C=84=EB=A1=9C?= =?UTF-8?q?=20=EC=9E=90=EC=97=B0=EC=8A=A4=EB=9F=BD=EA=B2=8C=20=ED=9A=8C?= =?UTF-8?q?=EC=A0=84=ED=95=98=EB=A9=B0=201=EC=8B=9C=EA=B0=84=20=EC=99=84?= =?UTF-8?q?=EC=88=98=20=EC=8B=9C=2010=EB=B6=84=EA=B0=84=20=EC=B6=95?= =?UTF-8?q?=ED=95=98=20=EB=AC=B8=EA=B5=AC=20=ED=91=9C=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerRunningScreen.kt | 76 +++++++++++-------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index 096e092..757f996 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -2,31 +2,10 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -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.aspectRatio -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.layout.statusBarsPadding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -78,10 +57,12 @@ fun TimerRunningScreen( } } + // 🔹 타이머가 끝난 상태 감지 + val isFinished = elapsed >= totalSeconds + Surface( modifier = Modifier .fillMaxSize() - .statusBarsPadding() .background(Color.White), color = Color.White ) { @@ -123,7 +104,13 @@ fun TimerRunningScreen( Spacer(modifier = Modifier.height(24.dp)) - // 🔹 원형 타이머 + // 🔹 원형 타이머 색상 + val progressColor = when { + isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 + isRunning -> color + else -> Color(0xFF6D6D6D) + } + Box( modifier = Modifier .size(220.dp) @@ -135,15 +122,29 @@ fun TimerRunningScreen( totalSeconds = totalSeconds, elapsedSeconds = elapsed, modifier = Modifier.fillMaxSize(), - progressColor = color + progressColor = progressColor ) } Spacer(modifier = Modifier.height(24.dp)) - Text("👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!", textAlign = TextAlign.Center, color = Color.Gray) + + // 🔹 상태별 멘트 변경 + val motivationText = when { + isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" + isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" + else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" + } + + Text( + motivationText, + textAlign = TextAlign.Center, + color = if (isFinished) Color(0xFF4CAF50) else Color.Gray, // 완료 시 초록색 느낌 + fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal + ) + Spacer(modifier = Modifier.height(24.dp)) - // 🔹 남은 시간 + // 🔹 남은 시간 / 집중 시간 Text("남은 시간", fontSize = 14.sp, color = Color.Gray) Text(timeText, fontSize = 40.sp, fontWeight = FontWeight.Bold) @@ -157,14 +158,25 @@ fun TimerRunningScreen( Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Button( onClick = { - // ⏸ 일시정지 / ▶ 재시작 토글 - isRunning = !isRunning + if (isFinished) { + // ✅ 완료된 상태일 때 -> 타이머 목록 화면으로 이동 + navController.navigate("timer") { + popUpTo("timer") { inclusive = true } + } + } else { + // ⏸ 일시정지 / ▶ 재시작 토글 + isRunning = !isRunning + } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), shape = RoundedCornerShape(8.dp) ) { Text( - if (isRunning) "⏸ 일시 정지" else "▶ 실행", + when { + isFinished -> "✅ 완료됨" + isRunning -> "⏸ 일시 정지" + else -> "▶ 실행" + }, color = Color.Black ) } From a66bd60792299eaabe1ed049bdf869c21e132ae0 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Wed, 12 Nov 2025 01:22:59 +0900 Subject: [PATCH 18/44] =?UTF-8?q?feat:=20=ED=83=AD=20=EB=B0=94=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EB=9D=84=EC=9A=B8?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20RecordItemCard=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 176 +++++++++++------- .../timer/ui/component/RecordItemCard.kt | 153 +++++++++++++++ 2 files changed, 260 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index f8687fe..4ca30bc 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.R +import com.sprinter.studyspot.timer.ui.component.RecordItemCard import com.sprinter.studyspot.timer.ui.component.SwipeableStudyItem import com.sprinter.studyspot.timer.ui.component.TimerItemData import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @@ -87,7 +88,6 @@ fun TimerScreen( val s = remainingSeconds % 60 val timeText = String.format("%02d:%02d:%02d", h, m, s) - // ✅ 전체 배경 완전 흰색 (상단 연분홍 제거) Surface( modifier = Modifier.fillMaxSize(), color = Color.Transparent @@ -95,13 +95,13 @@ fun TimerScreen( Box( modifier = Modifier .fillMaxSize() - .background(Color.White) // ✅ 상단 StatusBar까지 완전히 흰색으로 덮음 + .background(Color.White) ) { Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - // ✅ 상단 회색 영역 (타이머 영역) + // ✅ 상단 타이머 영역 Box( modifier = Modifier .fillMaxWidth() @@ -136,7 +136,7 @@ fun TimerScreen( } } - // ✅ 하단 흰색 영역 + // ✅ 하단 영역 (탭 전환) Surface( modifier = Modifier .fillMaxWidth() @@ -144,7 +144,7 @@ fun TimerScreen( color = Color.White ) { Column { - // 🔹 타이머 / 기록 탭 바 + // 🔹 탭 바 Row( modifier = Modifier .fillMaxWidth() @@ -189,75 +189,109 @@ fun TimerScreen( Spacer(modifier = Modifier.height(16.dp)) - // 🔹 리스트 + 아이콘 고정 부분 + // 🔹 탭 내용 분기 Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 80.dp) - ) { - items(timerList) { timer -> - SwipeableStudyItem( - color = timer.color, - title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), - onEdit = { - val encodedTitle = - URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" - ) - }, - onDelete = { timerViewModel.deleteTimer(timer.id) }, - onClick = { - val encodedTitle = - URLEncoder.encode(timer.title, "UTF-8") - val colorValueStr = timer.color.value.toString() - val total = - timer.hour * 3600 + timer.minute * 60 + timer.second - navController.navigate( - "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" - ) - } - ) - Spacer(modifier = Modifier.height(8.dp)) + if (selectedTab == 0) { + // ✅ 타이머 탭 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 80.dp) + ) { + items(timerList) { timer -> + SwipeableStudyItem( + color = timer.color, + title = timer.title, + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), + onEdit = { + val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + navController.navigate( + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + ) + }, + onDelete = { timerViewModel.deleteTimer(timer.id) }, + onClick = { + val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") + val colorValueStr = timer.color.value.toString() + val total = + timer.hour * 3600 + timer.minute * 60 + timer.second + navController.navigate( + "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" + ) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } } - } - // 🔹 오른쪽 하단 고정 아이콘 - IconButton( - onClick = { - timerViewModel.addTimer( - TimerItemData( - id = java.util.UUID.randomUUID().toString(), - title = "새 타이머", - color = Color(0xFFD9D9D9), - hour = 0, - minute = 0, - second = 0 + // 추가 버튼 + IconButton( + onClick = { + timerViewModel.addTimer( + TimerItemData( + id = java.util.UUID.randomUUID().toString(), + title = "새 타이머", + color = Color(0xFFD9D9D9), + hour = 0, + minute = 0, + second = 0 + ) ) + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_button), + contentDescription = "추가", + tint = Color.Unspecified, + modifier = Modifier.size(32.dp) ) - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(24.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_add_button), - contentDescription = "추가", - tint = Color.Unspecified, - modifier = Modifier.size(32.dp) - ) + } + } else if (selectedTab == 1) { + // ✅ 기록 탭 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + RecordItemCard( + color = Color(0xFFFFFFFF), + title = "전체 시간", + totalTime = "00:45", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + ) + } + item { + RecordItemCard( + color = Color(0xFFFF8686), + title = "자바스크립트", + totalTime = "00:45", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + ) + } + item { + RecordItemCard( + color = Color(0xFFFFE382), + title = "파이썬", + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + ) + } + } } - } - Spacer(modifier = Modifier.height(24.dp)) + } } } } @@ -268,8 +302,12 @@ fun TimerScreen( @Preview(showBackground = true, device = Devices.PIXEL_4) @Composable fun TimerScreenPreview() { - val vm = TimerViewModel() + val dummyViewModel = remember { TimerViewModel() } + MaterialTheme { - TimerScreen(navController = rememberNavController(), timerViewModel = vm) + TimerScreen( + navController = rememberNavController(), + timerViewModel = dummyViewModel + ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt new file mode 100644 index 0000000..d526524 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -0,0 +1,153 @@ +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.min + +@Composable +fun RecordItemCard( + color: Color = Color(0xFF8BC34A), // 타이머 색상 (왼쪽 원) + title: String, + totalTime: String, + dayCounts: Map // 요일별 학습 횟수 +) { + // 최대 횟수 대비 비율 계산 + val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) + + // ✅ 잔디 색상 팔레트 (GitHub 느낌) + val grassPalette = listOf( + Color(0xFFE8F5E9), // 연한 녹색 + Color(0xFFC8E6C9), + Color(0xFF81C784), + Color(0xFF388E3C) // 진한 녹색 + ) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), + shape = RoundedCornerShape(12.dp), + color = Color(0xFFF5F5F5) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // ✅ 왼쪽: 타이틀 + 요일 + Column( + modifier = Modifier.align(Alignment.CenterStart) + ) { + // 상단 타이틀 + Row(verticalAlignment = Alignment.CenterVertically) { + // 🔹 타이머별 고유 색상 원 + Box( + modifier = Modifier + .size(26.dp) + .background(color = color, shape = RoundedCornerShape(50)) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 하단: 요일별 "잔디" 표시 (녹색 팔레트 기반) + val days = listOf("월", "화", "수", "목", "금", "토", "일") + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + days.forEachIndexed { index, day -> + val count = dayCounts[index + 1] ?: 0 + val ratio = count.toFloat() / maxCount.toFloat() + val colorIndex = when { + ratio == 0f -> -1 + ratio < 0.25f -> 0 + ratio < 0.5f -> 1 + ratio < 0.75f -> 2 + else -> 3 + } + + val grassColor = + if (colorIndex == -1) Color(0xFFE0E0E0) else grassPalette[colorIndex] + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(26.dp) + .background( + color = grassColor, + shape = RoundedCornerShape(8.dp) + ), + contentAlignment = Alignment.Center + ) {} + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = day, + fontSize = 12.sp, + color = if (count > 0) Color.Black else Color.Gray + ) + } + } + } + } + + // ✅ 오른쪽: 중앙 정렬된 타이머 박스 + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .height(74.dp) + .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = totalTime, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun RecordItemCardPreview() { + MaterialTheme { + Column(modifier = Modifier.padding(16.dp)) { + RecordItemCard( + color = Color(0xFFFFFFFF), // 타이머 색상 + title = "전체 시간", + totalTime = "00:45", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + ) + Spacer(modifier = Modifier.height(9.dp)) + RecordItemCard( + color = Color(0xFFFF8686), + title = "자바스크립트", + totalTime = "00:45", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + ) + Spacer(modifier = Modifier.height(9.dp)) + RecordItemCard( + color = Color(0xFFFFDB86), + title = "파이썬", + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + ) + } + } +} From 84979e17ba0b0963a3ca553a7276deaba980e551 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Thu, 13 Nov 2025 01:59:24 +0900 Subject: [PATCH 19/44] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EA=B8=B0=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 23 +- .../studyspot/timer/ui/RecordDetailScreen.kt | 382 ++++++++++++++++++ .../main/res/drawable/ic_left_triangle.png | Bin 0 -> 4446 bytes .../main/res/drawable/ic_right_triangle.png | Bin 0 -> 4614 bytes app/src/main/res/drawable/ic_trashcan.png | Bin 0 -> 12111 bytes 5 files changed, 399 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt create mode 100644 app/src/main/res/drawable/ic_left_triangle.png create mode 100644 app/src/main/res/drawable/ic_right_triangle.png create mode 100644 app/src/main/res/drawable/ic_trashcan.png diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 330178c..ad1c4f0 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -17,20 +17,21 @@ import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 @Composable fun AppNavGraph(navController: NavHostController) { - // ✅ NavGraph 전역에서 공유되는 ViewModel val timerViewModel: TimerViewModel = viewModel() - // ✅ 현재 라우트 감지 (BottomBar 표시 조건에 사용) val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route ?: "" Scaffold( bottomBar = { - // ✅ TimerSetting 화면에서는 하단바 숨기기 - if (!currentRoute.startsWith("timerSetting") && !currentRoute.startsWith("timerRunning")) { + if (!currentRoute.startsWith("timerSetting") && + !currentRoute.startsWith("timerRunning") && + !currentRoute.startsWith("recordDetail") + ) { BottomNavBar(navController = navController, currentRoute = currentRoute) } } @@ -40,7 +41,7 @@ fun AppNavGraph(navController: NavHostController) { startDestination = "timer", modifier = androidx.compose.ui.Modifier.padding(innerPadding) ) { - // 타이머 메인 화면 + // ✅ 타이머 메인 화면 composable( route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", arguments = listOf( @@ -68,7 +69,7 @@ fun AppNavGraph(navController: NavHostController) { ) } - // 타이머 설정 화면 + // ✅ 타이머 설정 화면 composable( route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", arguments = listOf( @@ -109,6 +110,7 @@ fun AppNavGraph(navController: NavHostController) { ) } + // ✅ 타이머 실행 화면 composable( route = "timerRunning?title={title}&color={color}&total={total}", arguments = listOf( @@ -135,6 +137,15 @@ fun AppNavGraph(navController: NavHostController) { totalSeconds = total ) } + + // ✅ 기록 상세 화면 추가 + composable( + route = "recordDetail/{recordId}", + arguments = listOf(navArgument("recordId") { type = NavType.StringType }) + ) { backStackEntry -> + val recordId = backStackEntry.arguments?.getString("recordId") ?: "" + RecordDetailScreen(navController = navController, recordId = recordId) + } } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt new file mode 100644 index 0000000..8bac667 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -0,0 +1,382 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.R +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun RecordDetailScreen( + navController: NavController, + recordId: String +) { + val isPreview = LocalInspectionMode.current + val monthState = remember { if (isPreview) null else mutableStateOf(YearMonth.now()) } + val monthTitle = monthState?.value?.monthValue?.let { "${it}월" } ?: "11월" + + val runtimeStudyDates = listOf( + LocalDate.of(2025, 11, 4), + LocalDate.of(2025, 11, 6), + LocalDate.of(2025, 11, 12), + LocalDate.of(2025, 11, 13), + LocalDate.of(2025, 11, 19) + ) + + // 달력 계산 + var firstDayOfWeek: Int + var daysInMonth: Int + var studyDaySet: Set + + if (monthState == null) { + firstDayOfWeek = 0 + daysInMonth = 30 + studyDaySet = setOf(4, 6, 12, 13, 19) + } else { + val ym = monthState.value + daysInMonth = ym.lengthOfMonth() + firstDayOfWeek = when (ym.atDay(1).dayOfWeek.value) { + 1 -> 0; 2 -> 1; 3 -> 2; 4 -> 3; 5 -> 4; 6 -> 5; 7 -> 6 + else -> 0 + } + studyDaySet = runtimeStudyDates + .filter { it.year == ym.year && it.monthValue == ym.monthValue } + .map { it.dayOfMonth } + .toSet() + } + + val gridCells = buildList { + repeat(firstDayOfWeek) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) + } + + var selectedDay by remember { + mutableStateOf(if (isPreview) 12 else null) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + // ⬅️ 뒤로가기 버튼 + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + IconButton( + onClick = { if (!isPreview) navController.popBackStack() }, + modifier = Modifier.size(28.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_arrow), + contentDescription = "Back", + tint = Color.Unspecified, + modifier = Modifier.size(28.dp) + ) + } + } + + Spacer(modifier = Modifier.height(21.dp)) + + // 🔹 상단 이름 섹션 + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_triangle), + contentDescription = "Prev Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(38.dp)) + Box( + modifier = Modifier + .size(26.dp) + .clip(CircleShape) + .background(Color(0xFFE57373)) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(text = "자바스크립트", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(12.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_trashcan), + contentDescription = "Delete", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(38.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_right_triangle), + contentDescription = "Next Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + } + + Spacer(modifier = Modifier.height(21.dp)) + + // ✅ 달력 영역 + Box( + modifier = Modifier + .width(363.dp) + .height(402.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFF3F3F3)) + .align(Alignment.CenterHorizontally) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(66.dp)) + + // 🔹 월 변경 헤더 + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { + monthState?.let { it.value = it.value.minusMonths(1) } + }) { + Icon( + painter = painterResource(id = R.drawable.ic_left_triangle), + contentDescription = "Prev Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + } + + Spacer(modifier = Modifier.width(38.dp)) + Text(text = monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(38.dp)) + + IconButton(onClick = { + monthState?.let { it.value = it.value.plusMonths(1) } + }) { + Icon( + painter = painterResource(id = R.drawable.ic_right_triangle), + contentDescription = "Next Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 🔹 요일 헤더 + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 49.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + listOf("월", "화", "수", "목", "금", "토", "일").forEach { + Text( + text = it, + fontSize = 14.sp, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 🔹 달력 (5행) + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 6.dp, bottom = 4.dp) + ) { + gridCells.chunked(7).forEach { week -> + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + week.forEach { dayOrNull -> + val isStudy = dayOrNull != null && studyDaySet.contains(dayOrNull) + val isSelected = selectedDay == dayOrNull + val color = when { + dayOrNull == null -> Color(0xFFE0E0E0) + isSelected -> Color(0xFFFF8686) + isStudy -> Color(0xFFB3E5C2) + else -> Color(0xFFE0E0E0) + } + + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) + .clickable(enabled = isStudy) { + selectedDay = dayOrNull + } + ) + } + } + } + } + } + } + + // ✅ 선택된 날짜의 타이머 정보 + selectedDay?.let { + Spacer(modifier = Modifier.height(20.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + // 🔹 "총 집중 시간" + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End + ) { + Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) + } + + // 🔹 색상 + 타이머명 + 총 시간 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(33.dp) + .clip(CircleShape) + .background(Color(0xFFE57373)) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("자바스크립트", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + + Text( + "00:20:00", + fontSize = 32.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(end = 18.dp) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + Spacer(modifier = Modifier.height(8.dp)) + + // 🔹 헤더 (정렬 간격 반영 + 고정 폭) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "시작", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + modifier = Modifier.width(90.dp) + ) + Text( + "종료", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + modifier = Modifier.width(90.dp) + ) + Text( + "집중 시간", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + +// 🔹 데이터 행 (고정 폭 + 수평 정렬) + val focusList = listOf( + Triple("13:00:00 ~", "13:10:00", "00:10:00"), + Triple("14:00:00 ~", "14:10:00", "00:10:00") + ) + + focusList.forEach { (start, end, duration) -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = start, + fontSize = 16.sp, + modifier = Modifier.width(90.dp) + ) + Text( + text = end, + fontSize = 16.sp, + modifier = Modifier.width(90.dp) + ) + Text( + text = duration, + fontSize = 16.sp + ) + } + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +fun PreviewRecordDetailScreen() { + val navController = rememberNavController() + RecordDetailScreen(navController = navController, recordId = "1") +} diff --git a/app/src/main/res/drawable/ic_left_triangle.png b/app/src/main/res/drawable/ic_left_triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..4cdbc5db4ba7709ee5de8ff5296d2bca141ff450 GIT binary patch literal 4446 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_right_triangle.png b/app/src/main/res/drawable/ic_right_triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..bc4c9f2d6d3b2c1560836429ee330dd9aac3bc17 GIT binary patch literal 4614 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_trashcan.png b/app/src/main/res/drawable/ic_trashcan.png new file mode 100644 index 0000000000000000000000000000000000000000..e04fd4d656cdcfd436ed163750ee457c941614fd GIT binary patch literal 12111 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 Date: Thu, 13 Nov 2025 02:15:52 +0900 Subject: [PATCH 20/44] =?UTF-8?q?style:=20=EC=8B=9C=EC=9E=91,=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C,=20=EC=A7=91=EC=A4=91=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=9C=84=EC=B9=98=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/RecordDetailScreen.kt | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 8bac667..3b76aae 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -1,5 +1,6 @@ package com.sprinter.studyspot.timer.ui +import android.R.attr.firstDayOfWeek import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -58,32 +59,33 @@ fun RecordDetailScreen( LocalDate.of(2025, 11, 19) ) - // 달력 계산 - var firstDayOfWeek: Int - var daysInMonth: Int - var studyDaySet: Set - - if (monthState == null) { - firstDayOfWeek = 0 - daysInMonth = 30 - studyDaySet = setOf(4, 6, 12, 13, 19) - } else { - val ym = monthState.value - daysInMonth = ym.lengthOfMonth() - firstDayOfWeek = when (ym.atDay(1).dayOfWeek.value) { - 1 -> 0; 2 -> 1; 3 -> 2; 4 -> 3; 5 -> 4; 6 -> 5; 7 -> 6 - else -> 0 - } - studyDaySet = runtimeStudyDates +// ✅ 달력 계산 (recomposition 시 불필요 연산 방지) + val isPreviewMode = LocalInspectionMode.current + + val ym = remember(monthState?.value) { + monthState?.value ?: YearMonth.now() + } + + val daysInMonth = remember(ym) { ym.lengthOfMonth() } + + val firstDayOfWeek = remember(ym) { + (ym.atDay(1).dayOfWeek.value + 6) % 7 // 월요일=0, 일요일=6 + } + + val studyDaySet = remember(ym) { + if (isPreviewMode) setOf(4, 6, 12, 13, 19) + else runtimeStudyDates .filter { it.year == ym.year && it.monthValue == ym.monthValue } .map { it.dayOfMonth } .toSet() } - val gridCells = buildList { - repeat(firstDayOfWeek) { add(null) } - for (d in 1..daysInMonth) add(d) - while (size < 35) add(null) + val gridCells = remember(ym, firstDayOfWeek) { + buildList { + repeat(firstDayOfWeek) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) + } } var selectedDay by remember { From 627afa7e8c45ded1dc387d8812401f09816e9a32 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Thu, 13 Nov 2025 03:05:01 +0900 Subject: [PATCH 21/44] =?UTF-8?q?feat:=20RecordItemCard=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20RecordDetailScreen=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EB=B0=8F=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 23 ++- .../studyspot/timer/ui/RecordDetailScreen.kt | 152 +++++++----------- .../studyspot/timer/ui/TimerScreen.kt | 69 +++----- .../timer/ui/component/RecordData.kt | 11 ++ .../timer/ui/component/RecordItemCard.kt | 38 ++++- .../timer/viewmodel/TimerViewModel.kt | 35 +++- 6 files changed, 170 insertions(+), 158 deletions(-) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index ad1c4f0..7ea6dac 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -138,14 +138,29 @@ fun AppNavGraph(navController: NavHostController) { ) } - // ✅ 기록 상세 화면 추가 + // ✅ 기록 상세 화면 composable( - route = "recordDetail/{recordId}", - arguments = listOf(navArgument("recordId") { type = NavType.StringType }) + route = "recordDetail/{recordId}?title={title}&color={color}", + arguments = listOf( + navArgument("recordId") { type = NavType.StringType }, + navArgument("title") { type = NavType.StringType; defaultValue = "" }, + navArgument("color") { type = NavType.LongType; defaultValue = 0xFFFFFFFF } + ) ) { backStackEntry -> val recordId = backStackEntry.arguments?.getString("recordId") ?: "" - RecordDetailScreen(navController = navController, recordId = recordId) + val title = backStackEntry.arguments?.getString("title") ?: "" + val colorValue = backStackEntry.arguments?.getLong("color") ?: 0xFFFFFFFF + val color = Color(colorValue) + + RecordDetailScreen( + navController = navController, + recordId = recordId, + title = title, + color = color, + timerViewModel = timerViewModel // ✅ ViewModel 전달 + ) } + } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 3b76aae..17989ba 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -1,35 +1,19 @@ package com.sprinter.studyspot.timer.ui -import android.R.attr.firstDayOfWeek import androidx.compose.foundation.background import androidx.compose.foundation.clickable -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.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -39,48 +23,38 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.R -import java.time.LocalDate +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import java.time.YearMonth @Composable fun RecordDetailScreen( navController: NavController, - recordId: String + recordId: String, + title: String? = null, + color: Color? = null, + timerViewModel: TimerViewModel ) { - val isPreview = LocalInspectionMode.current - val monthState = remember { if (isPreview) null else mutableStateOf(YearMonth.now()) } - val monthTitle = monthState?.value?.monthValue?.let { "${it}월" } ?: "11월" - - val runtimeStudyDates = listOf( - LocalDate.of(2025, 11, 4), - LocalDate.of(2025, 11, 6), - LocalDate.of(2025, 11, 12), - LocalDate.of(2025, 11, 13), - LocalDate.of(2025, 11, 19) - ) - -// ✅ 달력 계산 (recomposition 시 불필요 연산 방지) - val isPreviewMode = LocalInspectionMode.current - - val ym = remember(monthState?.value) { - monthState?.value ?: YearMonth.now() + // ✅ ViewModel에서 데이터 조회 + val record = timerViewModel.getRecordById(recordId) + val recordTitle = record?.title ?: title ?: "기록 상세" + + // ✅ Color 타입 강제 고정 (핵심 수정) + val recordColor: Color = when (val c = record?.color) { + is Color -> c + is Int -> Color(c) + else -> color ?: Color(0xFFD9D9D9) } - val daysInMonth = remember(ym) { ym.lengthOfMonth() } - - val firstDayOfWeek = remember(ym) { - (ym.atDay(1).dayOfWeek.value + 6) % 7 // 월요일=0, 일요일=6 - } + // ✅ 달력 관련 상태 정의 + val monthState = remember { mutableStateOf(YearMonth.now()) } + val monthTitle = "${monthState.value.monthValue}월" + val ym = monthState.value + val daysInMonth = ym.lengthOfMonth() + val firstDayOfWeek = (ym.atDay(1).dayOfWeek.value + 6) % 7 - val studyDaySet = remember(ym) { - if (isPreviewMode) setOf(4, 6, 12, 13, 19) - else runtimeStudyDates - .filter { it.year == ym.year && it.monthValue == ym.monthValue } - .map { it.dayOfMonth } - .toSet() - } + val studyDaySet = setOf(4, 6, 12, 13, 19) // 이후 실제 데이터 연결 가능 - val gridCells = remember(ym, firstDayOfWeek) { + val gridCells = remember(ym) { buildList { repeat(firstDayOfWeek) { add(null) } for (d in 1..daysInMonth) add(d) @@ -88,10 +62,9 @@ fun RecordDetailScreen( } } - var selectedDay by remember { - mutableStateOf(if (isPreview) 12 else null) - } + var selectedDay by remember { mutableStateOf(null) } + // ✅ 메인 레이아웃 Column( modifier = Modifier .fillMaxSize() @@ -100,27 +73,20 @@ fun RecordDetailScreen( ) { Spacer(modifier = Modifier.height(16.dp)) - // ⬅️ 뒤로가기 버튼 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - IconButton( - onClick = { if (!isPreview) navController.popBackStack() }, - modifier = Modifier.size(28.dp) - ) { + // 🔙 뒤로가기 버튼 + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "Back", - tint = Color.Unspecified, - modifier = Modifier.size(28.dp) + tint = Color.Unspecified ) } } Spacer(modifier = Modifier.height(21.dp)) - // 🔹 상단 이름 섹션 + // 🔹 상단 이름 섹션 (루틴명 + 아이콘) Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -137,10 +103,10 @@ fun RecordDetailScreen( modifier = Modifier .size(26.dp) .clip(CircleShape) - .background(Color(0xFFE57373)) + .background(color = recordColor) ) Spacer(modifier = Modifier.width(12.dp)) - Text(text = "자바스크립트", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(text = recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(12.dp)) Icon( painter = painterResource(id = R.drawable.ic_trashcan), @@ -159,26 +125,24 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(21.dp)) - // ✅ 달력 영역 + // ✅ 달력 박스 Box( modifier = Modifier - .width(363.dp) - .height(402.dp) + .fillMaxWidth() + .aspectRatio(1f) .clip(RoundedCornerShape(10.dp)) .background(Color(0xFFF3F3F3)) .align(Alignment.CenterHorizontally) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(66.dp)) + Spacer(modifier = Modifier.height(20.dp)) // 🔹 월 변경 헤더 Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { - monthState?.let { it.value = it.value.minusMonths(1) } - }) { + IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), contentDescription = "Prev Month", @@ -191,9 +155,7 @@ fun RecordDetailScreen( Text(text = monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(38.dp)) - IconButton(onClick = { - monthState?.let { it.value = it.value.plusMonths(1) } - }) { + IconButton(onClick = { monthState.value = monthState.value.plusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_right_triangle), contentDescription = "Next Month", @@ -224,7 +186,7 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(8.dp)) - // 🔹 달력 (5행) + // 🔹 달력 (5주) Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -261,7 +223,7 @@ fun RecordDetailScreen( } } - // ✅ 선택된 날짜의 타이머 정보 + // ✅ 선택된 날짜의 타이머 기록 영역 selectedDay?.let { Spacer(modifier = Modifier.height(20.dp)) @@ -293,10 +255,10 @@ fun RecordDetailScreen( modifier = Modifier .size(33.dp) .clip(CircleShape) - .background(Color(0xFFE57373)) + .background(color = recordColor) ) Spacer(modifier = Modifier.width(8.dp)) - Text("자바스크립트", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } Text( @@ -311,7 +273,7 @@ fun RecordDetailScreen( Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) Spacer(modifier = Modifier.height(8.dp)) - // 🔹 헤더 (정렬 간격 반영 + 고정 폭) + // 🔹 헤더 Row( modifier = Modifier .fillMaxWidth() @@ -342,7 +304,7 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(6.dp)) -// 🔹 데이터 행 (고정 폭 + 수평 정렬) + // 🔹 예시 데이터 행 val focusList = listOf( Triple("13:00:00 ~", "13:10:00", "00:10:00"), Triple("14:00:00 ~", "14:10:00", "00:10:00") @@ -355,20 +317,9 @@ fun RecordDetailScreen( .padding(start = 46.dp), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = start, - fontSize = 16.sp, - modifier = Modifier.width(90.dp) - ) - Text( - text = end, - fontSize = 16.sp, - modifier = Modifier.width(90.dp) - ) - Text( - text = duration, - fontSize = 16.sp - ) + Text(text = start, fontSize = 16.sp, modifier = Modifier.width(90.dp)) + Text(text = end, fontSize = 16.sp, modifier = Modifier.width(90.dp)) + Text(text = duration, fontSize = 16.sp) } } } @@ -380,5 +331,10 @@ fun RecordDetailScreen( @Composable fun PreviewRecordDetailScreen() { val navController = rememberNavController() - RecordDetailScreen(navController = navController, recordId = "1") + val dummyViewModel = remember { TimerViewModel() } // ✅ Preview용 + RecordDetailScreen( + navController = navController, + recordId = "1", + timerViewModel = dummyViewModel + ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 4ca30bc..814844a 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,33 +1,13 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -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.aspectRatio -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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -264,33 +244,32 @@ fun TimerScreen( .fillMaxSize() .padding(horizontal = 16.dp) ) { - item { - RecordItemCard( - color = Color(0xFFFFFFFF), - title = "전체 시간", - totalTime = "00:45", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) - ) - } - item { - RecordItemCard( - color = Color(0xFFFF8686), - title = "자바스크립트", - totalTime = "00:45", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) - ) - } - item { + items(timerViewModel.records) { record -> + // 🔹 안전한 Color 변환 (핵심 수정) + val recordColor: Color = when (val c = record.color) { + is Color -> c + is Int -> Color(c) + else -> Color(0xFFD9D9D9) + } + + val encodedTitle = URLEncoder.encode(record.title, "UTF-8") + val colorValueStr = recordColor.value.toString() + RecordItemCard( - color = Color(0xFFFFE382), - title = "파이썬", - totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + color = recordColor, + title = record.title, + totalTime = record.totalTime, + dayCounts = record.dayCounts, + onClick = { + navController.navigate( + "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr" + ) + } ) + Spacer(modifier = Modifier.height(8.dp)) } } } - } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt new file mode 100644 index 0000000..edbc887 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt @@ -0,0 +1,11 @@ +package com.sprinter.studyspot.timer.ui.component + +import androidx.compose.ui.graphics.Color + +data class RecordData( + val id: String, + val title: String, + val color: Color, + val totalTime: String, + val dayCounts: Map +) \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index d526524..5f0bf9c 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -1,7 +1,17 @@ package com.sprinter.studyspot.timer.ui.component import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -10,18 +20,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlin.math.min @Composable fun RecordItemCard( color: Color = Color(0xFF8BC34A), // 타이머 색상 (왼쪽 원) title: String, totalTime: String, - dayCounts: Map // 요일별 학습 횟수 + dayCounts: Map, // 요일별 학습 횟수 + onClick: (() -> Unit)? = null ) { // 최대 횟수 대비 비율 계산 val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) @@ -34,17 +45,28 @@ fun RecordItemCard( Color(0xFF388E3C) // 진한 녹색 ) + // ✅ 반응형 기기 크기 계산 + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val screenHeight = configuration.screenHeightDp.dp + + // 화면 비율 기반으로 크기 조정 + val cardPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) + val boxHeight = (screenHeight.value * 0.085f).dp.coerceAtLeast(60.dp) + val boxPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) + Surface( modifier = Modifier .fillMaxWidth() - .padding(vertical = 6.dp), + .padding(vertical = 6.dp) + .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), color = Color(0xFFF5F5F5) ) { Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(cardPadding) ) { // ✅ 왼쪽: 타이틀 + 요일 Column( @@ -103,13 +125,13 @@ fun RecordItemCard( } } - // ✅ 오른쪽: 중앙 정렬된 타이머 박스 + // ✅ 오른쪽: 중앙 정렬된 타이머 박스 (반응형) Box( modifier = Modifier .align(Alignment.CenterEnd) - .height(74.dp) + .height(boxHeight) .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) - .padding(horizontal = 16.dp), + .padding(horizontal = boxPadding), contentAlignment = Alignment.Center ) { Text( diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index 711dadf..dff5d29 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -1,13 +1,40 @@ package com.sprinter.studyspot.timer.viewmodel import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import com.sprinter.studyspot.timer.ui.component.RecordData import com.sprinter.studyspot.timer.ui.component.TimerItemData class TimerViewModel : ViewModel() { private val _timers = mutableStateListOf() val timers: List get() = _timers + // ✅ 기록 데이터 + val records = listOf( + RecordData( + id = "0", + title = "전체 시간", + color = Color(0xFFFFFFFF), + totalTime = "02:10", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + ), + RecordData( + id = "1", + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + ), + RecordData( + id = "2", + title = "파이썬", + color = Color(0xFFFFE382), + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + ) + ) + fun addTimer(timer: TimerItemData) { _timers.add(timer) } @@ -18,7 +45,9 @@ class TimerViewModel : ViewModel() { fun updateTimer(updated: TimerItemData) { val idx = _timers.indexOfFirst { it.id == updated.id } - if (idx != -1) _timers[idx] = updated - else _timers.add(updated) + if (idx != -1) _timers[idx] = updated else _timers.add(updated) } -} + + // ✅ recordId로 RecordData 가져오기 + fun getRecordById(id: String): RecordData? = records.find { it.id == id } +} \ No newline at end of file From 307cbc25c5add0f848e760c266d1f477817b0ce6 Mon Sep 17 00:00:00 2001 From: MyKnow Date: Thu, 13 Nov 2025 05:16:12 +0900 Subject: [PATCH 22/44] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EC=97=90=20ktlint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ./gradlew ktlintformat 실행 - Wildcard import 제거 --- .../com/sprinter/studyspot/MainActivity.kt | 2 +- .../studyspot/navigation/AppNavGraph.kt | 150 ++++++++---- .../studyspot/timer/ui/RecordDetailScreen.kt | 197 +++++++++------- .../studyspot/timer/ui/TimerProgress.kt | 29 +-- .../studyspot/timer/ui/TimerRunningScreen.kt | 137 ++++++----- .../studyspot/timer/ui/TimerScreen.kt | 201 +++++++++------- .../studyspot/timer/ui/TimerSettingScreen.kt | 220 ++++++++++-------- .../timer/ui/component/BottomNavBar.kt | 48 ++-- .../timer/ui/component/RecordData.kt | 4 +- .../timer/ui/component/RecordItemCard.kt | 95 ++++---- .../studyspot/timer/ui/component/StudyItem.kt | 31 ++- .../timer/ui/component/SwipeableStudyItem.kt | 147 +++++++----- .../timer/ui/component/TimerItemData.kt | 2 +- .../timer/viewmodel/TimerViewModel.kt | 47 ++-- 14 files changed, 768 insertions(+), 542 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt index 83e991d..63b5389 100644 --- a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt +++ b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt @@ -17,4 +17,4 @@ class MainActivity : ComponentActivity() { AppNavGraph(navController = navController) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 7ea6dac..539fb42 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -12,12 +12,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument +import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 import com.sprinter.studyspot.timer.ui.TimerRunningScreen import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel -import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 @Composable fun AppNavGraph(navController: NavHostController) { @@ -34,23 +34,41 @@ fun AppNavGraph(navController: NavHostController) { ) { BottomNavBar(navController = navController, currentRoute = currentRoute) } - } + }, ) { innerPadding -> NavHost( navController = navController, startDestination = "timer", - modifier = androidx.compose.ui.Modifier.padding(innerPadding) + modifier = + androidx.compose.ui.Modifier + .padding(innerPadding), ) { // ✅ 타이머 메인 화면 composable( route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "기본 타이머" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" @@ -65,21 +83,40 @@ fun AppNavGraph(navController: NavHostController) { title = title, hour = hour, minute = minute, - second = second + second = second, ) } // ✅ 타이머 설정 화면 composable( route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("id") { type = NavType.StringType; defaultValue = "" }, - navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("title") { + type = NavType.StringType + defaultValue = "자바스크립트" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" @@ -88,15 +125,16 @@ fun AppNavGraph(navController: NavHostController) { val minute = backStackEntry.arguments?.getInt("minute") ?: 0 val second = backStackEntry.arguments?.getInt("second") ?: 0 - val initialColor = if (colorValue == "0" || colorValue.isBlank()) { - null - } else { - try { - Color(colorValue.toULong()) - } catch (e: Exception) { + val initialColor = + if (colorValue == "0" || colorValue.isBlank()) { null + } else { + try { + Color(colorValue.toULong()) + } catch (e: Exception) { + null + } } - } TimerSettingScreen( navController = navController, @@ -106,46 +144,67 @@ fun AppNavGraph(navController: NavHostController) { initialMinute = minute, initialSecond = second, initialColor = initialColor, - timerId = if (id.isNotEmpty()) id else null + timerId = if (id.isNotEmpty()) id else null, ) } // ✅ 타이머 실행 화면 composable( route = "timerRunning?title={title}&color={color}&total={total}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("total") { type = NavType.LongType; defaultValue = 0L } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("total") { + type = NavType.LongType + defaultValue = 0L + }, + ), ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val total = backStackEntry.arguments?.getLong("total") ?: 0L - val color = try { - if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) - else Color(colorValue.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val color = + try { + if (colorValue == "0" || colorValue.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(colorValue.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } TimerRunningScreen( navController = navController, title = title, color = color, - totalSeconds = total + totalSeconds = total, ) } // ✅ 기록 상세 화면 composable( route = "recordDetail/{recordId}?title={title}&color={color}", - arguments = listOf( - navArgument("recordId") { type = NavType.StringType }, - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.LongType; defaultValue = 0xFFFFFFFF } - ) + arguments = + listOf( + navArgument("recordId") { type = NavType.StringType }, + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.LongType + defaultValue = 0xFFFFFFFF + }, + ), ) { backStackEntry -> val recordId = backStackEntry.arguments?.getString("recordId") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "" @@ -157,10 +216,9 @@ fun AppNavGraph(navController: NavHostController) { recordId = recordId, title = title, color = color, - timerViewModel = timerViewModel // ✅ ViewModel 전달 + timerViewModel = timerViewModel, // ✅ ViewModel 전달 ) } - } } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 17989ba..d57968b 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -2,14 +2,29 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -32,18 +47,19 @@ fun RecordDetailScreen( recordId: String, title: String? = null, color: Color? = null, - timerViewModel: TimerViewModel + timerViewModel: TimerViewModel, ) { // ✅ ViewModel에서 데이터 조회 val record = timerViewModel.getRecordById(recordId) val recordTitle = record?.title ?: title ?: "기록 상세" // ✅ Color 타입 강제 고정 (핵심 수정) - val recordColor: Color = when (val c = record?.color) { - is Color -> c - is Int -> Color(c) - else -> color ?: Color(0xFFD9D9D9) - } + val recordColor: Color = + when (val c = record?.color) { + is Color -> c + is Int -> Color(c) + else -> color ?: Color(0xFFD9D9D9) + } // ✅ 달력 관련 상태 정의 val monthState = remember { mutableStateOf(YearMonth.now()) } @@ -54,22 +70,24 @@ fun RecordDetailScreen( val studyDaySet = setOf(4, 6, 12, 13, 19) // 이후 실제 데이터 연결 가능 - val gridCells = remember(ym) { - buildList { - repeat(firstDayOfWeek) { add(null) } - for (d in 1..daysInMonth) add(d) - while (size < 35) add(null) + val gridCells = + remember(ym) { + buildList { + repeat(firstDayOfWeek) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) + } } - } var selectedDay by remember { mutableStateOf(null) } // ✅ 메인 레이아웃 Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 20.dp) + modifier = + Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 20.dp), ) { Spacer(modifier = Modifier.height(16.dp)) @@ -79,7 +97,7 @@ fun RecordDetailScreen( Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "Back", - tint = Color.Unspecified + tint = Color.Unspecified, ) } } @@ -90,20 +108,21 @@ fun RecordDetailScreen( Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, ) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), contentDescription = "Prev Month", tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) Spacer(modifier = Modifier.width(38.dp)) Box( - modifier = Modifier - .size(26.dp) - .clip(CircleShape) - .background(color = recordColor) + modifier = + Modifier + .size(26.dp) + .clip(CircleShape) + .background(color = recordColor), ) Spacer(modifier = Modifier.width(12.dp)) Text(text = recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -112,14 +131,14 @@ fun RecordDetailScreen( painter = painterResource(id = R.drawable.ic_trashcan), contentDescription = "Delete", tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) Spacer(modifier = Modifier.width(38.dp)) Icon( painter = painterResource(id = R.drawable.ic_right_triangle), contentDescription = "Next Month", tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } @@ -127,12 +146,13 @@ fun RecordDetailScreen( // ✅ 달력 박스 Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFF3F3F3)) - .align(Alignment.CenterHorizontally) + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFF3F3F3)) + .align(Alignment.CenterHorizontally), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.height(20.dp)) @@ -140,14 +160,14 @@ fun RecordDetailScreen( // 🔹 월 변경 헤더 Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), contentDescription = "Prev Month", tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } @@ -160,7 +180,7 @@ fun RecordDetailScreen( painter = painterResource(id = R.drawable.ic_right_triangle), contentDescription = "Next Month", tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } } @@ -172,14 +192,14 @@ fun RecordDetailScreen( Modifier .fillMaxWidth() .padding(horizontal = 49.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp) + horizontalArrangement = Arrangement.spacedBy(30.dp), ) { listOf("월", "화", "수", "목", "금", "토", "일").forEach { Text( text = it, fontSize = 14.sp, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) } } @@ -190,31 +210,33 @@ fun RecordDetailScreen( Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 6.dp, bottom = 4.dp) + modifier = Modifier.padding(top = 6.dp, bottom = 4.dp), ) { gridCells.chunked(7).forEach { week -> Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { week.forEach { dayOrNull -> val isStudy = dayOrNull != null && studyDaySet.contains(dayOrNull) val isSelected = selectedDay == dayOrNull - val color = when { - dayOrNull == null -> Color(0xFFE0E0E0) - isSelected -> Color(0xFFFF8686) - isStudy -> Color(0xFFB3E5C2) - else -> Color(0xFFE0E0E0) - } + val color = + when { + dayOrNull == null -> Color(0xFFE0E0E0) + isSelected -> Color(0xFFFF8686) + isStudy -> Color(0xFFB3E5C2) + else -> Color(0xFFE0E0E0) + } Box( - modifier = Modifier - .size(34.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color) - .clickable(enabled = isStudy) { - selectedDay = dayOrNull - } + modifier = + Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) + .clickable(enabled = isStudy) { + selectedDay = dayOrNull + }, ) } } @@ -228,34 +250,38 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(20.dp)) Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { // 🔹 "총 집중 시간" Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 49.dp), - horizontalArrangement = Arrangement.End + modifier = + Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End, ) { Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) } // 🔹 색상 + 타이머명 + 총 시간 Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 2.dp), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( - modifier = Modifier - .size(33.dp) - .clip(CircleShape) - .background(color = recordColor) + modifier = + Modifier + .size(33.dp) + .clip(CircleShape) + .background(color = recordColor), ) Spacer(modifier = Modifier.width(8.dp)) Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) @@ -265,7 +291,7 @@ fun RecordDetailScreen( "00:20:00", fontSize = 32.sp, fontWeight = FontWeight.Normal, - modifier = Modifier.padding(end = 18.dp) + modifier = Modifier.padding(end = 18.dp), ) } @@ -275,24 +301,25 @@ fun RecordDetailScreen( // 🔹 헤더 Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically, ) { Text( "시작", fontWeight = FontWeight.SemiBold, color = Color.Gray, fontSize = 15.sp, - modifier = Modifier.width(90.dp) + modifier = Modifier.width(90.dp), ) Text( "종료", fontWeight = FontWeight.SemiBold, color = Color.Gray, fontSize = 15.sp, - modifier = Modifier.width(90.dp) + modifier = Modifier.width(90.dp), ) Text( "집중 시간", @@ -305,17 +332,19 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(6.dp)) // 🔹 예시 데이터 행 - val focusList = listOf( - Triple("13:00:00 ~", "13:10:00", "00:10:00"), - Triple("14:00:00 ~", "14:10:00", "00:10:00") - ) + val focusList = + listOf( + Triple("13:00:00 ~", "13:10:00", "00:10:00"), + Triple("14:00:00 ~", "14:10:00", "00:10:00"), + ) focusList.forEach { (start, end, duration) -> Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically, ) { Text(text = start, fontSize = 16.sp, modifier = Modifier.width(90.dp)) Text(text = end, fontSize = 16.sp, modifier = Modifier.width(90.dp)) @@ -335,6 +364,6 @@ fun PreviewRecordDetailScreen() { RecordDetailScreen( navController = navController, recordId = "1", - timerViewModel = dummyViewModel + timerViewModel = dummyViewModel, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt index be64014..2daa0d4 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt @@ -29,14 +29,14 @@ fun TimerProgress( elapsedSeconds: Long, maxDisplaySeconds: Long = 60 * 60L, modifier: Modifier = Modifier, - progressColor: Color = Color(0xFFB8E9BB) + progressColor: Color = Color(0xFFB8E9BB), ) { val density = LocalDensity.current val remainingSeconds = (totalSeconds - elapsedSeconds).coerceAtLeast(0L) val remainingRatio = (remainingSeconds.toFloat() / maxDisplaySeconds.toFloat()).coerceIn(0f, 1f) val animatedProgress by animateFloatAsState( targetValue = remainingRatio, - animationSpec = tween(300, easing = LinearEasing) + animationSpec = tween(300, easing = LinearEasing), ) Canvas(modifier = modifier.aspectRatio(1f)) { @@ -51,7 +51,7 @@ fun TimerProgress( startAngle = -90f, sweepAngle = 360f * animatedProgress, useCenter = true, - style = Fill + style = Fill, ) // 물방울 @@ -61,13 +61,14 @@ fun TimerProgress( // 뿔 val hornWidthPx = 23.dp.toPx() val hornHeightPx = 25.dp.toPx() - val path = Path().apply { - val tipY = center.y - dropRadiusPx - hornHeightPx / 2 - moveTo(center.x, tipY) - lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - close() - } + val path = + Path().apply { + val tipY = center.y - dropRadiusPx - hornHeightPx / 2 + moveTo(center.x, tipY) + lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + close() + } drawPath(path, color = Color.White) } } @@ -91,8 +92,8 @@ fun TimerProgressDemo() { @Preview( showBackground = true, name = "45분 시작 - 실시간 감소", - widthDp = 500, // 그대로 - heightDp = 500 // 그대로 + widthDp = 500, // 그대로 + heightDp = 500, // 그대로 ) @Composable fun Preview45MinInteractive() { @@ -110,6 +111,6 @@ fun Preview45MinInteractive() { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.size(190.dp) + modifier = Modifier.size(190.dp), ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index 757f996..8268bfe 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -2,10 +2,30 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -28,26 +48,28 @@ fun TimerRunningScreen( totalSeconds: Long, ) { // 🔹 상태 변수들 - var elapsed by remember { mutableStateOf(0L) } // 경과 시간 - var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 + var elapsed by remember { mutableStateOf(0L) } // 경과 시간 + var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 // 🔹 남은시간 / 집중시간 계산 val remainingSeconds = (totalSeconds - elapsed).coerceAtLeast(0L) val focusSeconds = totalSeconds - remainingSeconds // ⏰ 시/분/초 포맷 - val timeText = String.format( - "%02d:%02d:%02d", - remainingSeconds / 3600, - (remainingSeconds % 3600) / 60, - remainingSeconds % 60 - ) - val focusTimeText = String.format( - "%02d:%02d:%02d", - focusSeconds / 3600, - (focusSeconds % 3600) / 60, - focusSeconds % 60 - ) + val timeText = + String.format( + "%02d:%02d:%02d", + remainingSeconds / 3600, + (remainingSeconds % 3600) / 60, + remainingSeconds % 60, + ) + val focusTimeText = + String.format( + "%02d:%02d:%02d", + focusSeconds / 3600, + (focusSeconds % 3600) / 60, + focusSeconds % 60, + ) // ⏱ 타이머 동작 제어 LaunchedEffect(isRunning) { @@ -61,28 +83,31 @@ fun TimerRunningScreen( val isFinished = elapsed >= totalSeconds Surface( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - color = Color.White + modifier = + Modifier + .fillMaxSize() + .background(Color.White), + color = Color.White, ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { // 🔹 상단 바 Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = Modifier - .size(28.dp) - .clickable { navController.popBackStack() } + modifier = + Modifier + .size(28.dp) + .clickable { navController.popBackStack() }, ) } @@ -91,12 +116,13 @@ fun TimerRunningScreen( // 🔹 타이머명 + 색상 표시 Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, ) { Box( - modifier = Modifier - .size(16.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(16.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(8.dp)) Text(title, fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -105,41 +131,44 @@ fun TimerRunningScreen( Spacer(modifier = Modifier.height(24.dp)) // 🔹 원형 타이머 색상 - val progressColor = when { - isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 - isRunning -> color - else -> Color(0xFF6D6D6D) - } + val progressColor = + when { + isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 + isRunning -> color + else -> Color(0xFF6D6D6D) + } Box( - modifier = Modifier - .size(220.dp) - .aspectRatio(1f) - .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(220.dp) + .aspectRatio(1f) + .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center, ) { TimerProgress( totalSeconds = totalSeconds, elapsedSeconds = elapsed, modifier = Modifier.fillMaxSize(), - progressColor = progressColor + progressColor = progressColor, ) } Spacer(modifier = Modifier.height(24.dp)) // 🔹 상태별 멘트 변경 - val motivationText = when { - isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" - isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" - else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" - } + val motivationText = + when { + isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" + isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" + else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" + } Text( motivationText, textAlign = TextAlign.Center, color = if (isFinished) Color(0xFF4CAF50) else Color.Gray, // 완료 시 초록색 느낌 - fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal + fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal, ) Spacer(modifier = Modifier.height(24.dp)) @@ -169,7 +198,7 @@ fun TimerRunningScreen( } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text( when { @@ -177,7 +206,7 @@ fun TimerRunningScreen( isRunning -> "⏸ 일시 정지" else -> "▶ 실행" }, - color = Color.Black + color = Color.Black, ) } @@ -188,7 +217,7 @@ fun TimerRunningScreen( isRunning = false }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text("🔄 초기화", color = Color.Black) } @@ -204,6 +233,6 @@ fun TimerRunningPreview() { navController = rememberNavController(), color = Color(0xFFFF8686), title = "자바스크립트", - totalSeconds = 60 * 60L + totalSeconds = 60 * 60L, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 814844a..bb34425 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,13 +1,33 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +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.aspectRatio +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.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,30 +56,35 @@ fun TimerScreen( hour: Int = 0, minute: Int = 0, second: Int = 0, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val timerList by remember { derivedStateOf { timerViewModel.timers } } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp - val circleSize = when { - screenWidth < 360.dp -> 150.dp - screenWidth < 600.dp -> 190.dp - else -> 250.dp - } + val circleSize = + when { + screenWidth < 360.dp -> 150.dp + screenWidth < 600.dp -> 190.dp + else -> 250.dp + } var elapsed by remember { mutableStateOf(0L) } var selectedTab by remember { mutableStateOf(0) } - val selectedColor = remember(colorValue) { - try { - if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) - else Color(colorValue.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) + val selectedColor = + remember(colorValue) { + try { + if (colorValue == "0" || colorValue.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(colorValue.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } } - } val total = (hour * 3600L) + (minute * 60L) + second val remainingSeconds = (total - elapsed).coerceAtLeast(0L) @@ -70,37 +95,40 @@ fun TimerScreen( Surface( modifier = Modifier.fillMaxSize(), - color = Color.Transparent + color = Color.Transparent, ) { Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) + modifier = + Modifier + .fillMaxSize() + .background(Color.White), ) { Column( modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { // ✅ 상단 타이머 영역 Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.45f) - .background(Color(0xFFF5F5F5)), - contentAlignment = Alignment.Center + modifier = + Modifier + .fillMaxWidth() + .weight(0.45f) + .background(Color(0xFFF5F5F5)), + contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier - .size(circleSize) - .aspectRatio(1f) - .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(circleSize) + .aspectRatio(1f) + .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center, ) { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } @@ -111,40 +139,44 @@ fun TimerScreen( text = timeText, fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, fontWeight = FontWeight.Bold, - color = Color.Black + color = Color.Black, ) } } // ✅ 하단 영역 (탭 전환) Surface( - modifier = Modifier - .fillMaxWidth() - .weight(0.55f), - color = Color.White + modifier = + Modifier + .fillMaxWidth() + .weight(0.55f), + color = Color.White, ) { Column { // 🔹 탭 바 Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 41.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 41.dp), ) { TabRow( selectedTabIndex = selectedTab, containerColor = Color.Transparent, divider = { }, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), indicator = { tabPositions -> Box( - modifier = Modifier - .tabIndicatorOffset(tabPositions[selectedTab]) - .height(2.dp) - .background(Color.Black) + modifier = + Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black), ) - } + }, ) { Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text( @@ -152,7 +184,7 @@ fun TimerScreen( fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 0) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) + modifier = Modifier.padding(vertical = 12.dp), ) } Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { @@ -161,7 +193,7 @@ fun TimerScreen( fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 1) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) + modifier = Modifier.padding(vertical = 12.dp), ) } } @@ -174,26 +206,29 @@ fun TimerScreen( if (selectedTab == 0) { // ✅ 타이머 탭 LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 80.dp) + modifier = + Modifier + .fillMaxSize() + .padding(bottom = 80.dp), ) { items(timerList) { timer -> SwipeableStudyItem( color = timer.color, title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), + time = + String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second, + ), onEdit = { val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") val colorValueStr = timer.color.value.toString() navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}", ) }, onDelete = { timerViewModel.deleteTimer(timer.id) }, @@ -204,9 +239,9 @@ fun TimerScreen( val total = timer.hour * 3600 + timer.minute * 60 + timer.second navController.navigate( - "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" + "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total", ) - } + }, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -217,40 +252,46 @@ fun TimerScreen( onClick = { timerViewModel.addTimer( TimerItemData( - id = java.util.UUID.randomUUID().toString(), + id = + java.util.UUID + .randomUUID() + .toString(), title = "새 타이머", color = Color(0xFFD9D9D9), hour = 0, minute = 0, - second = 0 - ) + second = 0, + ), ) }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(24.dp) + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(24.dp), ) { Icon( painter = painterResource(id = R.drawable.ic_add_button), contentDescription = "추가", tint = Color.Unspecified, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(32.dp), ) } } else if (selectedTab == 1) { // ✅ 기록 탭 LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), ) { items(timerViewModel.records) { record -> // 🔹 안전한 Color 변환 (핵심 수정) - val recordColor: Color = when (val c = record.color) { - is Color -> c - is Int -> Color(c) - else -> Color(0xFFD9D9D9) - } + val recordColor: Color = + when (val c = record.color) { + is Color -> c + is Int -> Color(c) + else -> Color(0xFFD9D9D9) + } val encodedTitle = URLEncoder.encode(record.title, "UTF-8") val colorValueStr = recordColor.value.toString() @@ -262,9 +303,9 @@ fun TimerScreen( dayCounts = record.dayCounts, onClick = { navController.navigate( - "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr" + "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr", ) - } + }, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -286,7 +327,7 @@ fun TimerScreenPreview() { MaterialTheme { TimerScreen( navController = rememberNavController(), - timerViewModel = dummyViewModel + timerViewModel = dummyViewModel, ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 2ce675c..a19db57 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -1,5 +1,6 @@ package com.sprinter.studyspot.timer.ui +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -54,7 +55,7 @@ private fun TimeUnitField( value: String, onValueChange: (String) -> Unit, maxLength: Int, - maxValue: Int + maxValue: Int, ) { var internalValue by remember(value) { mutableStateOf(value) } @@ -74,47 +75,54 @@ private fun TimeUnitField( onValueChange(digits) } }, - modifier = Modifier - .width(64.dp) - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) - .border( - width = if (isError) 2.dp else 0.dp, - color = if (isError) Color.Red else Color.Transparent, - shape = RoundedCornerShape(30.dp) + modifier = + Modifier + .width(64.dp) + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) + .border( + width = if (isError) 2.dp else 0.dp, + color = if (isError) Color.Red else Color.Transparent, + shape = RoundedCornerShape(30.dp), + ), + textStyle = + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = if (isError) Color.Red else Color.Black, + textAlign = TextAlign.Center, ), - textStyle = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = if (isError) Color.Red else Color.Black, - textAlign = TextAlign.Center - ), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), visualTransformation = { text -> val padded = text.text.padStart(2, '0').take(2) TransformedText( - text = androidx.compose.ui.text.AnnotatedString(padded), - offsetMapping = object : OffsetMapping { - override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) - override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) - } + text = + androidx.compose.ui.text + .AnnotatedString(padded), + offsetMapping = + object : OffsetMapping { + override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) + + override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) + }, ) }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ), + colors = + TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ), placeholder = { Text( "00", color = Color.Gray, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) - } + }, ) } @@ -128,7 +136,7 @@ fun TimerSettingScreen( initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, - timerId: String? = null + timerId: String? = null, ) { var selectedColor by remember { mutableStateOf(initialColor) } var timerTitle by remember { mutableStateOf(initialTitle) } @@ -144,23 +152,26 @@ fun TimerSettingScreen( Surface(modifier = Modifier.fillMaxSize(), color = Color.White) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, start = 11.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 11.dp), + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = Modifier - .size(28.dp) - .clickable { navController?.popBackStack() } + modifier = + Modifier + .size(28.dp) + .clickable { navController?.popBackStack() }, ) } @@ -170,28 +181,34 @@ fun TimerSettingScreen( fontSize = 14.sp, color = Color.DarkGray, textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.height(28.dp)) // ✅ 색상 선택 - val colors = listOf( - Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), - Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) - ) + val colors = + listOf( + Color(0xFFB8E9BB), + Color(0xFFFFDB86), + Color(0xFFFF8686), + Color(0xFFF9C0EB), + Color(0xFFD2ACAC), + Color(0xFFD9D9D9), + Color(0xFFAECDE5), + ) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { colors.forEach { color -> Box( - modifier = Modifier - .size(27.dp, 26.dp) - .background(color, RoundedCornerShape(8.dp)) - .border( - width = if (selectedColor == color) 3.dp else 0.dp, - color = if (selectedColor == color) Color.Black else Color.Transparent, - shape = RoundedCornerShape(8.dp) - ) - .clickable { selectedColor = color } + modifier = + Modifier + .size(27.dp, 26.dp) + .background(color, RoundedCornerShape(8.dp)) + .border( + width = if (selectedColor == color) 3.dp else 0.dp, + color = if (selectedColor == color) Color.Black else Color.Transparent, + shape = RoundedCornerShape(8.dp), + ).clickable { selectedColor = color }, ) } } @@ -204,21 +221,24 @@ fun TimerSettingScreen( onValueChange = { timerTitle = it }, shape = RoundedCornerShape(30.dp), singleLine = true, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), + modifier = + Modifier + .fillMaxWidth() + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 - ), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ) + keyboardOptions = + KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 + ), + colors = + TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ), ) Spacer(modifier = Modifier.height(32.dp)) @@ -226,13 +246,13 @@ fun TimerSettingScreen( Spacer(modifier = Modifier.height(12.dp)) Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { TimeUnitField( value = hourStr, onValueChange = { hourStr = it }, maxLength = 2, - maxValue = 99 + maxValue = 99, ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -241,7 +261,7 @@ fun TimerSettingScreen( value = minuteStr, onValueChange = { minuteStr = it }, maxLength = 2, - maxValue = 59 + maxValue = 59, ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -250,50 +270,53 @@ fun TimerSettingScreen( value = secondStr, onValueChange = { secondStr = it }, maxLength = 2, - maxValue = 59 + maxValue = 59, ) } Spacer(modifier = Modifier.height(30.dp)) Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 16.dp), - contentAlignment = Alignment.CenterEnd + modifier = + Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd, ) { Surface( - modifier = Modifier - .size(77.dp, 41.dp) - .clickable(enabled = isValid) { - val safeTitle = timerTitle.ifBlank { "기본 타이머" } - val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") - val colorValue = selectedColor?.value?.toString() ?: "0" + modifier = + Modifier + .size(77.dp, 41.dp) + .clickable(enabled = isValid) { + val safeTitle = timerTitle.ifBlank { "기본 타이머" } + val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") + val colorValue = selectedColor?.value?.toString() ?: "0" - val updatedTimer = TimerItemData( - id = timerId ?: UUID.randomUUID().toString(), - title = safeTitle, - color = selectedColor ?: Color(0xFFD9D9D9), - hour = hour, - minute = minute, - second = second - ) - timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 + val updatedTimer = + TimerItemData( + id = timerId ?: UUID.randomUUID().toString(), + title = safeTitle, + color = selectedColor ?: Color(0xFFD9D9D9), + hour = hour, + minute = minute, + second = second, + ) + timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 - navController?.navigate( - "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" - ) { - popUpTo("timerSetting") { inclusive = true } - } - }, + navController?.navigate( + "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second", + ) { + popUpTo("timerSetting") { inclusive = true } + } + }, shape = RoundedCornerShape(8.dp), - color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) + color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0), ) { Box(contentAlignment = Alignment.Center) { Icon( painter = painterResource(id = R.drawable.ic_confirm_check), contentDescription = "완료", tint = if (isValid) Color.Unspecified else Color.Gray, - modifier = Modifier.size(48.dp) + modifier = Modifier.size(48.dp), ) } } @@ -302,6 +325,7 @@ fun TimerSettingScreen( } } +@SuppressLint("ViewModelConstructorInComposable") @Preview(showBackground = true) @Composable fun TimerSettingPreview() { diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt index 3055fa8..f518145 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt @@ -2,7 +2,15 @@ package com.sprinter.studyspot.timer.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,36 +28,37 @@ import com.sprinter.studyspot.R @Composable fun BottomNavBar( navController: androidx.navigation.NavController, - currentRoute: String + currentRoute: String, ) { Row( - modifier = Modifier - .fillMaxWidth() - .height(76.dp) - .background(Color.White) - .padding(horizontal = 36.dp), + modifier = + Modifier + .fillMaxWidth() + .height(76.dp) + .background(Color.White) + .padding(horizontal = 36.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, ) { BottomNavItem( icon = R.drawable.ic_clock, label = "타이머", isSelected = currentRoute == "timer", - onClick = { navController.navigate("timer") } + onClick = { navController.navigate("timer") }, ) BottomNavItem( icon = R.drawable.ic_home, label = "홈", isSelected = currentRoute == "home", - onClick = { navController.navigate("home") } + onClick = { navController.navigate("home") }, ) BottomNavItem( icon = R.drawable.ic_search, label = "검색", isSelected = currentRoute == "search", - onClick = { navController.navigate("search") } + onClick = { navController.navigate("search") }, ) } } @@ -59,26 +68,27 @@ private fun BottomNavItem( icon: Int, label: String, isSelected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, ) { Column( - modifier = Modifier - .width(60.dp) - .clickable(onClick = onClick), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .width(60.dp) + .clickable(onClick = onClick), + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( painter = painterResource(id = icon), contentDescription = label, modifier = Modifier.size(20.dp), - tint = Color.Black + tint = Color.Black, ) Spacer(modifier = Modifier.height(4.dp)) Text( text = label, fontSize = 12.sp, color = Color.Black, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -88,6 +98,6 @@ private fun BottomNavItem( fun BottomNavBarPreview() { BottomNavBar( navController = rememberNavController(), - currentRoute = "home" + currentRoute = "home", ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt index edbc887..e1ddc70 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt @@ -7,5 +7,5 @@ data class RecordData( val title: String, val color: Color, val totalTime: String, - val dayCounts: Map -) \ No newline at end of file + val dayCounts: Map, +) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index 5f0bf9c..f460419 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -32,18 +32,19 @@ fun RecordItemCard( title: String, totalTime: String, dayCounts: Map, // 요일별 학습 횟수 - onClick: (() -> Unit)? = null + onClick: (() -> Unit)? = null, ) { // 최대 횟수 대비 비율 계산 val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) // ✅ 잔디 색상 팔레트 (GitHub 느낌) - val grassPalette = listOf( - Color(0xFFE8F5E9), // 연한 녹색 - Color(0xFFC8E6C9), - Color(0xFF81C784), - Color(0xFF388E3C) // 진한 녹색 - ) + val grassPalette = + listOf( + Color(0xFFE8F5E9), // 연한 녹색 + Color(0xFFC8E6C9), + Color(0xFF81C784), + Color(0xFF388E3C), // 진한 녹색 + ) // ✅ 반응형 기기 크기 계산 val configuration = LocalConfiguration.current @@ -56,29 +57,32 @@ fun RecordItemCard( val boxPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) Surface( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - .clickable(enabled = onClick != null) { onClick?.invoke() }, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), - color = Color(0xFFF5F5F5) + color = Color(0xFFF5F5F5), ) { Box( - modifier = Modifier - .fillMaxWidth() - .padding(cardPadding) + modifier = + Modifier + .fillMaxWidth() + .padding(cardPadding), ) { // ✅ 왼쪽: 타이틀 + 요일 Column( - modifier = Modifier.align(Alignment.CenterStart) + modifier = Modifier.align(Alignment.CenterStart), ) { // 상단 타이틀 Row(verticalAlignment = Alignment.CenterVertically) { // 🔹 타이머별 고유 색상 원 Box( - modifier = Modifier - .size(26.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(26.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(12.dp)) Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold) @@ -93,32 +97,34 @@ fun RecordItemCard( days.forEachIndexed { index, day -> val count = dayCounts[index + 1] ?: 0 val ratio = count.toFloat() / maxCount.toFloat() - val colorIndex = when { - ratio == 0f -> -1 - ratio < 0.25f -> 0 - ratio < 0.5f -> 1 - ratio < 0.75f -> 2 - else -> 3 - } + val colorIndex = + when { + ratio == 0f -> -1 + ratio < 0.25f -> 0 + ratio < 0.5f -> 1 + ratio < 0.75f -> 2 + else -> 3 + } val grassColor = if (colorIndex == -1) Color(0xFFE0E0E0) else grassPalette[colorIndex] Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier - .size(26.dp) - .background( - color = grassColor, - shape = RoundedCornerShape(8.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(26.dp) + .background( + color = grassColor, + shape = RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.Center, ) {} Spacer(modifier = Modifier.height(4.dp)) Text( text = day, fontSize = 12.sp, - color = if (count > 0) Color.Black else Color.Gray + color = if (count > 0) Color.Black else Color.Gray, ) } } @@ -127,18 +133,19 @@ fun RecordItemCard( // ✅ 오른쪽: 중앙 정렬된 타이머 박스 (반응형) Box( - modifier = Modifier - .align(Alignment.CenterEnd) - .height(boxHeight) - .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) - .padding(horizontal = boxPadding), - contentAlignment = Alignment.Center + modifier = + Modifier + .align(Alignment.CenterEnd) + .height(boxHeight) + .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) + .padding(horizontal = boxPadding), + contentAlignment = Alignment.Center, ) { Text( text = totalTime, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = Color.Black + color = Color.Black, ) } } @@ -154,21 +161,21 @@ fun RecordItemCardPreview() { color = Color(0xFFFFFFFF), // 타이머 색상 title = "전체 시간", totalTime = "00:45", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFF8686), title = "자바스크립트", totalTime = "00:45", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFFDB86), title = "파이썬", totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt index be00561..95e61ea 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt @@ -22,36 +22,43 @@ import androidx.compose.ui.unit.sp // StudyItem (그대로 유지) @Composable -fun StudyItem(color: Color, title: String, time: String, onClick: () -> Unit) { +fun StudyItem( + color: Color, + title: String, + time: String, + onClick: () -> Unit, +) { val isCompact = LocalConfiguration.current.screenWidthDp < 360 Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 28.dp) - .padding(vertical = if (isCompact) 6.dp else 8.dp) - .clickable { onClick() } + modifier = + Modifier + .fillMaxWidth() + .padding(start = 28.dp) + .padding(vertical = if (isCompact) 6.dp else 8.dp) + .clickable { onClick() }, ) { Box( - modifier = Modifier - .size(if (isCompact) 31.dp else 33.dp) - .background(color, CircleShape) + modifier = + Modifier + .size(if (isCompact) 31.dp else 33.dp) + .background(color, CircleShape), ) Spacer(modifier = Modifier.width(if (isCompact) 8.dp else 12.dp)) Text( title, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 14.sp else 16.sp, - color = Color.Black + color = Color.Black, ) Spacer(modifier = Modifier.weight(1f)) Text( time, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 30.sp else 32.sp, - color = Color.Gray + color = Color.Gray, ) Spacer(modifier = Modifier.width(34.dp)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt index bd2ec06..f77a6b9 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt @@ -8,18 +8,31 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -41,23 +54,24 @@ fun SwipeableStudyItem( time: String, onEdit: () -> Unit, onDelete: () -> Unit, - onClick: () -> Unit + onClick: () -> Unit, ) { val density = LocalDensity.current var widthPx by remember { mutableStateOf(0f) } val decay = rememberSplineBasedDecay() - val state = remember { - AnchoredDraggableState( - initialValue = SwipeState.Closed, - positionalThreshold = { distance -> distance * 0.5f }, - velocityThreshold = { with(density) { 100.dp.toPx() } }, - snapAnimationSpec = tween(), - decayAnimationSpec = decay, - confirmValueChange = { true } - ) - } + val state = + remember { + AnchoredDraggableState( + initialValue = SwipeState.Closed, + positionalThreshold = { distance -> distance * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = tween(), + decayAnimationSpec = decay, + confirmValueChange = { true }, + ) + } // 앵커 업데이트: Swiped = 오른쪽으로 40% 이동 LaunchedEffect(widthPx) { @@ -65,8 +79,8 @@ fun SwipeableStudyItem( state.updateAnchors( DraggableAnchors { SwipeState.Closed at 0f - SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 - } + SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 + }, ) } } @@ -78,94 +92,99 @@ fun SwipeableStudyItem( if (current.isNaN()) return@derivedStateOf 0f val swipedPos = state.anchors.positionOf(SwipeState.Swiped) ?: 0f - current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) + current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) } } Box( - modifier = Modifier - .fillMaxWidth() - .height(64.dp) - .onGloballyPositioned { coordinates -> - widthPx = coordinates.size.width.toFloat() - } + modifier = + Modifier + .fillMaxWidth() + .height(64.dp) + .onGloballyPositioned { coordinates -> + widthPx = coordinates.size.width.toFloat() + }, ) { // 배경 버튼 (왼쪽에 고정) Row( - modifier = Modifier - .matchParentSize() - .background(Color(0xFFF0F0F0)) - .padding(horizontal = 24.dp), - horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .matchParentSize() + .background(Color(0xFFF0F0F0)) + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_edit), contentDescription = "Edit", tint = Color.Black, - modifier = Modifier - .size(24.dp) - .clickable { onEdit() } + modifier = + Modifier + .size(24.dp) + .clickable { onEdit() }, ) Spacer(modifier = Modifier.width(24.dp)) Icon( painter = painterResource(id = R.drawable.ic_delete), contentDescription = "Delete", tint = Color.Black, - modifier = Modifier - .size(24.dp) - .clickable { onDelete() } + modifier = + Modifier + .size(24.dp) + .clickable { onDelete() }, ) } // 앞쪽 카드: 오른쪽으로 스와이프 Surface( color = Color.White, - modifier = Modifier - .offset { - if (widthPx <= 0f) { - IntOffset(0, 0) - } else { - IntOffset(safeOffsetX.roundToInt(), 0) - } - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal - ) - .background(Color.White) - .clickable( - enabled = state.currentValue == SwipeState.Closed, - onClick = onClick - ) + modifier = + Modifier + .offset { + if (widthPx <= 0f) { + IntOffset(0, 0) + } else { + IntOffset(safeOffsetX.roundToInt(), 0) + } + }.anchoredDraggable( + state = state, + orientation = Orientation.Horizontal, + ).background(Color.White) + .clickable( + enabled = state.currentValue == SwipeState.Closed, + onClick = onClick, + ), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 28.dp, end = 16.dp) - .height(64.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(start = 28.dp, end = 16.dp) + .height(64.dp), ) { Box( - modifier = Modifier - .size(33.dp) - .background(color, CircleShape) + modifier = + Modifier + .size(33.dp) + .background(color, CircleShape), ) Spacer(modifier = Modifier.width(12.dp)) Text( text = title, fontWeight = FontWeight.Medium, fontSize = 16.sp, - color = Color.Black + color = Color.Black, ) Spacer(modifier = Modifier.weight(1f)) Text( text = time, fontWeight = FontWeight.Medium, fontSize = 28.sp, - color = Color.Gray + color = Color.Gray, ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt index 9d8b944..e92a093 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt @@ -9,5 +9,5 @@ data class TimerItemData( val color: Color, val hour: Int, val minute: Int, - val second: Int + val second: Int, ) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index dff5d29..a980379 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -11,29 +11,30 @@ class TimerViewModel : ViewModel() { val timers: List get() = _timers // ✅ 기록 데이터 - val records = listOf( - RecordData( - id = "0", - title = "전체 시간", - color = Color(0xFFFFFFFF), - totalTime = "02:10", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) - ), - RecordData( - id = "1", - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTime = "01:20", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) - ), - RecordData( - id = "2", - title = "파이썬", - color = Color(0xFFFFE382), - totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + val records = + listOf( + RecordData( + id = "0", + title = "전체 시간", + color = Color(0xFFFFFFFF), + totalTime = "02:10", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), + ), + RecordData( + id = "1", + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), + ), + RecordData( + id = "2", + title = "파이썬", + color = Color(0xFFFFE382), + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), + ), ) - ) fun addTimer(timer: TimerItemData) { _timers.add(timer) @@ -50,4 +51,4 @@ class TimerViewModel : ViewModel() { // ✅ recordId로 RecordData 가져오기 fun getRecordById(id: String): RecordData? = records.find { it.id == id } -} \ No newline at end of file +} From 561f262cc306d4d18cb8bf2da0f3c0ecb4ac41f2 Mon Sep 17 00:00:00 2001 From: MyKnow Date: Thu, 13 Nov 2025 05:40:11 +0900 Subject: [PATCH 23/44] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kotlin.test (1.9.0) - junit.jupiter.api 추가 (6.0.1) - junit.jupiter.params 추가 (6.0.1) - junit.jupiter.engine 추가 (6.0.1) - mockk 추가 (1.14.6) --- app/build.gradle.kts | 6 ++++++ gradle/libs.versions.toml | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01efa91..ca57523 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,12 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.3") testImplementation(libs.junit) + testImplementation(libs.kotlin.test) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockk) + testRuntimeOnly(libs.junit.jupiter.engine) + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 236801c..4815f00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,18 @@ [versions] agp = "8.12.3" +junitJupiterApi = "6.0.1" +junitJupiterEngine = "6.0.1" +junitJupiterParams = "6.0.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" +kotlinTest = "1.9.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.8.0" composeBom = "2024.09.00" +mockk = "1.14.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +29,11 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 07e540a4e0d1bd250440cc0d7e2c2c3288e99ddc Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 15 Nov 2025 10:44:29 +0900 Subject: [PATCH 24/44] =?UTF-8?q?chore:=20unit=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Junit=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01efa91..116682b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,9 @@ dependencies { // 네비게이션 바를 위한 의존성 추가 implementation("androidx.navigation:navigation-compose:2.8.3") + // Test를 위한 JUnit 의존성 추가 + testImplementation("junit:junit:4.13.2") + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) From be515f68cef52603c9bc186e7f4afe5869040a3c Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sat, 15 Nov 2025 11:15:21 +0900 Subject: [PATCH 25/44] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20Ju?= =?UTF-8?q?nit=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 116682b..01efa91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,9 +54,6 @@ dependencies { // 네비게이션 바를 위한 의존성 추가 implementation("androidx.navigation:navigation-compose:2.8.3") - // Test를 위한 JUnit 의존성 추가 - testImplementation("junit:junit:4.13.2") - testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) From 907aa4898c3423b97fb8a01d3cbf5c7fb3f77ad2 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 18:57:57 +0900 Subject: [PATCH 26/44] =?UTF-8?q?test:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TimerScreen 기본 UI 렌더링 테스트 추가 - RecordTabNavigationTest 추가 (기록 탭 전환 및 카드 표시 검증) - TimerRunningScreenTest 추가 (타이머 실행 화면 UI 요소 검증) - 테스트용 ViewModel 세팅 및 mock 데이터 구성 - 테스트 안정화를 위한 구조 조정 (testTag, 파라미터 정리 등) --- .../RecordDetailScreenCalendarTest.kt | 63 ++++++++ .../studyspot/RecordDetailScreenUITest.kt | 70 +++++++++ .../studyspot/RecordFlowNavigationTest.kt | 85 +++++++++++ .../sprinter/studyspot/RecordItemCardTest.kt | 55 +++++++ .../studyspot/RecordItemCardUITest.kt | 59 ++++++++ .../studyspot/RecordNavigationTest.kt | 84 +++++++++++ .../studyspot/RecordTabNavigationTest.kt | 70 +++++++++ .../studyspot/TimerProgressComposableTest.kt | 44 ++++++ .../studyspot/TimerRecordDetailBackTest.kt | 65 ++++++++ .../TimerRecordDetailNavigationTest.kt | 50 +++++++ .../studyspot/TimerRunningScreenTest.kt | 47 ++++++ .../studyspot/TimerScreenNavigationTest.kt | 53 +++++++ .../studyspot/TimerScreenRecordTabTest.kt | 39 +++++ .../com/sprinter/studyspot/TimerScreenTest.kt | 50 +++++++ .../studyspot/TimerSettingScreenInputTest.kt | 38 +++++ .../viewmodel/TimerViewModelRecordTest.kt | 45 ++++++ .../timer/viewmodel/TimerViewModelTest.kt | 140 ++++++++++++++++++ 17 files changed, 1057 insertions(+) create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt create mode 100644 app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt create mode 100644 app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt create mode 100644 app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt new file mode 100644 index 0000000..24d3e39 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt @@ -0,0 +1,63 @@ +package com.sprinter.studyspot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.ui.RecordDetailScreen +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * RecordDetailScreen 내부의 달력 UI 동작 테스트 + * 요일 표시, 5주 그리드 생성, 날짜 정렬 여부 확인 + * 공부한 날짜 강조 및 날짜 선택 기능 검증 + **/ +class RecordDetailScreenCalendarTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun calendar_dayClick_selectsStudyDay() { + composeRule.setContent { + RecordDetailScreen( + navController = rememberNavController(), + recordId = "1", + timerViewModel = TimerViewModel() + ) + } + + // 4일은 studyDaySet = setOf(4, 6, 12, 13, 19)에 포함됨 + composeRule.onNodeWithContentDescription("day-4").assertExists() + + // 클릭 + composeRule.onNodeWithContentDescription("day-4").performClick() + + // 선택되면 색상이 FF8686 (선택 색) + composeRule.onNodeWithContentDescription("day-4") + .assert(SemanticsMatcher.expectValue( + SemanticsProperties.ContentDescription, + listOf("day-4") + )) + } + + @Test + fun monthNavigation_buttonsWork() { + composeRule.setContent { + RecordDetailScreen( + navController = rememberNavController(), + recordId = "1", + timerViewModel = TimerViewModel() + ) + } + + // → 버튼 클릭 (다음 달) + composeRule.onNodeWithContentDescription("Next Month").performClick() + + // ← 버튼 클릭 (이전 달) + composeRule.onNodeWithContentDescription("Prev Month").performClick() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt new file mode 100644 index 0000000..16b871c --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt @@ -0,0 +1,70 @@ +package com.sprinter.studyspot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.ui.RecordDetailScreen +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * RecordDetailScreen의 전체 UI 구성 요소가 정상 렌더링되는지 확인 + * 제목, 색상 배지, 달력 UI, 월 이동 버튼, 일별 기록 리스트 등의 UI 체크 + */ +class RecordDetailScreenUITest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun recordDetailScreen_displaysTitleAndHeaders() { + // 🔸 테스트용 ViewModel (records 더미 데이터 포함) + val timerViewModel = TimerViewModel() + + composeRule.setContent { + val navController = rememberNavController() + + RecordDetailScreen( + navController = navController, + recordId = "1", // TimerViewModel의 records에서 id = "1" → "자바스크립트" + title = null, // title은 ViewModel 데이터 사용 + color = null, // color도 ViewModel 데이터 사용 + timerViewModel = timerViewModel + ) + } + + // 🔹 제목이 잘 보이는지 + composeRule.onNodeWithText("자바스크립트").assertExists() + + // 🔹 헤더 텍스트가 잘 렌더링되는지 + composeRule.onNodeWithText("시작").assertExists() + composeRule.onNodeWithText("종료").assertExists() + composeRule.onNodeWithText("집중 시간").assertExists() + } + + @Test + fun backButton_isClickable() { + composeRule.setContent { + val navController = rememberNavController() + val timerViewModel = TimerViewModel() + + RecordDetailScreen( + navController = navController, + recordId = "1", + title = null, + color = null, + timerViewModel = timerViewModel + ) + } + + composeRule + .onNodeWithContentDescription("Back") + .assertExists() + .performClick() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt new file mode 100644 index 0000000..cd65318 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt @@ -0,0 +1,85 @@ +package com.sprinter.studyspot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.ui.RecordDetailScreen +import com.sprinter.studyspot.timer.ui.component.RecordItemCard +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen -> 기록 탭 -> 기록 상세 화면까지의 이동 흐름을 검증 + * 뒤로 가기 버튼 클릭 시 '기록' 탭 상태가 유지되는지 확인 + */ +class RecordFlowNavigationTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun clickingRecordItem_navigatesToDetailScreen() { + + composeRule.setContent { + + val navController = rememberNavController() + val timerViewModel = TimerViewModel() + + NavHost(navController = navController, startDestination = "list") { + + composable("list") { + RecordItemCard( + color = Color(0xFFFF8686), + title = "자바스크립트", + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 3), + onClick = { + navController.navigate( + "recordDetail/1?title=자바스크립트&color=FFFF8686" + ) + } + ) + } + + composable("recordDetail/{id}?title={title}&color={color}") { backStackEntry -> + + val id = backStackEntry.arguments?.getString("id") ?: "" + val title = backStackEntry.arguments?.getString("title") + val colorHex = backStackEntry.arguments?.getString("color") ?: "FFFF8686" + + // HEX string → Color 변환 + val color = try { + Color(colorHex.toLong(16)) + } catch (_: Exception) { + Color.Gray + } + + RecordDetailScreen( + navController = navController, + recordId = id, + title = title, + color = color, + timerViewModel = timerViewModel + ) + } + } + } + + // 1) 목록에서 "자바스크립트" 카드 클릭 + composeRule.onNodeWithText("자바스크립트").performClick() + + // 2) DetailScreen 제목 표시 확인 + composeRule.onNodeWithText("자바스크립트").assertExists() + + // 3) 헤더 텍스트 확인 (정상 렌더링 확인) + composeRule.onNodeWithText("시작").assertExists() + composeRule.onNodeWithText("종료").assertExists() + composeRule.onNodeWithText("집중 시간").assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt new file mode 100644 index 0000000..0fef0e7 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt @@ -0,0 +1,55 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.sprinter.studyspot.timer.ui.component.RecordItemCard +import org.junit.Rule +import org.junit.Test + +/** + * RecordItemCard 내부 데이터 로직 테스트 + * 색상 표시, 타이틀, 총 공부 시간, 요일별 dayCounts 로직 검증 + */ +class RecordItemCardTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun recordItemCard_rendersCorrectly() { + composeRule.setContent { + RecordItemCard( + color = Color(0xFFFF8686), + title = "파이썬", + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 2), + onClick = {} + ) + } + + composeRule.onNodeWithText("파이썬").assertExists() + composeRule.onNodeWithText("00:45").assertExists() + } + + @Test + fun recordItemCard_click_works() { + var clicked = false + + composeRule.setContent { + RecordItemCard( + color = Color(0xFFFF8686), + title = "파이썬", + totalTime = "00:45", + dayCounts = emptyMap(), + onClick = { clicked = true } + ) + } + + composeRule.onNodeWithText("파이썬").performClick() + + assert(clicked) + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt new file mode 100644 index 0000000..54f7180 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt @@ -0,0 +1,59 @@ +package com.sprinter.studyspot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.sprinter.studyspot.timer.ui.component.RecordItemCard +import org.junit.Rule +import org.junit.Test + +/** + * RecordItemCard UI 컴포저블의 실제 화면 렌더링 테스트 + * 클릭 동작, 레이아웃 배치, 색상 표시 등이 정상적으로 보이는지 검증 + */ +class RecordItemCardUITest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun recordItemCard_rendersCorrectly() { + + composeRule.setContent { + RecordItemCard( + color = Color(0xFFFF8686), + title = "자바스크립트", + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 3 to 1), + onClick = {} + ) + } + + // 타이틀 + composeRule.onNodeWithText("자바스크립트").assertExists() + + // 총 시간 표시 + composeRule.onNodeWithText("01:20").assertExists() + } + + @Test + fun recordItemCard_onClick_isTriggered() { + var clicked = false + + composeRule.setContent { + RecordItemCard( + color = Color(0xFFFF8686), + title = "테스트 카드", + totalTime = "00:30", + dayCounts = mapOf(), + onClick = { clicked = true } + ) + } + + composeRule.onNodeWithText("테스트 카드").performClick() + + assert(clicked) + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt new file mode 100644 index 0000000..2ffbbef --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt @@ -0,0 +1,84 @@ +package com.sprinter.studyspot + +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.ui.RecordDetailScreen +import com.sprinter.studyspot.timer.ui.component.RecordItemCard +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen에서 기록 탭 전환 및 기록 리스트 렌더링 검증 + * RecordItemCard가 정상적으로 표시되는지 확인 + */ +class RecordNavigationTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun clickingRecordItem_navigatesToDetailScreen() { + + composeRule.setContent { + + val navController = rememberNavController() + + // 🔥 테스트 환경에서 ViewModel 정상 생성 + val timerViewModel = ViewModelProvider( + composeRule.activity + )[TimerViewModel::class.java] + + NavHost(navController = navController, startDestination = "list") { + + composable("list") { + RecordItemCard( + color = Color(0xFFFF8686), + title = "자바스크립트", + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 3), + onClick = { + navController.navigate( + "recordDetail/1?title=자바스크립트&color=FFFF8686" + ) + } + ) + } + + composable("recordDetail/{id}?title={title}&color={color}") { backStackEntry -> + + val id = backStackEntry.arguments?.getString("id") ?: "" + val title = backStackEntry.arguments?.getString("title") ?: "" + val colorHex = backStackEntry.arguments?.getString("color") ?: "FF8686" + + val color = try { + Color(colorHex.toLong(16)) + } catch (_: Exception) { + Color.Gray + } + + RecordDetailScreen( + navController = navController, + recordId = id, + title = title, + color = color, + timerViewModel = timerViewModel // 🔥 수정된 ViewModel 전달 + ) + } + } + } + + // 목록 화면에서 → 클릭 + composeRule.onNodeWithText("자바스크립트").performClick() + + // 상세 화면 표시 확인 + composeRule.onNodeWithText("자바스크립트").assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt new file mode 100644 index 0000000..fc89568 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt @@ -0,0 +1,70 @@ +package com.sprinter.studyspot.record.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.ui.TimerScreen +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +class RecordTabNavigationTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun 기록탭_클릭시_기록화면으로_전환된다() { + + composeRule.setContent { + val navController = rememberNavController() + val viewModel = TimerViewModel() + + TimerScreen( + navController = navController, + timerViewModel = viewModel + ) + } + + // 기록 탭 클릭 + composeRule.onNodeWithText("기록") + .assertExists() + .performClick() + + // "총 집중 시간"이 기록 화면에 존재하는지 확인 + composeRule.onNodeWithText("총 집중 시간") + .assertExists() + } + + @Test + fun 기록탭_클릭하면_기록카드들이_표시된다() { + + composeRule.setContent { + val navController = rememberNavController() + val viewModel = TimerViewModel() + + // 더미 기록 추가 + viewModel.addRecordForTest( + title = "테스트 기록", + color = androidx.compose.ui.graphics.Color.Red, + totalTime = "00:20:00", + dayCounts = mapOf(1 to 1) + ) + + TimerScreen( + navController = navController, + timerViewModel = viewModel + ) + } + + // 기록 탭 클릭 + composeRule.onNodeWithText("기록") + .performClick() + + val nodes = composeRule.onAllNodes(hasTestTag("record_card")) + .fetchSemanticsNodes() + + assert(nodes.isNotEmpty()) + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt new file mode 100644 index 0000000..09174cc --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt @@ -0,0 +1,44 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen의 "타이머" ↔ "기록" 탭 전환을 테스트 + * ViewModel의 selectedTab 상태 유지 여부 검증 + */ +class TimerProgressComposableTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun timerProgress_rendersCanvas() { + composeRule.setContent { + TimerProgress( + totalSeconds = 3600L, // 1시간 + elapsedSeconds = 0L // 아직 경과 X + ) + } + + // Canvas가 정상적으로 렌더링되는지 체크 + composeRule.onNodeWithTag("TimerProgressCanvas").assertExists() + } + + @Test + fun timerProgress_updatesOnElapsedTimeChange() { + + composeRule.setContent { + TimerProgress( + totalSeconds = 3600L, + elapsedSeconds = 1800L // 절반 경과 + ) + } + + // 절반 진행이라도, 렌더링 존재 여부 확인 + composeRule.onNodeWithTag("TimerProgressCanvas").assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt new file mode 100644 index 0000000..52536fb --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt @@ -0,0 +1,65 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.* +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerProgress 컴포저블의 원형 진행바 그리기 로직 테스트 + * totalSeconds / elapsedSeconds 변화에 따른 UI 업데이트 검증 + */ +class TimerRecordDetailBackTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun recordDetail_back_returnsToRecordTab() { + val vm = TimerViewModel() + + composeTestRule.setContent { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = "timer" + ) { + composable("timer") { + TimerScreen( + navController = navController, + timerViewModel = vm, + modifier = Modifier.fillMaxSize() + ) + } + composable("recordDetail/{id}") { + RecordDetailScreen( + navController = navController + ) + } + } + } + + // 1) 기록 탭으로 이동 + composeTestRule.onNodeWithText("기록").performClick() + + // 2) 첫 번째 기록 카드 클릭 → 상세화면으로 이동 + val record = vm.records.first() + composeTestRule.onNodeWithText(record.title).performClick() + + // 상세화면에서 "뒤로가기" 버튼 클릭 + composeTestRule.onNodeWithContentDescription("뒤로가기").performClick() + + // 돌아왔을 때 "기록" 탭이 선택되어 있는지 확인 + composeTestRule.onNodeWithText("기록").assertExists() + + // 기록 탭 내 기록 리스트가 다시 보이는지 체크 + composeTestRule.onNodeWithText(record.title).assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt new file mode 100644 index 0000000..f824cda --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt @@ -0,0 +1,50 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.* +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * 기록 상세 화면에서 뒤로가기 버튼 클릭 시 TimerScreen의 ‘기록’ 탭이 유지되는지 검증 + */ +class TimerRecordDetailNavigationTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun clickRecordCard_navigatesToRecordDetailScreen() { + // GIVEN - Fake ViewModel 준비 + val vm = TimerViewModel() + + composeTestRule.setContent { + val navController = rememberNavController() + + TimerScreen( + navController = navController, + timerViewModel = vm, + modifier = Modifier.fillMaxSize() + ) + } + + // WHEN - 기록 탭으로 이동 + composeTestRule.onNodeWithText("기록").performClick() + + // WHEN - 첫 번째 기록 카드 클릭 + val firstRecord = vm.records.first() + + composeTestRule.onNodeWithText(firstRecord.title).performClick() + + // THEN - NavController 의 backstack 확인 + composeTestRule.runOnIdle { + val currentRoute = composeTestRule.activityRule.scenario.result.resultData + println("📌 current route = $currentRoute") + } + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt new file mode 100644 index 0000000..b81f6d1 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt @@ -0,0 +1,47 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test +/** + * TimerRunningScreen의 기본 UI 요소 테스트 + * 남은 시간 표시, 일시정지/재생 버튼, 종료 버튼 UI 확인 + */ +class TimerRunningScreenTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun 타이머실행화면_UI가_정상_렌더링된다() { + + composeRule.setContent { + val navController = rememberNavController() + val viewModel = TimerViewModel() + + TimerRunningScreen( + navController = navController, + color = Color(0xFFFF8686), + title = "공부", + totalSeconds = 600 // 10분 + ) + } + + // 남은 시간 텍스트 확인 + composeRule.onNodeWithText("남은시간").assertExists() + + // 재생/일시정지 버튼 존재 확인 + composeRule.onNode(hasContentDescription("pause_button") + .or(hasContentDescription("play_button"))) + .assertExists() + + // 종료 버튼 존재 확인 + composeRule.onNode(hasContentDescription("stop_button")) + .assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt new file mode 100644 index 0000000..ce0ffa0 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt @@ -0,0 +1,53 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.* +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen에서 TimerSettingScreen / TimerRunningScreen으로 이동 테스트 + * title, color, hour/minute/second 값 전달 여부 검증 + */ +class TimerScreenNavigationTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun navigate_from_timer_to_setting() { + + composeRule.setContent { + val navController = rememberNavController() + val vm = TimerViewModel() // <- 필수 + + NavHost(navController = navController, startDestination = "timer") { + + composable("timer") { + TimerScreen( + navController = navController, + timerViewModel = vm // <- 필수 + ) + } + + composable("setting") { + TimerSettingScreen( + navController = navController, + timerViewModel = vm // <- 필수 + ) + } + } + } + + // "+" 버튼 클릭 (contentDescription은 "추가") + composeRule.onNodeWithContentDescription("추가").performClick() + + // TimerSettingScreen 진입 확인 + composeRule.onNodeWithText("타이머 이름").assertExists() + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt new file mode 100644 index 0000000..40b8ad2 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt @@ -0,0 +1,39 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen에서 기록 탭 UI 렌더링을 테스트 + * RecordItemCard 목록이 정상적으로 표시되는지 확인 + */ +class TimerScreenRecordTabTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun 기록탭_선택시_기록리스트가_표시된다() { + composeRule.setContent { + val viewModel = TimerViewModel() + TimerScreen( + navController = rememberNavController(), + timerViewModel = viewModel + ) + } + + // when: 탭 바에서 "기록" 탭 클릭 + composeRule.onNodeWithText("기록").performClick() + + // then: Dummy 기록 데이터 중 하나의 제목이 화면에 표시되어야 한다. + // 🔹 TimerViewModelTest 에서 getRecordById("1") 의 title 이 "자바스크립트" 였으니까 그걸 기준으로 검증 + composeRule.onNodeWithText("자바스크립트") + .assertExists("기록 탭 선택 후 '자바스크립트' 기록 카드가 표시되지 않았습니다.") + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt new file mode 100644 index 0000000..01cfb75 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt @@ -0,0 +1,50 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.graphics.Color +import androidx.navigation.compose.rememberNavController +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerScreen 기본 UI 렌더링 테스트 + * 전체 시간, 원형 그래프, 탭 UI, 타이머 리스트 표시 확인 + */ +class TimerScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun 기록탭_선택시_RecordItemCard_표시된다() { + + // 더미 ViewModel 준비 + val viewModel = TimerViewModel().apply { + // 기록이 존재하도록 더미 데이터 추가 + addRecordForTest( + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 3 to 4) + ) + } + + composeTestRule.setContent { + TimerScreen( + navController = rememberNavController(), + timerViewModel = viewModel + ) + } + + // 탭 바에서 "기록" 클릭 + composeTestRule.onNode( + hasText("기록") and hasClickAction() + ).performClick() + + // RecordItemCard 표시 여부 확인 + composeTestRule.onNodeWithTag("record_card") + .assertExists("RecordItemCard가 표시되지 않았습니다.") + } +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt new file mode 100644 index 0000000..084af88 --- /dev/null +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt @@ -0,0 +1,38 @@ +package com.sprinter.studyspot.timer.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.sprinter.studyspot.timer.ui.TimerSettingScreen +import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import org.junit.Rule +import org.junit.Test + +/** + * TimerSettingScreen의 입력 동작을 테스트 + * 타이틀 입력, 시간 선택, 색상 선택, 저장/수정 기능 검증 + */ +class TimerSettingScreenInputTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun timerSetting_hoursInputWorks() { + + composeRule.setContent { + val vm = TimerViewModel() + + TimerSettingScreen( + navController = null, + timerViewModel = vm, // ✅ 필수 파라미터 추가 + initialTitle = "테스트", + initialHour = 0, + initialMinute = 0, + initialSecond = 0 + ) + } + + composeRule.onNodeWithText("시간").performClick() + } +} diff --git a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt new file mode 100644 index 0000000..9117f40 --- /dev/null +++ b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt @@ -0,0 +1,45 @@ +package com.sprinter.studyspot.timer.viewmodel + +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import org.junit.Before +import org.junit.Test + +/** + * ViewModel의 기록 데이터 로직 테스트 + * addRecordForTest(), getRecordById(), records 상태 변화 확인 + */ +class TimerViewModelRecordTest { + + private lateinit var viewModel: TimerViewModel + + @Before + fun setup() { + viewModel = TimerViewModel() + } + + @Test + fun `기록 ID로 올바른 레코드를 조회한다`() { + val record = viewModel.getRecordById("1") + + assertNotNull(record) + assertEquals("자바스크립트", record?.title) + } + + @Test + fun `존재하지 않는 기록 ID 조회 시 null 반환`() { + val record = viewModel.getRecordById("999") + + assertNull(record) + } + + @Test + fun `요일별 집중시간 Map이 올바르게 매핑되어 있다`() { + val record = viewModel.getRecordById("2") + + assertEquals(1, record?.dayCounts?.get(1)) + assertEquals(1, record?.dayCounts?.get(3)) + assertEquals(1, record?.dayCounts?.get(4)) + } +} diff --git a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt new file mode 100644 index 0000000..0798446 --- /dev/null +++ b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt @@ -0,0 +1,140 @@ +package com.sprinter.studyspot.timer.viewmodel + +import androidx.compose.ui.graphics.Color +import com.sprinter.studyspot.timer.ui.component.TimerItemData +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * 타이머 추가/삭제/수정 로직 검증 + * 타이머 전체 시간 계산 기능 테스트 + * selectedTab 상태 유지 여부 확인 + */ +class TimerViewModelTest { + + private lateinit var viewModel: TimerViewModel + + @Before + fun setup() { + viewModel = TimerViewModel() + } + + @Test + fun `타이머 추가 시 리스트에 정상적으로 포함된다`() { + val timer = TimerItemData( + id = "test", + title = "테스트 타이머", + color = Color(0xFFFF0000), + hour = 1, + minute = 10, + second = 5 + ) + + viewModel.addTimer(timer) + + assertTrue(viewModel.timers.contains(timer)) + } + + @Test + fun `타이머 삭제 시 리스트에서 제거된다`() { + val timer = TimerItemData( + id = "delete-test", + title = "삭제용 타이머", + color = Color(0xFFFF0000), + hour = 0, + minute = 30, + second = 0 + ) + + viewModel.addTimer(timer) + viewModel.deleteTimer("delete-test") + + assertFalse(viewModel.timers.contains(timer)) + } + + @Test + fun `기록 ID로 레코드를 정상적으로 조회한다`() { + val record = viewModel.getRecordById("1") + assertEquals("자바스크립트", record?.title) + } + + @Test + fun `타이머 수정 시 기존 값이 업데이트된다`() { + val timer = TimerItemData( + id = "update-test", + title = "Old", + color = Color(0xFFFF0000), + hour = 0, + minute = 30, + second = 0 + ) + + viewModel.addTimer(timer) + + val updated = TimerItemData( + id = "update-test", + title = "New", + color = Color(0xFF00FF00), + hour = 1, + minute = 0, + second = 0 + ) + + viewModel.updateTimer(updated) + + assertEquals("New", viewModel.timers.first { it.id == "update-test" }.title) + } + + @Test + fun `존재하지 않는 ID 삭제 시 오류 없이 유지된다`() { + val initialSize = viewModel.timers.size + viewModel.deleteTimer("not-found") + assertEquals(initialSize, viewModel.timers.size) + } + + @Test + fun `타이머 수정 시 리스트 크기는 유지된다`() { + val timer = TimerItemData( + id = "size-test", + title = "원본", + color = Color(0xFFFF0000), + hour = 0, + minute = 10, + second = 0 + ) + + viewModel.addTimer(timer) + val sizeBefore = viewModel.timers.size + + val updated = timer.copy( + title = "수정된 타이머", + minute = 20 + ) + viewModel.updateTimer(updated) + + val sizeAfter = viewModel.timers.size + assertEquals(sizeBefore, sizeAfter) + } + + @Test + fun `여러 타이머 추가 후 일부 삭제 시 남은 타이머 수가 예상과 같다`() { + val t1 = TimerItemData("t1", "타이머1", Color(0xFFFF0000), 0, 10, 0) + val t2 = TimerItemData("t2", "타이머2", Color(0xFFFF0000), 0, 20, 0) + val t3 = TimerItemData("t3", "타이머3", Color(0xFFFF0000), 0, 30, 0) + + viewModel.addTimer(t1) + viewModel.addTimer(t2) + viewModel.addTimer(t3) + + viewModel.deleteTimer("t2") + + assertEquals(2, viewModel.timers.size) + assertTrue(viewModel.timers.any { it.id == "t1" }) + assertTrue(viewModel.timers.any { it.id == "t3" }) + assertFalse(viewModel.timers.any { it.id == "t2" }) + } + +} \ No newline at end of file From 9b21ebdc02dcc62a379930d776933734448682f6 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 19:00:32 +0900 Subject: [PATCH 27/44] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=EC=9D=84=20=EC=9C=84=ED=95=9C=20AppNavGraph?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/navigation/AppNavGraph.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 7ea6dac..ce63bd3 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -18,6 +18,8 @@ import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 +import java.net.URLDecoder +import java.nio.charset.StandardCharsets @Composable fun AppNavGraph(navController: NavHostController) { @@ -119,7 +121,8 @@ fun AppNavGraph(navController: NavHostController) { navArgument("total") { type = NavType.LongType; defaultValue = 0L } ) ) { backStackEntry -> - val title = backStackEntry.arguments?.getString("title") ?: "" + val rawTitle = backStackEntry.arguments?.getString("title") ?: "기본 타이머" + val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val total = backStackEntry.arguments?.getLong("total") ?: 0L @@ -138,29 +141,39 @@ fun AppNavGraph(navController: NavHostController) { ) } - // ✅ 기록 상세 화면 +// ✅ 기록 상세 화면 composable( route = "recordDetail/{recordId}?title={title}&color={color}", arguments = listOf( navArgument("recordId") { type = NavType.StringType }, navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.LongType; defaultValue = 0xFFFFFFFF } + navArgument("color") { type = NavType.StringType; defaultValue = "0" } // ← 수정됨 ) ) { backStackEntry -> + val recordId = backStackEntry.arguments?.getString("recordId") ?: "" - val title = backStackEntry.arguments?.getString("title") ?: "" - val colorValue = backStackEntry.arguments?.getLong("color") ?: 0xFFFFFFFF - val color = Color(colorValue) + + // title 디코딩 + val rawTitle = backStackEntry.arguments?.getString("title") ?: "" + val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) + + // color 변환 (문자열 → Color) + val rawColor = backStackEntry.arguments?.getString("color") ?: "0" + val color = try { + if (rawColor == "0" || rawColor.isBlank()) Color(0xFFD9D9D9) + else Color(rawColor.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) + } RecordDetailScreen( navController = navController, recordId = recordId, title = title, color = color, - timerViewModel = timerViewModel // ✅ ViewModel 전달 + timerViewModel = timerViewModel ) } - } } } From 29cee2fa494799b33bf92df19aa620f6841d1111 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 19:00:46 +0900 Subject: [PATCH 28/44] =?UTF-8?q?refactor:=20TimerScreen=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=9C=A0=EC=A7=80=20=EC=9C=84=ED=95=B4=20selectedT?= =?UTF-8?q?ab=EC=9D=84=20ViewModel=EB=A1=9C=20=EC=9D=B4=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timer/viewmodel/TimerViewModel.kt | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index dff5d29..0c26233 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -1,6 +1,9 @@ package com.sprinter.studyspot.timer.viewmodel +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import com.sprinter.studyspot.timer.ui.component.RecordData @@ -10,8 +13,14 @@ class TimerViewModel : ViewModel() { private val _timers = mutableStateListOf() val timers: List get() = _timers - // ✅ 기록 데이터 - val records = listOf( + // 타이머의 총 소요시간 + fun getTotalTimerSeconds(): Long { + return timers.sumOf { timer -> + (timer.hour * 3600L) + (timer.minute * 60L) + timer.second + } + } + // ✅ 기록 데이터 (내부 mutable, 외부는 읽기 전용으로 노출) + private val _records = mutableStateListOf( RecordData( id = "0", title = "전체 시간", @@ -34,6 +43,7 @@ class TimerViewModel : ViewModel() { dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) ) ) + val records: List get() = _records fun addTimer(timer: TimerItemData) { _timers.add(timer) @@ -50,4 +60,30 @@ class TimerViewModel : ViewModel() { // ✅ recordId로 RecordData 가져오기 fun getRecordById(id: String): RecordData? = records.find { it.id == id } + + // 테스트에서 기록을 부여하기 위한 함수 + fun addRecordForTest( + title: String, + color: Color, + totalTime: String, + dayCounts: Map + ) { + _records.add( + RecordData( + id = System.currentTimeMillis().toString(), + title = title, + color = color, + totalTime = totalTime, + dayCounts = dayCounts + ) + ) + } + var selectedTab by mutableStateOf(0) + private set + + fun updateSelectedTab(tabIndex: Int) { + selectedTab = tabIndex + } + + } \ No newline at end of file From e388d94e0c931b915322f07da1f4e88b7cf78826 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 19:00:56 +0900 Subject: [PATCH 29/44] =?UTF-8?q?refactor:=20TimerScreen=20=ED=83=AD=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20ViewModel=EA=B3=BC=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=ED=95=98=EA=B3=A0=20UI=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sprinter/studyspot/timer/ui/TimerScreen.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 814844a..2550b9f 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -50,7 +50,7 @@ fun TimerScreen( } var elapsed by remember { mutableStateOf(0L) } - var selectedTab by remember { mutableStateOf(0) } + val selectedTab = timerViewModel.selectedTab val selectedColor = remember(colorValue) { try { @@ -61,7 +61,7 @@ fun TimerScreen( } } - val total = (hour * 3600L) + (minute * 60L) + second + val total = timerViewModel.getTotalTimerSeconds() val remainingSeconds = (total - elapsed).coerceAtLeast(0L) val h = remainingSeconds / 3600 val m = (remainingSeconds % 3600) / 60 @@ -146,7 +146,8 @@ fun TimerScreen( ) } ) { - Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { + Tab(selected = selectedTab == 0, onClick = { timerViewModel.updateSelectedTab(0) } + ) { Text( "타이머", fontWeight = FontWeight.Medium, @@ -155,8 +156,9 @@ fun TimerScreen( modifier = Modifier.padding(vertical = 12.dp) ) } - Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { - Text( + Tab(selected = selectedTab == 1, onClick = { timerViewModel.updateSelectedTab(1) } + ) { + Text( "기록", fontWeight = FontWeight.Medium, fontSize = 14.sp, From 56ac8bcd322d4827f3c732fa3ee02bd75a1ba879 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 19:01:08 +0900 Subject: [PATCH 30/44] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B0=8F=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=ED=99=94=EB=A9=B4=EC=9D=98=20UI=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../studyspot/timer/ui/RecordDetailScreen.kt | 12 +- .../studyspot/timer/ui/TimerRunningScreen.kt | 111 ++++++++++++++---- .../studyspot/timer/ui/TimerSettingScreen.kt | 10 +- .../timer/ui/component/RecordItemCard.kt | 2 + 5 files changed, 111 insertions(+), 28 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01efa91..8448aa7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,9 @@ dependencies { // 네비게이션 바를 위한 의존성 추가 implementation("androidx.navigation:navigation-compose:2.8.3") - testImplementation(libs.junit) + // 테스트 케이스 작성을 위한 의존성 추가 + testImplementation("junit:junit:4.13.2") + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 17989ba..c236fa1 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -1,6 +1,7 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -78,7 +79,7 @@ fun RecordDetailScreen( IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), - contentDescription = "Back", + contentDescription = "뒤로가기", tint = Color.Unspecified ) } @@ -256,6 +257,15 @@ fun RecordDetailScreen( .size(33.dp) .clip(CircleShape) .background(color = recordColor) + .then( + if (recordColor == Color.White) + Modifier.border( + width = 2.dp, + color = Color.LightGray, + shape = CircleShape + ) + else Modifier + ) ) Spacer(modifier = Modifier.width(8.dp)) Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index 757f996..a600c39 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -2,10 +2,30 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -27,13 +47,60 @@ fun TimerRunningScreen( title: String, totalSeconds: Long, ) { - // 🔹 상태 변수들 - var elapsed by remember { mutableStateOf(0L) } // 경과 시간 - var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 + // 🔹 경과 시간 + var elapsed by remember { mutableStateOf(0L) } + + // 🔹 타이머 동작 여부 + var isRunning by remember { mutableStateOf(true) } + + // 초기화 버튼 클릭 여부 + var isReset by remember { mutableStateOf(false) } + + // 🔹 1시간 이벤트 발동 여부 + var hourReached by remember { mutableStateOf(false) } + + // 🔹 1시간 이벤트 유지 시간 (10분 유지) + var hourEventElapsed by remember { mutableStateOf(0L) } + + // 🔹 1시간 이벤트 체크 + LaunchedEffect(isRunning) { + while (isRunning && elapsed < totalSeconds) { + + delay(1000) + elapsed++ + + // ⭐ 정확히 1시간(3600초) 도달 시 + if (elapsed == 3600L && !hourReached) { + hourReached = true + hourEventElapsed = 0L + } + } + } + + // 🔹 1시간 이벤트가 발동하면 10분 유지 후 게이지 리셋 + LaunchedEffect(hourReached) { + if (hourReached) { + + // 10분 = 600초 동안 유지 + while (hourEventElapsed < 600L) { + delay(1000) + hourEventElapsed++ + } + + // 10분 지나면 이벤트 종료 + 게이지 리셋 + hourReached = false + elapsed = 0L // ⭐ 게이지 다시 맨 처음부터 돌게 하기 + } + } // 🔹 남은시간 / 집중시간 계산 - val remainingSeconds = (totalSeconds - elapsed).coerceAtLeast(0L) - val focusSeconds = totalSeconds - remainingSeconds + val remainingSeconds = + if (hourReached) (3600 - hourEventElapsed).coerceAtLeast(0L) + else (totalSeconds - elapsed).coerceAtLeast(0L) + + val focusSeconds = + if (hourReached) 3600 + else totalSeconds - remainingSeconds // ⏰ 시/분/초 포맷 val timeText = String.format( @@ -49,14 +116,6 @@ fun TimerRunningScreen( focusSeconds % 60 ) - // ⏱ 타이머 동작 제어 - LaunchedEffect(isRunning) { - while (isRunning && elapsed < totalSeconds) { - delay(1000) - elapsed++ - } - } - // 🔹 타이머가 끝난 상태 감지 val isFinished = elapsed >= totalSeconds @@ -106,11 +165,13 @@ fun TimerRunningScreen( // 🔹 원형 타이머 색상 val progressColor = when { - isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 - isRunning -> color - else -> Color(0xFF6D6D6D) + isFinished -> Color(0xFF6D6D6D) // 완료 → 회색 + isRunning -> color // 실행 → 본래 색 + isReset -> color // 초기화 → 본래 색 유지 + else -> Color(0xFF6D6D6D) // 일시정지 → 회색 } + Box( modifier = Modifier .size(220.dp) @@ -119,8 +180,8 @@ fun TimerRunningScreen( contentAlignment = Alignment.Center ) { TimerProgress( - totalSeconds = totalSeconds, - elapsedSeconds = elapsed, + totalSeconds = if (hourReached) 3600 else totalSeconds, + elapsedSeconds = if (hourReached) hourEventElapsed else elapsed, modifier = Modifier.fillMaxSize(), progressColor = progressColor ) @@ -130,6 +191,7 @@ fun TimerRunningScreen( // 🔹 상태별 멘트 변경 val motivationText = when { + hourReached -> "🎉🎊축하합니다!🎊🎉\n1시간을 완수했어요!" isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" @@ -138,8 +200,8 @@ fun TimerRunningScreen( Text( motivationText, textAlign = TextAlign.Center, - color = if (isFinished) Color(0xFF4CAF50) else Color.Gray, // 완료 시 초록색 느낌 - fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal + color = if (hourReached || isFinished) Color(0xFF4CAF50) else Color.Gray, + fontWeight = if (hourReached || isFinished) FontWeight.Bold else FontWeight.Normal ) Spacer(modifier = Modifier.height(24.dp)) @@ -166,6 +228,7 @@ fun TimerRunningScreen( } else { // ⏸ 일시정지 / ▶ 재시작 토글 isRunning = !isRunning + isReset = false } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), @@ -183,9 +246,9 @@ fun TimerRunningScreen( Button( onClick = { - // 🔄 초기화 elapsed = 0L isRunning = false + isReset = true // ⭐ 초기화 상태 활성화 }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), shape = RoundedCornerShape(8.dp) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 2ce675c..9c2c810 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -123,15 +123,21 @@ private fun TimeUnitField( fun TimerSettingScreen( navController: NavController? = null, timerViewModel: TimerViewModel, - initialTitle: String = "자바스크립트", + initialTitle: String = "새 타이머", initialHour: Int = 0, initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, timerId: String? = null ) { + val decodedTitle = try { + java.net.URLDecoder.decode(initialTitle, "UTF-8") + } catch (e: Exception) { + initialTitle + } + var selectedColor by remember { mutableStateOf(initialColor) } - var timerTitle by remember { mutableStateOf(initialTitle) } + var timerTitle by remember { mutableStateOf(decodedTitle) } var hourStr by remember { mutableStateOf(initialHour.toString().padStart(2, '0')) } var minuteStr by remember { mutableStateOf(initialMinute.toString().padStart(2, '0')) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index 5f0bf9c..9406166 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -59,6 +60,7 @@ fun RecordItemCard( modifier = Modifier .fillMaxWidth() .padding(vertical = 6.dp) + .testTag("record_card") .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), color = Color(0xFFF5F5F5) From 7d84a3f56860d122d8e2aab1558b28784b65be89 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 19:07:38 +0900 Subject: [PATCH 31/44] =?UTF-8?q?test:=20=EA=B8=B0=EB=A1=9D=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20navController=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/TimerRecordDetailBackTest.kt | 8 +++-- .../TimerRecordDetailNavigationTest.kt | 34 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt index 52536fb..5244176 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt @@ -38,9 +38,13 @@ class TimerRecordDetailBackTest { modifier = Modifier.fillMaxSize() ) } - composable("recordDetail/{id}") { + composable("recordDetail/{id}") { backStackEntry -> + + val recordId = backStackEntry.arguments?.getString("id") ?: "" RecordDetailScreen( - navController = navController + navController = navController, + recordId = recordId, + timerViewModel = vm ) } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt index f824cda..36b69a4 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt @@ -2,29 +2,32 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.* +import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import org.junit.Rule import org.junit.Test /** - * 기록 상세 화면에서 뒤로가기 버튼 클릭 시 TimerScreen의 ‘기록’ 탭이 유지되는지 검증 + * 기록 상세 화면으로 이동 + 뒤로가기 시 ‘기록’ 탭이 유지되는지 검증하는 테스트 */ class TimerRecordDetailNavigationTest { @get:Rule val composeTestRule = createComposeRule() + // navController를 외부에서 접근 가능하게 저장 + private lateinit var navController: NavHostController + @Test fun clickRecordCard_navigatesToRecordDetailScreen() { - // GIVEN - Fake ViewModel 준비 + // GIVEN - Fake ViewModel val vm = TimerViewModel() composeTestRule.setContent { - val navController = rememberNavController() + navController = rememberNavController() TimerScreen( navController = navController, @@ -33,18 +36,27 @@ class TimerRecordDetailNavigationTest { ) } - // WHEN - 기록 탭으로 이동 - composeTestRule.onNodeWithText("기록").performClick() + // WHEN - 기록 탭 선택 + composeTestRule.onNodeWithText("기록") + .assertExists() + .performClick() - // WHEN - 첫 번째 기록 카드 클릭 + // WHEN - 첫 번째 기록 카드 선택 val firstRecord = vm.records.first() - composeTestRule.onNodeWithText(firstRecord.title).performClick() + composeTestRule.onNodeWithText(firstRecord.title) + .assertExists() + .performClick() - // THEN - NavController 의 backstack 확인 + // THEN - NavController 의 현재 route 확인 + var currentRoute: String? = null composeTestRule.runOnIdle { - val currentRoute = composeTestRule.activityRule.scenario.result.resultData - println("📌 current route = $currentRoute") + currentRoute = navController.currentBackStackEntry?.destination?.route } + + println("📌 current route = $currentRoute") + + // 상세화면 경로 검증 + assert(currentRoute?.startsWith("recordDetail") == true) } } From 056a893a7d98c7c24e2c19fbd267581dfda54468 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 21:56:50 +0900 Subject: [PATCH 32/44] =?UTF-8?q?refactor:=20record=20detail=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=AA=A8=EB=8D=B8=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=EC=A1=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sprinter/studyspot/timer/model/RecordDetailItem.kt | 10 ++++++++++ .../studyspot/timer/model/RecordDetailRowData.kt | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt new file mode 100644 index 0000000..2e6849b --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt @@ -0,0 +1,10 @@ +package com.sprinter.studyspot.timer.model + +import androidx.compose.ui.graphics.Color + +data class RecordDetailItem( + val title: String, // 타이머명 + val color: Color, // 컬러 + val totalTimeFormatted: String, // "00:20:00" + val records: List // 시작/종료/집중 세부 기록 +) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt new file mode 100644 index 0000000..1eec6ef --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt @@ -0,0 +1,7 @@ +package com.sprinter.studyspot.timer.model + +data class RecordDetailRowData( + val start: String, // ex: "13:00:00" + val end: String, // ex: "13:10:00" + val duration: String // ex: "00:10:00" +) From 7aac7ef9c62787935881870d042a073c98cf30d9 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 21:57:15 +0900 Subject: [PATCH 33/44] =?UTF-8?q?feat:=20TimerViewModel=EC=97=90=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=EB=B3=84=20=EC=83=81=EC=84=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timer/viewmodel/TimerViewModel.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index 0c26233..185c9e3 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -6,6 +6,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import com.sprinter.studyspot.timer.model.RecordDetailItem +import com.sprinter.studyspot.timer.model.RecordDetailRowData +import com.sprinter.studyspot.timer.ui.component.DailyFocus import com.sprinter.studyspot.timer.ui.component.RecordData import com.sprinter.studyspot.timer.ui.component.TimerItemData @@ -85,5 +88,70 @@ class TimerViewModel : ViewModel() { selectedTab = tabIndex } + // 다음 타이머 세부 내용 파악을 위한 함수 + fun getNextRecordId(currentId: String): String? { + val index = _records.indexOfFirst { it.id == currentId } + if (index != -1 && index < _records.lastIndex) { + return _records[index + 1].id + } + return null + } + + // 이전 타이머 세부 내용 파악을 위한 함수 + fun getPrevRecordId(currentId: String): String? { + val index = _records.indexOfFirst { it.id == currentId } + if (index > 0) { + return _records[index - 1].id + } + return null + } + + fun getDailyGroupedRecords(day: Int): Map> { + val result = mutableMapOf>() + + records.filter { it.id != "0" }.forEach { record -> + val count = record.dayCounts[day] ?: return@forEach + + // 예시: count 만큼 더미 데이터 생성 (지금은 더미 → 나중에 실제 데이터 연결) + repeat(count) { + val item = DailyFocus( + timerTitle = record.title, + color = record.color, + start = "13:00:00", + end = "13:10:00", + duration = "00:10:00" + ) + result.getOrPut(record.title) { mutableListOf() }.add(item) + } + } + + return result + } + + fun getRecordDetailByDay(day: Int): List { + + // TODO: 실제 기록 DB 연동 시 교체 가능 + val example = listOf( + RecordDetailItem( + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTimeFormatted = "00:20:00", + records = listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), + RecordDetailRowData("14:00:00", "14:10:00", "00:10:00") + ) + ), + RecordDetailItem( + title = "파이썬", + color = Color(0xFFFFE382), + totalTimeFormatted = "00:10:00", + records = listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00") + ) + ) + ) + + return example + } } \ No newline at end of file From 800e5d18f1d177648d9205686110a4cf5dc14bd6 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 21:57:39 +0900 Subject: [PATCH 34/44] =?UTF-8?q?feat:=20RecordDetailScreen=20UI=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4/=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=EB=B6=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/RecordDetailScreen.kt | 403 +++++++++++------- 1 file changed, 255 insertions(+), 148 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index c236fa1..40d5c50 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -3,14 +3,31 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.aspectRatio +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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -35,29 +52,37 @@ fun RecordDetailScreen( color: Color? = null, timerViewModel: TimerViewModel ) { - // ✅ ViewModel에서 데이터 조회 + val record = timerViewModel.getRecordById(recordId) val recordTitle = record?.title ?: title ?: "기록 상세" - // ✅ Color 타입 강제 고정 (핵심 수정) val recordColor: Color = when (val c = record?.color) { is Color -> c is Int -> Color(c) else -> color ?: Color(0xFFD9D9D9) } - // ✅ 달력 관련 상태 정의 val monthState = remember { mutableStateOf(YearMonth.now()) } val monthTitle = "${monthState.value.monthValue}월" val ym = monthState.value val daysInMonth = ym.lengthOfMonth() - val firstDayOfWeek = (ym.atDay(1).dayOfWeek.value + 6) % 7 + val firstDay = (ym.atDay(1).dayOfWeek.value + 6) % 7 + + val combinedDayCounts: Map = + if (recordId == "0") + timerViewModel.records + .filter { it.id != "0" } + .flatMap { it.dayCounts.entries } + .groupBy { it.key } + .mapValues { it.value.sumOf { pair -> pair.value } } + else + record?.dayCounts ?: emptyMap() - val studyDaySet = setOf(4, 6, 12, 13, 19) // 이후 실제 데이터 연결 가능 + val studyDaySet = combinedDayCounts.keys val gridCells = remember(ym) { buildList { - repeat(firstDayOfWeek) { add(null) } + repeat(firstDay) { add(null) } for (d in 1..daysInMonth) add(d) while (size < 35) add(null) } @@ -65,7 +90,6 @@ fun RecordDetailScreen( var selectedDay by remember { mutableStateOf(null) } - // ✅ 메인 레이아웃 Column( modifier = Modifier .fillMaxSize() @@ -74,12 +98,11 @@ fun RecordDetailScreen( ) { Spacer(modifier = Modifier.height(16.dp)) - // 🔙 뒤로가기 버튼 Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), - contentDescription = "뒤로가기", + contentDescription = null, tint = Color.Unspecified ) } @@ -87,58 +110,90 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(21.dp)) - // 🔹 상단 이름 섹션 (루틴명 + 아이콘) Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = "Prev Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp) - ) + // 🔙 이전 기록으로 이동 + IconButton( + onClick = { + val prevId = timerViewModel.getPrevRecordId(recordId) + if (prevId != null) { + // title / color는 생략해도 ViewModel에서 다시 가져오니까 OK + navController.navigate("recordDetail/$prevId") + } + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_triangle), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + } Spacer(modifier = Modifier.width(38.dp)) + Box( modifier = Modifier .size(26.dp) .clip(CircleShape) - .background(color = recordColor) + .background(recordColor) + .then( + if (recordId == "0" || recordColor == Color.White) + Modifier.border( + width = 2.dp, + color = Color.LightGray, + shape = CircleShape + ) + else Modifier + ) ) + Spacer(modifier = Modifier.width(12.dp)) - Text(text = recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(12.dp)) + Icon( painter = painterResource(id = R.drawable.ic_trashcan), - contentDescription = "Delete", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) + Spacer(modifier = Modifier.width(38.dp)) - Icon( - painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = "Next Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp) - ) + // ▶ 다음 기록으로 이동 + IconButton( + onClick = { + val nextId = timerViewModel.getNextRecordId(recordId) + if (nextId != null) { + navController.navigate("recordDetail/$nextId") + } + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_right_triangle), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + } } Spacer(modifier = Modifier.height(21.dp)) - // ✅ 달력 박스 + // 달력 UI Box( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) .clip(RoundedCornerShape(10.dp)) .background(Color(0xFFF3F3F3)) - .align(Alignment.CenterHorizontally) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(20.dp)) - // 🔹 월 변경 헤더 Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically @@ -146,38 +201,37 @@ fun RecordDetailScreen( IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = "Prev Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) } Spacer(modifier = Modifier.width(38.dp)) - Text(text = monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(38.dp)) IconButton(onClick = { monthState.value = monthState.value.plusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = "Next Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp) + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) } } Spacer(modifier = Modifier.height(8.dp)) - // 🔹 요일 헤더 Row( - Modifier + modifier = Modifier .fillMaxWidth() .padding(horizontal = 49.dp), horizontalArrangement = Arrangement.spacedBy(30.dp) ) { listOf("월", "화", "수", "목", "금", "토", "일").forEach { Text( - text = it, + it, fontSize = 14.sp, textAlign = TextAlign.Center, modifier = Modifier.weight(1f) @@ -187,26 +241,28 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(8.dp)) - // 🔹 달력 (5주) + // Calendar grid Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 6.dp, bottom = 4.dp) + modifier = Modifier.padding(vertical = 6.dp) ) { gridCells.chunked(7).forEach { week -> Row( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { - week.forEach { dayOrNull -> - val isStudy = dayOrNull != null && studyDaySet.contains(dayOrNull) - val isSelected = selectedDay == dayOrNull - val color = when { - dayOrNull == null -> Color(0xFFE0E0E0) - isSelected -> Color(0xFFFF8686) - isStudy -> Color(0xFFB3E5C2) - else -> Color(0xFFE0E0E0) - } + week.forEach { dayVal -> + val isStudy = dayVal != null && studyDaySet.contains(dayVal) + val isSelected = selectedDay == dayVal + + val color = + when { + dayVal == null -> Color(0xFFE0E0E0) + isSelected -> Color(0xFFFF8686) + isStudy -> Color(0xFFB3E5C2) + else -> Color(0xFFE0E0E0) + } Box( modifier = Modifier @@ -214,7 +270,7 @@ fun RecordDetailScreen( .clip(RoundedCornerShape(8.dp)) .background(color) .clickable(enabled = isStudy) { - selectedDay = dayOrNull + selectedDay = dayVal } ) } @@ -223,128 +279,179 @@ fun RecordDetailScreen( } } } + // ------------------------- + // 상세 데이터 영역 + // ------------------------- + selectedDay?.let { day -> - // ✅ 선택된 날짜의 타이머 기록 영역 - selectedDay?.let { - Spacer(modifier = Modifier.height(20.dp)) + val details = + if (recordId == "0") { + // 전체 타이머 → 해당 날짜 기록 전체 + timerViewModel.getRecordDetailByDay(day) + } else { + val currentTitle = recordTitle // 예: "자바스크립트" + timerViewModel.getRecordDetailByDay(day) + .filter { it.title == currentTitle } + } - Column( + + val totalSec = details.sumOf { item -> + item.records.sumOf { r -> + val t = r.duration.split(":") + val h = t[0].toInt() + val m = t[1].toInt() + val s = t[2].toInt() + h * 3600 + m * 60 + s + } + } + + val totalFormatted = String.format( + "%02d:%02d:%02d", + totalSec / 3600, + (totalSec % 3600) / 60, + totalSec % 60 + ) + + LazyColumn( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 8.dp) + .weight(1f) ) { - // 🔹 "총 집중 시간" - Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 49.dp), - horizontalArrangement = Arrangement.End - ) { - Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) - } - // 🔹 색상 + 타이머명 + 총 시간 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( + // 🔥 전체(recordId == "0")일 때만 전체시간 카드 노출 + if (recordId == "0") { + item { + Spacer(modifier = Modifier.height(20.dp)) + + Row( modifier = Modifier - .size(33.dp) - .clip(CircleShape) - .background(color = recordColor) - .then( - if (recordColor == Color.White) - Modifier.border( - width = 2.dp, - color = Color.LightGray, - shape = CircleShape - ) - else Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End + ) { + Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + + Box( + modifier = Modifier + .size(33.dp) + .clip(CircleShape) + .background(Color.White) + .border(2.dp, Color.LightGray, CircleShape) ) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) - } - Text( - "00:20:00", - fontSize = 32.sp, - fontWeight = FontWeight.Normal, - modifier = Modifier.padding(end = 18.dp) - ) - } + Spacer(modifier = Modifier.width(8.dp)) - Spacer(modifier = Modifier.height(8.dp)) - Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) - Spacer(modifier = Modifier.height(8.dp)) + Text("전체 시간", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } - // 🔹 헤더 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "시작", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - modifier = Modifier.width(90.dp) - ) - Text( - "종료", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - modifier = Modifier.width(90.dp) - ) - Text( - "집중 시간", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - ) + Text( + totalFormatted, + fontSize = 32.sp, + modifier = Modifier.padding(end = 18.dp) + ) + } + + Spacer(modifier = Modifier.height(13.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + } } - Spacer(modifier = Modifier.height(6.dp)) + // 🔥🔥🔥 (중요) 개별 타이머 리스트 — 전체/특정 모두 공통 + items( + items = details, + key = { it.title } // 고유 key + ) { item -> - // 🔹 예시 데이터 행 - val focusList = listOf( - Triple("13:00:00 ~", "13:10:00", "00:10:00"), - Triple("14:00:00 ~", "14:10:00", "00:10:00") - ) + Spacer(modifier = Modifier.height(26.dp)) - focusList.forEach { (start, end, duration) -> Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 46.dp), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(text = start, fontSize = 16.sp, modifier = Modifier.width(90.dp)) - Text(text = end, fontSize = 16.sp, modifier = Modifier.width(90.dp)) - Text(text = duration, fontSize = 16.sp) + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(33.dp) + .clip(CircleShape) + .background(item.color) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(item.title, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + + Text( + item.totalTimeFormatted, + fontSize = 32.sp, + modifier = Modifier.padding(end = 18.dp) + ) } + + Spacer(modifier = Modifier.height(13.dp)) + + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "시작", + modifier = Modifier.width(90.dp), + color = Color.Gray, + fontSize = 15.sp + ) + Text( + "종료", + modifier = Modifier.width(90.dp), + color = Color.Gray, + fontSize = 15.sp + ) + Text("집중 시간", color = Color.Gray, fontSize = 15.sp) + } + + Spacer(modifier = Modifier.height(6.dp)) + + item.records.forEach { row -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 46.dp) + ) { + Text(row.start, modifier = Modifier.width(90.dp)) + Text(row.end, modifier = Modifier.width(90.dp)) + Text(row.duration) + } + } + + Spacer(modifier = Modifier.height(18.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) } } } } } -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Preview(showBackground = true) @Composable fun PreviewRecordDetailScreen() { val navController = rememberNavController() - val dummyViewModel = remember { TimerViewModel() } // ✅ Preview용 RecordDetailScreen( navController = navController, recordId = "1", - timerViewModel = dummyViewModel + timerViewModel = TimerViewModel() ) } From f1a3f6df7b44827308ca44ccc8eb40d6081bce0e Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 16 Nov 2025 21:58:10 +0900 Subject: [PATCH 35/44] =?UTF-8?q?refactor:=20DailyFocus=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sprinter/studyspot/timer/ui/component/DailyFocus.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt new file mode 100644 index 0000000..ce02148 --- /dev/null +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt @@ -0,0 +1,9 @@ +package com.sprinter.studyspot.timer.ui.component + +data class DailyFocus( + val timerTitle: String, + val color: androidx.compose.ui.graphics.Color, + val start: String, + val end: String, + val duration: String +) From 86e5c962e64b8686161d0c5cf0d07ba82def257c Mon Sep 17 00:00:00 2001 From: MyKnow Date: Mon, 17 Nov 2025 11:12:00 +0900 Subject: [PATCH 36/44] =?UTF-8?q?chore:=20test=EC=97=90=EC=84=9C=20ktlint?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 35fccf7..8bf3def 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,11 @@ [*.{kt,kts}] ktlint_standard_annotation = disabled -ktlint_function_naming_ignore_when_annotated_with = Composable \ No newline at end of file +ktlint_function_naming_ignore_when_annotated_with = Composable + +[app/src/test/java/**.kt] +ktlint_function_naming = disabled +ktlint_standard_function-naming = disabled + +[app/src/androidTest/java/**.kt] +ktlint_function_naming = disabled +ktlint_standard_function-naming = disabled \ No newline at end of file From 1d91fcf1e0573c34ee381c1d1dc40226b612f1e8 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 18:35:31 +0900 Subject: [PATCH 37/44] =?UTF-8?q?chore:=20ktlint=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sprinter/studyspot/MainActivity.kt | 2 +- .../studyspot/timer/ui/TimerProgress.kt | 29 ++- .../studyspot/timer/ui/TimerScreen.kt | 201 +++++++--------- .../studyspot/timer/ui/TimerSettingScreen.kt | 220 ++++++++---------- .../timer/ui/component/BottomNavBar.kt | 48 ++-- .../timer/ui/component/RecordData.kt | 4 +- .../studyspot/timer/ui/component/StudyItem.kt | 31 +-- .../timer/ui/component/SwipeableStudyItem.kt | 147 +++++------- .../timer/ui/component/TimerItemData.kt | 2 +- gradle/libs.versions.toml | 10 - 10 files changed, 291 insertions(+), 403 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt index 63b5389..83e991d 100644 --- a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt +++ b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt @@ -17,4 +17,4 @@ class MainActivity : ComponentActivity() { AppNavGraph(navController = navController) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt index 2daa0d4..be64014 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt @@ -29,14 +29,14 @@ fun TimerProgress( elapsedSeconds: Long, maxDisplaySeconds: Long = 60 * 60L, modifier: Modifier = Modifier, - progressColor: Color = Color(0xFFB8E9BB), + progressColor: Color = Color(0xFFB8E9BB) ) { val density = LocalDensity.current val remainingSeconds = (totalSeconds - elapsedSeconds).coerceAtLeast(0L) val remainingRatio = (remainingSeconds.toFloat() / maxDisplaySeconds.toFloat()).coerceIn(0f, 1f) val animatedProgress by animateFloatAsState( targetValue = remainingRatio, - animationSpec = tween(300, easing = LinearEasing), + animationSpec = tween(300, easing = LinearEasing) ) Canvas(modifier = modifier.aspectRatio(1f)) { @@ -51,7 +51,7 @@ fun TimerProgress( startAngle = -90f, sweepAngle = 360f * animatedProgress, useCenter = true, - style = Fill, + style = Fill ) // 물방울 @@ -61,14 +61,13 @@ fun TimerProgress( // 뿔 val hornWidthPx = 23.dp.toPx() val hornHeightPx = 25.dp.toPx() - val path = - Path().apply { - val tipY = center.y - dropRadiusPx - hornHeightPx / 2 - moveTo(center.x, tipY) - lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - close() - } + val path = Path().apply { + val tipY = center.y - dropRadiusPx - hornHeightPx / 2 + moveTo(center.x, tipY) + lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + close() + } drawPath(path, color = Color.White) } } @@ -92,8 +91,8 @@ fun TimerProgressDemo() { @Preview( showBackground = true, name = "45분 시작 - 실시간 감소", - widthDp = 500, // 그대로 - heightDp = 500, // 그대로 + widthDp = 500, // 그대로 + heightDp = 500 // 그대로 ) @Composable fun Preview45MinInteractive() { @@ -111,6 +110,6 @@ fun Preview45MinInteractive() { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.size(190.dp), + modifier = Modifier.size(190.dp) ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 21fe4a6..2550b9f 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,33 +1,13 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -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.aspectRatio -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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.* import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,35 +36,30 @@ fun TimerScreen( hour: Int = 0, minute: Int = 0, second: Int = 0, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { val timerList by remember { derivedStateOf { timerViewModel.timers } } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp - val circleSize = - when { - screenWidth < 360.dp -> 150.dp - screenWidth < 600.dp -> 190.dp - else -> 250.dp - } + val circleSize = when { + screenWidth < 360.dp -> 150.dp + screenWidth < 600.dp -> 190.dp + else -> 250.dp + } var elapsed by remember { mutableStateOf(0L) } val selectedTab = timerViewModel.selectedTab - val selectedColor = - remember(colorValue) { - try { - if (colorValue == "0" || colorValue.isBlank()) { - Color(0xFFD9D9D9) - } else { - Color(colorValue.toULong()) - } - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val selectedColor = remember(colorValue) { + try { + if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) + else Color(colorValue.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) } + } val total = timerViewModel.getTotalTimerSeconds() val remainingSeconds = (total - elapsed).coerceAtLeast(0L) @@ -95,40 +70,37 @@ fun TimerScreen( Surface( modifier = Modifier.fillMaxSize(), - color = Color.Transparent, + color = Color.Transparent ) { Box( - modifier = - Modifier - .fillMaxSize() - .background(Color.White), + modifier = Modifier + .fillMaxSize() + .background(Color.White) ) { Column( modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { // ✅ 상단 타이머 영역 Box( - modifier = - Modifier - .fillMaxWidth() - .weight(0.45f) - .background(Color(0xFFF5F5F5)), - contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(0.45f) + .background(Color(0xFFF5F5F5)), + contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = - Modifier - .size(circleSize) - .aspectRatio(1f) - .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center, + modifier = Modifier + .size(circleSize) + .aspectRatio(1f) + .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center ) { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() ) } @@ -139,44 +111,40 @@ fun TimerScreen( text = timeText, fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, fontWeight = FontWeight.Bold, - color = Color.Black, + color = Color.Black ) } } // ✅ 하단 영역 (탭 전환) Surface( - modifier = - Modifier - .fillMaxWidth() - .weight(0.55f), - color = Color.White, + modifier = Modifier + .fillMaxWidth() + .weight(0.55f), + color = Color.White ) { Column { // 🔹 탭 바 Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 41.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 41.dp) ) { TabRow( selectedTabIndex = selectedTab, containerColor = Color.Transparent, divider = { }, - modifier = - Modifier - .fillMaxWidth() - .height(48.dp), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), indicator = { tabPositions -> Box( - modifier = - Modifier - .tabIndicatorOffset(tabPositions[selectedTab]) - .height(2.dp) - .background(Color.Black), + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black) ) - }, + } ) { Tab(selected = selectedTab == 0, onClick = { timerViewModel.updateSelectedTab(0) } ) { @@ -185,7 +153,7 @@ fun TimerScreen( fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 0) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier.padding(vertical = 12.dp) ) } Tab(selected = selectedTab == 1, onClick = { timerViewModel.updateSelectedTab(1) } @@ -195,7 +163,7 @@ fun TimerScreen( fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 1) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier.padding(vertical = 12.dp) ) } } @@ -208,29 +176,26 @@ fun TimerScreen( if (selectedTab == 0) { // ✅ 타이머 탭 LazyColumn( - modifier = - Modifier - .fillMaxSize() - .padding(bottom = 80.dp), + modifier = Modifier + .fillMaxSize() + .padding(bottom = 80.dp) ) { items(timerList) { timer -> SwipeableStudyItem( color = timer.color, title = timer.title, - time = - String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second, - ), + time = String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second + ), onEdit = { val encodedTitle = - URLEncoder.encode(timer.title, "UTF-8") val colorValueStr = timer.color.value.toString() navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}", + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" ) }, onDelete = { timerViewModel.deleteTimer(timer.id) }, @@ -241,9 +206,9 @@ fun TimerScreen( val total = timer.hour * 3600 + timer.minute * 60 + timer.second navController.navigate( - "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total", + "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" ) - }, + } ) Spacer(modifier = Modifier.height(8.dp)) } @@ -254,46 +219,40 @@ fun TimerScreen( onClick = { timerViewModel.addTimer( TimerItemData( - id = - java.util.UUID - .randomUUID() - .toString(), + id = java.util.UUID.randomUUID().toString(), title = "새 타이머", color = Color(0xFFD9D9D9), hour = 0, minute = 0, - second = 0, - ), + second = 0 + ) ) }, - modifier = - Modifier - .align(Alignment.BottomEnd) - .padding(24.dp), + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(24.dp) ) { Icon( painter = painterResource(id = R.drawable.ic_add_button), contentDescription = "추가", tint = Color.Unspecified, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(32.dp) ) } } else if (selectedTab == 1) { // ✅ 기록 탭 LazyColumn( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) ) { items(timerViewModel.records) { record -> // 🔹 안전한 Color 변환 (핵심 수정) - val recordColor: Color = - when (val c = record.color) { - is Color -> c - is Int -> Color(c) - else -> Color(0xFFD9D9D9) - } + val recordColor: Color = when (val c = record.color) { + is Color -> c + is Int -> Color(c) + else -> Color(0xFFD9D9D9) + } val encodedTitle = URLEncoder.encode(record.title, "UTF-8") val colorValueStr = recordColor.value.toString() @@ -305,9 +264,9 @@ fun TimerScreen( dayCounts = record.dayCounts, onClick = { navController.navigate( - "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr", + "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr" ) - }, + } ) Spacer(modifier = Modifier.height(8.dp)) } @@ -329,7 +288,7 @@ fun TimerScreenPreview() { MaterialTheme { TimerScreen( navController = rememberNavController(), - timerViewModel = dummyViewModel, + timerViewModel = dummyViewModel ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index bcc9a1e..9c2c810 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -1,6 +1,5 @@ package com.sprinter.studyspot.timer.ui -import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -55,7 +54,7 @@ private fun TimeUnitField( value: String, onValueChange: (String) -> Unit, maxLength: Int, - maxValue: Int, + maxValue: Int ) { var internalValue by remember(value) { mutableStateOf(value) } @@ -75,54 +74,47 @@ private fun TimeUnitField( onValueChange(digits) } }, - modifier = - Modifier - .width(64.dp) - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) - .border( - width = if (isError) 2.dp else 0.dp, - color = if (isError) Color.Red else Color.Transparent, - shape = RoundedCornerShape(30.dp), - ), - textStyle = - TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = if (isError) Color.Red else Color.Black, - textAlign = TextAlign.Center, + modifier = Modifier + .width(64.dp) + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) + .border( + width = if (isError) 2.dp else 0.dp, + color = if (isError) Color.Red else Color.Transparent, + shape = RoundedCornerShape(30.dp) ), + textStyle = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = if (isError) Color.Red else Color.Black, + textAlign = TextAlign.Center + ), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), visualTransformation = { text -> val padded = text.text.padStart(2, '0').take(2) TransformedText( - text = - androidx.compose.ui.text - .AnnotatedString(padded), - offsetMapping = - object : OffsetMapping { - override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) - - override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) - }, + text = androidx.compose.ui.text.AnnotatedString(padded), + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) + override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) + } ) }, - colors = - TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - ), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), placeholder = { Text( "00", color = Color.Gray, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) - }, + } ) } @@ -136,7 +128,7 @@ fun TimerSettingScreen( initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, - timerId: String? = null, + timerId: String? = null ) { val decodedTitle = try { java.net.URLDecoder.decode(initialTitle, "UTF-8") @@ -158,26 +150,23 @@ fun TimerSettingScreen( Surface(modifier = Modifier.fillMaxSize(), color = Color.White) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(top = 20.dp, start = 11.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 11.dp), + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = - Modifier - .size(28.dp) - .clickable { navController?.popBackStack() }, + modifier = Modifier + .size(28.dp) + .clickable { navController?.popBackStack() } ) } @@ -187,34 +176,28 @@ fun TimerSettingScreen( fontSize = 14.sp, color = Color.DarkGray, textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(28.dp)) // ✅ 색상 선택 - val colors = - listOf( - Color(0xFFB8E9BB), - Color(0xFFFFDB86), - Color(0xFFFF8686), - Color(0xFFF9C0EB), - Color(0xFFD2ACAC), - Color(0xFFD9D9D9), - Color(0xFFAECDE5), - ) + val colors = listOf( + Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), + Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) + ) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { colors.forEach { color -> Box( - modifier = - Modifier - .size(27.dp, 26.dp) - .background(color, RoundedCornerShape(8.dp)) - .border( - width = if (selectedColor == color) 3.dp else 0.dp, - color = if (selectedColor == color) Color.Black else Color.Transparent, - shape = RoundedCornerShape(8.dp), - ).clickable { selectedColor = color }, + modifier = Modifier + .size(27.dp, 26.dp) + .background(color, RoundedCornerShape(8.dp)) + .border( + width = if (selectedColor == color) 3.dp else 0.dp, + color = if (selectedColor == color) Color.Black else Color.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable { selectedColor = color } ) } } @@ -227,24 +210,21 @@ fun TimerSettingScreen( onValueChange = { timerTitle = it }, shape = RoundedCornerShape(30.dp), singleLine = true, - modifier = - Modifier - .fillMaxWidth() - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, - keyboardOptions = - KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 - ), - colors = - TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - ), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 + ), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ) ) Spacer(modifier = Modifier.height(32.dp)) @@ -252,13 +232,13 @@ fun TimerSettingScreen( Spacer(modifier = Modifier.height(12.dp)) Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { TimeUnitField( value = hourStr, onValueChange = { hourStr = it }, maxLength = 2, - maxValue = 99, + maxValue = 99 ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -267,7 +247,7 @@ fun TimerSettingScreen( value = minuteStr, onValueChange = { minuteStr = it }, maxLength = 2, - maxValue = 59, + maxValue = 59 ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -276,53 +256,50 @@ fun TimerSettingScreen( value = secondStr, onValueChange = { secondStr = it }, maxLength = 2, - maxValue = 59, + maxValue = 59 ) } Spacer(modifier = Modifier.height(30.dp)) Box( - modifier = - Modifier - .fillMaxWidth() - .padding(end = 16.dp), - contentAlignment = Alignment.CenterEnd, + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd ) { Surface( - modifier = - Modifier - .size(77.dp, 41.dp) - .clickable(enabled = isValid) { - val safeTitle = timerTitle.ifBlank { "기본 타이머" } - val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") - val colorValue = selectedColor?.value?.toString() ?: "0" + modifier = Modifier + .size(77.dp, 41.dp) + .clickable(enabled = isValid) { + val safeTitle = timerTitle.ifBlank { "기본 타이머" } + val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") + val colorValue = selectedColor?.value?.toString() ?: "0" - val updatedTimer = - TimerItemData( - id = timerId ?: UUID.randomUUID().toString(), - title = safeTitle, - color = selectedColor ?: Color(0xFFD9D9D9), - hour = hour, - minute = minute, - second = second, - ) - timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 + val updatedTimer = TimerItemData( + id = timerId ?: UUID.randomUUID().toString(), + title = safeTitle, + color = selectedColor ?: Color(0xFFD9D9D9), + hour = hour, + minute = minute, + second = second + ) + timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 - navController?.navigate( - "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second", - ) { - popUpTo("timerSetting") { inclusive = true } - } - }, + navController?.navigate( + "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" + ) { + popUpTo("timerSetting") { inclusive = true } + } + }, shape = RoundedCornerShape(8.dp), - color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0), + color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) ) { Box(contentAlignment = Alignment.Center) { Icon( painter = painterResource(id = R.drawable.ic_confirm_check), contentDescription = "완료", tint = if (isValid) Color.Unspecified else Color.Gray, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(48.dp) ) } } @@ -331,7 +308,6 @@ fun TimerSettingScreen( } } -@SuppressLint("ViewModelConstructorInComposable") @Preview(showBackground = true) @Composable fun TimerSettingPreview() { diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt index f518145..3055fa8 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt @@ -2,15 +2,7 @@ package com.sprinter.studyspot.timer.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -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.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,37 +20,36 @@ import com.sprinter.studyspot.R @Composable fun BottomNavBar( navController: androidx.navigation.NavController, - currentRoute: String, + currentRoute: String ) { Row( - modifier = - Modifier - .fillMaxWidth() - .height(76.dp) - .background(Color.White) - .padding(horizontal = 36.dp), + modifier = Modifier + .fillMaxWidth() + .height(76.dp) + .background(Color.White) + .padding(horizontal = 36.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceBetween ) { BottomNavItem( icon = R.drawable.ic_clock, label = "타이머", isSelected = currentRoute == "timer", - onClick = { navController.navigate("timer") }, + onClick = { navController.navigate("timer") } ) BottomNavItem( icon = R.drawable.ic_home, label = "홈", isSelected = currentRoute == "home", - onClick = { navController.navigate("home") }, + onClick = { navController.navigate("home") } ) BottomNavItem( icon = R.drawable.ic_search, label = "검색", isSelected = currentRoute == "search", - onClick = { navController.navigate("search") }, + onClick = { navController.navigate("search") } ) } } @@ -68,27 +59,26 @@ private fun BottomNavItem( icon: Int, label: String, isSelected: Boolean, - onClick: () -> Unit, + onClick: () -> Unit ) { Column( - modifier = - Modifier - .width(60.dp) - .clickable(onClick = onClick), - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .width(60.dp) + .clickable(onClick = onClick), + horizontalAlignment = Alignment.CenterHorizontally ) { Icon( painter = painterResource(id = icon), contentDescription = label, modifier = Modifier.size(20.dp), - tint = Color.Black, + tint = Color.Black ) Spacer(modifier = Modifier.height(4.dp)) Text( text = label, fontSize = 12.sp, color = Color.Black, - fontWeight = FontWeight.Medium, + fontWeight = FontWeight.Medium ) } } @@ -98,6 +88,6 @@ private fun BottomNavItem( fun BottomNavBarPreview() { BottomNavBar( navController = rememberNavController(), - currentRoute = "home", + currentRoute = "home" ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt index e1ddc70..edbc887 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt @@ -7,5 +7,5 @@ data class RecordData( val title: String, val color: Color, val totalTime: String, - val dayCounts: Map, -) + val dayCounts: Map +) \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt index 95e61ea..be00561 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt @@ -22,43 +22,36 @@ import androidx.compose.ui.unit.sp // StudyItem (그대로 유지) @Composable -fun StudyItem( - color: Color, - title: String, - time: String, - onClick: () -> Unit, -) { +fun StudyItem(color: Color, title: String, time: String, onClick: () -> Unit) { val isCompact = LocalConfiguration.current.screenWidthDp < 360 Row( verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 28.dp) - .padding(vertical = if (isCompact) 6.dp else 8.dp) - .clickable { onClick() }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 28.dp) + .padding(vertical = if (isCompact) 6.dp else 8.dp) + .clickable { onClick() } ) { Box( - modifier = - Modifier - .size(if (isCompact) 31.dp else 33.dp) - .background(color, CircleShape), + modifier = Modifier + .size(if (isCompact) 31.dp else 33.dp) + .background(color, CircleShape) ) Spacer(modifier = Modifier.width(if (isCompact) 8.dp else 12.dp)) Text( title, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 14.sp else 16.sp, - color = Color.Black, + color = Color.Black ) Spacer(modifier = Modifier.weight(1f)) Text( time, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 30.sp else 32.sp, - color = Color.Gray, + color = Color.Gray ) Spacer(modifier = Modifier.width(34.dp)) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt index f77a6b9..bd2ec06 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt @@ -8,31 +8,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -54,24 +41,23 @@ fun SwipeableStudyItem( time: String, onEdit: () -> Unit, onDelete: () -> Unit, - onClick: () -> Unit, + onClick: () -> Unit ) { val density = LocalDensity.current var widthPx by remember { mutableStateOf(0f) } val decay = rememberSplineBasedDecay() - val state = - remember { - AnchoredDraggableState( - initialValue = SwipeState.Closed, - positionalThreshold = { distance -> distance * 0.5f }, - velocityThreshold = { with(density) { 100.dp.toPx() } }, - snapAnimationSpec = tween(), - decayAnimationSpec = decay, - confirmValueChange = { true }, - ) - } + val state = remember { + AnchoredDraggableState( + initialValue = SwipeState.Closed, + positionalThreshold = { distance -> distance * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = tween(), + decayAnimationSpec = decay, + confirmValueChange = { true } + ) + } // 앵커 업데이트: Swiped = 오른쪽으로 40% 이동 LaunchedEffect(widthPx) { @@ -79,8 +65,8 @@ fun SwipeableStudyItem( state.updateAnchors( DraggableAnchors { SwipeState.Closed at 0f - SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 - }, + SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 + } ) } } @@ -92,99 +78,94 @@ fun SwipeableStudyItem( if (current.isNaN()) return@derivedStateOf 0f val swipedPos = state.anchors.positionOf(SwipeState.Swiped) ?: 0f - current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) + current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) } } Box( - modifier = - Modifier - .fillMaxWidth() - .height(64.dp) - .onGloballyPositioned { coordinates -> - widthPx = coordinates.size.width.toFloat() - }, + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .onGloballyPositioned { coordinates -> + widthPx = coordinates.size.width.toFloat() + } ) { // 배경 버튼 (왼쪽에 고정) Row( - modifier = - Modifier - .matchParentSize() - .background(Color(0xFFF0F0F0)) - .padding(horizontal = 24.dp), - horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .matchParentSize() + .background(Color(0xFFF0F0F0)) + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(id = R.drawable.ic_edit), contentDescription = "Edit", tint = Color.Black, - modifier = - Modifier - .size(24.dp) - .clickable { onEdit() }, + modifier = Modifier + .size(24.dp) + .clickable { onEdit() } ) Spacer(modifier = Modifier.width(24.dp)) Icon( painter = painterResource(id = R.drawable.ic_delete), contentDescription = "Delete", tint = Color.Black, - modifier = - Modifier - .size(24.dp) - .clickable { onDelete() }, + modifier = Modifier + .size(24.dp) + .clickable { onDelete() } ) } // 앞쪽 카드: 오른쪽으로 스와이프 Surface( color = Color.White, - modifier = - Modifier - .offset { - if (widthPx <= 0f) { - IntOffset(0, 0) - } else { - IntOffset(safeOffsetX.roundToInt(), 0) - } - }.anchoredDraggable( - state = state, - orientation = Orientation.Horizontal, - ).background(Color.White) - .clickable( - enabled = state.currentValue == SwipeState.Closed, - onClick = onClick, - ), + modifier = Modifier + .offset { + if (widthPx <= 0f) { + IntOffset(0, 0) + } else { + IntOffset(safeOffsetX.roundToInt(), 0) + } + } + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal + ) + .background(Color.White) + .clickable( + enabled = state.currentValue == SwipeState.Closed, + onClick = onClick + ) ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 28.dp, end = 16.dp) - .height(64.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 28.dp, end = 16.dp) + .height(64.dp) ) { Box( - modifier = - Modifier - .size(33.dp) - .background(color, CircleShape), + modifier = Modifier + .size(33.dp) + .background(color, CircleShape) ) Spacer(modifier = Modifier.width(12.dp)) Text( text = title, fontWeight = FontWeight.Medium, fontSize = 16.sp, - color = Color.Black, + color = Color.Black ) Spacer(modifier = Modifier.weight(1f)) Text( text = time, fontWeight = FontWeight.Medium, fontSize = 28.sp, - color = Color.Gray, + color = Color.Gray ) } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt index e92a093..9d8b944 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt @@ -9,5 +9,5 @@ data class TimerItemData( val color: Color, val hour: Int, val minute: Int, - val second: Int, + val second: Int ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4815f00..236801c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,13 @@ [versions] agp = "8.12.3" -junitJupiterApi = "6.0.1" -junitJupiterEngine = "6.0.1" -junitJupiterParams = "6.0.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" -kotlinTest = "1.9.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.8.0" composeBom = "2024.09.00" -mockk = "1.14.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -29,11 +24,6 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" } -junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" } -junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } -mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 7e63039fe8bdc44a0880d59c8935afae95c76c65 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 20:39:01 +0900 Subject: [PATCH 38/44] =?UTF-8?q?restore:=20=ED=8A=B9=EC=A0=95=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EB=8F=99=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 8 ++++++-- gradle/libs.versions.toml | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8448aa7..ca57523 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,8 +54,12 @@ dependencies { // 네비게이션 바를 위한 의존성 추가 implementation("androidx.navigation:navigation-compose:2.8.3") - // 테스트 케이스 작성을 위한 의존성 추가 - testImplementation("junit:junit:4.13.2") + testImplementation(libs.junit) + testImplementation(libs.kotlin.test) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockk) + testRuntimeOnly(libs.junit.jupiter.engine) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 236801c..4815f00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,18 @@ [versions] agp = "8.12.3" +junitJupiterApi = "6.0.1" +junitJupiterEngine = "6.0.1" +junitJupiterParams = "6.0.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" +kotlinTest = "1.9.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.8.0" composeBom = "2024.09.00" +mockk = "1.14.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +29,11 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 0c2e0a6b3e99aad5c017285d5cdd1bd1afc05a88 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 20:40:47 +0900 Subject: [PATCH 39/44] =?UTF-8?q?style:=20=EC=99=80=EC=9D=BC=EB=93=9C?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyspot/timer/ui/TimerScreen.kt | 26 ++++++++++++++++--- .../timer/ui/component/BottomNavBar.kt | 10 ++++++- .../timer/ui/component/SwipeableStudyItem.kt | 23 ++++++++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 2550b9f..b2e57c6 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -1,13 +1,33 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +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.aspectRatio +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.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt index 3055fa8..4b3c75e 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt @@ -2,7 +2,15 @@ package com.sprinter.studyspot.timer.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt index bd2ec06..b0690cf 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt @@ -8,18 +8,31 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity From 12a273b6c43b299b41623ad61b43c09ff7a1822c Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 20:58:01 +0900 Subject: [PATCH 40/44] =?UTF-8?q?restore:=20=ED=8A=B9=EC=A0=95=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EB=8F=99=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 10 +- .../com/sprinter/studyspot/MainActivity.kt | 2 +- .../studyspot/navigation/AppNavGraph.kt | 173 ++++--- .../studyspot/timer/ui/RecordDetailScreen.kt | 442 +++++++----------- .../studyspot/timer/ui/TimerProgress.kt | 29 +- .../studyspot/timer/ui/TimerRunningScreen.kt | 176 +++---- .../studyspot/timer/ui/TimerScreen.kt | 187 ++++---- .../studyspot/timer/ui/TimerSettingScreen.kt | 230 ++++----- .../timer/ui/component/BottomNavBar.kt | 38 +- .../timer/ui/component/RecordData.kt | 4 +- .../timer/ui/component/RecordItemCard.kt | 97 ++-- .../studyspot/timer/ui/component/StudyItem.kt | 31 +- .../timer/ui/component/SwipeableStudyItem.kt | 124 ++--- .../timer/ui/component/TimerItemData.kt | 2 +- .../timer/viewmodel/TimerViewModel.kt | 153 +----- 15 files changed, 784 insertions(+), 914 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8bf3def..35fccf7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,3 @@ [*.{kt,kts}] ktlint_standard_annotation = disabled -ktlint_function_naming_ignore_when_annotated_with = Composable - -[app/src/test/java/**.kt] -ktlint_function_naming = disabled -ktlint_standard_function-naming = disabled - -[app/src/androidTest/java/**.kt] -ktlint_function_naming = disabled -ktlint_standard_function-naming = disabled \ No newline at end of file +ktlint_function_naming_ignore_when_annotated_with = Composable \ No newline at end of file diff --git a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt index 83e991d..63b5389 100644 --- a/app/src/main/java/com/sprinter/studyspot/MainActivity.kt +++ b/app/src/main/java/com/sprinter/studyspot/MainActivity.kt @@ -17,4 +17,4 @@ class MainActivity : ComponentActivity() { AppNavGraph(navController = navController) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index ce63bd3..539fb42 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -12,14 +12,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument +import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 import com.sprinter.studyspot.timer.ui.TimerRunningScreen import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel -import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 -import java.net.URLDecoder -import java.nio.charset.StandardCharsets @Composable fun AppNavGraph(navController: NavHostController) { @@ -36,23 +34,41 @@ fun AppNavGraph(navController: NavHostController) { ) { BottomNavBar(navController = navController, currentRoute = currentRoute) } - } + }, ) { innerPadding -> NavHost( navController = navController, startDestination = "timer", - modifier = androidx.compose.ui.Modifier.padding(innerPadding) + modifier = + androidx.compose.ui.Modifier + .padding(innerPadding), ) { // ✅ 타이머 메인 화면 composable( route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "기본 타이머" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" @@ -67,21 +83,40 @@ fun AppNavGraph(navController: NavHostController) { title = title, hour = hour, minute = minute, - second = second + second = second, ) } // ✅ 타이머 설정 화면 composable( route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("id") { type = NavType.StringType; defaultValue = "" }, - navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("title") { + type = NavType.StringType + defaultValue = "자바스크립트" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" @@ -90,15 +125,16 @@ fun AppNavGraph(navController: NavHostController) { val minute = backStackEntry.arguments?.getInt("minute") ?: 0 val second = backStackEntry.arguments?.getInt("second") ?: 0 - val initialColor = if (colorValue == "0" || colorValue.isBlank()) { - null - } else { - try { - Color(colorValue.toULong()) - } catch (e: Exception) { + val initialColor = + if (colorValue == "0" || colorValue.isBlank()) { null + } else { + try { + Color(colorValue.toULong()) + } catch (e: Exception) { + null + } } - } TimerSettingScreen( navController = navController, @@ -108,70 +144,79 @@ fun AppNavGraph(navController: NavHostController) { initialMinute = minute, initialSecond = second, initialColor = initialColor, - timerId = if (id.isNotEmpty()) id else null + timerId = if (id.isNotEmpty()) id else null, ) } // ✅ 타이머 실행 화면 composable( route = "timerRunning?title={title}&color={color}&total={total}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("total") { type = NavType.LongType; defaultValue = 0L } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("total") { + type = NavType.LongType + defaultValue = 0L + }, + ), ) { backStackEntry -> - val rawTitle = backStackEntry.arguments?.getString("title") ?: "기본 타이머" - val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) + val title = backStackEntry.arguments?.getString("title") ?: "" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val total = backStackEntry.arguments?.getLong("total") ?: 0L - val color = try { - if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) - else Color(colorValue.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val color = + try { + if (colorValue == "0" || colorValue.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(colorValue.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } TimerRunningScreen( navController = navController, title = title, color = color, - totalSeconds = total + totalSeconds = total, ) } -// ✅ 기록 상세 화면 + // ✅ 기록 상세 화면 composable( route = "recordDetail/{recordId}?title={title}&color={color}", - arguments = listOf( - navArgument("recordId") { type = NavType.StringType }, - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" } // ← 수정됨 - ) + arguments = + listOf( + navArgument("recordId") { type = NavType.StringType }, + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.LongType + defaultValue = 0xFFFFFFFF + }, + ), ) { backStackEntry -> - val recordId = backStackEntry.arguments?.getString("recordId") ?: "" - - // title 디코딩 - val rawTitle = backStackEntry.arguments?.getString("title") ?: "" - val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) - - // color 변환 (문자열 → Color) - val rawColor = backStackEntry.arguments?.getString("color") ?: "0" - val color = try { - if (rawColor == "0" || rawColor.isBlank()) Color(0xFFD9D9D9) - else Color(rawColor.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val title = backStackEntry.arguments?.getString("title") ?: "" + val colorValue = backStackEntry.arguments?.getLong("color") ?: 0xFFFFFFFF + val color = Color(colorValue) RecordDetailScreen( navController = navController, recordId = recordId, title = title, color = color, - timerViewModel = timerViewModel + timerViewModel = timerViewModel, // ✅ ViewModel 전달 ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 40d5c50..d57968b 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -1,7 +1,6 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,8 +14,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider @@ -50,228 +47,196 @@ fun RecordDetailScreen( recordId: String, title: String? = null, color: Color? = null, - timerViewModel: TimerViewModel + timerViewModel: TimerViewModel, ) { - + // ✅ ViewModel에서 데이터 조회 val record = timerViewModel.getRecordById(recordId) val recordTitle = record?.title ?: title ?: "기록 상세" - val recordColor: Color = when (val c = record?.color) { - is Color -> c - is Int -> Color(c) - else -> color ?: Color(0xFFD9D9D9) - } + // ✅ Color 타입 강제 고정 (핵심 수정) + val recordColor: Color = + when (val c = record?.color) { + is Color -> c + is Int -> Color(c) + else -> color ?: Color(0xFFD9D9D9) + } + // ✅ 달력 관련 상태 정의 val monthState = remember { mutableStateOf(YearMonth.now()) } val monthTitle = "${monthState.value.monthValue}월" val ym = monthState.value val daysInMonth = ym.lengthOfMonth() - val firstDay = (ym.atDay(1).dayOfWeek.value + 6) % 7 + val firstDayOfWeek = (ym.atDay(1).dayOfWeek.value + 6) % 7 - val combinedDayCounts: Map = - if (recordId == "0") - timerViewModel.records - .filter { it.id != "0" } - .flatMap { it.dayCounts.entries } - .groupBy { it.key } - .mapValues { it.value.sumOf { pair -> pair.value } } - else - record?.dayCounts ?: emptyMap() + val studyDaySet = setOf(4, 6, 12, 13, 19) // 이후 실제 데이터 연결 가능 - val studyDaySet = combinedDayCounts.keys - - val gridCells = remember(ym) { - buildList { - repeat(firstDay) { add(null) } - for (d in 1..daysInMonth) add(d) - while (size < 35) add(null) + val gridCells = + remember(ym) { + buildList { + repeat(firstDayOfWeek) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) + } } - } var selectedDay by remember { mutableStateOf(null) } + // ✅ 메인 레이아웃 Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 20.dp) + modifier = + Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 20.dp), ) { Spacer(modifier = Modifier.height(16.dp)) + // 🔙 뒤로가기 버튼 Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), - contentDescription = null, - tint = Color.Unspecified + contentDescription = "Back", + tint = Color.Unspecified, ) } } Spacer(modifier = Modifier.height(21.dp)) + // 🔹 상단 이름 섹션 (루틴명 + 아이콘) Row( modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically ) { - // 🔙 이전 기록으로 이동 - IconButton( - onClick = { - val prevId = timerViewModel.getPrevRecordId(recordId) - if (prevId != null) { - // title / color는 생략해도 ViewModel에서 다시 가져오니까 OK - navController.navigate("recordDetail/$prevId") - } - } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = Color.Unspecified - ) - } + Icon( + painter = painterResource(id = R.drawable.ic_left_triangle), + contentDescription = "Prev Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp), + ) Spacer(modifier = Modifier.width(38.dp)) - Box( - modifier = Modifier - .size(26.dp) - .clip(CircleShape) - .background(recordColor) - .then( - if (recordId == "0" || recordColor == Color.White) - Modifier.border( - width = 2.dp, - color = Color.LightGray, - shape = CircleShape - ) - else Modifier - ) + modifier = + Modifier + .size(26.dp) + .clip(CircleShape) + .background(color = recordColor), ) - Spacer(modifier = Modifier.width(12.dp)) - Text(recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(text = recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(12.dp)) - Icon( painter = painterResource(id = R.drawable.ic_trashcan), - contentDescription = null, + contentDescription = "Delete", + tint = Color.Unspecified, modifier = Modifier.size(20.dp), - tint = Color.Unspecified ) - Spacer(modifier = Modifier.width(38.dp)) - // ▶ 다음 기록으로 이동 - IconButton( - onClick = { - val nextId = timerViewModel.getNextRecordId(recordId) - if (nextId != null) { - navController.navigate("recordDetail/$nextId") - } - } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = Color.Unspecified - ) - } + Icon( + painter = painterResource(id = R.drawable.ic_right_triangle), + contentDescription = "Next Month", + tint = Color.Unspecified, + modifier = Modifier.size(20.dp), + ) } Spacer(modifier = Modifier.height(21.dp)) - // 달력 UI + // ✅ 달력 박스 Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFF3F3F3)) + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFF3F3F3)) + .align(Alignment.CenterHorizontally), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(20.dp)) + // 🔹 월 변경 헤더 Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = null, + contentDescription = "Prev Month", + tint = Color.Unspecified, modifier = Modifier.size(20.dp), - tint = Color.Unspecified ) } Spacer(modifier = Modifier.width(38.dp)) - Text(monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(text = monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(38.dp)) IconButton(onClick = { monthState.value = monthState.value.plusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = null, + contentDescription = "Next Month", + tint = Color.Unspecified, modifier = Modifier.size(20.dp), - tint = Color.Unspecified ) } } Spacer(modifier = Modifier.height(8.dp)) + // 🔹 요일 헤더 Row( - modifier = Modifier + Modifier .fillMaxWidth() .padding(horizontal = 49.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp) + horizontalArrangement = Arrangement.spacedBy(30.dp), ) { listOf("월", "화", "수", "목", "금", "토", "일").forEach { Text( - it, + text = it, fontSize = 14.sp, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) } } Spacer(modifier = Modifier.height(8.dp)) - // Calendar grid + // 🔹 달력 (5주) Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(vertical = 6.dp) + modifier = Modifier.padding(top = 6.dp, bottom = 4.dp), ) { gridCells.chunked(7).forEach { week -> Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { - week.forEach { dayVal -> - val isStudy = dayVal != null && studyDaySet.contains(dayVal) - val isSelected = selectedDay == dayVal - + week.forEach { dayOrNull -> + val isStudy = dayOrNull != null && studyDaySet.contains(dayOrNull) + val isSelected = selectedDay == dayOrNull val color = when { - dayVal == null -> Color(0xFFE0E0E0) + dayOrNull == null -> Color(0xFFE0E0E0) isSelected -> Color(0xFFFF8686) isStudy -> Color(0xFFB3E5C2) else -> Color(0xFFE0E0E0) } Box( - modifier = Modifier - .size(34.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color) - .clickable(enabled = isStudy) { - selectedDay = dayVal - } + modifier = + Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) + .clickable(enabled = isStudy) { + selectedDay = dayOrNull + }, ) } } @@ -279,179 +244,126 @@ fun RecordDetailScreen( } } } - // ------------------------- - // 상세 데이터 영역 - // ------------------------- - selectedDay?.let { day -> - - val details = - if (recordId == "0") { - // 전체 타이머 → 해당 날짜 기록 전체 - timerViewModel.getRecordDetailByDay(day) - } else { - val currentTitle = recordTitle // 예: "자바스크립트" - timerViewModel.getRecordDetailByDay(day) - .filter { it.title == currentTitle } - } - - - val totalSec = details.sumOf { item -> - item.records.sumOf { r -> - val t = r.duration.split(":") - val h = t[0].toInt() - val m = t[1].toInt() - val s = t[2].toInt() - h * 3600 + m * 60 + s - } - } - val totalFormatted = String.format( - "%02d:%02d:%02d", - totalSec / 3600, - (totalSec % 3600) / 60, - totalSec % 60 - ) + // ✅ 선택된 날짜의 타이머 기록 영역 + selectedDay?.let { + Spacer(modifier = Modifier.height(20.dp)) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .weight(1f) + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { - - // 🔥 전체(recordId == "0")일 때만 전체시간 카드 노출 - if (recordId == "0") { - item { - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 49.dp), - horizontalArrangement = Arrangement.End - ) { - Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) - } - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - - Box( - modifier = Modifier - .size(33.dp) - .clip(CircleShape) - .background(Color.White) - .border(2.dp, Color.LightGray, CircleShape) - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text("전체 시간", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) - } - - Text( - totalFormatted, - fontSize = 32.sp, - modifier = Modifier.padding(end = 18.dp) - ) - } - - Spacer(modifier = Modifier.height(13.dp)) - Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) - } + // 🔹 "총 집중 시간" + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End, + ) { + Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) } - // 🔥🔥🔥 (중요) 개별 타이머 리스트 — 전체/특정 모두 공통 - items( - items = details, - key = { it.title } // 고유 key - ) { item -> - - Spacer(modifier = Modifier.height(26.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier + // 🔹 색상 + 타이머명 + 총 시간 + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier .size(33.dp) .clip(CircleShape) - .background(item.color) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(item.title, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) - } - - Text( - item.totalTimeFormatted, - fontSize = 32.sp, - modifier = Modifier.padding(end = 18.dp) + .background(color = recordColor), ) + Spacer(modifier = Modifier.width(8.dp)) + Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } - Spacer(modifier = Modifier.height(13.dp)) + Text( + "00:20:00", + fontSize = 32.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(end = 18.dp), + ) + } - Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + Spacer(modifier = Modifier.height(8.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + Spacer(modifier = Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(4.dp)) + // 🔹 헤더 + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "시작", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + modifier = Modifier.width(90.dp), + ) + Text( + "종료", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + modifier = Modifier.width(90.dp), + ) + Text( + "집중 시간", + fontWeight = FontWeight.SemiBold, + color = Color.Gray, + fontSize = 15.sp, + ) + } - Row( - modifier = Modifier.padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "시작", - modifier = Modifier.width(90.dp), - color = Color.Gray, - fontSize = 15.sp - ) - Text( - "종료", - modifier = Modifier.width(90.dp), - color = Color.Gray, - fontSize = 15.sp - ) - Text("집중 시간", color = Color.Gray, fontSize = 15.sp) - } + Spacer(modifier = Modifier.height(6.dp)) - Spacer(modifier = Modifier.height(6.dp)) + // 🔹 예시 데이터 행 + val focusList = + listOf( + Triple("13:00:00 ~", "13:10:00", "00:10:00"), + Triple("14:00:00 ~", "14:10:00", "00:10:00"), + ) - item.records.forEach { row -> - Row( - modifier = Modifier + focusList.forEach { (start, end, duration) -> + Row( + modifier = + Modifier .fillMaxWidth() - .padding(start = 46.dp) - ) { - Text(row.start, modifier = Modifier.width(90.dp)) - Text(row.end, modifier = Modifier.width(90.dp)) - Text(row.duration) - } + .padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = start, fontSize = 16.sp, modifier = Modifier.width(90.dp)) + Text(text = end, fontSize = 16.sp, modifier = Modifier.width(90.dp)) + Text(text = duration, fontSize = 16.sp) } - - Spacer(modifier = Modifier.height(18.dp)) - Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) } } } } } -@Preview(showBackground = true) +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Composable fun PreviewRecordDetailScreen() { val navController = rememberNavController() + val dummyViewModel = remember { TimerViewModel() } // ✅ Preview용 RecordDetailScreen( navController = navController, recordId = "1", - timerViewModel = TimerViewModel() + timerViewModel = dummyViewModel, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt index be64014..2daa0d4 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerProgress.kt @@ -29,14 +29,14 @@ fun TimerProgress( elapsedSeconds: Long, maxDisplaySeconds: Long = 60 * 60L, modifier: Modifier = Modifier, - progressColor: Color = Color(0xFFB8E9BB) + progressColor: Color = Color(0xFFB8E9BB), ) { val density = LocalDensity.current val remainingSeconds = (totalSeconds - elapsedSeconds).coerceAtLeast(0L) val remainingRatio = (remainingSeconds.toFloat() / maxDisplaySeconds.toFloat()).coerceIn(0f, 1f) val animatedProgress by animateFloatAsState( targetValue = remainingRatio, - animationSpec = tween(300, easing = LinearEasing) + animationSpec = tween(300, easing = LinearEasing), ) Canvas(modifier = modifier.aspectRatio(1f)) { @@ -51,7 +51,7 @@ fun TimerProgress( startAngle = -90f, sweepAngle = 360f * animatedProgress, useCenter = true, - style = Fill + style = Fill, ) // 물방울 @@ -61,13 +61,14 @@ fun TimerProgress( // 뿔 val hornWidthPx = 23.dp.toPx() val hornHeightPx = 25.dp.toPx() - val path = Path().apply { - val tipY = center.y - dropRadiusPx - hornHeightPx / 2 - moveTo(center.x, tipY) - lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) - close() - } + val path = + Path().apply { + val tipY = center.y - dropRadiusPx - hornHeightPx / 2 + moveTo(center.x, tipY) + lineTo(center.x - hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + lineTo(center.x + hornWidthPx / 2, center.y - dropRadiusPx + hornHeightPx / 2) + close() + } drawPath(path, color = Color.White) } } @@ -91,8 +92,8 @@ fun TimerProgressDemo() { @Preview( showBackground = true, name = "45분 시작 - 실시간 감소", - widthDp = 500, // 그대로 - heightDp = 500 // 그대로 + widthDp = 500, // 그대로 + heightDp = 500, // 그대로 ) @Composable fun Preview45MinInteractive() { @@ -110,6 +111,6 @@ fun Preview45MinInteractive() { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.size(190.dp) + modifier = Modifier.size(190.dp), ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index a600c39..8268bfe 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -47,101 +47,67 @@ fun TimerRunningScreen( title: String, totalSeconds: Long, ) { - // 🔹 경과 시간 - var elapsed by remember { mutableStateOf(0L) } + // 🔹 상태 변수들 + var elapsed by remember { mutableStateOf(0L) } // 경과 시간 + var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 - // 🔹 타이머 동작 여부 - var isRunning by remember { mutableStateOf(true) } - - // 초기화 버튼 클릭 여부 - var isReset by remember { mutableStateOf(false) } - - // 🔹 1시간 이벤트 발동 여부 - var hourReached by remember { mutableStateOf(false) } - - // 🔹 1시간 이벤트 유지 시간 (10분 유지) - var hourEventElapsed by remember { mutableStateOf(0L) } + // 🔹 남은시간 / 집중시간 계산 + val remainingSeconds = (totalSeconds - elapsed).coerceAtLeast(0L) + val focusSeconds = totalSeconds - remainingSeconds - // 🔹 1시간 이벤트 체크 + // ⏰ 시/분/초 포맷 + val timeText = + String.format( + "%02d:%02d:%02d", + remainingSeconds / 3600, + (remainingSeconds % 3600) / 60, + remainingSeconds % 60, + ) + val focusTimeText = + String.format( + "%02d:%02d:%02d", + focusSeconds / 3600, + (focusSeconds % 3600) / 60, + focusSeconds % 60, + ) + + // ⏱ 타이머 동작 제어 LaunchedEffect(isRunning) { while (isRunning && elapsed < totalSeconds) { - delay(1000) elapsed++ - - // ⭐ 정확히 1시간(3600초) 도달 시 - if (elapsed == 3600L && !hourReached) { - hourReached = true - hourEventElapsed = 0L - } } } - // 🔹 1시간 이벤트가 발동하면 10분 유지 후 게이지 리셋 - LaunchedEffect(hourReached) { - if (hourReached) { - - // 10분 = 600초 동안 유지 - while (hourEventElapsed < 600L) { - delay(1000) - hourEventElapsed++ - } - - // 10분 지나면 이벤트 종료 + 게이지 리셋 - hourReached = false - elapsed = 0L // ⭐ 게이지 다시 맨 처음부터 돌게 하기 - } - } - - // 🔹 남은시간 / 집중시간 계산 - val remainingSeconds = - if (hourReached) (3600 - hourEventElapsed).coerceAtLeast(0L) - else (totalSeconds - elapsed).coerceAtLeast(0L) - - val focusSeconds = - if (hourReached) 3600 - else totalSeconds - remainingSeconds - - // ⏰ 시/분/초 포맷 - val timeText = String.format( - "%02d:%02d:%02d", - remainingSeconds / 3600, - (remainingSeconds % 3600) / 60, - remainingSeconds % 60 - ) - val focusTimeText = String.format( - "%02d:%02d:%02d", - focusSeconds / 3600, - (focusSeconds % 3600) / 60, - focusSeconds % 60 - ) - // 🔹 타이머가 끝난 상태 감지 val isFinished = elapsed >= totalSeconds Surface( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - color = Color.White + modifier = + Modifier + .fillMaxSize() + .background(Color.White), + color = Color.White, ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { // 🔹 상단 바 Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = Modifier - .size(28.dp) - .clickable { navController.popBackStack() } + modifier = + Modifier + .size(28.dp) + .clickable { navController.popBackStack() }, ) } @@ -150,12 +116,13 @@ fun TimerRunningScreen( // 🔹 타이머명 + 색상 표시 Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, ) { Box( - modifier = Modifier - .size(16.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(16.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(8.dp)) Text(title, fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -164,44 +131,44 @@ fun TimerRunningScreen( Spacer(modifier = Modifier.height(24.dp)) // 🔹 원형 타이머 색상 - val progressColor = when { - isFinished -> Color(0xFF6D6D6D) // 완료 → 회색 - isRunning -> color // 실행 → 본래 색 - isReset -> color // 초기화 → 본래 색 유지 - else -> Color(0xFF6D6D6D) // 일시정지 → 회색 - } - + val progressColor = + when { + isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 + isRunning -> color + else -> Color(0xFF6D6D6D) + } Box( - modifier = Modifier - .size(220.dp) - .aspectRatio(1f) - .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(220.dp) + .aspectRatio(1f) + .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center, ) { TimerProgress( - totalSeconds = if (hourReached) 3600 else totalSeconds, - elapsedSeconds = if (hourReached) hourEventElapsed else elapsed, + totalSeconds = totalSeconds, + elapsedSeconds = elapsed, modifier = Modifier.fillMaxSize(), - progressColor = progressColor + progressColor = progressColor, ) } Spacer(modifier = Modifier.height(24.dp)) // 🔹 상태별 멘트 변경 - val motivationText = when { - hourReached -> "🎉🎊축하합니다!🎊🎉\n1시간을 완수했어요!" - isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" - isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" - else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" - } + val motivationText = + when { + isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" + isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" + else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" + } Text( motivationText, textAlign = TextAlign.Center, - color = if (hourReached || isFinished) Color(0xFF4CAF50) else Color.Gray, - fontWeight = if (hourReached || isFinished) FontWeight.Bold else FontWeight.Normal + color = if (isFinished) Color(0xFF4CAF50) else Color.Gray, // 완료 시 초록색 느낌 + fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal, ) Spacer(modifier = Modifier.height(24.dp)) @@ -228,11 +195,10 @@ fun TimerRunningScreen( } else { // ⏸ 일시정지 / ▶ 재시작 토글 isRunning = !isRunning - isReset = false } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text( when { @@ -240,18 +206,18 @@ fun TimerRunningScreen( isRunning -> "⏸ 일시 정지" else -> "▶ 실행" }, - color = Color.Black + color = Color.Black, ) } Button( onClick = { + // 🔄 초기화 elapsed = 0L isRunning = false - isReset = true // ⭐ 초기화 상태 활성화 }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text("🔄 초기화", color = Color.Black) } @@ -267,6 +233,6 @@ fun TimerRunningPreview() { navController = rememberNavController(), color = Color(0xFFFF8686), title = "자바스크립트", - totalSeconds = 60 * 60L + totalSeconds = 60 * 60L, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index b2e57c6..bb34425 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -56,32 +56,37 @@ fun TimerScreen( hour: Int = 0, minute: Int = 0, second: Int = 0, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val timerList by remember { derivedStateOf { timerViewModel.timers } } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp - val circleSize = when { - screenWidth < 360.dp -> 150.dp - screenWidth < 600.dp -> 190.dp - else -> 250.dp - } + val circleSize = + when { + screenWidth < 360.dp -> 150.dp + screenWidth < 600.dp -> 190.dp + else -> 250.dp + } var elapsed by remember { mutableStateOf(0L) } - val selectedTab = timerViewModel.selectedTab + var selectedTab by remember { mutableStateOf(0) } - val selectedColor = remember(colorValue) { - try { - if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) - else Color(colorValue.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) + val selectedColor = + remember(colorValue) { + try { + if (colorValue == "0" || colorValue.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(colorValue.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } } - } - val total = timerViewModel.getTotalTimerSeconds() + val total = (hour * 3600L) + (minute * 60L) + second val remainingSeconds = (total - elapsed).coerceAtLeast(0L) val h = remainingSeconds / 3600 val m = (remainingSeconds % 3600) / 60 @@ -90,37 +95,40 @@ fun TimerScreen( Surface( modifier = Modifier.fillMaxSize(), - color = Color.Transparent + color = Color.Transparent, ) { Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) + modifier = + Modifier + .fillMaxSize() + .background(Color.White), ) { Column( modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { // ✅ 상단 타이머 영역 Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.45f) - .background(Color(0xFFF5F5F5)), - contentAlignment = Alignment.Center + modifier = + Modifier + .fillMaxWidth() + .weight(0.45f) + .background(Color(0xFFF5F5F5)), + contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier - .size(circleSize) - .aspectRatio(1f) - .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(circleSize) + .aspectRatio(1f) + .background(Color(0xFFD9D9D9), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center, ) { TimerProgress( totalSeconds = total, elapsedSeconds = elapsed, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } @@ -131,59 +139,61 @@ fun TimerScreen( text = timeText, fontSize = if (screenWidth < 360.dp) 28.sp else 36.sp, fontWeight = FontWeight.Bold, - color = Color.Black + color = Color.Black, ) } } // ✅ 하단 영역 (탭 전환) Surface( - modifier = Modifier - .fillMaxWidth() - .weight(0.55f), - color = Color.White + modifier = + Modifier + .fillMaxWidth() + .weight(0.55f), + color = Color.White, ) { Column { // 🔹 탭 바 Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 41.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 41.dp), ) { TabRow( selectedTabIndex = selectedTab, containerColor = Color.Transparent, divider = { }, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), indicator = { tabPositions -> Box( - modifier = Modifier - .tabIndicatorOffset(tabPositions[selectedTab]) - .height(2.dp) - .background(Color.Black) + modifier = + Modifier + .tabIndicatorOffset(tabPositions[selectedTab]) + .height(2.dp) + .background(Color.Black), ) - } + }, ) { - Tab(selected = selectedTab == 0, onClick = { timerViewModel.updateSelectedTab(0) } - ) { + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text( "타이머", fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 0) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) + modifier = Modifier.padding(vertical = 12.dp), ) } - Tab(selected = selectedTab == 1, onClick = { timerViewModel.updateSelectedTab(1) } - ) { - Text( + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { + Text( "기록", fontWeight = FontWeight.Medium, fontSize = 14.sp, color = if (selectedTab == 1) Color.Black else Color.Gray, - modifier = Modifier.padding(vertical = 12.dp) + modifier = Modifier.padding(vertical = 12.dp), ) } } @@ -196,26 +206,29 @@ fun TimerScreen( if (selectedTab == 0) { // ✅ 타이머 탭 LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 80.dp) + modifier = + Modifier + .fillMaxSize() + .padding(bottom = 80.dp), ) { items(timerList) { timer -> SwipeableStudyItem( color = timer.color, title = timer.title, - time = String.format( - "%02d:%02d:%02d", - timer.hour, - timer.minute, - timer.second - ), + time = + String.format( + "%02d:%02d:%02d", + timer.hour, + timer.minute, + timer.second, + ), onEdit = { val encodedTitle = + URLEncoder.encode(timer.title, "UTF-8") val colorValueStr = timer.color.value.toString() navController.navigate( - "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}" + "timerSetting?title=$encodedTitle&color=$colorValueStr&hour=${timer.hour}&minute=${timer.minute}&second=${timer.second}&id=${timer.id}", ) }, onDelete = { timerViewModel.deleteTimer(timer.id) }, @@ -226,9 +239,9 @@ fun TimerScreen( val total = timer.hour * 3600 + timer.minute * 60 + timer.second navController.navigate( - "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total" + "timerRunning?title=$encodedTitle&color=$colorValueStr&total=$total", ) - } + }, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -239,40 +252,46 @@ fun TimerScreen( onClick = { timerViewModel.addTimer( TimerItemData( - id = java.util.UUID.randomUUID().toString(), + id = + java.util.UUID + .randomUUID() + .toString(), title = "새 타이머", color = Color(0xFFD9D9D9), hour = 0, minute = 0, - second = 0 - ) + second = 0, + ), ) }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(24.dp) + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(24.dp), ) { Icon( painter = painterResource(id = R.drawable.ic_add_button), contentDescription = "추가", tint = Color.Unspecified, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(32.dp), ) } } else if (selectedTab == 1) { // ✅ 기록 탭 LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), ) { items(timerViewModel.records) { record -> // 🔹 안전한 Color 변환 (핵심 수정) - val recordColor: Color = when (val c = record.color) { - is Color -> c - is Int -> Color(c) - else -> Color(0xFFD9D9D9) - } + val recordColor: Color = + when (val c = record.color) { + is Color -> c + is Int -> Color(c) + else -> Color(0xFFD9D9D9) + } val encodedTitle = URLEncoder.encode(record.title, "UTF-8") val colorValueStr = recordColor.value.toString() @@ -284,9 +303,9 @@ fun TimerScreen( dayCounts = record.dayCounts, onClick = { navController.navigate( - "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr" + "recordDetail/${record.id}?title=$encodedTitle&color=$colorValueStr", ) - } + }, ) Spacer(modifier = Modifier.height(8.dp)) } @@ -308,7 +327,7 @@ fun TimerScreenPreview() { MaterialTheme { TimerScreen( navController = rememberNavController(), - timerViewModel = dummyViewModel + timerViewModel = dummyViewModel, ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index 9c2c810..a19db57 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -1,5 +1,6 @@ package com.sprinter.studyspot.timer.ui +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -54,7 +55,7 @@ private fun TimeUnitField( value: String, onValueChange: (String) -> Unit, maxLength: Int, - maxValue: Int + maxValue: Int, ) { var internalValue by remember(value) { mutableStateOf(value) } @@ -74,47 +75,54 @@ private fun TimeUnitField( onValueChange(digits) } }, - modifier = Modifier - .width(64.dp) - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) - .border( - width = if (isError) 2.dp else 0.dp, - color = if (isError) Color.Red else Color.Transparent, - shape = RoundedCornerShape(30.dp) + modifier = + Modifier + .width(64.dp) + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)) + .border( + width = if (isError) 2.dp else 0.dp, + color = if (isError) Color.Red else Color.Transparent, + shape = RoundedCornerShape(30.dp), + ), + textStyle = + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = if (isError) Color.Red else Color.Black, + textAlign = TextAlign.Center, ), - textStyle = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = if (isError) Color.Red else Color.Black, - textAlign = TextAlign.Center - ), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), visualTransformation = { text -> val padded = text.text.padStart(2, '0').take(2) TransformedText( - text = androidx.compose.ui.text.AnnotatedString(padded), - offsetMapping = object : OffsetMapping { - override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) - override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) - } + text = + androidx.compose.ui.text + .AnnotatedString(padded), + offsetMapping = + object : OffsetMapping { + override fun originalToTransformed(offset: Int) = offset.coerceAtMost(2) + + override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(2) + }, ) }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ), + colors = + TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ), placeholder = { Text( "00", color = Color.Gray, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) - } + }, ) } @@ -123,21 +131,15 @@ private fun TimeUnitField( fun TimerSettingScreen( navController: NavController? = null, timerViewModel: TimerViewModel, - initialTitle: String = "새 타이머", + initialTitle: String = "자바스크립트", initialHour: Int = 0, initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, - timerId: String? = null + timerId: String? = null, ) { - val decodedTitle = try { - java.net.URLDecoder.decode(initialTitle, "UTF-8") - } catch (e: Exception) { - initialTitle - } - var selectedColor by remember { mutableStateOf(initialColor) } - var timerTitle by remember { mutableStateOf(decodedTitle) } + var timerTitle by remember { mutableStateOf(initialTitle) } var hourStr by remember { mutableStateOf(initialHour.toString().padStart(2, '0')) } var minuteStr by remember { mutableStateOf(initialMinute.toString().padStart(2, '0')) } @@ -150,23 +152,26 @@ fun TimerSettingScreen( Surface(modifier = Modifier.fillMaxSize(), color = Color.White) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, start = 11.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 11.dp), + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = Modifier - .size(28.dp) - .clickable { navController?.popBackStack() } + modifier = + Modifier + .size(28.dp) + .clickable { navController?.popBackStack() }, ) } @@ -176,28 +181,34 @@ fun TimerSettingScreen( fontSize = 14.sp, color = Color.DarkGray, textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Spacer(modifier = Modifier.height(28.dp)) // ✅ 색상 선택 - val colors = listOf( - Color(0xFFB8E9BB), Color(0xFFFFDB86), Color(0xFFFF8686), - Color(0xFFF9C0EB), Color(0xFFD2ACAC), Color(0xFFD9D9D9), Color(0xFFAECDE5) - ) + val colors = + listOf( + Color(0xFFB8E9BB), + Color(0xFFFFDB86), + Color(0xFFFF8686), + Color(0xFFF9C0EB), + Color(0xFFD2ACAC), + Color(0xFFD9D9D9), + Color(0xFFAECDE5), + ) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { colors.forEach { color -> Box( - modifier = Modifier - .size(27.dp, 26.dp) - .background(color, RoundedCornerShape(8.dp)) - .border( - width = if (selectedColor == color) 3.dp else 0.dp, - color = if (selectedColor == color) Color.Black else Color.Transparent, - shape = RoundedCornerShape(8.dp) - ) - .clickable { selectedColor = color } + modifier = + Modifier + .size(27.dp, 26.dp) + .background(color, RoundedCornerShape(8.dp)) + .border( + width = if (selectedColor == color) 3.dp else 0.dp, + color = if (selectedColor == color) Color.Black else Color.Transparent, + shape = RoundedCornerShape(8.dp), + ).clickable { selectedColor = color }, ) } } @@ -210,21 +221,24 @@ fun TimerSettingScreen( onValueChange = { timerTitle = it }, shape = RoundedCornerShape(30.dp), singleLine = true, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), + modifier = + Modifier + .fillMaxWidth() + .height(56.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(30.dp)), textStyle = TextStyle(fontSize = 16.sp, color = Color.Black), placeholder = { Text("타이머 이름", color = Color.Gray, fontSize = 16.sp) }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 - ), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ) + keyboardOptions = + KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text, // 한글/영문 둘 다 가능 + ), + colors = + TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ), ) Spacer(modifier = Modifier.height(32.dp)) @@ -232,13 +246,13 @@ fun TimerSettingScreen( Spacer(modifier = Modifier.height(12.dp)) Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { TimeUnitField( value = hourStr, onValueChange = { hourStr = it }, maxLength = 2, - maxValue = 99 + maxValue = 99, ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -247,7 +261,7 @@ fun TimerSettingScreen( value = minuteStr, onValueChange = { minuteStr = it }, maxLength = 2, - maxValue = 59 + maxValue = 59, ) Spacer(modifier = Modifier.width(8.dp)) Text(":", fontSize = 28.sp) @@ -256,50 +270,53 @@ fun TimerSettingScreen( value = secondStr, onValueChange = { secondStr = it }, maxLength = 2, - maxValue = 59 + maxValue = 59, ) } Spacer(modifier = Modifier.height(30.dp)) Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 16.dp), - contentAlignment = Alignment.CenterEnd + modifier = + Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd, ) { Surface( - modifier = Modifier - .size(77.dp, 41.dp) - .clickable(enabled = isValid) { - val safeTitle = timerTitle.ifBlank { "기본 타이머" } - val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") - val colorValue = selectedColor?.value?.toString() ?: "0" + modifier = + Modifier + .size(77.dp, 41.dp) + .clickable(enabled = isValid) { + val safeTitle = timerTitle.ifBlank { "기본 타이머" } + val encodedTitle = URLEncoder.encode(safeTitle, "UTF-8") + val colorValue = selectedColor?.value?.toString() ?: "0" - val updatedTimer = TimerItemData( - id = timerId ?: UUID.randomUUID().toString(), - title = safeTitle, - color = selectedColor ?: Color(0xFFD9D9D9), - hour = hour, - minute = minute, - second = second - ) - timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 + val updatedTimer = + TimerItemData( + id = timerId ?: UUID.randomUUID().toString(), + title = safeTitle, + color = selectedColor ?: Color(0xFFD9D9D9), + hour = hour, + minute = minute, + second = second, + ) + timerViewModel.updateTimer(updatedTimer) // ✅ ViewModel 업데이트 - navController?.navigate( - "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second" - ) { - popUpTo("timerSetting") { inclusive = true } - } - }, + navController?.navigate( + "timer?title=$encodedTitle&color=$colorValue&hour=$hour&minute=$minute&second=$second", + ) { + popUpTo("timerSetting") { inclusive = true } + } + }, shape = RoundedCornerShape(8.dp), - color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0) + color = if (isValid) Color(0xFFF0F0F0) else Color(0xFFE0E0E0), ) { Box(contentAlignment = Alignment.Center) { Icon( painter = painterResource(id = R.drawable.ic_confirm_check), contentDescription = "완료", tint = if (isValid) Color.Unspecified else Color.Gray, - modifier = Modifier.size(48.dp) + modifier = Modifier.size(48.dp), ) } } @@ -308,6 +325,7 @@ fun TimerSettingScreen( } } +@SuppressLint("ViewModelConstructorInComposable") @Preview(showBackground = true) @Composable fun TimerSettingPreview() { diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt index 4b3c75e..f518145 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/BottomNavBar.kt @@ -28,36 +28,37 @@ import com.sprinter.studyspot.R @Composable fun BottomNavBar( navController: androidx.navigation.NavController, - currentRoute: String + currentRoute: String, ) { Row( - modifier = Modifier - .fillMaxWidth() - .height(76.dp) - .background(Color.White) - .padding(horizontal = 36.dp), + modifier = + Modifier + .fillMaxWidth() + .height(76.dp) + .background(Color.White) + .padding(horizontal = 36.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, ) { BottomNavItem( icon = R.drawable.ic_clock, label = "타이머", isSelected = currentRoute == "timer", - onClick = { navController.navigate("timer") } + onClick = { navController.navigate("timer") }, ) BottomNavItem( icon = R.drawable.ic_home, label = "홈", isSelected = currentRoute == "home", - onClick = { navController.navigate("home") } + onClick = { navController.navigate("home") }, ) BottomNavItem( icon = R.drawable.ic_search, label = "검색", isSelected = currentRoute == "search", - onClick = { navController.navigate("search") } + onClick = { navController.navigate("search") }, ) } } @@ -67,26 +68,27 @@ private fun BottomNavItem( icon: Int, label: String, isSelected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, ) { Column( - modifier = Modifier - .width(60.dp) - .clickable(onClick = onClick), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .width(60.dp) + .clickable(onClick = onClick), + horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( painter = painterResource(id = icon), contentDescription = label, modifier = Modifier.size(20.dp), - tint = Color.Black + tint = Color.Black, ) Spacer(modifier = Modifier.height(4.dp)) Text( text = label, fontSize = 12.sp, color = Color.Black, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ) } } @@ -96,6 +98,6 @@ private fun BottomNavItem( fun BottomNavBarPreview() { BottomNavBar( navController = rememberNavController(), - currentRoute = "home" + currentRoute = "home", ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt index edbc887..e1ddc70 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordData.kt @@ -7,5 +7,5 @@ data class RecordData( val title: String, val color: Color, val totalTime: String, - val dayCounts: Map -) \ No newline at end of file + val dayCounts: Map, +) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index 9406166..f460419 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,18 +32,19 @@ fun RecordItemCard( title: String, totalTime: String, dayCounts: Map, // 요일별 학습 횟수 - onClick: (() -> Unit)? = null + onClick: (() -> Unit)? = null, ) { // 최대 횟수 대비 비율 계산 val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) // ✅ 잔디 색상 팔레트 (GitHub 느낌) - val grassPalette = listOf( - Color(0xFFE8F5E9), // 연한 녹색 - Color(0xFFC8E6C9), - Color(0xFF81C784), - Color(0xFF388E3C) // 진한 녹색 - ) + val grassPalette = + listOf( + Color(0xFFE8F5E9), // 연한 녹색 + Color(0xFFC8E6C9), + Color(0xFF81C784), + Color(0xFF388E3C), // 진한 녹색 + ) // ✅ 반응형 기기 크기 계산 val configuration = LocalConfiguration.current @@ -57,30 +57,32 @@ fun RecordItemCard( val boxPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) Surface( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - .testTag("record_card") - .clickable(enabled = onClick != null) { onClick?.invoke() }, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), - color = Color(0xFFF5F5F5) + color = Color(0xFFF5F5F5), ) { Box( - modifier = Modifier - .fillMaxWidth() - .padding(cardPadding) + modifier = + Modifier + .fillMaxWidth() + .padding(cardPadding), ) { // ✅ 왼쪽: 타이틀 + 요일 Column( - modifier = Modifier.align(Alignment.CenterStart) + modifier = Modifier.align(Alignment.CenterStart), ) { // 상단 타이틀 Row(verticalAlignment = Alignment.CenterVertically) { // 🔹 타이머별 고유 색상 원 Box( - modifier = Modifier - .size(26.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(26.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(12.dp)) Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold) @@ -95,32 +97,34 @@ fun RecordItemCard( days.forEachIndexed { index, day -> val count = dayCounts[index + 1] ?: 0 val ratio = count.toFloat() / maxCount.toFloat() - val colorIndex = when { - ratio == 0f -> -1 - ratio < 0.25f -> 0 - ratio < 0.5f -> 1 - ratio < 0.75f -> 2 - else -> 3 - } + val colorIndex = + when { + ratio == 0f -> -1 + ratio < 0.25f -> 0 + ratio < 0.5f -> 1 + ratio < 0.75f -> 2 + else -> 3 + } val grassColor = if (colorIndex == -1) Color(0xFFE0E0E0) else grassPalette[colorIndex] Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier - .size(26.dp) - .background( - color = grassColor, - shape = RoundedCornerShape(8.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(26.dp) + .background( + color = grassColor, + shape = RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.Center, ) {} Spacer(modifier = Modifier.height(4.dp)) Text( text = day, fontSize = 12.sp, - color = if (count > 0) Color.Black else Color.Gray + color = if (count > 0) Color.Black else Color.Gray, ) } } @@ -129,18 +133,19 @@ fun RecordItemCard( // ✅ 오른쪽: 중앙 정렬된 타이머 박스 (반응형) Box( - modifier = Modifier - .align(Alignment.CenterEnd) - .height(boxHeight) - .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) - .padding(horizontal = boxPadding), - contentAlignment = Alignment.Center + modifier = + Modifier + .align(Alignment.CenterEnd) + .height(boxHeight) + .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) + .padding(horizontal = boxPadding), + contentAlignment = Alignment.Center, ) { Text( text = totalTime, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = Color.Black + color = Color.Black, ) } } @@ -156,21 +161,21 @@ fun RecordItemCardPreview() { color = Color(0xFFFFFFFF), // 타이머 색상 title = "전체 시간", totalTime = "00:45", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFF8686), title = "자바스크립트", totalTime = "00:45", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFFDB86), title = "파이썬", totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt index be00561..95e61ea 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/StudyItem.kt @@ -22,36 +22,43 @@ import androidx.compose.ui.unit.sp // StudyItem (그대로 유지) @Composable -fun StudyItem(color: Color, title: String, time: String, onClick: () -> Unit) { +fun StudyItem( + color: Color, + title: String, + time: String, + onClick: () -> Unit, +) { val isCompact = LocalConfiguration.current.screenWidthDp < 360 Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 28.dp) - .padding(vertical = if (isCompact) 6.dp else 8.dp) - .clickable { onClick() } + modifier = + Modifier + .fillMaxWidth() + .padding(start = 28.dp) + .padding(vertical = if (isCompact) 6.dp else 8.dp) + .clickable { onClick() }, ) { Box( - modifier = Modifier - .size(if (isCompact) 31.dp else 33.dp) - .background(color, CircleShape) + modifier = + Modifier + .size(if (isCompact) 31.dp else 33.dp) + .background(color, CircleShape), ) Spacer(modifier = Modifier.width(if (isCompact) 8.dp else 12.dp)) Text( title, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 14.sp else 16.sp, - color = Color.Black + color = Color.Black, ) Spacer(modifier = Modifier.weight(1f)) Text( time, fontWeight = FontWeight.Medium, fontSize = if (isCompact) 30.sp else 32.sp, - color = Color.Gray + color = Color.Gray, ) Spacer(modifier = Modifier.width(34.dp)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt index b0690cf..f77a6b9 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/SwipeableStudyItem.kt @@ -54,23 +54,24 @@ fun SwipeableStudyItem( time: String, onEdit: () -> Unit, onDelete: () -> Unit, - onClick: () -> Unit + onClick: () -> Unit, ) { val density = LocalDensity.current var widthPx by remember { mutableStateOf(0f) } val decay = rememberSplineBasedDecay() - val state = remember { - AnchoredDraggableState( - initialValue = SwipeState.Closed, - positionalThreshold = { distance -> distance * 0.5f }, - velocityThreshold = { with(density) { 100.dp.toPx() } }, - snapAnimationSpec = tween(), - decayAnimationSpec = decay, - confirmValueChange = { true } - ) - } + val state = + remember { + AnchoredDraggableState( + initialValue = SwipeState.Closed, + positionalThreshold = { distance -> distance * 0.5f }, + velocityThreshold = { with(density) { 100.dp.toPx() } }, + snapAnimationSpec = tween(), + decayAnimationSpec = decay, + confirmValueChange = { true }, + ) + } // 앵커 업데이트: Swiped = 오른쪽으로 40% 이동 LaunchedEffect(widthPx) { @@ -78,8 +79,8 @@ fun SwipeableStudyItem( state.updateAnchors( DraggableAnchors { SwipeState.Closed at 0f - SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 - } + SwipeState.Swiped at +widthPx * 0.4f // 오른쪽으로 이동 + }, ) } } @@ -91,94 +92,99 @@ fun SwipeableStudyItem( if (current.isNaN()) return@derivedStateOf 0f val swipedPos = state.anchors.positionOf(SwipeState.Swiped) ?: 0f - current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) + current.coerceIn(0f, swipedPos) // min: 0f, max: swipedPos (양수) } } Box( - modifier = Modifier - .fillMaxWidth() - .height(64.dp) - .onGloballyPositioned { coordinates -> - widthPx = coordinates.size.width.toFloat() - } + modifier = + Modifier + .fillMaxWidth() + .height(64.dp) + .onGloballyPositioned { coordinates -> + widthPx = coordinates.size.width.toFloat() + }, ) { // 배경 버튼 (왼쪽에 고정) Row( - modifier = Modifier - .matchParentSize() - .background(Color(0xFFF0F0F0)) - .padding(horizontal = 24.dp), - horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier + .matchParentSize() + .background(Color(0xFFF0F0F0)) + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Start, // 왼쪽 정렬 + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_edit), contentDescription = "Edit", tint = Color.Black, - modifier = Modifier - .size(24.dp) - .clickable { onEdit() } + modifier = + Modifier + .size(24.dp) + .clickable { onEdit() }, ) Spacer(modifier = Modifier.width(24.dp)) Icon( painter = painterResource(id = R.drawable.ic_delete), contentDescription = "Delete", tint = Color.Black, - modifier = Modifier - .size(24.dp) - .clickable { onDelete() } + modifier = + Modifier + .size(24.dp) + .clickable { onDelete() }, ) } // 앞쪽 카드: 오른쪽으로 스와이프 Surface( color = Color.White, - modifier = Modifier - .offset { - if (widthPx <= 0f) { - IntOffset(0, 0) - } else { - IntOffset(safeOffsetX.roundToInt(), 0) - } - } - .anchoredDraggable( - state = state, - orientation = Orientation.Horizontal - ) - .background(Color.White) - .clickable( - enabled = state.currentValue == SwipeState.Closed, - onClick = onClick - ) + modifier = + Modifier + .offset { + if (widthPx <= 0f) { + IntOffset(0, 0) + } else { + IntOffset(safeOffsetX.roundToInt(), 0) + } + }.anchoredDraggable( + state = state, + orientation = Orientation.Horizontal, + ).background(Color.White) + .clickable( + enabled = state.currentValue == SwipeState.Closed, + onClick = onClick, + ), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 28.dp, end = 16.dp) - .height(64.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(start = 28.dp, end = 16.dp) + .height(64.dp), ) { Box( - modifier = Modifier - .size(33.dp) - .background(color, CircleShape) + modifier = + Modifier + .size(33.dp) + .background(color, CircleShape), ) Spacer(modifier = Modifier.width(12.dp)) Text( text = title, fontWeight = FontWeight.Medium, fontSize = 16.sp, - color = Color.Black + color = Color.Black, ) Spacer(modifier = Modifier.weight(1f)) Text( text = time, fontWeight = FontWeight.Medium, fontSize = 28.sp, - color = Color.Gray + color = Color.Gray, ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt index 9d8b944..e92a093 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/TimerItemData.kt @@ -9,5 +9,5 @@ data class TimerItemData( val color: Color, val hour: Int, val minute: Int, - val second: Int + val second: Int, ) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index 185c9e3..a980379 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -1,14 +1,8 @@ package com.sprinter.studyspot.timer.viewmodel -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel -import com.sprinter.studyspot.timer.model.RecordDetailItem -import com.sprinter.studyspot.timer.model.RecordDetailRowData -import com.sprinter.studyspot.timer.ui.component.DailyFocus import com.sprinter.studyspot.timer.ui.component.RecordData import com.sprinter.studyspot.timer.ui.component.TimerItemData @@ -16,37 +10,31 @@ class TimerViewModel : ViewModel() { private val _timers = mutableStateListOf() val timers: List get() = _timers - // 타이머의 총 소요시간 - fun getTotalTimerSeconds(): Long { - return timers.sumOf { timer -> - (timer.hour * 3600L) + (timer.minute * 60L) + timer.second - } - } - // ✅ 기록 데이터 (내부 mutable, 외부는 읽기 전용으로 노출) - private val _records = mutableStateListOf( - RecordData( - id = "0", - title = "전체 시간", - color = Color(0xFFFFFFFF), - totalTime = "02:10", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) - ), - RecordData( - id = "1", - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTime = "01:20", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) - ), - RecordData( - id = "2", - title = "파이썬", - color = Color(0xFFFFE382), - totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + // ✅ 기록 데이터 + val records = + listOf( + RecordData( + id = "0", + title = "전체 시간", + color = Color(0xFFFFFFFF), + totalTime = "02:10", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), + ), + RecordData( + id = "1", + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), + ), + RecordData( + id = "2", + title = "파이썬", + color = Color(0xFFFFE382), + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), + ), ) - ) - val records: List get() = _records fun addTimer(timer: TimerItemData) { _timers.add(timer) @@ -63,95 +51,4 @@ class TimerViewModel : ViewModel() { // ✅ recordId로 RecordData 가져오기 fun getRecordById(id: String): RecordData? = records.find { it.id == id } - - // 테스트에서 기록을 부여하기 위한 함수 - fun addRecordForTest( - title: String, - color: Color, - totalTime: String, - dayCounts: Map - ) { - _records.add( - RecordData( - id = System.currentTimeMillis().toString(), - title = title, - color = color, - totalTime = totalTime, - dayCounts = dayCounts - ) - ) - } - var selectedTab by mutableStateOf(0) - private set - - fun updateSelectedTab(tabIndex: Int) { - selectedTab = tabIndex - } - - // 다음 타이머 세부 내용 파악을 위한 함수 - fun getNextRecordId(currentId: String): String? { - val index = _records.indexOfFirst { it.id == currentId } - if (index != -1 && index < _records.lastIndex) { - return _records[index + 1].id - } - return null - } - - // 이전 타이머 세부 내용 파악을 위한 함수 - fun getPrevRecordId(currentId: String): String? { - val index = _records.indexOfFirst { it.id == currentId } - if (index > 0) { - return _records[index - 1].id - } - return null - } - - fun getDailyGroupedRecords(day: Int): Map> { - val result = mutableMapOf>() - - records.filter { it.id != "0" }.forEach { record -> - val count = record.dayCounts[day] ?: return@forEach - - // 예시: count 만큼 더미 데이터 생성 (지금은 더미 → 나중에 실제 데이터 연결) - repeat(count) { - val item = DailyFocus( - timerTitle = record.title, - color = record.color, - start = "13:00:00", - end = "13:10:00", - duration = "00:10:00" - ) - result.getOrPut(record.title) { mutableListOf() }.add(item) - } - } - - return result - } - - fun getRecordDetailByDay(day: Int): List { - - // TODO: 실제 기록 DB 연동 시 교체 가능 - val example = listOf( - RecordDetailItem( - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTimeFormatted = "00:20:00", - records = listOf( - RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), - RecordDetailRowData("14:00:00", "14:10:00", "00:10:00") - ) - ), - RecordDetailItem( - title = "파이썬", - color = Color(0xFFFFE382), - totalTimeFormatted = "00:10:00", - records = listOf( - RecordDetailRowData("13:00:00", "13:10:00", "00:10:00") - ) - ) - ) - - return example - } - -} \ No newline at end of file +} From 7b8adedf3b2736a73384b0ec2e30e04290474d4f Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 20:58:37 +0900 Subject: [PATCH 41/44] =?UTF-8?q?restore:=20=ED=8A=B9=EC=A0=95=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EB=8F=99=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 6 ------ gradle/libs.versions.toml | 10 ---------- 2 files changed, 16 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca57523..01efa91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,12 +55,6 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.3") testImplementation(libs.junit) - testImplementation(libs.kotlin.test) - testImplementation(libs.junit.jupiter.api) - testImplementation(libs.junit.jupiter.params) - testImplementation(libs.mockk) - testRuntimeOnly(libs.junit.jupiter.engine) - androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4815f00..236801c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,13 @@ [versions] agp = "8.12.3" -junitJupiterApi = "6.0.1" -junitJupiterEngine = "6.0.1" -junitJupiterParams = "6.0.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" -kotlinTest = "1.9.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.8.0" composeBom = "2024.09.00" -mockk = "1.14.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -29,11 +24,6 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" } -junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" } -junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } -mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From fe948453dcfa56b072dd8ceba9e1c8b9880f00f5 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 20:59:05 +0900 Subject: [PATCH 42/44] =?UTF-8?q?restore:=20=ED=8A=B9=EC=A0=95=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EB=8F=99=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 10 +- app/build.gradle.kts | 4 +- .../studyspot/navigation/AppNavGraph.kt | 173 +++---- .../studyspot/timer/ui/RecordDetailScreen.kt | 442 +++++++++++------- .../studyspot/timer/ui/TimerRunningScreen.kt | 176 ++++--- .../studyspot/timer/ui/TimerScreen.kt | 12 +- .../studyspot/timer/ui/TimerSettingScreen.kt | 10 +- .../timer/ui/component/RecordItemCard.kt | 97 ++-- .../timer/viewmodel/TimerViewModel.kt | 153 +++++- gradle/libs.versions.toml | 10 + 10 files changed, 645 insertions(+), 442 deletions(-) diff --git a/.editorconfig b/.editorconfig index 35fccf7..8bf3def 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,11 @@ [*.{kt,kts}] ktlint_standard_annotation = disabled -ktlint_function_naming_ignore_when_annotated_with = Composable \ No newline at end of file +ktlint_function_naming_ignore_when_annotated_with = Composable + +[app/src/test/java/**.kt] +ktlint_function_naming = disabled +ktlint_standard_function-naming = disabled + +[app/src/androidTest/java/**.kt] +ktlint_function_naming = disabled +ktlint_standard_function-naming = disabled \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01efa91..8448aa7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,9 @@ dependencies { // 네비게이션 바를 위한 의존성 추가 implementation("androidx.navigation:navigation-compose:2.8.3") - testImplementation(libs.junit) + // 테스트 케이스 작성을 위한 의존성 추가 + testImplementation("junit:junit:4.13.2") + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index 539fb42..ce63bd3 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -12,12 +12,14 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument -import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 import com.sprinter.studyspot.timer.ui.TimerRunningScreen import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel +import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 +import java.net.URLDecoder +import java.nio.charset.StandardCharsets @Composable fun AppNavGraph(navController: NavHostController) { @@ -34,41 +36,23 @@ fun AppNavGraph(navController: NavHostController) { ) { BottomNavBar(navController = navController, currentRoute = currentRoute) } - }, + } ) { innerPadding -> NavHost( navController = navController, startDestination = "timer", - modifier = - androidx.compose.ui.Modifier - .padding(innerPadding), + modifier = androidx.compose.ui.Modifier.padding(innerPadding) ) { // ✅ 타이머 메인 화면 composable( route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = - listOf( - navArgument("title") { - type = NavType.StringType - defaultValue = "기본 타이머" - }, - navArgument("color") { - type = NavType.StringType - defaultValue = "0" - }, - navArgument("hour") { - type = NavType.IntType - defaultValue = 0 - }, - navArgument("minute") { - type = NavType.IntType - defaultValue = 0 - }, - navArgument("second") { - type = NavType.IntType - defaultValue = 0 - }, - ), + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" @@ -83,40 +67,21 @@ fun AppNavGraph(navController: NavHostController) { title = title, hour = hour, minute = minute, - second = second, + second = second ) } // ✅ 타이머 설정 화면 composable( route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = - listOf( - navArgument("id") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("title") { - type = NavType.StringType - defaultValue = "자바스크립트" - }, - navArgument("color") { - type = NavType.StringType - defaultValue = "0" - }, - navArgument("hour") { - type = NavType.IntType - defaultValue = 0 - }, - navArgument("minute") { - type = NavType.IntType - defaultValue = 0 - }, - navArgument("second") { - type = NavType.IntType - defaultValue = 0 - }, - ), + arguments = listOf( + navArgument("id") { type = NavType.StringType; defaultValue = "" }, + navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, + navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, + navArgument("second") { type = NavType.IntType; defaultValue = 0 } + ) ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" @@ -125,16 +90,15 @@ fun AppNavGraph(navController: NavHostController) { val minute = backStackEntry.arguments?.getInt("minute") ?: 0 val second = backStackEntry.arguments?.getInt("second") ?: 0 - val initialColor = - if (colorValue == "0" || colorValue.isBlank()) { + val initialColor = if (colorValue == "0" || colorValue.isBlank()) { + null + } else { + try { + Color(colorValue.toULong()) + } catch (e: Exception) { null - } else { - try { - Color(colorValue.toULong()) - } catch (e: Exception) { - null - } } + } TimerSettingScreen( navController = navController, @@ -144,79 +108,70 @@ fun AppNavGraph(navController: NavHostController) { initialMinute = minute, initialSecond = second, initialColor = initialColor, - timerId = if (id.isNotEmpty()) id else null, + timerId = if (id.isNotEmpty()) id else null ) } // ✅ 타이머 실행 화면 composable( route = "timerRunning?title={title}&color={color}&total={total}", - arguments = - listOf( - navArgument("title") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("color") { - type = NavType.StringType - defaultValue = "0" - }, - navArgument("total") { - type = NavType.LongType - defaultValue = 0L - }, - ), + arguments = listOf( + navArgument("title") { type = NavType.StringType; defaultValue = "" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" }, + navArgument("total") { type = NavType.LongType; defaultValue = 0L } + ) ) { backStackEntry -> - val title = backStackEntry.arguments?.getString("title") ?: "" + val rawTitle = backStackEntry.arguments?.getString("title") ?: "기본 타이머" + val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val total = backStackEntry.arguments?.getLong("total") ?: 0L - val color = - try { - if (colorValue == "0" || colorValue.isBlank()) { - Color(0xFFD9D9D9) - } else { - Color(colorValue.toULong()) - } - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val color = try { + if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) + else Color(colorValue.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) + } TimerRunningScreen( navController = navController, title = title, color = color, - totalSeconds = total, + totalSeconds = total ) } - // ✅ 기록 상세 화면 +// ✅ 기록 상세 화면 composable( route = "recordDetail/{recordId}?title={title}&color={color}", - arguments = - listOf( - navArgument("recordId") { type = NavType.StringType }, - navArgument("title") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("color") { - type = NavType.LongType - defaultValue = 0xFFFFFFFF - }, - ), + arguments = listOf( + navArgument("recordId") { type = NavType.StringType }, + navArgument("title") { type = NavType.StringType; defaultValue = "" }, + navArgument("color") { type = NavType.StringType; defaultValue = "0" } // ← 수정됨 + ) ) { backStackEntry -> + val recordId = backStackEntry.arguments?.getString("recordId") ?: "" - val title = backStackEntry.arguments?.getString("title") ?: "" - val colorValue = backStackEntry.arguments?.getLong("color") ?: 0xFFFFFFFF - val color = Color(colorValue) + + // title 디코딩 + val rawTitle = backStackEntry.arguments?.getString("title") ?: "" + val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) + + // color 변환 (문자열 → Color) + val rawColor = backStackEntry.arguments?.getString("color") ?: "0" + val color = try { + if (rawColor == "0" || rawColor.isBlank()) Color(0xFFD9D9D9) + else Color(rawColor.toULong()) + } catch (e: Exception) { + Color(0xFFD9D9D9) + } RecordDetailScreen( navController = navController, recordId = recordId, title = title, color = color, - timerViewModel = timerViewModel, // ✅ ViewModel 전달 + timerViewModel = timerViewModel ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index d57968b..40d5c50 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -1,6 +1,7 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,6 +15,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider @@ -47,196 +50,228 @@ fun RecordDetailScreen( recordId: String, title: String? = null, color: Color? = null, - timerViewModel: TimerViewModel, + timerViewModel: TimerViewModel ) { - // ✅ ViewModel에서 데이터 조회 + val record = timerViewModel.getRecordById(recordId) val recordTitle = record?.title ?: title ?: "기록 상세" - // ✅ Color 타입 강제 고정 (핵심 수정) - val recordColor: Color = - when (val c = record?.color) { - is Color -> c - is Int -> Color(c) - else -> color ?: Color(0xFFD9D9D9) - } + val recordColor: Color = when (val c = record?.color) { + is Color -> c + is Int -> Color(c) + else -> color ?: Color(0xFFD9D9D9) + } - // ✅ 달력 관련 상태 정의 val monthState = remember { mutableStateOf(YearMonth.now()) } val monthTitle = "${monthState.value.monthValue}월" val ym = monthState.value val daysInMonth = ym.lengthOfMonth() - val firstDayOfWeek = (ym.atDay(1).dayOfWeek.value + 6) % 7 + val firstDay = (ym.atDay(1).dayOfWeek.value + 6) % 7 - val studyDaySet = setOf(4, 6, 12, 13, 19) // 이후 실제 데이터 연결 가능 + val combinedDayCounts: Map = + if (recordId == "0") + timerViewModel.records + .filter { it.id != "0" } + .flatMap { it.dayCounts.entries } + .groupBy { it.key } + .mapValues { it.value.sumOf { pair -> pair.value } } + else + record?.dayCounts ?: emptyMap() - val gridCells = - remember(ym) { - buildList { - repeat(firstDayOfWeek) { add(null) } - for (d in 1..daysInMonth) add(d) - while (size < 35) add(null) - } + val studyDaySet = combinedDayCounts.keys + + val gridCells = remember(ym) { + buildList { + repeat(firstDay) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) } + } var selectedDay by remember { mutableStateOf(null) } - // ✅ 메인 레이아웃 Column( - modifier = - Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 20.dp), + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 20.dp) ) { Spacer(modifier = Modifier.height(16.dp)) - // 🔙 뒤로가기 버튼 Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = { navController.popBackStack() }) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), - contentDescription = "Back", - tint = Color.Unspecified, + contentDescription = null, + tint = Color.Unspecified ) } } Spacer(modifier = Modifier.height(21.dp)) - // 🔹 상단 이름 섹션 (루틴명 + 아이콘) Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = "Prev Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp), - ) + // 🔙 이전 기록으로 이동 + IconButton( + onClick = { + val prevId = timerViewModel.getPrevRecordId(recordId) + if (prevId != null) { + // title / color는 생략해도 ViewModel에서 다시 가져오니까 OK + navController.navigate("recordDetail/$prevId") + } + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_left_triangle), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + } Spacer(modifier = Modifier.width(38.dp)) + Box( - modifier = - Modifier - .size(26.dp) - .clip(CircleShape) - .background(color = recordColor), + modifier = Modifier + .size(26.dp) + .clip(CircleShape) + .background(recordColor) + .then( + if (recordId == "0" || recordColor == Color.White) + Modifier.border( + width = 2.dp, + color = Color.LightGray, + shape = CircleShape + ) + else Modifier + ) ) + Spacer(modifier = Modifier.width(12.dp)) - Text(text = recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(recordTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(12.dp)) + Icon( painter = painterResource(id = R.drawable.ic_trashcan), - contentDescription = "Delete", - tint = Color.Unspecified, + contentDescription = null, modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) + Spacer(modifier = Modifier.width(38.dp)) - Icon( - painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = "Next Month", - tint = Color.Unspecified, - modifier = Modifier.size(20.dp), - ) + // ▶ 다음 기록으로 이동 + IconButton( + onClick = { + val nextId = timerViewModel.getNextRecordId(recordId) + if (nextId != null) { + navController.navigate("recordDetail/$nextId") + } + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_right_triangle), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + } } Spacer(modifier = Modifier.height(21.dp)) - // ✅ 달력 박스 + // 달력 UI Box( - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFF3F3F3)) - .align(Alignment.CenterHorizontally), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFF3F3F3)) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(20.dp)) - // 🔹 월 변경 헤더 Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), - contentDescription = "Prev Month", - tint = Color.Unspecified, + contentDescription = null, modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) } Spacer(modifier = Modifier.width(38.dp)) - Text(text = monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(monthTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.width(38.dp)) IconButton(onClick = { monthState.value = monthState.value.plusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_right_triangle), - contentDescription = "Next Month", - tint = Color.Unspecified, + contentDescription = null, modifier = Modifier.size(20.dp), + tint = Color.Unspecified ) } } Spacer(modifier = Modifier.height(8.dp)) - // 🔹 요일 헤더 Row( - Modifier + modifier = Modifier .fillMaxWidth() .padding(horizontal = 49.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) ) { listOf("월", "화", "수", "목", "금", "토", "일").forEach { Text( - text = it, + it, fontSize = 14.sp, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) } } Spacer(modifier = Modifier.height(8.dp)) - // 🔹 달력 (5주) + // Calendar grid Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 6.dp, bottom = 4.dp), + modifier = Modifier.padding(vertical = 6.dp) ) { gridCells.chunked(7).forEach { week -> Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { - week.forEach { dayOrNull -> - val isStudy = dayOrNull != null && studyDaySet.contains(dayOrNull) - val isSelected = selectedDay == dayOrNull + week.forEach { dayVal -> + val isStudy = dayVal != null && studyDaySet.contains(dayVal) + val isSelected = selectedDay == dayVal + val color = when { - dayOrNull == null -> Color(0xFFE0E0E0) + dayVal == null -> Color(0xFFE0E0E0) isSelected -> Color(0xFFFF8686) isStudy -> Color(0xFFB3E5C2) else -> Color(0xFFE0E0E0) } Box( - modifier = - Modifier - .size(34.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color) - .clickable(enabled = isStudy) { - selectedDay = dayOrNull - }, + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) + .clickable(enabled = isStudy) { + selectedDay = dayVal + } ) } } @@ -244,126 +279,179 @@ fun RecordDetailScreen( } } } + // ------------------------- + // 상세 데이터 영역 + // ------------------------- + selectedDay?.let { day -> + + val details = + if (recordId == "0") { + // 전체 타이머 → 해당 날짜 기록 전체 + timerViewModel.getRecordDetailByDay(day) + } else { + val currentTitle = recordTitle // 예: "자바스크립트" + timerViewModel.getRecordDetailByDay(day) + .filter { it.title == currentTitle } + } - // ✅ 선택된 날짜의 타이머 기록 영역 - selectedDay?.let { - Spacer(modifier = Modifier.height(20.dp)) - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + val totalSec = details.sumOf { item -> + item.records.sumOf { r -> + val t = r.duration.split(":") + val h = t[0].toInt() + val m = t[1].toInt() + val s = t[2].toInt() + h * 3600 + m * 60 + s + } + } + + val totalFormatted = String.format( + "%02d:%02d:%02d", + totalSec / 3600, + (totalSec % 3600) / 60, + totalSec % 60 + ) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .weight(1f) ) { - // 🔹 "총 집중 시간" - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(end = 49.dp), - horizontalArrangement = Arrangement.End, - ) { - Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) + + // 🔥 전체(recordId == "0")일 때만 전체시간 카드 노출 + if (recordId == "0") { + item { + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End + ) { + Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + + Box( + modifier = Modifier + .size(33.dp) + .clip(CircleShape) + .background(Color.White) + .border(2.dp, Color.LightGray, CircleShape) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text("전체 시간", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + + Text( + totalFormatted, + fontSize = 32.sp, + modifier = Modifier.padding(end = 18.dp) + ) + } + + Spacer(modifier = Modifier.height(13.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) + } } - // 🔹 색상 + 타이머명 + 총 시간 - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(top = 2.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier + // 🔥🔥🔥 (중요) 개별 타이머 리스트 — 전체/특정 모두 공통 + items( + items = details, + key = { it.title } // 고유 key + ) { item -> + + Spacer(modifier = Modifier.height(26.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier .size(33.dp) .clip(CircleShape) - .background(color = recordColor), + .background(item.color) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(item.title, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + } + + Text( + item.totalTimeFormatted, + fontSize = 32.sp, + modifier = Modifier.padding(end = 18.dp) ) - Spacer(modifier = Modifier.width(8.dp)) - Text(recordTitle, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } - Text( - "00:20:00", - fontSize = 32.sp, - fontWeight = FontWeight.Normal, - modifier = Modifier.padding(end = 18.dp), - ) - } + Spacer(modifier = Modifier.height(13.dp)) - Spacer(modifier = Modifier.height(8.dp)) - Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) - Spacer(modifier = Modifier.height(8.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) - // 🔹 헤더 - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - "시작", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - modifier = Modifier.width(90.dp), - ) - Text( - "종료", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - modifier = Modifier.width(90.dp), - ) - Text( - "집중 시간", - fontWeight = FontWeight.SemiBold, - color = Color.Gray, - fontSize = 15.sp, - ) - } + Spacer(modifier = Modifier.height(4.dp)) - Spacer(modifier = Modifier.height(6.dp)) + Row( + modifier = Modifier.padding(start = 46.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "시작", + modifier = Modifier.width(90.dp), + color = Color.Gray, + fontSize = 15.sp + ) + Text( + "종료", + modifier = Modifier.width(90.dp), + color = Color.Gray, + fontSize = 15.sp + ) + Text("집중 시간", color = Color.Gray, fontSize = 15.sp) + } - // 🔹 예시 데이터 행 - val focusList = - listOf( - Triple("13:00:00 ~", "13:10:00", "00:10:00"), - Triple("14:00:00 ~", "14:10:00", "00:10:00"), - ) + Spacer(modifier = Modifier.height(6.dp)) - focusList.forEach { (start, end, duration) -> - Row( - modifier = - Modifier + item.records.forEach { row -> + Row( + modifier = Modifier .fillMaxWidth() - .padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = start, fontSize = 16.sp, modifier = Modifier.width(90.dp)) - Text(text = end, fontSize = 16.sp, modifier = Modifier.width(90.dp)) - Text(text = duration, fontSize = 16.sp) + .padding(start = 46.dp) + ) { + Text(row.start, modifier = Modifier.width(90.dp)) + Text(row.end, modifier = Modifier.width(90.dp)) + Text(row.duration) + } } + + Spacer(modifier = Modifier.height(18.dp)) + Divider(thickness = 1.dp, color = Color(0xFFDDDDDD)) } } } } } -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Preview(showBackground = true) @Composable fun PreviewRecordDetailScreen() { val navController = rememberNavController() - val dummyViewModel = remember { TimerViewModel() } // ✅ Preview용 RecordDetailScreen( navController = navController, recordId = "1", - timerViewModel = dummyViewModel, + timerViewModel = TimerViewModel() ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index 8268bfe..a600c39 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -47,67 +47,101 @@ fun TimerRunningScreen( title: String, totalSeconds: Long, ) { - // 🔹 상태 변수들 - var elapsed by remember { mutableStateOf(0L) } // 경과 시간 - var isRunning by remember { mutableStateOf(true) } // 타이머 동작 여부 + // 🔹 경과 시간 + var elapsed by remember { mutableStateOf(0L) } - // 🔹 남은시간 / 집중시간 계산 - val remainingSeconds = (totalSeconds - elapsed).coerceAtLeast(0L) - val focusSeconds = totalSeconds - remainingSeconds + // 🔹 타이머 동작 여부 + var isRunning by remember { mutableStateOf(true) } - // ⏰ 시/분/초 포맷 - val timeText = - String.format( - "%02d:%02d:%02d", - remainingSeconds / 3600, - (remainingSeconds % 3600) / 60, - remainingSeconds % 60, - ) - val focusTimeText = - String.format( - "%02d:%02d:%02d", - focusSeconds / 3600, - (focusSeconds % 3600) / 60, - focusSeconds % 60, - ) - - // ⏱ 타이머 동작 제어 + // 초기화 버튼 클릭 여부 + var isReset by remember { mutableStateOf(false) } + + // 🔹 1시간 이벤트 발동 여부 + var hourReached by remember { mutableStateOf(false) } + + // 🔹 1시간 이벤트 유지 시간 (10분 유지) + var hourEventElapsed by remember { mutableStateOf(0L) } + + // 🔹 1시간 이벤트 체크 LaunchedEffect(isRunning) { while (isRunning && elapsed < totalSeconds) { + delay(1000) elapsed++ + + // ⭐ 정확히 1시간(3600초) 도달 시 + if (elapsed == 3600L && !hourReached) { + hourReached = true + hourEventElapsed = 0L + } } } + // 🔹 1시간 이벤트가 발동하면 10분 유지 후 게이지 리셋 + LaunchedEffect(hourReached) { + if (hourReached) { + + // 10분 = 600초 동안 유지 + while (hourEventElapsed < 600L) { + delay(1000) + hourEventElapsed++ + } + + // 10분 지나면 이벤트 종료 + 게이지 리셋 + hourReached = false + elapsed = 0L // ⭐ 게이지 다시 맨 처음부터 돌게 하기 + } + } + + // 🔹 남은시간 / 집중시간 계산 + val remainingSeconds = + if (hourReached) (3600 - hourEventElapsed).coerceAtLeast(0L) + else (totalSeconds - elapsed).coerceAtLeast(0L) + + val focusSeconds = + if (hourReached) 3600 + else totalSeconds - remainingSeconds + + // ⏰ 시/분/초 포맷 + val timeText = String.format( + "%02d:%02d:%02d", + remainingSeconds / 3600, + (remainingSeconds % 3600) / 60, + remainingSeconds % 60 + ) + val focusTimeText = String.format( + "%02d:%02d:%02d", + focusSeconds / 3600, + (focusSeconds % 3600) / 60, + focusSeconds % 60 + ) + // 🔹 타이머가 끝난 상태 감지 val isFinished = elapsed >= totalSeconds Surface( - modifier = - Modifier - .fillMaxSize() - .background(Color.White), - color = Color.White, + modifier = Modifier + .fillMaxSize() + .background(Color.White), + color = Color.White ) { Column( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { // 🔹 상단 바 Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = - Modifier - .size(28.dp) - .clickable { navController.popBackStack() }, + modifier = Modifier + .size(28.dp) + .clickable { navController.popBackStack() } ) } @@ -116,13 +150,12 @@ fun TimerRunningScreen( // 🔹 타이머명 + 색상 표시 Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Center ) { Box( - modifier = - Modifier - .size(16.dp) - .background(color = color, shape = RoundedCornerShape(50)), + modifier = Modifier + .size(16.dp) + .background(color = color, shape = RoundedCornerShape(50)) ) Spacer(modifier = Modifier.width(8.dp)) Text(title, fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -131,44 +164,44 @@ fun TimerRunningScreen( Spacer(modifier = Modifier.height(24.dp)) // 🔹 원형 타이머 색상 - val progressColor = - when { - isFinished -> Color(0xFF6D6D6D) // 완료 시 회색 고정 - isRunning -> color - else -> Color(0xFF6D6D6D) - } + val progressColor = when { + isFinished -> Color(0xFF6D6D6D) // 완료 → 회색 + isRunning -> color // 실행 → 본래 색 + isReset -> color // 초기화 → 본래 색 유지 + else -> Color(0xFF6D6D6D) // 일시정지 → 회색 + } + Box( - modifier = - Modifier - .size(220.dp) - .aspectRatio(1f) - .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center, + modifier = Modifier + .size(220.dp) + .aspectRatio(1f) + .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center ) { TimerProgress( - totalSeconds = totalSeconds, - elapsedSeconds = elapsed, + totalSeconds = if (hourReached) 3600 else totalSeconds, + elapsedSeconds = if (hourReached) hourEventElapsed else elapsed, modifier = Modifier.fillMaxSize(), - progressColor = progressColor, + progressColor = progressColor ) } Spacer(modifier = Modifier.height(24.dp)) // 🔹 상태별 멘트 변경 - val motivationText = - when { - isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" - isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" - else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" - } + val motivationText = when { + hourReached -> "🎉🎊축하합니다!🎊🎉\n1시간을 완수했어요!" + isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" + isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" + else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" + } Text( motivationText, textAlign = TextAlign.Center, - color = if (isFinished) Color(0xFF4CAF50) else Color.Gray, // 완료 시 초록색 느낌 - fontWeight = if (isFinished) FontWeight.Bold else FontWeight.Normal, + color = if (hourReached || isFinished) Color(0xFF4CAF50) else Color.Gray, + fontWeight = if (hourReached || isFinished) FontWeight.Bold else FontWeight.Normal ) Spacer(modifier = Modifier.height(24.dp)) @@ -195,10 +228,11 @@ fun TimerRunningScreen( } else { // ⏸ 일시정지 / ▶ 재시작 토글 isRunning = !isRunning + isReset = false } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp), + shape = RoundedCornerShape(8.dp) ) { Text( when { @@ -206,18 +240,18 @@ fun TimerRunningScreen( isRunning -> "⏸ 일시 정지" else -> "▶ 실행" }, - color = Color.Black, + color = Color.Black ) } Button( onClick = { - // 🔄 초기화 elapsed = 0L isRunning = false + isReset = true // ⭐ 초기화 상태 활성화 }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp), + shape = RoundedCornerShape(8.dp) ) { Text("🔄 초기화", color = Color.Black) } @@ -233,6 +267,6 @@ fun TimerRunningPreview() { navController = rememberNavController(), color = Color(0xFFFF8686), title = "자바스크립트", - totalSeconds = 60 * 60L, + totalSeconds = 60 * 60L ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index bb34425..21fe4a6 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -71,7 +71,7 @@ fun TimerScreen( } var elapsed by remember { mutableStateOf(0L) } - var selectedTab by remember { mutableStateOf(0) } + val selectedTab = timerViewModel.selectedTab val selectedColor = remember(colorValue) { @@ -86,7 +86,7 @@ fun TimerScreen( } } - val total = (hour * 3600L) + (minute * 60L) + second + val total = timerViewModel.getTotalTimerSeconds() val remainingSeconds = (total - elapsed).coerceAtLeast(0L) val h = remainingSeconds / 3600 val m = (remainingSeconds % 3600) / 60 @@ -178,7 +178,8 @@ fun TimerScreen( ) }, ) { - Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { + Tab(selected = selectedTab == 0, onClick = { timerViewModel.updateSelectedTab(0) } + ) { Text( "타이머", fontWeight = FontWeight.Medium, @@ -187,8 +188,9 @@ fun TimerScreen( modifier = Modifier.padding(vertical = 12.dp), ) } - Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { - Text( + Tab(selected = selectedTab == 1, onClick = { timerViewModel.updateSelectedTab(1) } + ) { + Text( "기록", fontWeight = FontWeight.Medium, fontSize = 14.sp, diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index a19db57..bcc9a1e 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -131,15 +131,21 @@ private fun TimeUnitField( fun TimerSettingScreen( navController: NavController? = null, timerViewModel: TimerViewModel, - initialTitle: String = "자바스크립트", + initialTitle: String = "새 타이머", initialHour: Int = 0, initialMinute: Int = 0, initialSecond: Int = 0, initialColor: Color? = null, timerId: String? = null, ) { + val decodedTitle = try { + java.net.URLDecoder.decode(initialTitle, "UTF-8") + } catch (e: Exception) { + initialTitle + } + var selectedColor by remember { mutableStateOf(initialColor) } - var timerTitle by remember { mutableStateOf(initialTitle) } + var timerTitle by remember { mutableStateOf(decodedTitle) } var hourStr by remember { mutableStateOf(initialHour.toString().padStart(2, '0')) } var minuteStr by remember { mutableStateOf(initialMinute.toString().padStart(2, '0')) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index f460419..9406166 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -32,19 +33,18 @@ fun RecordItemCard( title: String, totalTime: String, dayCounts: Map, // 요일별 학습 횟수 - onClick: (() -> Unit)? = null, + onClick: (() -> Unit)? = null ) { // 최대 횟수 대비 비율 계산 val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) // ✅ 잔디 색상 팔레트 (GitHub 느낌) - val grassPalette = - listOf( - Color(0xFFE8F5E9), // 연한 녹색 - Color(0xFFC8E6C9), - Color(0xFF81C784), - Color(0xFF388E3C), // 진한 녹색 - ) + val grassPalette = listOf( + Color(0xFFE8F5E9), // 연한 녹색 + Color(0xFFC8E6C9), + Color(0xFF81C784), + Color(0xFF388E3C) // 진한 녹색 + ) // ✅ 반응형 기기 크기 계산 val configuration = LocalConfiguration.current @@ -57,32 +57,30 @@ fun RecordItemCard( val boxPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) Surface( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - .clickable(enabled = onClick != null) { onClick?.invoke() }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .testTag("record_card") + .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), - color = Color(0xFFF5F5F5), + color = Color(0xFFF5F5F5) ) { Box( - modifier = - Modifier - .fillMaxWidth() - .padding(cardPadding), + modifier = Modifier + .fillMaxWidth() + .padding(cardPadding) ) { // ✅ 왼쪽: 타이틀 + 요일 Column( - modifier = Modifier.align(Alignment.CenterStart), + modifier = Modifier.align(Alignment.CenterStart) ) { // 상단 타이틀 Row(verticalAlignment = Alignment.CenterVertically) { // 🔹 타이머별 고유 색상 원 Box( - modifier = - Modifier - .size(26.dp) - .background(color = color, shape = RoundedCornerShape(50)), + modifier = Modifier + .size(26.dp) + .background(color = color, shape = RoundedCornerShape(50)) ) Spacer(modifier = Modifier.width(12.dp)) Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold) @@ -97,34 +95,32 @@ fun RecordItemCard( days.forEachIndexed { index, day -> val count = dayCounts[index + 1] ?: 0 val ratio = count.toFloat() / maxCount.toFloat() - val colorIndex = - when { - ratio == 0f -> -1 - ratio < 0.25f -> 0 - ratio < 0.5f -> 1 - ratio < 0.75f -> 2 - else -> 3 - } + val colorIndex = when { + ratio == 0f -> -1 + ratio < 0.25f -> 0 + ratio < 0.5f -> 1 + ratio < 0.75f -> 2 + else -> 3 + } val grassColor = if (colorIndex == -1) Color(0xFFE0E0E0) else grassPalette[colorIndex] Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = - Modifier - .size(26.dp) - .background( - color = grassColor, - shape = RoundedCornerShape(8.dp), - ), - contentAlignment = Alignment.Center, + modifier = Modifier + .size(26.dp) + .background( + color = grassColor, + shape = RoundedCornerShape(8.dp) + ), + contentAlignment = Alignment.Center ) {} Spacer(modifier = Modifier.height(4.dp)) Text( text = day, fontSize = 12.sp, - color = if (count > 0) Color.Black else Color.Gray, + color = if (count > 0) Color.Black else Color.Gray ) } } @@ -133,19 +129,18 @@ fun RecordItemCard( // ✅ 오른쪽: 중앙 정렬된 타이머 박스 (반응형) Box( - modifier = - Modifier - .align(Alignment.CenterEnd) - .height(boxHeight) - .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) - .padding(horizontal = boxPadding), - contentAlignment = Alignment.Center, + modifier = Modifier + .align(Alignment.CenterEnd) + .height(boxHeight) + .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) + .padding(horizontal = boxPadding), + contentAlignment = Alignment.Center ) { Text( text = totalTime, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = Color.Black, + color = Color.Black ) } } @@ -161,21 +156,21 @@ fun RecordItemCardPreview() { color = Color(0xFFFFFFFF), // 타이머 색상 title = "전체 시간", totalTime = "00:45", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFF8686), title = "자바스크립트", totalTime = "00:45", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFFDB86), title = "파이썬", totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index a980379..185c9e3 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -1,8 +1,14 @@ package com.sprinter.studyspot.timer.viewmodel +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import com.sprinter.studyspot.timer.model.RecordDetailItem +import com.sprinter.studyspot.timer.model.RecordDetailRowData +import com.sprinter.studyspot.timer.ui.component.DailyFocus import com.sprinter.studyspot.timer.ui.component.RecordData import com.sprinter.studyspot.timer.ui.component.TimerItemData @@ -10,31 +16,37 @@ class TimerViewModel : ViewModel() { private val _timers = mutableStateListOf() val timers: List get() = _timers - // ✅ 기록 데이터 - val records = - listOf( - RecordData( - id = "0", - title = "전체 시간", - color = Color(0xFFFFFFFF), - totalTime = "02:10", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), - ), - RecordData( - id = "1", - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTime = "01:20", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), - ), - RecordData( - id = "2", - title = "파이썬", - color = Color(0xFFFFE382), - totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), - ), + // 타이머의 총 소요시간 + fun getTotalTimerSeconds(): Long { + return timers.sumOf { timer -> + (timer.hour * 3600L) + (timer.minute * 60L) + timer.second + } + } + // ✅ 기록 데이터 (내부 mutable, 외부는 읽기 전용으로 노출) + private val _records = mutableStateListOf( + RecordData( + id = "0", + title = "전체 시간", + color = Color(0xFFFFFFFF), + totalTime = "02:10", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + ), + RecordData( + id = "1", + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + ), + RecordData( + id = "2", + title = "파이썬", + color = Color(0xFFFFE382), + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) ) + ) + val records: List get() = _records fun addTimer(timer: TimerItemData) { _timers.add(timer) @@ -51,4 +63,95 @@ class TimerViewModel : ViewModel() { // ✅ recordId로 RecordData 가져오기 fun getRecordById(id: String): RecordData? = records.find { it.id == id } -} + + // 테스트에서 기록을 부여하기 위한 함수 + fun addRecordForTest( + title: String, + color: Color, + totalTime: String, + dayCounts: Map + ) { + _records.add( + RecordData( + id = System.currentTimeMillis().toString(), + title = title, + color = color, + totalTime = totalTime, + dayCounts = dayCounts + ) + ) + } + var selectedTab by mutableStateOf(0) + private set + + fun updateSelectedTab(tabIndex: Int) { + selectedTab = tabIndex + } + + // 다음 타이머 세부 내용 파악을 위한 함수 + fun getNextRecordId(currentId: String): String? { + val index = _records.indexOfFirst { it.id == currentId } + if (index != -1 && index < _records.lastIndex) { + return _records[index + 1].id + } + return null + } + + // 이전 타이머 세부 내용 파악을 위한 함수 + fun getPrevRecordId(currentId: String): String? { + val index = _records.indexOfFirst { it.id == currentId } + if (index > 0) { + return _records[index - 1].id + } + return null + } + + fun getDailyGroupedRecords(day: Int): Map> { + val result = mutableMapOf>() + + records.filter { it.id != "0" }.forEach { record -> + val count = record.dayCounts[day] ?: return@forEach + + // 예시: count 만큼 더미 데이터 생성 (지금은 더미 → 나중에 실제 데이터 연결) + repeat(count) { + val item = DailyFocus( + timerTitle = record.title, + color = record.color, + start = "13:00:00", + end = "13:10:00", + duration = "00:10:00" + ) + result.getOrPut(record.title) { mutableListOf() }.add(item) + } + } + + return result + } + + fun getRecordDetailByDay(day: Int): List { + + // TODO: 실제 기록 DB 연동 시 교체 가능 + val example = listOf( + RecordDetailItem( + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTimeFormatted = "00:20:00", + records = listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), + RecordDetailRowData("14:00:00", "14:10:00", "00:10:00") + ) + ), + RecordDetailItem( + title = "파이썬", + color = Color(0xFFFFE382), + totalTimeFormatted = "00:10:00", + records = listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00") + ) + ) + ) + + return example + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 236801c..4815f00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,18 @@ [versions] agp = "8.12.3" +junitJupiterApi = "6.0.1" +junitJupiterEngine = "6.0.1" +junitJupiterParams = "6.0.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" +kotlinTest = "1.9.0" lifecycleRuntimeKtx = "2.9.4" activityCompose = "1.8.0" composeBom = "2024.09.00" +mockk = "1.14.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +29,11 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 7efdc3cd0cb76004d1a0f7e5af9d8a413ce053a5 Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 21:29:26 +0900 Subject: [PATCH 43/44] =?UTF-8?q?restore:=20=ED=8A=B9=EC=A0=95=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EB=8F=99=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 283 ++++++++++++++--------- gradlew.bat | 40 ++-- 3 files changed, 198 insertions(+), 130 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6a2fbcc..b82aa23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Thu Nov 06 20:50:04 KST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,114 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From fae44550c157129db1ed3511f8ed440582191abe Mon Sep 17 00:00:00 2001 From: chohs4164 Date: Sun, 23 Nov 2025 21:48:54 +0900 Subject: [PATCH 44/44] =?UTF-8?q?style:=20androidTest=EC=9D=98=20=EC=99=80?= =?UTF-8?q?=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecordDetailScreenCalendarTest.kt | 23 +- .../studyspot/RecordDetailScreenUITest.kt | 14 +- .../studyspot/RecordFlowNavigationTest.kt | 21 +- .../sprinter/studyspot/RecordItemCardTest.kt | 5 +- .../studyspot/RecordItemCardUITest.kt | 6 +- .../studyspot/RecordNavigationTest.kt | 28 +-- .../studyspot/RecordTabNavigationTest.kt | 28 ++- .../studyspot/TimerProgressComposableTest.kt | 8 +- .../studyspot/TimerRecordDetailBackTest.kt | 11 +- .../TimerRecordDetailNavigationTest.kt | 12 +- .../studyspot/TimerRunningScreenTest.kt | 19 +- .../studyspot/TimerScreenNavigationTest.kt | 13 +- .../studyspot/TimerScreenRecordTabTest.kt | 6 +- .../com/sprinter/studyspot/TimerScreenTest.kt | 40 +-- .../studyspot/TimerSettingScreenInputTest.kt | 10 +- .../studyspot/navigation/AppNavGraph.kt | 165 +++++++++---- .../studyspot/timer/model/RecordDetailItem.kt | 8 +- .../timer/model/RecordDetailRowData.kt | 6 +- .../studyspot/timer/ui/RecordDetailScreen.kt | 233 +++++++++--------- .../studyspot/timer/ui/TimerRunningScreen.kt | 132 +++++----- .../studyspot/timer/ui/TimerScreen.kt | 10 +- .../studyspot/timer/ui/TimerSettingScreen.kt | 11 +- .../timer/ui/component/DailyFocus.kt | 2 +- .../timer/ui/component/RecordItemCard.kt | 97 ++++---- .../timer/viewmodel/TimerViewModel.kt | 116 ++++----- .../viewmodel/TimerViewModelRecordTest.kt | 1 - .../timer/viewmodel/TimerViewModelTest.kt | 98 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 28 files changed, 618 insertions(+), 507 deletions(-) diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt index 24d3e39..69ed9f7 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenCalendarTest.kt @@ -2,8 +2,11 @@ package com.sprinter.studyspot import androidx.activity.ComponentActivity import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.* +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.ui.RecordDetailScreen import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @@ -16,7 +19,6 @@ import org.junit.Test * 공부한 날짜 강조 및 날짜 선택 기능 검증 **/ class RecordDetailScreenCalendarTest { - @get:Rule val composeRule = createAndroidComposeRule() @@ -26,7 +28,7 @@ class RecordDetailScreenCalendarTest { RecordDetailScreen( navController = rememberNavController(), recordId = "1", - timerViewModel = TimerViewModel() + timerViewModel = TimerViewModel(), ) } @@ -37,11 +39,14 @@ class RecordDetailScreenCalendarTest { composeRule.onNodeWithContentDescription("day-4").performClick() // 선택되면 색상이 FF8686 (선택 색) - composeRule.onNodeWithContentDescription("day-4") - .assert(SemanticsMatcher.expectValue( - SemanticsProperties.ContentDescription, - listOf("day-4") - )) + composeRule + .onNodeWithContentDescription("day-4") + .assert( + SemanticsMatcher.expectValue( + SemanticsProperties.ContentDescription, + listOf("day-4"), + ), + ) } @Test @@ -50,7 +55,7 @@ class RecordDetailScreenCalendarTest { RecordDetailScreen( navController = rememberNavController(), recordId = "1", - timerViewModel = TimerViewModel() + timerViewModel = TimerViewModel(), ) } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt index 16b871c..e6d650c 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordDetailScreenUITest.kt @@ -16,7 +16,6 @@ import org.junit.Test * 제목, 색상 배지, 달력 UI, 월 이동 버튼, 일별 기록 리스트 등의 UI 체크 */ class RecordDetailScreenUITest { - @get:Rule val composeRule = createAndroidComposeRule() @@ -30,10 +29,10 @@ class RecordDetailScreenUITest { RecordDetailScreen( navController = navController, - recordId = "1", // TimerViewModel의 records에서 id = "1" → "자바스크립트" - title = null, // title은 ViewModel 데이터 사용 - color = null, // color도 ViewModel 데이터 사용 - timerViewModel = timerViewModel + recordId = "1", // TimerViewModel의 records에서 id = "1" → "자바스크립트" + title = null, // title은 ViewModel 데이터 사용 + color = null, // color도 ViewModel 데이터 사용 + timerViewModel = timerViewModel, ) } @@ -57,7 +56,7 @@ class RecordDetailScreenUITest { recordId = "1", title = null, color = null, - timerViewModel = timerViewModel + timerViewModel = timerViewModel, ) } @@ -66,5 +65,4 @@ class RecordDetailScreenUITest { .assertExists() .performClick() } - -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt index cd65318..8d0cee4 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordFlowNavigationTest.kt @@ -19,20 +19,16 @@ import org.junit.Test * 뒤로 가기 버튼 클릭 시 '기록' 탭 상태가 유지되는지 확인 */ class RecordFlowNavigationTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun clickingRecordItem_navigatesToDetailScreen() { - composeRule.setContent { - val navController = rememberNavController() val timerViewModel = TimerViewModel() NavHost(navController = navController, startDestination = "list") { - composable("list") { RecordItemCard( color = Color(0xFFFF8686), @@ -41,9 +37,9 @@ class RecordFlowNavigationTest { dayCounts = mapOf(1 to 2, 2 to 3), onClick = { navController.navigate( - "recordDetail/1?title=자바스크립트&color=FFFF8686" + "recordDetail/1?title=자바스크립트&color=FFFF8686", ) - } + }, ) } @@ -54,18 +50,19 @@ class RecordFlowNavigationTest { val colorHex = backStackEntry.arguments?.getString("color") ?: "FFFF8686" // HEX string → Color 변환 - val color = try { - Color(colorHex.toLong(16)) - } catch (_: Exception) { - Color.Gray - } + val color = + try { + Color(colorHex.toLong(16)) + } catch (_: Exception) { + Color.Gray + } RecordDetailScreen( navController = navController, recordId = id, title = title, color = color, - timerViewModel = timerViewModel + timerViewModel = timerViewModel, ) } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt index 0fef0e7..ca8828b 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardTest.kt @@ -14,7 +14,6 @@ import org.junit.Test * 색상 표시, 타이틀, 총 공부 시간, 요일별 dayCounts 로직 검증 */ class RecordItemCardTest { - @get:Rule val composeRule = createAndroidComposeRule() @@ -26,7 +25,7 @@ class RecordItemCardTest { title = "파이썬", totalTime = "00:45", dayCounts = mapOf(1 to 1, 3 to 2), - onClick = {} + onClick = {}, ) } @@ -44,7 +43,7 @@ class RecordItemCardTest { title = "파이썬", totalTime = "00:45", dayCounts = emptyMap(), - onClick = { clicked = true } + onClick = { clicked = true }, ) } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt index 54f7180..def21d2 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordItemCardUITest.kt @@ -14,20 +14,18 @@ import org.junit.Test * 클릭 동작, 레이아웃 배치, 색상 표시 등이 정상적으로 보이는지 검증 */ class RecordItemCardUITest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun recordItemCard_rendersCorrectly() { - composeRule.setContent { RecordItemCard( color = Color(0xFFFF8686), title = "자바스크립트", totalTime = "01:20", dayCounts = mapOf(1 to 2, 3 to 1), - onClick = {} + onClick = {}, ) } @@ -48,7 +46,7 @@ class RecordItemCardUITest { title = "테스트 카드", totalTime = "00:30", dayCounts = mapOf(), - onClick = { clicked = true } + onClick = { clicked = true }, ) } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt index 2ffbbef..dece1c2 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordNavigationTest.kt @@ -20,24 +20,21 @@ import org.junit.Test * RecordItemCard가 정상적으로 표시되는지 확인 */ class RecordNavigationTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun clickingRecordItem_navigatesToDetailScreen() { - composeRule.setContent { - val navController = rememberNavController() // 🔥 테스트 환경에서 ViewModel 정상 생성 - val timerViewModel = ViewModelProvider( - composeRule.activity - )[TimerViewModel::class.java] + val timerViewModel = + ViewModelProvider( + composeRule.activity, + )[TimerViewModel::class.java] NavHost(navController = navController, startDestination = "list") { - composable("list") { RecordItemCard( color = Color(0xFFFF8686), @@ -46,9 +43,9 @@ class RecordNavigationTest { dayCounts = mapOf(1 to 2, 2 to 3), onClick = { navController.navigate( - "recordDetail/1?title=자바스크립트&color=FFFF8686" + "recordDetail/1?title=자바스크립트&color=FFFF8686", ) - } + }, ) } @@ -58,18 +55,19 @@ class RecordNavigationTest { val title = backStackEntry.arguments?.getString("title") ?: "" val colorHex = backStackEntry.arguments?.getString("color") ?: "FF8686" - val color = try { - Color(colorHex.toLong(16)) - } catch (_: Exception) { - Color.Gray - } + val color = + try { + Color(colorHex.toLong(16)) + } catch (_: Exception) { + Color.Gray + } RecordDetailScreen( navController = navController, recordId = id, title = title, color = color, - timerViewModel = timerViewModel // 🔥 수정된 ViewModel 전달 + timerViewModel = timerViewModel, // 🔥 수정된 ViewModel 전달 ) } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt index fc89568..36f25ad 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/RecordTabNavigationTest.kt @@ -1,8 +1,10 @@ package com.sprinter.studyspot.record.ui import androidx.activity.ComponentActivity -import androidx.compose.ui.test.* +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @@ -10,36 +12,35 @@ import org.junit.Rule import org.junit.Test class RecordTabNavigationTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun 기록탭_클릭시_기록화면으로_전환된다() { - composeRule.setContent { val navController = rememberNavController() val viewModel = TimerViewModel() TimerScreen( navController = navController, - timerViewModel = viewModel + timerViewModel = viewModel, ) } // 기록 탭 클릭 - composeRule.onNodeWithText("기록") + composeRule + .onNodeWithText("기록") .assertExists() .performClick() // "총 집중 시간"이 기록 화면에 존재하는지 확인 - composeRule.onNodeWithText("총 집중 시간") + composeRule + .onNodeWithText("총 집중 시간") .assertExists() } @Test fun 기록탭_클릭하면_기록카드들이_표시된다() { - composeRule.setContent { val navController = rememberNavController() val viewModel = TimerViewModel() @@ -49,21 +50,24 @@ class RecordTabNavigationTest { title = "테스트 기록", color = androidx.compose.ui.graphics.Color.Red, totalTime = "00:20:00", - dayCounts = mapOf(1 to 1) + dayCounts = mapOf(1 to 1), ) TimerScreen( navController = navController, - timerViewModel = viewModel + timerViewModel = viewModel, ) } // 기록 탭 클릭 - composeRule.onNodeWithText("기록") + composeRule + .onNodeWithText("기록") .performClick() - val nodes = composeRule.onAllNodes(hasTestTag("record_card")) - .fetchSemanticsNodes() + val nodes = + composeRule + .onAllNodes(hasTestTag("record_card")) + .fetchSemanticsNodes() assert(nodes.isNotEmpty()) } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt index 09174cc..b1706f5 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerProgressComposableTest.kt @@ -11,7 +11,6 @@ import org.junit.Test * ViewModel의 selectedTab 상태 유지 여부 검증 */ class TimerProgressComposableTest { - @get:Rule val composeRule = createAndroidComposeRule() @@ -19,8 +18,8 @@ class TimerProgressComposableTest { fun timerProgress_rendersCanvas() { composeRule.setContent { TimerProgress( - totalSeconds = 3600L, // 1시간 - elapsedSeconds = 0L // 아직 경과 X + totalSeconds = 3600L, // 1시간 + elapsedSeconds = 0L, // 아직 경과 X ) } @@ -30,11 +29,10 @@ class TimerProgressComposableTest { @Test fun timerProgress_updatesOnElapsedTimeChange() { - composeRule.setContent { TimerProgress( totalSeconds = 3600L, - elapsedSeconds = 1800L // 절반 경과 + elapsedSeconds = 1800L, // 절반 경과 ) } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt index 5244176..a435c42 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailBackTest.kt @@ -3,7 +3,9 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.* +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -16,7 +18,6 @@ import org.junit.Test * totalSeconds / elapsedSeconds 변화에 따른 UI 업데이트 검증 */ class TimerRecordDetailBackTest { - @get:Rule val composeTestRule = createComposeRule() @@ -29,13 +30,13 @@ class TimerRecordDetailBackTest { NavHost( navController = navController, - startDestination = "timer" + startDestination = "timer", ) { composable("timer") { TimerScreen( navController = navController, timerViewModel = vm, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } composable("recordDetail/{id}") { backStackEntry -> @@ -44,7 +45,7 @@ class TimerRecordDetailBackTest { RecordDetailScreen( navController = navController, recordId = recordId, - timerViewModel = vm + timerViewModel = vm, ) } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt index 36b69a4..effc33e 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRecordDetailNavigationTest.kt @@ -3,7 +3,8 @@ package com.sprinter.studyspot.timer.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.* +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.viewmodel.TimerViewModel @@ -14,7 +15,6 @@ import org.junit.Test * 기록 상세 화면으로 이동 + 뒤로가기 시 ‘기록’ 탭이 유지되는지 검증하는 테스트 */ class TimerRecordDetailNavigationTest { - @get:Rule val composeTestRule = createComposeRule() @@ -32,19 +32,21 @@ class TimerRecordDetailNavigationTest { TimerScreen( navController = navController, timerViewModel = vm, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } // WHEN - 기록 탭 선택 - composeTestRule.onNodeWithText("기록") + composeTestRule + .onNodeWithText("기록") .assertExists() .performClick() // WHEN - 첫 번째 기록 카드 선택 val firstRecord = vm.records.first() - composeTestRule.onNodeWithText(firstRecord.title) + composeTestRule + .onNodeWithText(firstRecord.title) .assertExists() .performClick() diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt index b81f6d1..7361c39 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerRunningScreenTest.kt @@ -2,24 +2,24 @@ package com.sprinter.studyspot.timer.ui import androidx.activity.ComponentActivity import androidx.compose.ui.graphics.Color -import androidx.compose.ui.test.* +import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import org.junit.Rule import org.junit.Test + /** * TimerRunningScreen의 기본 UI 요소 테스트 * 남은 시간 표시, 일시정지/재생 버튼, 종료 버튼 UI 확인 */ class TimerRunningScreenTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun 타이머실행화면_UI가_정상_렌더링된다() { - composeRule.setContent { val navController = rememberNavController() val viewModel = TimerViewModel() @@ -28,7 +28,7 @@ class TimerRunningScreenTest { navController = navController, color = Color(0xFFFF8686), title = "공부", - totalSeconds = 600 // 10분 + totalSeconds = 600, // 10분 ) } @@ -36,12 +36,15 @@ class TimerRunningScreenTest { composeRule.onNodeWithText("남은시간").assertExists() // 재생/일시정지 버튼 존재 확인 - composeRule.onNode(hasContentDescription("pause_button") - .or(hasContentDescription("play_button"))) - .assertExists() + composeRule + .onNode( + hasContentDescription("pause_button") + .or(hasContentDescription("play_button")), + ).assertExists() // 종료 버튼 존재 확인 - composeRule.onNode(hasContentDescription("stop_button")) + composeRule + .onNode(hasContentDescription("stop_button")) .assertExists() } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt index ce0ffa0..ac4eae5 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenNavigationTest.kt @@ -2,7 +2,9 @@ package com.sprinter.studyspot.timer.ui import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.* +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -15,30 +17,27 @@ import org.junit.Test * title, color, hour/minute/second 값 전달 여부 검증 */ class TimerScreenNavigationTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun navigate_from_timer_to_setting() { - composeRule.setContent { val navController = rememberNavController() - val vm = TimerViewModel() // <- 필수 + val vm = TimerViewModel() // <- 필수 NavHost(navController = navController, startDestination = "timer") { - composable("timer") { TimerScreen( navController = navController, - timerViewModel = vm // <- 필수 + timerViewModel = vm, // <- 필수 ) } composable("setting") { TimerSettingScreen( navController = navController, - timerViewModel = vm // <- 필수 + timerViewModel = vm, // <- 필수 ) } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt index 40b8ad2..5e3ace0 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenRecordTabTest.kt @@ -14,7 +14,6 @@ import org.junit.Test * RecordItemCard 목록이 정상적으로 표시되는지 확인 */ class TimerScreenRecordTabTest { - @get:Rule val composeRule = createAndroidComposeRule() @@ -24,7 +23,7 @@ class TimerScreenRecordTabTest { val viewModel = TimerViewModel() TimerScreen( navController = rememberNavController(), - timerViewModel = viewModel + timerViewModel = viewModel, ) } @@ -33,7 +32,8 @@ class TimerScreenRecordTabTest { // then: Dummy 기록 데이터 중 하나의 제목이 화면에 표시되어야 한다. // 🔹 TimerViewModelTest 에서 getRecordById("1") 의 title 이 "자바스크립트" 였으니까 그걸 기준으로 검증 - composeRule.onNodeWithText("자바스크립트") + composeRule + .onNodeWithText("자바스크립트") .assertExists("기록 탭 선택 후 '자바스크립트' 기록 카드가 표시되지 않았습니다.") } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt index 01cfb75..6a5cebe 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerScreenTest.kt @@ -1,8 +1,11 @@ package com.sprinter.studyspot.timer.ui -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.navigation.compose.rememberNavController import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import org.junit.Rule @@ -13,38 +16,39 @@ import org.junit.Test * 전체 시간, 원형 그래프, 탭 UI, 타이머 리스트 표시 확인 */ class TimerScreenTest { - @get:Rule val composeTestRule = createComposeRule() @Test fun 기록탭_선택시_RecordItemCard_표시된다() { - // 더미 ViewModel 준비 - val viewModel = TimerViewModel().apply { - // 기록이 존재하도록 더미 데이터 추가 - addRecordForTest( - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTime = "01:20", - dayCounts = mapOf(1 to 2, 3 to 4) - ) - } + val viewModel = + TimerViewModel().apply { + // 기록이 존재하도록 더미 데이터 추가 + addRecordForTest( + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 3 to 4), + ) + } composeTestRule.setContent { TimerScreen( navController = rememberNavController(), - timerViewModel = viewModel + timerViewModel = viewModel, ) } // 탭 바에서 "기록" 클릭 - composeTestRule.onNode( - hasText("기록") and hasClickAction() - ).performClick() + composeTestRule + .onNode( + hasText("기록") and hasClickAction(), + ).performClick() // RecordItemCard 표시 여부 확인 - composeTestRule.onNodeWithTag("record_card") + composeTestRule + .onNodeWithTag("record_card") .assertExists("RecordItemCard가 표시되지 않았습니다.") } } diff --git a/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt b/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt index 084af88..3caa169 100644 --- a/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt +++ b/app/src/androidTest/java/com/sprinter/studyspot/TimerSettingScreenInputTest.kt @@ -1,9 +1,9 @@ package com.sprinter.studyspot.timer.ui import androidx.activity.ComponentActivity -import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.sprinter.studyspot.timer.ui.TimerSettingScreen +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import com.sprinter.studyspot.timer.viewmodel.TimerViewModel import org.junit.Rule import org.junit.Test @@ -13,23 +13,21 @@ import org.junit.Test * 타이틀 입력, 시간 선택, 색상 선택, 저장/수정 기능 검증 */ class TimerSettingScreenInputTest { - @get:Rule val composeRule = createAndroidComposeRule() @Test fun timerSetting_hoursInputWorks() { - composeRule.setContent { val vm = TimerViewModel() TimerSettingScreen( navController = null, - timerViewModel = vm, // ✅ 필수 파라미터 추가 + timerViewModel = vm, // ✅ 필수 파라미터 추가 initialTitle = "테스트", initialHour = 0, initialMinute = 0, - initialSecond = 0 + initialSecond = 0, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt index ce63bd3..c8b30d3 100644 --- a/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/sprinter/studyspot/navigation/AppNavGraph.kt @@ -12,12 +12,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument +import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 import com.sprinter.studyspot.timer.ui.TimerRunningScreen import com.sprinter.studyspot.timer.ui.TimerScreen import com.sprinter.studyspot.timer.ui.TimerSettingScreen import com.sprinter.studyspot.timer.ui.component.BottomNavBar import com.sprinter.studyspot.timer.viewmodel.TimerViewModel -import com.sprinter.studyspot.timer.ui.RecordDetailScreen // ✅ 추가 import java.net.URLDecoder import java.nio.charset.StandardCharsets @@ -36,23 +36,41 @@ fun AppNavGraph(navController: NavHostController) { ) { BottomNavBar(navController = navController, currentRoute = currentRoute) } - } + }, ) { innerPadding -> NavHost( navController = navController, startDestination = "timer", - modifier = androidx.compose.ui.Modifier.padding(innerPadding) + modifier = + androidx.compose.ui.Modifier + .padding(innerPadding), ) { // ✅ 타이머 메인 화면 composable( route = "timer?title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "기본 타이머" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "기본 타이머" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "기본 타이머" val colorValue = backStackEntry.arguments?.getString("color") ?: "0" @@ -67,21 +85,40 @@ fun AppNavGraph(navController: NavHostController) { title = title, hour = hour, minute = minute, - second = second + second = second, ) } // ✅ 타이머 설정 화면 composable( route = "timerSetting?id={id}&title={title}&color={color}&hour={hour}&minute={minute}&second={second}", - arguments = listOf( - navArgument("id") { type = NavType.StringType; defaultValue = "" }, - navArgument("title") { type = NavType.StringType; defaultValue = "자바스크립트" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("hour") { type = NavType.IntType; defaultValue = 0 }, - navArgument("minute") { type = NavType.IntType; defaultValue = 0 }, - navArgument("second") { type = NavType.IntType; defaultValue = 0 } - ) + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("title") { + type = NavType.StringType + defaultValue = "자바스크립트" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("hour") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("minute") { + type = NavType.IntType + defaultValue = 0 + }, + navArgument("second") { + type = NavType.IntType + defaultValue = 0 + }, + ), ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "" val title = backStackEntry.arguments?.getString("title") ?: "자바스크립트" @@ -90,15 +127,16 @@ fun AppNavGraph(navController: NavHostController) { val minute = backStackEntry.arguments?.getInt("minute") ?: 0 val second = backStackEntry.arguments?.getInt("second") ?: 0 - val initialColor = if (colorValue == "0" || colorValue.isBlank()) { - null - } else { - try { - Color(colorValue.toULong()) - } catch (e: Exception) { + val initialColor = + if (colorValue == "0" || colorValue.isBlank()) { null + } else { + try { + Color(colorValue.toULong()) + } catch (e: Exception) { + null + } } - } TimerSettingScreen( navController = navController, @@ -108,47 +146,68 @@ fun AppNavGraph(navController: NavHostController) { initialMinute = minute, initialSecond = second, initialColor = initialColor, - timerId = if (id.isNotEmpty()) id else null + timerId = if (id.isNotEmpty()) id else null, ) } // ✅ 타이머 실행 화면 composable( route = "timerRunning?title={title}&color={color}&total={total}", - arguments = listOf( - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" }, - navArgument("total") { type = NavType.LongType; defaultValue = 0L } - ) + arguments = + listOf( + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("total") { + type = NavType.LongType + defaultValue = 0L + }, + ), ) { backStackEntry -> val rawTitle = backStackEntry.arguments?.getString("title") ?: "기본 타이머" val title = URLDecoder.decode(rawTitle, StandardCharsets.UTF_8.toString()) val colorValue = backStackEntry.arguments?.getString("color") ?: "0" val total = backStackEntry.arguments?.getLong("total") ?: 0L - val color = try { - if (colorValue == "0" || colorValue.isBlank()) Color(0xFFD9D9D9) - else Color(colorValue.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val color = + try { + if (colorValue == "0" || colorValue.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(colorValue.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } TimerRunningScreen( navController = navController, title = title, color = color, - totalSeconds = total + totalSeconds = total, ) } // ✅ 기록 상세 화면 composable( route = "recordDetail/{recordId}?title={title}&color={color}", - arguments = listOf( - navArgument("recordId") { type = NavType.StringType }, - navArgument("title") { type = NavType.StringType; defaultValue = "" }, - navArgument("color") { type = NavType.StringType; defaultValue = "0" } // ← 수정됨 - ) + arguments = + listOf( + navArgument("recordId") { type = NavType.StringType }, + navArgument("title") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("color") { + type = NavType.StringType + defaultValue = "0" + }, // ← 수정됨 + ), ) { backStackEntry -> val recordId = backStackEntry.arguments?.getString("recordId") ?: "" @@ -159,19 +218,23 @@ fun AppNavGraph(navController: NavHostController) { // color 변환 (문자열 → Color) val rawColor = backStackEntry.arguments?.getString("color") ?: "0" - val color = try { - if (rawColor == "0" || rawColor.isBlank()) Color(0xFFD9D9D9) - else Color(rawColor.toULong()) - } catch (e: Exception) { - Color(0xFFD9D9D9) - } + val color = + try { + if (rawColor == "0" || rawColor.isBlank()) { + Color(0xFFD9D9D9) + } else { + Color(rawColor.toULong()) + } + } catch (e: Exception) { + Color(0xFFD9D9D9) + } RecordDetailScreen( navController = navController, recordId = recordId, title = title, color = color, - timerViewModel = timerViewModel + timerViewModel = timerViewModel, ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt index 2e6849b..df04ed0 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailItem.kt @@ -3,8 +3,8 @@ package com.sprinter.studyspot.timer.model import androidx.compose.ui.graphics.Color data class RecordDetailItem( - val title: String, // 타이머명 - val color: Color, // 컬러 - val totalTimeFormatted: String, // "00:20:00" - val records: List // 시작/종료/집중 세부 기록 + val title: String, // 타이머명 + val color: Color, // 컬러 + val totalTimeFormatted: String, // "00:20:00" + val records: List, // 시작/종료/집중 세부 기록 ) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt index 1eec6ef..b6f813f 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/model/RecordDetailRowData.kt @@ -1,7 +1,7 @@ package com.sprinter.studyspot.timer.model data class RecordDetailRowData( - val start: String, // ex: "13:00:00" - val end: String, // ex: "13:10:00" - val duration: String // ex: "00:10:00" + val start: String, // ex: "13:00:00" + val end: String, // ex: "13:10:00" + val duration: String, // ex: "00:10:00" ) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt index 40d5c50..2c415b3 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/RecordDetailScreen.kt @@ -50,17 +50,17 @@ fun RecordDetailScreen( recordId: String, title: String? = null, color: Color? = null, - timerViewModel: TimerViewModel + timerViewModel: TimerViewModel, ) { - val record = timerViewModel.getRecordById(recordId) val recordTitle = record?.title ?: title ?: "기록 상세" - val recordColor: Color = when (val c = record?.color) { - is Color -> c - is Int -> Color(c) - else -> color ?: Color(0xFFD9D9D9) - } + val recordColor: Color = + when (val c = record?.color) { + is Color -> c + is Int -> Color(c) + else -> color ?: Color(0xFFD9D9D9) + } val monthState = remember { mutableStateOf(YearMonth.now()) } val monthTitle = "${monthState.value.monthValue}월" @@ -69,32 +69,35 @@ fun RecordDetailScreen( val firstDay = (ym.atDay(1).dayOfWeek.value + 6) % 7 val combinedDayCounts: Map = - if (recordId == "0") + if (recordId == "0") { timerViewModel.records .filter { it.id != "0" } .flatMap { it.dayCounts.entries } .groupBy { it.key } .mapValues { it.value.sumOf { pair -> pair.value } } - else + } else { record?.dayCounts ?: emptyMap() + } val studyDaySet = combinedDayCounts.keys - val gridCells = remember(ym) { - buildList { - repeat(firstDay) { add(null) } - for (d in 1..daysInMonth) add(d) - while (size < 35) add(null) + val gridCells = + remember(ym) { + buildList { + repeat(firstDay) { add(null) } + for (d in 1..daysInMonth) add(d) + while (size < 35) add(null) + } } - } var selectedDay by remember { mutableStateOf(null) } Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 20.dp) + modifier = + Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 20.dp), ) { Spacer(modifier = Modifier.height(16.dp)) @@ -103,7 +106,7 @@ fun RecordDetailScreen( Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = null, - tint = Color.Unspecified + tint = Color.Unspecified, ) } } @@ -113,7 +116,7 @@ fun RecordDetailScreen( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { // 🔙 이전 기록으로 이동 IconButton( @@ -123,31 +126,34 @@ fun RecordDetailScreen( // title / color는 생략해도 ViewModel에서 다시 가져오니까 OK navController.navigate("recordDetail/$prevId") } - } + }, ) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), contentDescription = null, modifier = Modifier.size(20.dp), - tint = Color.Unspecified + tint = Color.Unspecified, ) } Spacer(modifier = Modifier.width(38.dp)) Box( - modifier = Modifier - .size(26.dp) - .clip(CircleShape) - .background(recordColor) - .then( - if (recordId == "0" || recordColor == Color.White) - Modifier.border( - width = 2.dp, - color = Color.LightGray, - shape = CircleShape - ) - else Modifier - ) + modifier = + Modifier + .size(26.dp) + .clip(CircleShape) + .background(recordColor) + .then( + if (recordId == "0" || recordColor == Color.White) { + Modifier.border( + width = 2.dp, + color = Color.LightGray, + shape = CircleShape, + ) + } else { + Modifier + }, + ), ) Spacer(modifier = Modifier.width(12.dp)) @@ -158,7 +164,7 @@ fun RecordDetailScreen( painter = painterResource(id = R.drawable.ic_trashcan), contentDescription = null, modifier = Modifier.size(20.dp), - tint = Color.Unspecified + tint = Color.Unspecified, ) Spacer(modifier = Modifier.width(38.dp)) @@ -169,13 +175,13 @@ fun RecordDetailScreen( if (nextId != null) { navController.navigate("recordDetail/$nextId") } - } + }, ) { Icon( painter = painterResource(id = R.drawable.ic_right_triangle), contentDescription = null, modifier = Modifier.size(20.dp), - tint = Color.Unspecified + tint = Color.Unspecified, ) } } @@ -184,26 +190,26 @@ fun RecordDetailScreen( // 달력 UI Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFF3F3F3)) + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFF3F3F3)), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(20.dp)) Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = { monthState.value = monthState.value.minusMonths(1) }) { Icon( painter = painterResource(id = R.drawable.ic_left_triangle), contentDescription = null, modifier = Modifier.size(20.dp), - tint = Color.Unspecified + tint = Color.Unspecified, ) } @@ -216,7 +222,7 @@ fun RecordDetailScreen( painter = painterResource(id = R.drawable.ic_right_triangle), contentDescription = null, modifier = Modifier.size(20.dp), - tint = Color.Unspecified + tint = Color.Unspecified, ) } } @@ -224,17 +230,18 @@ fun RecordDetailScreen( Spacer(modifier = Modifier.height(8.dp)) Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 49.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 49.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp), ) { listOf("월", "화", "수", "목", "금", "토", "일").forEach { Text( it, fontSize = 14.sp, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) } } @@ -245,12 +252,12 @@ fun RecordDetailScreen( Column( verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(vertical = 6.dp) + modifier = Modifier.padding(vertical = 6.dp), ) { gridCells.chunked(7).forEach { week -> Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { week.forEach { dayVal -> val isStudy = dayVal != null && studyDaySet.contains(dayVal) @@ -265,13 +272,14 @@ fun RecordDetailScreen( } Box( - modifier = Modifier - .size(34.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color) - .clickable(enabled = isStudy) { - selectedDay = dayVal - } + modifier = + Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) + .clickable(enabled = isStudy) { + selectedDay = dayVal + }, ) } } @@ -289,46 +297,49 @@ fun RecordDetailScreen( // 전체 타이머 → 해당 날짜 기록 전체 timerViewModel.getRecordDetailByDay(day) } else { - val currentTitle = recordTitle // 예: "자바스크립트" - timerViewModel.getRecordDetailByDay(day) + val currentTitle = recordTitle // 예: "자바스크립트" + timerViewModel + .getRecordDetailByDay(day) .filter { it.title == currentTitle } } - - val totalSec = details.sumOf { item -> - item.records.sumOf { r -> - val t = r.duration.split(":") - val h = t[0].toInt() - val m = t[1].toInt() - val s = t[2].toInt() - h * 3600 + m * 60 + s + val totalSec = + details.sumOf { item -> + item.records.sumOf { r -> + val t = r.duration.split(":") + val h = t[0].toInt() + val m = t[1].toInt() + val s = t[2].toInt() + h * 3600 + m * 60 + s + } } - } - val totalFormatted = String.format( - "%02d:%02d:%02d", - totalSec / 3600, - (totalSec % 3600) / 60, - totalSec % 60 - ) + val totalFormatted = + String.format( + "%02d:%02d:%02d", + totalSec / 3600, + (totalSec % 3600) / 60, + totalSec % 60, + ) LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .weight(1f) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .weight(1f), ) { - // 🔥 전체(recordId == "0")일 때만 전체시간 카드 노출 if (recordId == "0") { item { Spacer(modifier = Modifier.height(20.dp)) Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 49.dp), - horizontalArrangement = Arrangement.End + modifier = + Modifier + .fillMaxWidth() + .padding(end = 49.dp), + horizontalArrangement = Arrangement.End, ) { Text("총 집중 시간", fontSize = 15.sp, color = Color.Gray) } @@ -338,16 +349,16 @@ fun RecordDetailScreen( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(33.dp) - .clip(CircleShape) - .background(Color.White) - .border(2.dp, Color.LightGray, CircleShape) + modifier = + Modifier + .size(33.dp) + .clip(CircleShape) + .background(Color.White) + .border(2.dp, Color.LightGray, CircleShape), ) Spacer(modifier = Modifier.width(8.dp)) @@ -358,7 +369,7 @@ fun RecordDetailScreen( Text( totalFormatted, fontSize = 32.sp, - modifier = Modifier.padding(end = 18.dp) + modifier = Modifier.padding(end = 18.dp), ) } @@ -370,7 +381,7 @@ fun RecordDetailScreen( // 🔥🔥🔥 (중요) 개별 타이머 리스트 — 전체/특정 모두 공통 items( items = details, - key = { it.title } // 고유 key + key = { it.title }, // 고유 key ) { item -> Spacer(modifier = Modifier.height(26.dp)) @@ -378,14 +389,15 @@ fun RecordDetailScreen( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( - modifier = Modifier - .size(33.dp) - .clip(CircleShape) - .background(item.color) + modifier = + Modifier + .size(33.dp) + .clip(CircleShape) + .background(item.color), ) Spacer(modifier = Modifier.width(8.dp)) Text(item.title, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) @@ -394,7 +406,7 @@ fun RecordDetailScreen( Text( item.totalTimeFormatted, fontSize = 32.sp, - modifier = Modifier.padding(end = 18.dp) + modifier = Modifier.padding(end = 18.dp), ) } @@ -406,19 +418,19 @@ fun RecordDetailScreen( Row( modifier = Modifier.padding(start = 46.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( "시작", modifier = Modifier.width(90.dp), color = Color.Gray, - fontSize = 15.sp + fontSize = 15.sp, ) Text( "종료", modifier = Modifier.width(90.dp), color = Color.Gray, - fontSize = 15.sp + fontSize = 15.sp, ) Text("집중 시간", color = Color.Gray, fontSize = 15.sp) } @@ -427,9 +439,10 @@ fun RecordDetailScreen( item.records.forEach { row -> Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 46.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(start = 46.dp), ) { Text(row.start, modifier = Modifier.width(90.dp)) Text(row.end, modifier = Modifier.width(90.dp)) @@ -452,6 +465,6 @@ fun PreviewRecordDetailScreen() { RecordDetailScreen( navController = navController, recordId = "1", - timerViewModel = TimerViewModel() + timerViewModel = TimerViewModel(), ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt index a600c39..434ed3f 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerRunningScreen.kt @@ -65,7 +65,6 @@ fun TimerRunningScreen( // 🔹 1시간 이벤트 체크 LaunchedEffect(isRunning) { while (isRunning && elapsed < totalSeconds) { - delay(1000) elapsed++ @@ -80,7 +79,6 @@ fun TimerRunningScreen( // 🔹 1시간 이벤트가 발동하면 10분 유지 후 게이지 리셋 LaunchedEffect(hourReached) { if (hourReached) { - // 10분 = 600초 동안 유지 while (hourEventElapsed < 600L) { delay(1000) @@ -89,59 +87,70 @@ fun TimerRunningScreen( // 10분 지나면 이벤트 종료 + 게이지 리셋 hourReached = false - elapsed = 0L // ⭐ 게이지 다시 맨 처음부터 돌게 하기 + elapsed = 0L // ⭐ 게이지 다시 맨 처음부터 돌게 하기 } } // 🔹 남은시간 / 집중시간 계산 val remainingSeconds = - if (hourReached) (3600 - hourEventElapsed).coerceAtLeast(0L) - else (totalSeconds - elapsed).coerceAtLeast(0L) + if (hourReached) { + (3600 - hourEventElapsed).coerceAtLeast(0L) + } else { + (totalSeconds - elapsed).coerceAtLeast(0L) + } val focusSeconds = - if (hourReached) 3600 - else totalSeconds - remainingSeconds + if (hourReached) { + 3600 + } else { + totalSeconds - remainingSeconds + } // ⏰ 시/분/초 포맷 - val timeText = String.format( - "%02d:%02d:%02d", - remainingSeconds / 3600, - (remainingSeconds % 3600) / 60, - remainingSeconds % 60 - ) - val focusTimeText = String.format( - "%02d:%02d:%02d", - focusSeconds / 3600, - (focusSeconds % 3600) / 60, - focusSeconds % 60 - ) + val timeText = + String.format( + "%02d:%02d:%02d", + remainingSeconds / 3600, + (remainingSeconds % 3600) / 60, + remainingSeconds % 60, + ) + val focusTimeText = + String.format( + "%02d:%02d:%02d", + focusSeconds / 3600, + (focusSeconds % 3600) / 60, + focusSeconds % 60, + ) // 🔹 타이머가 끝난 상태 감지 val isFinished = elapsed >= totalSeconds Surface( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - color = Color.White + modifier = + Modifier + .fillMaxSize() + .background(Color.White), + color = Color.White, ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { // 🔹 상단 바 Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = painterResource(id = R.drawable.ic_left_arrow), contentDescription = "뒤로가기", - modifier = Modifier - .size(28.dp) - .clickable { navController.popBackStack() } + modifier = + Modifier + .size(28.dp) + .clickable { navController.popBackStack() }, ) } @@ -150,12 +159,13 @@ fun TimerRunningScreen( // 🔹 타이머명 + 색상 표시 Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, ) { Box( - modifier = Modifier - .size(16.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(16.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(8.dp)) Text(title, fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -164,44 +174,46 @@ fun TimerRunningScreen( Spacer(modifier = Modifier.height(24.dp)) // 🔹 원형 타이머 색상 - val progressColor = when { - isFinished -> Color(0xFF6D6D6D) // 완료 → 회색 - isRunning -> color // 실행 → 본래 색 - isReset -> color // 초기화 → 본래 색 유지 - else -> Color(0xFF6D6D6D) // 일시정지 → 회색 - } - + val progressColor = + when { + isFinished -> Color(0xFF6D6D6D) // 완료 → 회색 + isRunning -> color // 실행 → 본래 색 + isReset -> color // 초기화 → 본래 색 유지 + else -> Color(0xFF6D6D6D) // 일시정지 → 회색 + } Box( - modifier = Modifier - .size(220.dp) - .aspectRatio(1f) - .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(220.dp) + .aspectRatio(1f) + .background(Color(0xFFEAEAEA), RoundedCornerShape(999.dp)), + contentAlignment = Alignment.Center, ) { TimerProgress( totalSeconds = if (hourReached) 3600 else totalSeconds, elapsedSeconds = if (hourReached) hourEventElapsed else elapsed, modifier = Modifier.fillMaxSize(), - progressColor = progressColor + progressColor = progressColor, ) } Spacer(modifier = Modifier.height(24.dp)) // 🔹 상태별 멘트 변경 - val motivationText = when { - hourReached -> "🎉🎊축하합니다!🎊🎉\n1시간을 완수했어요!" - isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" - isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" - else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" - } + val motivationText = + when { + hourReached -> "🎉🎊축하합니다!🎊🎉\n1시간을 완수했어요!" + isFinished -> "🎉🎊축하합니다!🎊🎉\n목표를 완수했어요~!" + isRunning -> "👀 원하는 목표 만큼 공부해 보세요!\n화이팅~!" + else -> "👀 조금만 쉬었다 갈까요?\n준비가 다 되면 알려주세요!" + } Text( motivationText, textAlign = TextAlign.Center, color = if (hourReached || isFinished) Color(0xFF4CAF50) else Color.Gray, - fontWeight = if (hourReached || isFinished) FontWeight.Bold else FontWeight.Normal + fontWeight = if (hourReached || isFinished) FontWeight.Bold else FontWeight.Normal, ) Spacer(modifier = Modifier.height(24.dp)) @@ -232,7 +244,7 @@ fun TimerRunningScreen( } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text( when { @@ -240,7 +252,7 @@ fun TimerRunningScreen( isRunning -> "⏸ 일시 정지" else -> "▶ 실행" }, - color = Color.Black + color = Color.Black, ) } @@ -248,10 +260,10 @@ fun TimerRunningScreen( onClick = { elapsed = 0L isRunning = false - isReset = true // ⭐ 초기화 상태 활성화 + isReset = true // ⭐ 초기화 상태 활성화 }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF0F0F0)), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), ) { Text("🔄 초기화", color = Color.Black) } @@ -267,6 +279,6 @@ fun TimerRunningPreview() { navController = rememberNavController(), color = Color(0xFFFF8686), title = "자바스크립트", - totalSeconds = 60 * 60L + totalSeconds = 60 * 60L, ) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt index 21fe4a6..98d933c 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerScreen.kt @@ -178,7 +178,9 @@ fun TimerScreen( ) }, ) { - Tab(selected = selectedTab == 0, onClick = { timerViewModel.updateSelectedTab(0) } + Tab( + selected = selectedTab == 0, + onClick = { timerViewModel.updateSelectedTab(0) }, ) { Text( "타이머", @@ -188,9 +190,11 @@ fun TimerScreen( modifier = Modifier.padding(vertical = 12.dp), ) } - Tab(selected = selectedTab == 1, onClick = { timerViewModel.updateSelectedTab(1) } + Tab( + selected = selectedTab == 1, + onClick = { timerViewModel.updateSelectedTab(1) }, ) { - Text( + Text( "기록", fontWeight = FontWeight.Medium, fontSize = 14.sp, diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt index bcc9a1e..5d824db 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/TimerSettingScreen.kt @@ -138,11 +138,12 @@ fun TimerSettingScreen( initialColor: Color? = null, timerId: String? = null, ) { - val decodedTitle = try { - java.net.URLDecoder.decode(initialTitle, "UTF-8") - } catch (e: Exception) { - initialTitle - } + val decodedTitle = + try { + java.net.URLDecoder.decode(initialTitle, "UTF-8") + } catch (e: Exception) { + initialTitle + } var selectedColor by remember { mutableStateOf(initialColor) } var timerTitle by remember { mutableStateOf(decodedTitle) } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt index ce02148..9aebb12 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/DailyFocus.kt @@ -5,5 +5,5 @@ data class DailyFocus( val color: androidx.compose.ui.graphics.Color, val start: String, val end: String, - val duration: String + val duration: String, ) diff --git a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt index 9406166..f1f0e4c 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/ui/component/RecordItemCard.kt @@ -33,18 +33,19 @@ fun RecordItemCard( title: String, totalTime: String, dayCounts: Map, // 요일별 학습 횟수 - onClick: (() -> Unit)? = null + onClick: (() -> Unit)? = null, ) { // 최대 횟수 대비 비율 계산 val maxCount = (dayCounts.values.maxOrNull() ?: 1).coerceAtLeast(1) // ✅ 잔디 색상 팔레트 (GitHub 느낌) - val grassPalette = listOf( - Color(0xFFE8F5E9), // 연한 녹색 - Color(0xFFC8E6C9), - Color(0xFF81C784), - Color(0xFF388E3C) // 진한 녹색 - ) + val grassPalette = + listOf( + Color(0xFFE8F5E9), // 연한 녹색 + Color(0xFFC8E6C9), + Color(0xFF81C784), + Color(0xFF388E3C), // 진한 녹색 + ) // ✅ 반응형 기기 크기 계산 val configuration = LocalConfiguration.current @@ -57,30 +58,33 @@ fun RecordItemCard( val boxPadding = (screenWidth.value * 0.04f).dp.coerceIn(12.dp, 24.dp) Surface( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - .testTag("record_card") - .clickable(enabled = onClick != null) { onClick?.invoke() }, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .testTag("record_card") + .clickable(enabled = onClick != null) { onClick?.invoke() }, shape = RoundedCornerShape(12.dp), - color = Color(0xFFF5F5F5) + color = Color(0xFFF5F5F5), ) { Box( - modifier = Modifier - .fillMaxWidth() - .padding(cardPadding) + modifier = + Modifier + .fillMaxWidth() + .padding(cardPadding), ) { // ✅ 왼쪽: 타이틀 + 요일 Column( - modifier = Modifier.align(Alignment.CenterStart) + modifier = Modifier.align(Alignment.CenterStart), ) { // 상단 타이틀 Row(verticalAlignment = Alignment.CenterVertically) { // 🔹 타이머별 고유 색상 원 Box( - modifier = Modifier - .size(26.dp) - .background(color = color, shape = RoundedCornerShape(50)) + modifier = + Modifier + .size(26.dp) + .background(color = color, shape = RoundedCornerShape(50)), ) Spacer(modifier = Modifier.width(12.dp)) Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold) @@ -95,32 +99,34 @@ fun RecordItemCard( days.forEachIndexed { index, day -> val count = dayCounts[index + 1] ?: 0 val ratio = count.toFloat() / maxCount.toFloat() - val colorIndex = when { - ratio == 0f -> -1 - ratio < 0.25f -> 0 - ratio < 0.5f -> 1 - ratio < 0.75f -> 2 - else -> 3 - } + val colorIndex = + when { + ratio == 0f -> -1 + ratio < 0.25f -> 0 + ratio < 0.5f -> 1 + ratio < 0.75f -> 2 + else -> 3 + } val grassColor = if (colorIndex == -1) Color(0xFFE0E0E0) else grassPalette[colorIndex] Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier - .size(26.dp) - .background( - color = grassColor, - shape = RoundedCornerShape(8.dp) - ), - contentAlignment = Alignment.Center + modifier = + Modifier + .size(26.dp) + .background( + color = grassColor, + shape = RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.Center, ) {} Spacer(modifier = Modifier.height(4.dp)) Text( text = day, fontSize = 12.sp, - color = if (count > 0) Color.Black else Color.Gray + color = if (count > 0) Color.Black else Color.Gray, ) } } @@ -129,18 +135,19 @@ fun RecordItemCard( // ✅ 오른쪽: 중앙 정렬된 타이머 박스 (반응형) Box( - modifier = Modifier - .align(Alignment.CenterEnd) - .height(boxHeight) - .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) - .padding(horizontal = boxPadding), - contentAlignment = Alignment.Center + modifier = + Modifier + .align(Alignment.CenterEnd) + .height(boxHeight) + .background(Color(0xFFE0E0E0), RoundedCornerShape(8.dp)) + .padding(horizontal = boxPadding), + contentAlignment = Alignment.Center, ) { Text( text = totalTime, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = Color.Black + color = Color.Black, ) } } @@ -156,21 +163,21 @@ fun RecordItemCardPreview() { color = Color(0xFFFFFFFF), // 타이머 색상 title = "전체 시간", totalTime = "00:45", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFF8686), title = "자바스크립트", totalTime = "00:45", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), ) Spacer(modifier = Modifier.height(9.dp)) RecordItemCard( color = Color(0xFFFFDB86), title = "파이썬", totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), ) } } diff --git a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt index 185c9e3..f56b233 100644 --- a/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt +++ b/app/src/main/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModel.kt @@ -17,35 +17,36 @@ class TimerViewModel : ViewModel() { val timers: List get() = _timers // 타이머의 총 소요시간 - fun getTotalTimerSeconds(): Long { - return timers.sumOf { timer -> + fun getTotalTimerSeconds(): Long = + timers.sumOf { timer -> (timer.hour * 3600L) + (timer.minute * 60L) + timer.second } - } + // ✅ 기록 데이터 (내부 mutable, 외부는 읽기 전용으로 노출) - private val _records = mutableStateListOf( - RecordData( - id = "0", - title = "전체 시간", - color = Color(0xFFFFFFFF), - totalTime = "02:10", - dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5) - ), - RecordData( - id = "1", - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTime = "01:20", - dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6) - ), - RecordData( - id = "2", - title = "파이썬", - color = Color(0xFFFFE382), - totalTime = "00:45", - dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1) + private val _records = + mutableStateListOf( + RecordData( + id = "0", + title = "전체 시간", + color = Color(0xFFFFFFFF), + totalTime = "02:10", + dayCounts = mapOf(2 to 1, 3 to 3, 4 to 5), + ), + RecordData( + id = "1", + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTime = "01:20", + dayCounts = mapOf(1 to 2, 2 to 5, 3 to 6), + ), + RecordData( + id = "2", + title = "파이썬", + color = Color(0xFFFFE382), + totalTime = "00:45", + dayCounts = mapOf(1 to 1, 3 to 1, 4 to 1), + ), ) - ) val records: List get() = _records fun addTimer(timer: TimerItemData) { @@ -69,7 +70,7 @@ class TimerViewModel : ViewModel() { title: String, color: Color, totalTime: String, - dayCounts: Map + dayCounts: Map, ) { _records.add( RecordData( @@ -77,10 +78,11 @@ class TimerViewModel : ViewModel() { title = title, color = color, totalTime = totalTime, - dayCounts = dayCounts - ) + dayCounts = dayCounts, + ), ) } + var selectedTab by mutableStateOf(0) private set @@ -114,13 +116,14 @@ class TimerViewModel : ViewModel() { // 예시: count 만큼 더미 데이터 생성 (지금은 더미 → 나중에 실제 데이터 연결) repeat(count) { - val item = DailyFocus( - timerTitle = record.title, - color = record.color, - start = "13:00:00", - end = "13:10:00", - duration = "00:10:00" - ) + val item = + DailyFocus( + timerTitle = record.title, + color = record.color, + start = "13:00:00", + end = "13:10:00", + duration = "00:10:00", + ) result.getOrPut(record.title) { mutableListOf() }.add(item) } } @@ -129,29 +132,30 @@ class TimerViewModel : ViewModel() { } fun getRecordDetailByDay(day: Int): List { - // TODO: 실제 기록 DB 연동 시 교체 가능 - val example = listOf( - RecordDetailItem( - title = "자바스크립트", - color = Color(0xFFFF8686), - totalTimeFormatted = "00:20:00", - records = listOf( - RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), - RecordDetailRowData("14:00:00", "14:10:00", "00:10:00") - ) - ), - RecordDetailItem( - title = "파이썬", - color = Color(0xFFFFE382), - totalTimeFormatted = "00:10:00", - records = listOf( - RecordDetailRowData("13:00:00", "13:10:00", "00:10:00") - ) + val example = + listOf( + RecordDetailItem( + title = "자바스크립트", + color = Color(0xFFFF8686), + totalTimeFormatted = "00:20:00", + records = + listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), + RecordDetailRowData("14:00:00", "14:10:00", "00:10:00"), + ), + ), + RecordDetailItem( + title = "파이썬", + color = Color(0xFFFFE382), + totalTimeFormatted = "00:10:00", + records = + listOf( + RecordDetailRowData("13:00:00", "13:10:00", "00:10:00"), + ), + ), ) - ) return example } - -} \ No newline at end of file +} diff --git a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt index 9117f40..0a363ae 100644 --- a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt +++ b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelRecordTest.kt @@ -11,7 +11,6 @@ import org.junit.Test * addRecordForTest(), getRecordById(), records 상태 변화 확인 */ class TimerViewModelRecordTest { - private lateinit var viewModel: TimerViewModel @Before diff --git a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt index 0798446..78f3154 100644 --- a/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt +++ b/app/src/test/java/com/sprinter/studyspot/timer/viewmodel/TimerViewModelTest.kt @@ -14,7 +14,6 @@ import org.junit.Test * selectedTab 상태 유지 여부 확인 */ class TimerViewModelTest { - private lateinit var viewModel: TimerViewModel @Before @@ -24,14 +23,15 @@ class TimerViewModelTest { @Test fun `타이머 추가 시 리스트에 정상적으로 포함된다`() { - val timer = TimerItemData( - id = "test", - title = "테스트 타이머", - color = Color(0xFFFF0000), - hour = 1, - minute = 10, - second = 5 - ) + val timer = + TimerItemData( + id = "test", + title = "테스트 타이머", + color = Color(0xFFFF0000), + hour = 1, + minute = 10, + second = 5, + ) viewModel.addTimer(timer) @@ -40,14 +40,15 @@ class TimerViewModelTest { @Test fun `타이머 삭제 시 리스트에서 제거된다`() { - val timer = TimerItemData( - id = "delete-test", - title = "삭제용 타이머", - color = Color(0xFFFF0000), - hour = 0, - minute = 30, - second = 0 - ) + val timer = + TimerItemData( + id = "delete-test", + title = "삭제용 타이머", + color = Color(0xFFFF0000), + hour = 0, + minute = 30, + second = 0, + ) viewModel.addTimer(timer) viewModel.deleteTimer("delete-test") @@ -63,25 +64,27 @@ class TimerViewModelTest { @Test fun `타이머 수정 시 기존 값이 업데이트된다`() { - val timer = TimerItemData( - id = "update-test", - title = "Old", - color = Color(0xFFFF0000), - hour = 0, - minute = 30, - second = 0 - ) + val timer = + TimerItemData( + id = "update-test", + title = "Old", + color = Color(0xFFFF0000), + hour = 0, + minute = 30, + second = 0, + ) viewModel.addTimer(timer) - val updated = TimerItemData( - id = "update-test", - title = "New", - color = Color(0xFF00FF00), - hour = 1, - minute = 0, - second = 0 - ) + val updated = + TimerItemData( + id = "update-test", + title = "New", + color = Color(0xFF00FF00), + hour = 1, + minute = 0, + second = 0, + ) viewModel.updateTimer(updated) @@ -97,22 +100,24 @@ class TimerViewModelTest { @Test fun `타이머 수정 시 리스트 크기는 유지된다`() { - val timer = TimerItemData( - id = "size-test", - title = "원본", - color = Color(0xFFFF0000), - hour = 0, - minute = 10, - second = 0 - ) + val timer = + TimerItemData( + id = "size-test", + title = "원본", + color = Color(0xFFFF0000), + hour = 0, + minute = 10, + second = 0, + ) viewModel.addTimer(timer) val sizeBefore = viewModel.timers.size - val updated = timer.copy( - title = "수정된 타이머", - minute = 20 - ) + val updated = + timer.copy( + title = "수정된 타이머", + minute = 20, + ) viewModel.updateTimer(updated) val sizeAfter = viewModel.timers.size @@ -136,5 +141,4 @@ class TimerViewModelTest { assertTrue(viewModel.timers.any { it.id == "t3" }) assertFalse(viewModel.timers.any { it.id == "t2" }) } - -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME