From 64541b88a712659d9528deba50219bdd80b6fe58 Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 16:38:26 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat/#257=20:=20Dimens=EC=97=90=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EA=B4=80=EB=A0=A8=20=EB=B3=80=EC=88=98=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 --- .../main/java/com/depromeet/team6/presentation/util/Dimens.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/depromeet/team6/presentation/util/Dimens.kt b/app/src/main/java/com/depromeet/team6/presentation/util/Dimens.kt index dd7b5433..b9705123 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/util/Dimens.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/util/Dimens.kt @@ -12,5 +12,8 @@ object Dimens { val LegDepartureTimeWidth = 38.dp val LegDetailVerticalLineWidth = 40.dp val LegDetailLineTextMargin = 17.dp + val LegTimelineIconSize = 26.dp + val LegTimelineTimeIconGap = 5.dp + val LegTimelineTimeSlotWidth = 44.dp val WalkIconWithRippleSize = 48.dp } From 8824c1ec0f1c48f9f7b40fcfcdfe68841f52123c Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 16:42:59 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat/#257=20:=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=83=80=EC=9E=84=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ItineraryInfoDetail.kt | 41 ++- .../component/ItineraryInfoDetailLegs.kt | 330 ++++++++++++------ 2 files changed, 259 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetail.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetail.kt index 19b35c74..3a672ae5 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetail.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetail.kt @@ -2,15 +2,16 @@ package com.depromeet.team6.presentation.ui.itinerary.component import android.util.SparseArray import androidx.compose.foundation.Image +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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -21,6 +22,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.depromeet.team6.R import com.depromeet.team6.domain.model.RealTimeBusArrival import com.depromeet.team6.domain.model.course.LegInfo @@ -93,30 +95,43 @@ private fun ItineraryInfoSuffix( modifier: Modifier = Modifier ) { val markerIconId = if (isDestination) R.drawable.map_marker_arrival else R.drawable.map_marker_departure + val timelineIconSize = Dimens.LegTimelineIconSize + val markerYOffset = if (isDestination) 0.dp else 2.dp + val timelineTimeIconGap = Dimens.LegTimelineTimeIconGap + val timelineTimeSlotWidth = Dimens.LegTimelineTimeSlotWidth + val timelineColumnWidth = timelineTimeSlotWidth + timelineTimeIconGap + timelineIconSize + Row( - modifier = Modifier + modifier = modifier.zIndex(1f), + verticalAlignment = Alignment.CenterVertically ) { - Column( - modifier = Modifier.width(Dimens.LegDetailVerticalLineWidth), - horizontalAlignment = Alignment.CenterHorizontally + Row( + modifier = Modifier + .width(timelineColumnWidth) + .height(Dimens.LegDetailVerticalLineWidth), + verticalAlignment = Alignment.CenterVertically ) { + Box(modifier = Modifier.width(timelineTimeSlotWidth)) { + BoardingTime( + boardingDateTime = arrivalTime, + modifier = Modifier.align(Alignment.CenterStart) + ) + } + Spacer(modifier = Modifier.width(timelineTimeIconGap)) + Image( - modifier = Modifier.size(36.dp), + modifier = Modifier + .offset(y = markerYOffset) + .size(timelineIconSize), imageVector = ImageVector.vectorResource(markerIconId), contentDescription = "" ) - BoardingTime( - boardingDateTime = arrivalTime, - modifier = Modifier - ) } Spacer( modifier = Modifier.width(6.dp) ) Text( - modifier = Modifier - .height(36.dp) - .wrapContentSize(Alignment.Center), + modifier = Modifier, text = name, style = defaultTeam6Typography.body5_B5SB14, color = defaultTeam6Colors.white diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt index 74db2a20..debb67d6 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt @@ -10,10 +10,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box as LayoutBox import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -21,6 +23,7 @@ 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.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth @@ -41,6 +44,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -49,8 +57,10 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.depromeet.team6.R import com.depromeet.team6.domain.model.BusCongestion import com.depromeet.team6.domain.model.BusStatus @@ -85,6 +95,12 @@ fun ItineraryInfoDetailLegs( modifier: Modifier = Modifier, onClickBusInfo: (BusArrivalParameter) -> Unit = {} ) { + val timelineIconSize = Dimens.LegTimelineIconSize + val timelineTimeIconGap = Dimens.LegTimelineTimeIconGap + val timelineTimeSlotWidth = Dimens.LegTimelineTimeSlotWidth + val timelineColumnMinWidth = timelineTimeSlotWidth + timelineTimeIconGap + timelineIconSize + val timelineAxisOffset = timelineTimeSlotWidth + timelineTimeIconGap + (timelineIconSize / 2) + // Column의 실제 높이(px) var columnHeightPx by remember { mutableIntStateOf(0) } val density = LocalDensity.current @@ -104,12 +120,20 @@ fun ItineraryInfoDetailLegs( for ((idx, leg) in legs.withIndex()) { when (leg.transportType) { TransportType.WALK -> { - val verticalHeight = if (idx == 0 || idx == legs.size - 1) 48.dp else 60.dp + val verticalHeight = walkTimelineHeight( + distanceMeter = leg.distance, + isFirstWalk = idx == 0, + isLastWalk = idx == legs.size - 1 + ) + val walkLineOffsetY = if (idx == 0) (-3).dp else 0.dp DetailLegsWalk( boardingDateTime = leg.departureDateTime!!, timeMinute = leg.sectionTime / 60, distanceMeter = leg.distance, - verticalHeight = verticalHeight + verticalHeight = verticalHeight, + lineOffsetY = walkLineOffsetY, + timelineAxisOffset = timelineAxisOffset, + timelineColumnWidth = timelineColumnMinWidth ) } TransportType.BUS -> { @@ -123,6 +147,10 @@ fun ItineraryInfoDetailLegs( distanceMeter = leg.distance, busArrivalStatus = busArrivalStatus.get(idx), passStopList = leg.passStopList, + timelineIconSize = timelineIconSize, + timelineTimeSlotWidth = timelineTimeSlotWidth, + timelineTimeIconGap = timelineTimeIconGap, + timelineColumnMinWidth = timelineColumnMinWidth, onClickBusInfo = { routeName, stationName, subtypeIdx -> onClickBusInfo( BusArrivalParameter( @@ -146,7 +174,11 @@ fun ItineraryInfoDetailLegs( boardingDateTime = leg.departureDateTime!!, timeMinute = leg.sectionTime / 60, passStopList = leg.passStopList, - distanceMeter = leg.distance + distanceMeter = leg.distance, + timelineIconSize = timelineIconSize, + timelineTimeSlotWidth = timelineTimeSlotWidth, + timelineTimeIconGap = timelineTimeIconGap, + timelineColumnMinWidth = timelineColumnMinWidth ) } } @@ -228,6 +260,10 @@ private fun DetailLegsBus( distanceMeter: Int, busArrivalStatus: RealTimeBusArrival?, passStopList: List, + timelineIconSize: Dp, + timelineTimeSlotWidth: Dp, + timelineTimeIconGap: Dp, + timelineColumnMinWidth: Dp, modifier: Modifier = Modifier, onClickBusInfo: (String, String, Int) -> Unit = { routeName: String, stationName: String, subtypeIdx: Int -> } ) { @@ -237,9 +273,11 @@ private fun DetailLegsBus( .parse(boardingDateTime) .plusMinutes(timeMinute.toLong()) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val timelineAxisOffset = timelineTimeSlotWidth + timelineTimeIconGap + (timelineIconSize / 2) Row( modifier = modifier + .zIndex(1f) .wrapContentHeight() ) { val busColor = TransportTypeUiMapper.getColor(TransportType.BUS, subtypeIdx) @@ -247,50 +285,66 @@ private fun DetailLegsBus( // 좌측 버스 수직라인 Box( modifier = Modifier - .width(Dimens.LegDetailVerticalLineWidth) + .widthIn(min = timelineColumnMinWidth) + .wrapContentWidth() .height(rowHeight.dp), - contentAlignment = Alignment.TopCenter + contentAlignment = Alignment.TopStart ) { // 세로 직선 Box( modifier = Modifier .fillMaxHeight() - .width(4.dp) - .padding(top = 15.dp, bottom = 20.dp) + .width(3.dp) + .align(Alignment.TopStart) + .offset(x = timelineAxisOffset - 1.5.dp) + .padding(top = 12.dp, bottom = 8.dp) .background(busColor) - .align(Alignment.Center) ) Column( modifier = Modifier + .widthIn(min = timelineColumnMinWidth) .wrapContentWidth(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.Start ) { - // 버스 아이콘 - Image( - imageVector = ImageVector.vectorResource(busIconId), - contentDescription = null, - modifier = Modifier.size(36.dp) - ) - // 탑승 시각 - BoardingTime( - boardingDateTime = boardingDateTime, - modifier = Modifier - ) + Row( + modifier = Modifier.wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + BoardingTime( + boardingDateTime = boardingDateTime, + modifier = Modifier.align(Alignment.CenterStart) + ) + } + Spacer(modifier = Modifier.width(timelineTimeIconGap)) + Image( + imageVector = ImageVector.vectorResource(busIconId), + contentDescription = null, + modifier = Modifier.size(timelineIconSize) + ) + } Spacer(modifier = Modifier.weight(weight = 1f)) // 남은 공간 차지해서 아래로 밀어줌 - // 하차지점 - Box( - modifier = Modifier - .size(18.dp) // 지름 크기 - .clip(CircleShape) - .background(busColor) - ) - Spacer(modifier = Modifier.height(2.dp)) - BoardingTime( - boardingDateTime = disembarkingDateTime, - modifier = Modifier - ) + Row( + modifier = Modifier.wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + BoardingTime( + boardingDateTime = disembarkingDateTime, + modifier = Modifier.align(Alignment.CenterStart) + ) + } + Spacer(modifier = Modifier.width(timelineTimeIconGap)) + Box( + modifier = Modifier + .size(14.dp) + .offset(x = 6.dp) + .clip(CircleShape) + .background(busColor) + ) + } } } @@ -393,6 +447,8 @@ private fun DetailLegsBus( val stop = passStopList[i] Text( text = stop.stationName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, style = defaultTeam6Typography.body7_B7M13, color = defaultTeam6Colors.gray200 ) @@ -402,22 +458,10 @@ private fun DetailLegsBus( Spacer(Modifier.height(36.dp)) // 하차 - Row( - modifier = Modifier - .padding(bottom = 20.dp) - ) { - Text( - text = disembarkingStation, - style = defaultTeam6Typography.body5_B5SB14, - color = defaultTeam6Colors.white - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(R.string.itinerary_info_legs_disembarking), - style = defaultTeam6Typography.body5_B5SB14, - color = defaultTeam6Colors.gray200 - ) - } + DisembarkingStationText( + stationName = disembarkingStation, + modifier = Modifier.padding(bottom = 20.dp) + ) } } } @@ -432,6 +476,10 @@ private fun DetailLegsSubway( timeMinute: Int, distanceMeter: Int, passStopList: List, + timelineIconSize: Dp, + timelineTimeSlotWidth: Dp, + timelineTimeIconGap: Dp, + timelineColumnMinWidth: Dp, modifier: Modifier = Modifier ) { var rowHeight by remember { mutableStateOf(0) } @@ -440,9 +488,11 @@ private fun DetailLegsSubway( .parse(boardingDateTime) .plusMinutes(timeMinute.toLong()) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val timelineAxisOffset = timelineTimeSlotWidth + timelineTimeIconGap + (timelineIconSize / 2) Row( modifier = modifier + .zIndex(1f) .wrapContentHeight() ) { val subwayColor = TransportTypeUiMapper.getColor(TransportType.SUBWAY, subtypeIdx) @@ -450,50 +500,66 @@ private fun DetailLegsSubway( // 좌측 버스 수직라인 Box( modifier = Modifier - .width(Dimens.LegDetailVerticalLineWidth) + .widthIn(min = timelineColumnMinWidth) + .wrapContentWidth() .height(rowHeight.dp), - contentAlignment = Alignment.TopCenter + contentAlignment = Alignment.TopStart ) { // 세로 직선 Box( modifier = Modifier .fillMaxHeight() - .width(4.dp) - .padding(top = 15.dp, bottom = 20.dp) + .width(3.dp) + .align(Alignment.TopStart) + .offset(x = timelineAxisOffset - 1.5.dp) + .padding(top = 12.dp, bottom = 8.dp) .background(subwayColor) - .align(Alignment.Center) ) Column( modifier = Modifier + .widthIn(min = timelineColumnMinWidth) .wrapContentWidth(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.Start ) { - // 지하철 아이콘 - Image( - imageVector = ImageVector.vectorResource(subwayIconId), - contentDescription = null, - modifier = Modifier.size(36.dp) - ) - // 탑승 시각 - BoardingTime( - boardingDateTime = boardingDateTime, - modifier = Modifier - ) + Row( + modifier = Modifier.wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + BoardingTime( + boardingDateTime = boardingDateTime, + modifier = Modifier.align(Alignment.CenterStart) + ) + } + Spacer(modifier = Modifier.width(timelineTimeIconGap)) + Image( + imageVector = ImageVector.vectorResource(subwayIconId), + contentDescription = null, + modifier = Modifier.size(timelineIconSize) + ) + } Spacer(modifier = Modifier.weight(weight = 1f)) // 남은 공간 차지해서 아래로 밀어줌 - // 하차지점 - Box( - modifier = Modifier - .size(18.dp) // 지름 크기 - .clip(CircleShape) - .background(subwayColor) - ) - Spacer(modifier = Modifier.height(2.dp)) - BoardingTime( - boardingDateTime = disembarkingDateTime, - modifier = Modifier - ) + Row( + modifier = Modifier.wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + BoardingTime( + boardingDateTime = disembarkingDateTime, + modifier = Modifier.align(Alignment.CenterStart) + ) + } + Spacer(modifier = Modifier.width(timelineTimeIconGap)) + Box( + modifier = Modifier + .size(14.dp) + .offset(x = 6.dp) + .clip(CircleShape) + .background(subwayColor) + ) + } } } @@ -570,6 +636,8 @@ private fun DetailLegsSubway( val stop = passStopList[i] Text( text = stop.stationName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, style = defaultTeam6Typography.body7_B7M13, color = defaultTeam6Colors.gray200 ) @@ -579,22 +647,10 @@ private fun DetailLegsSubway( Spacer(Modifier.height(36.dp)) // 하차 - Row( - modifier = Modifier - .padding(bottom = 20.dp) - ) { - Text( - text = disembarkingStation, - style = defaultTeam6Typography.body5_B5SB14, - color = defaultTeam6Colors.white - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(R.string.itinerary_info_legs_disembarking), - style = defaultTeam6Typography.body5_B5SB14, - color = defaultTeam6Colors.gray200 - ) - } + DisembarkingStationText( + stationName = disembarkingStation, + modifier = Modifier.padding(bottom = 20.dp) + ) } } } @@ -605,6 +661,9 @@ private fun DetailLegsWalk( timeMinute: Int, distanceMeter: Int, verticalHeight: Dp, + lineOffsetY: Dp, + timelineAxisOffset: Dp, + timelineColumnWidth: Dp, modifier: Modifier = Modifier ) { Row( @@ -614,13 +673,11 @@ private fun DetailLegsWalk( // 좌측 점선 Column( modifier = Modifier - .width(Dimens.LegDetailVerticalLineWidth), - horizontalAlignment = Alignment.CenterHorizontally + .width(timelineColumnWidth), + horizontalAlignment = Alignment.Start ) { - Spacer( - modifier = Modifier.height(5.dp) - ) DottedLineWithCircles( + modifier = Modifier.offset(x = timelineAxisOffset - 2.5.dp, y = lineOffsetY), height = verticalHeight ) } @@ -656,7 +713,7 @@ fun BoardingTime( ) { val boardingTime = LocalDateTime.parse(boardingDateTime) Box( - modifier = Modifier + modifier = modifier .wrapContentSize() .border( width = 1.dp, @@ -720,6 +777,81 @@ private fun DottedLineWithCircles( } } +@Composable +private fun DisembarkingStationText( + stationName: String, + modifier: Modifier = Modifier +) { + val stationStyle = defaultTeam6Typography.body5_B5SB14 + val suffixStyle = defaultTeam6Typography.body5_B5SB14 + val suffixText = stringResource(R.string.itinerary_info_legs_disembarking) + val textMeasurer = rememberTextMeasurer() + val density = LocalDensity.current + + BoxWithConstraints(modifier = modifier.fillMaxWidth()) { + val maxWidthPx = with(density) { maxWidth.toPx() }.toInt() + val spacingPx = with(density) { 4.dp.roundToPx() } + val suffixWidthPx = textMeasurer.measure( + text = suffixText, + style = suffixStyle, + maxLines = 1 + ).size.width + + val stationLayout = textMeasurer.measure( + text = stationName, + style = stationStyle, + constraints = Constraints(maxWidth = maxWidthPx) + ) + val useCompactSingleLine = stationLayout.lineCount > 2 + + if (useCompactSingleLine) { + val stationMaxWidthDp = with(density) { + (maxWidthPx - suffixWidthPx - spacingPx).coerceAtLeast(0).toDp() + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.width(stationMaxWidthDp), + text = stationName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = stationStyle, + color = defaultTeam6Colors.white + ) + Spacer(Modifier.width(4.dp)) + Text( + text = suffixText, + style = suffixStyle, + color = defaultTeam6Colors.gray200 + ) + } + } else { + Text( + text = buildAnnotatedString { + withStyle(SpanStyle(color = defaultTeam6Colors.white)) { append(stationName) } + append(" ") + withStyle(SpanStyle(color = defaultTeam6Colors.gray200)) { append(suffixText) } + }, + maxLines = 2, + overflow = TextOverflow.Clip, + style = stationStyle + ) + } + } +} + +private fun walkTimelineHeight( + distanceMeter: Int, + isFirstWalk: Boolean, + isLastWalk: Boolean +): Dp { + val baseHeight = (distanceMeter / 10f).dp.coerceIn(44.dp, 120.dp) + return when { + isFirstWalk -> baseHeight + 10.dp + isLastWalk -> baseHeight - 4.dp + else -> baseHeight + }.coerceIn(40.dp, 130.dp) +} + @Composable private fun BusNumberButton( busName: String, From 0dd589d76e31004f32ce905284ea5bf4dba540bc Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 16:46:25 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat/#257=20:=20=ED=83=80=EC=9D=B4=ED=8B=80?= =?UTF-8?q?=20=EB=B0=8F=20=EC=B0=A8=ED=8A=B8=20=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itinerary/component/ItinerarySummary.kt | 8 +++--- .../ui/itinerary/component/SummaryBarChart.kt | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItinerarySummary.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItinerarySummary.kt index 5ea201a8..b0728aa8 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItinerarySummary.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItinerarySummary.kt @@ -1,6 +1,5 @@ package com.depromeet.team6.presentation.ui.itinerary.component -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -39,14 +38,13 @@ fun ItinerarySummary( // ) Column( modifier = modifier - .background(defaultTeam6Colors.gray950) ) { // 남은 시간 if (durationHour > 0) { Text( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp), + .padding(top = 2.dp, bottom = 8.dp), text = stringResource(R.string.itinerary_summary_duration_time, durationHour, durationMinute), style = defaultTeam6Typography.display4_D4SB28, fontSize = 28.sp, @@ -56,7 +54,7 @@ fun ItinerarySummary( Text( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp), + .padding(top = 2.dp, bottom = 8.dp), text = stringResource(R.string.itinerary_summary_duration_minute, durationMinute), style = defaultTeam6Typography.display4_D4SB28, fontSize = 28.sp, @@ -83,7 +81,7 @@ fun ItinerarySummary( // 대중교통 정보 요약 SummaryBarChart( modifier = Modifier - .padding(vertical = 16.dp), + .padding(top = 14.dp, bottom = 10.dp), legs = legs ) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt index 435b8a7a..fb5c35a1 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt @@ -45,9 +45,8 @@ fun SummaryBarChart( modifier: Modifier = Modifier, legs: List ) { - val total = legs.sumOf { it.sectionTime }.toFloat() var rowWidthPx by remember { mutableStateOf(0f) } // Row의 너비를 저장할 변수 - val minBarWidth = 25.dp + val minBarWidth = 18.dp val density = LocalDensity.current var finalWidths by remember { mutableStateOf(emptyList()) } @@ -156,19 +155,25 @@ private fun calculateFinalWidths( minBarWidth: Dp ): List { // 임시 데이터 구조: (인덱스, 해당 leg의 sectionTime) - val remainingLegs = legs.mapIndexed { index, leg -> - index to leg.sectionTime - }.toMutableList() + if (legs.isEmpty()) return emptyList() // 최소너비 미리 세팅해두고, 그만큼을 전체너비에서 제외 - val finalWidths = MutableList(legs.size) { minBarWidth } - var remainingWidth = totalWidth.value - (minBarWidth.value * finalWidths.size) // dp 단위의 Float 값으로 사용 + val totalWidthValue = totalWidth.value + val minWidthValue = minBarWidth.value + val totalMinWidth = minWidthValue * legs.size + + if (totalMinWidth >= totalWidthValue) { + val average = totalWidthValue / legs.size + return List(legs.size) { average.dp } + } // 남은 아이템에 대해 남은 너비를 비율로 분배 - val remainingTimeSum = remainingLegs.sumOf { it.second } - remainingLegs.forEach { (index, time) -> - val allocated = if (remainingTimeSum > 0) remainingWidth * (time / remainingTimeSum.toFloat()) else 0f - finalWidths[index] += allocated.dp + val remainingWidth = totalWidthValue - totalMinWidth + val remainingTimeSum = legs.sumOf { it.sectionTime }.coerceAtLeast(1) + val finalWidths = MutableList(legs.size) { minBarWidth } + legs.forEachIndexed { index, leg -> + val allocated = remainingWidth * (leg.sectionTime / remainingTimeSum.toFloat()) + finalWidths[index] = (minWidthValue + allocated).dp } return finalWidths From c569e0d8d833c430689bfa720caa29f79b2a258f Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 16:54:38 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore/#257=20:=20ktlint=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itinerary/component/ItineraryInfoDetailLegs.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt index debb67d6..122be3b2 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.Box as LayoutBox import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -44,20 +43,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -85,6 +84,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.math.max import kotlin.math.roundToInt +import androidx.compose.foundation.layout.Box as LayoutBox @Composable fun ItineraryInfoDetailLegs( From 34f89816a8a5246c78acd64c92263cbb12e00df1 Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 17:44:47 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat/#257=20:=20=EC=A4=91=EB=B3=B5=20Box=20?= =?UTF-8?q?import=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20'=ED=95=98=EC=B0=A8'?= =?UTF-8?q?=20suffix=20=EC=9E=98=EB=A6=BC=20=ED=8C=90=EB=8B=A8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ItineraryInfoDetailLegs.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt index 122be3b2..8f415762 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/ItineraryInfoDetailLegs.kt @@ -84,7 +84,6 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.math.max import kotlin.math.roundToInt -import androidx.compose.foundation.layout.Box as LayoutBox @Composable fun ItineraryInfoDetailLegs( @@ -274,6 +273,8 @@ private fun DetailLegsBus( .plusMinutes(timeMinute.toLong()) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) val timelineAxisOffset = timelineTimeSlotWidth + timelineTimeIconGap + (timelineIconSize / 2) + val disembarkingMarkerSize = timelineIconSize * (14f / 26f) + val disembarkingMarkerOffset = (timelineIconSize - disembarkingMarkerSize) / 2 Row( modifier = modifier @@ -310,7 +311,7 @@ private fun DetailLegsBus( modifier = Modifier.wrapContentWidth(), verticalAlignment = Alignment.CenterVertically ) { - LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + Box(modifier = Modifier.width(timelineTimeSlotWidth)) { BoardingTime( boardingDateTime = boardingDateTime, modifier = Modifier.align(Alignment.CenterStart) @@ -330,7 +331,7 @@ private fun DetailLegsBus( modifier = Modifier.wrapContentWidth(), verticalAlignment = Alignment.CenterVertically ) { - LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + Box(modifier = Modifier.width(timelineTimeSlotWidth)) { BoardingTime( boardingDateTime = disembarkingDateTime, modifier = Modifier.align(Alignment.CenterStart) @@ -339,8 +340,8 @@ private fun DetailLegsBus( Spacer(modifier = Modifier.width(timelineTimeIconGap)) Box( modifier = Modifier - .size(14.dp) - .offset(x = 6.dp) + .size(disembarkingMarkerSize) + .offset(x = disembarkingMarkerOffset) .clip(CircleShape) .background(busColor) ) @@ -489,6 +490,8 @@ private fun DetailLegsSubway( .plusMinutes(timeMinute.toLong()) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) val timelineAxisOffset = timelineTimeSlotWidth + timelineTimeIconGap + (timelineIconSize / 2) + val disembarkingMarkerSize = timelineIconSize * (14f / 26f) + val disembarkingMarkerOffset = (timelineIconSize - disembarkingMarkerSize) / 2 Row( modifier = modifier @@ -525,7 +528,7 @@ private fun DetailLegsSubway( modifier = Modifier.wrapContentWidth(), verticalAlignment = Alignment.CenterVertically ) { - LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + Box(modifier = Modifier.width(timelineTimeSlotWidth)) { BoardingTime( boardingDateTime = boardingDateTime, modifier = Modifier.align(Alignment.CenterStart) @@ -545,7 +548,7 @@ private fun DetailLegsSubway( modifier = Modifier.wrapContentWidth(), verticalAlignment = Alignment.CenterVertically ) { - LayoutBox(modifier = Modifier.width(timelineTimeSlotWidth)) { + Box(modifier = Modifier.width(timelineTimeSlotWidth)) { BoardingTime( boardingDateTime = disembarkingDateTime, modifier = Modifier.align(Alignment.CenterStart) @@ -554,8 +557,8 @@ private fun DetailLegsSubway( Spacer(modifier = Modifier.width(timelineTimeIconGap)) Box( modifier = Modifier - .size(14.dp) - .offset(x = 6.dp) + .size(disembarkingMarkerSize) + .offset(x = disembarkingMarkerOffset) .clip(CircleShape) .background(subwayColor) ) @@ -797,12 +800,16 @@ private fun DisembarkingStationText( maxLines = 1 ).size.width - val stationLayout = textMeasurer.measure( - text = stationName, + val combinedLayout = textMeasurer.measure( + text = buildAnnotatedString { + append(stationName) + append(" ") + append(suffixText) + }, style = stationStyle, constraints = Constraints(maxWidth = maxWidthPx) ) - val useCompactSingleLine = stationLayout.lineCount > 2 + val useCompactSingleLine = combinedLayout.lineCount > 2 if (useCompactSingleLine) { val stationMaxWidthDp = with(density) { From 5034fe532c021f1a3b8c6b2175d64114ee5ab16a Mon Sep 17 00:00:00 2001 From: Jieun Date: Sun, 15 Mar 2026 17:45:05 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat/#257=20:=20sectionTime=20=EC=9D=8C?= =?UTF-8?q?=EC=88=98=20=EB=B0=A9=EC=96=B4=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/itinerary/component/SummaryBarChart.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt index fb5c35a1..c2c09a91 100644 --- a/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt +++ b/app/src/main/java/com/depromeet/team6/presentation/ui/itinerary/component/SummaryBarChart.kt @@ -169,10 +169,10 @@ private fun calculateFinalWidths( // 남은 아이템에 대해 남은 너비를 비율로 분배 val remainingWidth = totalWidthValue - totalMinWidth - val remainingTimeSum = legs.sumOf { it.sectionTime }.coerceAtLeast(1) + val remainingTimeSum = legs.sumOf { it.sectionTime.coerceAtLeast(0) }.coerceAtLeast(1) val finalWidths = MutableList(legs.size) { minBarWidth } legs.forEachIndexed { index, leg -> - val allocated = remainingWidth * (leg.sectionTime / remainingTimeSum.toFloat()) + val allocated = remainingWidth * (leg.sectionTime.coerceAtLeast(0) / remainingTimeSum.toFloat()) finalWidths[index] = (minWidthValue + allocated).dp }