Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e4c2aaf
fix: TopNavigation 메뉴 아이콘 설정 아이콘으로 변경
seunghee17 Aug 19, 2026
07dc67f
fix: TopNavigation 메뉴 아이콘 설정 아이콘으로 변경
seunghee17 Aug 19, 2026
cb5041b
feat: 스낵바 컴포넌트 추가
seunghee17 Aug 19, 2026
869bb82
feat: 채팅방 진입 애니메이션 안끊기도록 개선
seunghee17 Aug 19, 2026
b034f71
feat: 토큰 사용량 10% 미만·소진 알림을 전역 1회로 통합
seunghee17 Aug 19, 2026
fd39936
feat: 채팅방 빈 화면 탭할때 키보드 내려가는 동작 추가
seunghee17 Aug 19, 2026
ac0e2cf
fix: 현재 화면에 보이는 메시지가 키보드 스크롤에 의해 가려지지 않도록 수정
seunghee17 Aug 19, 2026
d0a6ca2
fix: 토큰 소모 스낵바 표시 위치 변경
seunghee17 Aug 20, 2026
86a805f
fix: 대화 목록 손그림 시안 적용 및 디자인 반영
seunghee17 Aug 20, 2026
0ccdfd4
fix: 대화목록 헤더 컬러값 변경
seunghee17 Aug 20, 2026
2241626
Merge remote-tracking branch 'origin/develop' into fix/chatting-room-ui
seunghee17 Aug 20, 2026
d026c80
Merge remote-tracking branch 'origin/develop' into fix/chatting-room-ui
seunghee17 Aug 20, 2026
8121cab
fix: 채팅방 텍스트 필드 테두리 손그림 시안 적용 및 세부 디자인 수정
seunghee17 Aug 20, 2026
6113dd7
chore: 주석 삭제
seunghee17 Aug 20, 2026
f167543
feat: 토큰 소모 알림을 스낵바에서 토스트로 전환하고 전송 버튼 잠금 추가
seunghee17 Aug 20, 2026
a9f92cd
fix: 토큰 소진 토스트를 하루 1회로 제한하고 소진 시 입력창 잠금 추가
seunghee17 Aug 20, 2026
a68e7a8
fix: 토큰 소진 시 canSend에서도 전송을 막도록 방어 추가
seunghee17 Aug 20, 2026
9ac7f80
chore: detekt 정렬 수정
seunghee17 Aug 20, 2026
8d340db
fix: 메시지 전송 시 서버 응답을 기다리지 않고 입력칸을 즉시 비움
seunghee17 Aug 20, 2026
302b6e1
Merge remote-tracking branch 'origin/develop' into fix/chatting-room-ui
seunghee17 Aug 20, 2026
99408ae
fix: 아이콘명 불일치 문제 해결
seunghee17 Aug 20, 2026
ca87d15
Merge remote-tracking branch 'origin/develop' into fix/chatting-room-ui
seunghee17 Aug 21, 2026
4a42526
chore: 미사용 함수 제거
seunghee17 Aug 21, 2026
abb9bfd
fix: IME 인셋 보정 스크롤이 다른 스크롤에 뮤텍스를 뺏겨도 멈추지 않도록 수정
seunghee17 Aug 21, 2026
b9c9540
fix: 토큰 소진으로 입력창이 잠길 때 남아있던 입력 초안을 비움
seunghee17 Aug 21, 2026
ee4162e
fix: 토큰 사용량 알림이 소진 시점 화면에서만, 정확히 한 번만 뜨게 합니다
seunghee17 Aug 21, 2026
1039c8d
Merge branch 'develop' into fix/chatting-room-ui
seunghee17 Aug 21, 2026
6895469
chore: 주석 수정
seunghee17 Aug 21, 2026
dd18b3f
fix: ChatRoomScreen import 순서를 detekt 규칙에 맞게 정렬합니다
seunghee17 Aug 21, 2026
1728ec3
Merge remote-tracking branch 'origin/develop' into fix/chatting-room-ui
seunghee17 Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 54 additions & 7 deletions app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private fun mainEntryProvider(navigator: Navigator) = entryProvider {
entry<ChatKey> {
ChattingListScreen(
onChatClick = { navigator.navigate(ChatRoomKey(it)) },
onMenuClick = { navigator.navigate(SettingKey) },
onSettingClick = { navigator.navigate(SettingKey) },
)
}
entry<ChatRoomKey>(metadata = detailSlideTransition) { key ->
Expand Down Expand Up @@ -217,20 +217,67 @@ private val tabFadeThroughSpec: AnimatedContentTransitionScope<Scene<NavKey>>.()
* 탭 내부의 상세 화면(Setting/AccountInfo/NicknameChange/ChatRoom/WebView) push·pop 전용
* 트랜지션. 계층 이동이라는 방향감을 주기 위해 좌우 슬라이드(shared axis X)를 쓴다.
*
* slideIntoContainer/slideOutOfContainer는 initialOffset/targetOffset을 안 주면 화면 폭 전체를
* 미는 거리로 슬라이드한다 — Material의 shared axis X는 폭 전체가 아니라 일부만 이동해야 가벼운
* 느낌이 나는데, 전체 폭을 밀면 같은 duration이어도 이동 거리가 커서 둔탁하고 느리게 느껴진다.
* DETAIL_TRANSITION_SLIDE_FRACTION으로 이동 거리를 줄이고, duration도 같이 낮춘다.
*
* 트리거 경로마다 참조하는 spec이 달라서, 셋 다 지정하지 않으면 모션이 갈립니다.
*/
private val detailSlideTransition: Map<String, Any> = NavDisplay.transitionSpec {
(slideIntoContainer(SlideDirection.Start, tween(300)) + fadeIn(tween(300))) togetherWith
(slideOutOfContainer(SlideDirection.Start, tween(300)) + fadeOut(tween(150)))
(
slideIntoContainer(
SlideDirection.Start,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
initialOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeIn(tween(DETAIL_TRANSITION_DURATION_MILLIS))
) togetherWith
(
slideOutOfContainer(
SlideDirection.Start,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
targetOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeOut(tween(DETAIL_TRANSITION_EXIT_FADE_DURATION_MILLIS))
)
} + NavDisplay.popTransitionSpec {
(slideIntoContainer(SlideDirection.End, tween(300)) + fadeIn(tween(300))) togetherWith
(slideOutOfContainer(SlideDirection.End, tween(300)) + fadeOut(tween(150)))
(
slideIntoContainer(
SlideDirection.End,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
initialOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeIn(tween(DETAIL_TRANSITION_DURATION_MILLIS))
) togetherWith
(
slideOutOfContainer(
SlideDirection.End,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
targetOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeOut(tween(DETAIL_TRANSITION_EXIT_FADE_DURATION_MILLIS))
)
} + NavDisplay.predictivePopTransitionSpec { swipeEdge ->
val towards = if (swipeEdge == NavigationEvent.EDGE_RIGHT) SlideDirection.Start else SlideDirection.End
(slideIntoContainer(towards, tween(300)) + fadeIn(tween(300))) togetherWith
(slideOutOfContainer(towards, tween(300)) + fadeOut(tween(150)))
(
slideIntoContainer(
towards,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
initialOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeIn(tween(DETAIL_TRANSITION_DURATION_MILLIS))
) togetherWith
(
slideOutOfContainer(
towards,
tween(DETAIL_TRANSITION_DURATION_MILLIS),
targetOffset = { fullWidth -> fullWidth / DETAIL_TRANSITION_SLIDE_FRACTION },
) + fadeOut(tween(DETAIL_TRANSITION_EXIT_FADE_DURATION_MILLIS))
)
}

private const val DETAIL_TRANSITION_DURATION_MILLIS = 240
private const val DETAIL_TRANSITION_EXIT_FADE_DURATION_MILLIS = 120

// 화면 폭의 1/4만 슬라이드한다(= shared axis X). 폭 전체를 밀지 않아야 가볍게 느껴진다.
private const val DETAIL_TRANSITION_SLIDE_FRACTION = 4

/**
* 홈 루트에서는 NavDisplay의 previousEntries가 비어 자체 back handler가 꺼지므로 이 handler가 받습니다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.gamss.android.core.designsystem.component

import android.content.res.Configuration
import androidx.compose.foundation.Image
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
Expand All @@ -20,7 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
Expand All @@ -33,10 +30,9 @@ import com.gamss.android.core.designsystem.component.chat.ChatReplyQuote
import com.gamss.android.core.designsystem.theme.GamssTheme

/** 입력바 아래에 무언가를 띄우는 화면이 있어 공개한다. */
val GamssInputBarHeight = 53.dp
val GamssInputBarHeight = 48.dp

private val InputBarHeight = GamssInputBarHeight
private val InputBarBorderWidth = 1.5.dp
private val InputBarStartPadding = 16.dp

/** 전송 버튼은 배경까지 담긴 32x32 에셋이라 tint 하지 않고 그대로 그린다. */
Expand All @@ -45,15 +41,19 @@ private val SendButtonSize = 32.dp
private val SendButtonTouchSize = 48.dp
private val InputBarEndPadding = 8.dp

// 답장 미리보기가 있을 때만 쓰는 세로 여백/간격. 없을 때는 기존처럼 최소 높이 + 가운데 정렬로 채운다.
// 답장 미리보기가 없을 때 글과 테두리 사이에 항상 남겨야 하는 여백. 줄 수와 무관하게 고정값으로
// 둬야 두 줄 이상으로 늘어나도 글이 테두리에 붙지 않는다.
private val InputBarVerticalPadding = 8.dp

// 답장 미리보기가 있을 때 쓰는 세로 여백/간격.
private val InputBarReplyVerticalPadding = 14.dp
private val InputBarReplyGap = 14.dp

private val ReplyClearIconSize = 20.dp
private val ReplyClearTouchSize = 32.dp

// 140자 상한을 이 폭에서 담으려면 6줄이면 넉넉하다. 넘치면 입력칸 안에서 스크롤된다.
private const val INPUT_MAX_LINES = 6
private const val INPUT_MAX_LINES = 5

@Suppress("LongParameterList")
@Composable
Expand Down Expand Up @@ -85,8 +85,8 @@ fun GamssInputBar(

Column(
modifier = modifier
.background(GamssTheme.colors.white, RectangleShape)
.border(InputBarBorderWidth, GamssTheme.colors.black, RectangleShape)
.gamssSketchyBox(artRes = R.drawable.bg_textfield_default, background = GamssTheme.colors.white)
.heightIn(min = GamssInputBarHeight)
.padding(start = InputBarStartPadding, top = verticalPadding, bottom = verticalPadding),
) {
if (replyQuote != null) {
Expand All @@ -101,15 +101,18 @@ fun GamssInputBar(
}
Row(
modifier = Modifier
// 140자를 채우면 여러 줄이 된다. 한 줄일 때의 높이를 최소치로만 잡고 아래로 늘어나게 둔다.
// 전송 버튼 터치 영역(48dp)을 최소치로 잡는다. 140자를 채워 여러 줄이 되면 그
// 이상으로 늘어난다.
.heightIn(min = InputBarHeight)
.padding(end = InputBarEndPadding),
verticalAlignment = Alignment.CenterVertically,
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.let { if (replyQuote == null) it.padding(vertical = InputBarVerticalPadding) else it },
enabled = enabled,
// 한 줄로 묶으면 140자 상한에 걸려도 가로로 스크롤만 되어 잘린 게 보이지 않는다.
// 상한을 안 걸면 남은 세로 공간을 전부 채워 버리므로 줄 수로 묶는다.
Expand All @@ -130,7 +133,7 @@ fun GamssInputBar(
GamssText(
text = placeholder,
style = GamssTheme.typography.body4Medium,
color = GamssTheme.colors.gray500,
color = GamssTheme.colors.gray400,
)
}
innerTextField()
Expand Down Expand Up @@ -192,7 +195,7 @@ private fun ReplyPreviewRow(
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(R.drawable.ic_close),
painter = painterResource(GamssIcons.ClearButton),
contentDescription = clearContentDescription,
modifier = Modifier.size(ReplyClearIconSize),
tint = GamssTheme.colors.gray300,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.gamss.android.core.designsystem.modifier
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput

fun Modifier.addFocusCleaner(focusManager: FocusManager): Modifier {
return this.pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.gamss.android.core.designsystem.snackbar

import android.content.res.Configuration
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.gamss.android.core.designsystem.component.GamssIcons
import com.gamss.android.core.designsystem.theme.GamssTheme

private val SnackBarIconSize = 20.dp

/**
* 화면 하단에 뜨는 스낵바. 아이콘은 화면마다 다른 것을 붙일 수 있어 nullable 로 둔다.
* 다른 디자인시스템 컴포넌트들과 마찬가지로 drawable을 painterResource()로 그려 넘기는
* [Painter]를 받는다 — ImageVector는 material-icons 의존성이 따로 필요해 이 모듈엔 없다.
* 시안 문구는 한 줄 사용을 권장하지만, 길어지면 그대로 줄바꿈된다.
*/
@Composable
fun GamssSnackBar(
message: String,
modifier: Modifier = Modifier,
snackBarIcon: (@Composable () -> Unit)? = null,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(GamssTheme.radius.radius100))
.background(color = GamssTheme.colors.gray900)
.padding(
horizontal = GamssTheme.spacing.spacing300,
vertical = GamssTheme.spacing.spacing200,
),
horizontalArrangement = Arrangement.spacedBy(GamssTheme.spacing.spacing100),
verticalAlignment = Alignment.CenterVertically,
) {
if (snackBarIcon != null) {
Box(
modifier = Modifier.size(SnackBarIconSize),
contentAlignment = Alignment.Center,
) {
snackBarIcon()
}
}
Text(
text = message,
style = GamssTheme.typography.body4Medium,
color = GamssTheme.colors.gray025,
)
}
}

@Preview(name = "Light", showBackground = true, widthDp = 402)
@Suppress("UnusedPrivateMember")
@Composable
private fun GamssSnackBarLightPreview() {
GamssTheme(darkTheme = false) {
GamssSnackBarPreviewContent()
}
}

@Preview(
name = "Dark",
showBackground = true,
backgroundColor = 0xFF000000,
uiMode = Configuration.UI_MODE_NIGHT_YES,
widthDp = 402,
)
@Suppress("UnusedPrivateMember")
@Composable
private fun GamssSnackBarDarkPreview() {
GamssTheme(darkTheme = true) {
GamssSnackBarPreviewContent()
}
}

@Composable
private fun GamssSnackBarPreviewContent() {
Column(
modifier = Modifier.padding(GamssTheme.spacing.spacing300),
verticalArrangement = Arrangement.spacedBy(GamssTheme.spacing.spacing200),
) {
// 아이콘이 있는 스낵바.
GamssSnackBar(
message = "이것은 아이콘이 있는 스낵바입니다.\n스낵바는 한 줄로 사용하기를 권장합니다.",
snackBarIcon = {
Icon(
painter = painterResource(GamssIcons.InfoFilled),
contentDescription = null,
tint = GamssTheme.colors.yellow,
)
},
)
// 아이콘이 없는 스낵바.
GamssSnackBar(
message = "이것은 아이콘이 없는 스낵바입니다.",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ enum class GamssTopNavigationIcon(@DrawableRes internal val drawableRes: Int) {
LeftChevron(R.drawable.ic_left_chevron),

CreateCard(R.drawable.ic_create_card),
Menu(R.drawable.ic_menu),

Setting(R.drawable.ic_setting),

CheckToken(R.drawable.ic_token_check),

Expand Down Expand Up @@ -300,7 +301,7 @@ private fun TopNavigationPreviewContent() {
GamssTopNavigation(
content = GamssTopNavigationContent.Logo,
rightActions = listOf(
GamssTopNavigationIconAction(icon = GamssTopNavigationIcon.Menu, onClick = {}),
GamssTopNavigationIconAction(icon = GamssTopNavigationIcon.Setting, onClick = {}),
),
)
GamssTopNavigation(
Expand All @@ -309,7 +310,7 @@ private fun TopNavigationPreviewContent() {
showLeftIcon = true,
rightActions = listOf(
GamssTopNavigationIconAction(icon = GamssTopNavigationIcon.CreateCard, onClick = {}),
GamssTopNavigationIconAction(icon = GamssTopNavigationIcon.Menu, onClick = {}),
GamssTopNavigationIconAction(icon = GamssTopNavigationIcon.Setting, onClick = {}),
),
)
GamssTopNavigation(
Expand Down
9 changes: 9 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_alert.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.672,2C10.724,2 11.714,2.201 12.641,2.602C13.568,2.997 14.383,3.549 15.086,4.258C15.794,4.966 16.349,5.784 16.75,6.711C17.156,7.633 17.359,8.62 17.359,9.672C17.359,10.724 17.159,11.714 16.758,12.641C16.357,13.568 15.802,14.383 15.094,15.086C14.385,15.794 13.568,16.349 12.641,16.75C11.719,17.151 10.732,17.352 9.68,17.352C8.628,17.352 7.638,17.151 6.711,16.75C5.784,16.349 4.966,15.794 4.258,15.086C3.555,14.383 3.003,13.568 2.602,12.641C2.201,11.714 2,10.724 2,9.672C2,8.62 2.201,7.633 2.602,6.711C3.003,5.784 3.555,4.966 4.258,4.258C4.966,3.549 5.784,2.997 6.711,2.602C7.638,2.201 8.625,2 9.672,2ZM9.68,11.75C9.419,11.75 9.195,11.833 9.008,12C8.826,12.161 8.734,12.367 8.734,12.617C8.734,12.867 8.826,13.075 9.008,13.242C9.195,13.404 9.419,13.484 9.68,13.484C9.935,13.484 10.154,13.404 10.336,13.242C10.523,13.081 10.617,12.872 10.617,12.617C10.617,12.367 10.526,12.161 10.344,12C10.161,11.833 9.94,11.75 9.68,11.75ZM9.672,5.789C9.417,5.789 9.211,5.865 9.055,6.016C8.898,6.167 8.826,6.362 8.836,6.602L8.945,10.195C8.961,10.669 9.206,10.906 9.68,10.906C10.138,10.906 10.375,10.667 10.391,10.188L10.508,6.602C10.513,6.367 10.438,6.174 10.281,6.023C10.125,5.867 9.922,5.789 9.672,5.789Z"
android:fillColor="#FFC552"/>
</vector>
Loading
Loading