From 1a97d7925fe4a2e90603c55a5b3520d7ed47f6d6 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 7 May 2026 17:06:34 +0900 Subject: [PATCH 01/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20chore:=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=ED=8C=8C=ED=8A=B8=20=EC=98=A4=EB=A5=98=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 --- data/src/main/java/com/linku/data/api/FolderApi.kt | 1 - .../java/com/linku/file/ui/bottom/sheet/_ShareBottomSheet.kt | 2 -- 2 files changed, 3 deletions(-) diff --git a/data/src/main/java/com/linku/data/api/FolderApi.kt b/data/src/main/java/com/linku/data/api/FolderApi.kt index ce502ea1..fdb6b9ed 100644 --- a/data/src/main/java/com/linku/data/api/FolderApi.kt +++ b/data/src/main/java/com/linku/data/api/FolderApi.kt @@ -13,7 +13,6 @@ import com.linku.data.api.dto.folder.ShareFolderResponseDTO import com.linku.data.api.dto.folder.UpdateBookmarkRequestDTO import com.linku.data.api.dto.folder.UpdateBookmarkResponseDTO import com.linku.data.api.dto.folder.ViewerResponseDTO -import com.linku.data.api.dto.folder.share.InvitationInfoResponseDTO import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE diff --git a/feature/file/src/main/java/com/linku/file/ui/bottom/sheet/_ShareBottomSheet.kt b/feature/file/src/main/java/com/linku/file/ui/bottom/sheet/_ShareBottomSheet.kt index e8d6341f..ac4436eb 100644 --- a/feature/file/src/main/java/com/linku/file/ui/bottom/sheet/_ShareBottomSheet.kt +++ b/feature/file/src/main/java/com/linku/file/ui/bottom/sheet/_ShareBottomSheet.kt @@ -330,8 +330,6 @@ internal fun ShareBottomSheetLayout( .fillMaxWidth() .fillMaxHeight(SHEET_SELECT_SCREEN_HEIGHT_RATIO), colors = colors, - preSelectDepth = selectDepth, - folderTree = folderTree, ) } } From bb8fbf3b1f0f115690a773802e21446475fc48da Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:16:49 +0900 Subject: [PATCH 02/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/HomeApp.kt | 1 + .../com/linku/home/component/EmotionSelect.kt | 149 +++++++ .../com/linku/home/screen/SaveLinkScreen.kt | 415 ++++++++---------- .../home/src/main/res/drawable/ic_camera.xml | 9 + 4 files changed, 345 insertions(+), 229 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera.xml diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index 7e2ed91f..3422e80e 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -119,6 +119,7 @@ fun HomeApp( SaveLinkScreen( image = viewModel.image, url = viewModel.url, +// title = viewModel.title, memo = viewModel.memo, selectedEmotionId = viewModel.selectedEmotionId, onPickImage = { imagePicker.launch("image/*") }, diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt new file mode 100644 index 00000000..8526042c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -0,0 +1,149 @@ +package com.linku.home.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.color.Basic +import com.linku.home.R + +private data class EmotionUi( + val id: Long, + val label: String, + @DrawableRes val iconRes: Int +) + +private val EMOTIONS = listOf( + EmotionUi(1L, "즐거움", R.drawable.ic_joy), + EmotionUi(2L, "평온", R.drawable.ic_calm), + EmotionUi(3L, "설렘", R.drawable.ic_excite), + EmotionUi(4L, "우울", R.drawable.ic_sad), + EmotionUi(5L, "짜증", R.drawable.ic_irritation), + EmotionUi(6L, "분노", R.drawable.ic_anger), +) + +@Composable +fun EmotionSelect( + selectedEmotionId: Long?, + onEmotionSelect: (Long?) -> Unit +) { + val firstRow = EMOTIONS.take(3) + val secondRow = EMOTIONS.drop(3) + + Column( + modifier = Modifier.padding(top = 13.dp, start = 20.dp) + ) { + Row { + firstRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row { + secondRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + } +} + +@Composable +private fun EmotionBadgeImage( + @DrawableRes iconRes: Int, + label: String, + selected: Boolean, + onToggle: () -> Unit +) { + val boxBackground = Brush.horizontalGradient( + listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) + ) + + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background( + brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) + ) + .then( + if (selected) { + Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) + } + ) + .noRippleClickable { onToggle() } + .padding(horizontal = 15.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 아이콘 + Image( + painter = painterResource(id = iconRes), + contentDescription = label, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(5.dp)) + + // 라벨 + Text( + text = label, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], + fontFamily = LocalFontTheme.current.font + ) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewEmotionSelect() { + ThemeProvider { + EmotionSelect( + selectedEmotionId = 1, + onEmotionSelect = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 3fca190a..967eb32e 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -11,6 +11,7 @@ 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 @@ -36,32 +37,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import coil3.compose.rememberAsyncImagePainter +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R +import com.linku.home.component.EmotionSelect import java.io.File -private data class EmotionUi( - val id: Long, - val label: String, - @DrawableRes val iconRes: Int -) - -private val EMOTIONS = listOf( - EmotionUi(1L, "즐거움", R.drawable.ic_joy), - EmotionUi(2L, "평온", R.drawable.ic_calm), - EmotionUi(3L, "설렘", R.drawable.ic_excite), - EmotionUi(4L, "우울", R.drawable.ic_sad), - EmotionUi(5L, "짜증", R.drawable.ic_irritation), - EmotionUi(6L, "분노", R.drawable.ic_anger), -) - @Composable fun SaveLinkScreen( image: File?, url: String, + title: String? = "", memo: String, selectedEmotionId: Long?, onPickImage: () -> Unit, @@ -91,32 +82,35 @@ fun SaveLinkScreen( .padding(bottom = 70.dp) .verticalScroll(scrollState) ) { - Row( + Box( modifier = Modifier .fillMaxWidth() - .padding(top = 59.dp, start = 20.dp), - verticalAlignment = Alignment.CenterVertically + .padding(top = 59.dp, start = 20.dp, end = 20.dp) + .height(24.dp) ) { Image( painter = painterResource(R.drawable.ic_back), contentDescription = null, modifier = Modifier - .size(width = 10.dp, height = 16.25.dp) - .clickable { onBack() } + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } ) - Spacer(modifier = Modifier.width(131.dp)) - Text( text = "새로운 링크", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.black + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.align(Alignment.Center) ) } Text( - text = "URL 링크 입력", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + text = "URL 링크", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.black, modifier = Modifier.padding(top = 31.dp, start = 24.dp) ) @@ -124,16 +118,22 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp, bottom = 12.dp) - .height(50.dp) - .border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) - .padding(horizontal = 22.dp), + .padding(top = 13.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (url.isEmpty()) { Text( text = "링크를 입력하거나 붙여넣어 주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -147,76 +147,134 @@ fun SaveLinkScreen( ) } - // URL 검사 결과 메시지 - when { - url.isBlank() -> Unit - showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") - isCheckingUrl -> Text( - text = "링크를 확인 중입니다…", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[600], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 17.dp, start = 24.dp, end = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "링크 제목", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black, ) - isInvalidLink -> { - WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") - } - isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") - isDuplicateUrl == false -> Text( - text = "저장 가능한 링크예요.", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), + + Text( + text = "선택", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) ) - else -> Unit } -// if (isInvalidLink) { -// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 15.dp), + contentAlignment = Alignment.CenterStart + ) { + if (url.isEmpty()) { + Text( + text = "링크 제목을 입력해주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400] + ) + } + + BasicTextField( + value = url, // TODO: 추후 API 파라미터에 링크 제목 추가되면 바꾸기 + onValueChange = onUrlChange, + singleLine = true, + textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + modifier = Modifier.fillMaxWidth() + ) + } + +// // URL 검사 결과 메시지 +// when { +// url.isBlank() -> Unit +// showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +// isCheckingUrl -> Text( +// text = "링크를 확인 중입니다…", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.gray[600], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// isInvalidLink -> { +// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +// } +// isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") +// isDuplicateUrl == false -> Text( +// text = "저장 가능한 링크예요.", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.blue[200], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// else -> Unit // } // -// if (showVideoWarning) { -// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// if (isInvalidLink) { +//// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +//// } +//// +//// if (showVideoWarning) { +//// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// } +// +// // 둘 다 false일 때만 Spacer 추가 +// if (!isInvalidLink && !showVideoWarning) { +// Spacer(modifier = Modifier.height(12.dp)) // } - // 둘 다 false일 때만 Spacer 추가 - if (!isInvalidLink && !showVideoWarning) { - Spacer(modifier = Modifier.height(12.dp)) - } - Column( modifier = Modifier .fillMaxWidth() - .height(209.3.dp) - .padding(top = 18.dp, start = 20.dp, end = 20.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) - .clickable { onPickImage() }, - horizontalAlignment = Alignment.CenterHorizontally + .padding(start = 20.dp, end = 20.dp, top = 19.dp) + .noRippleClickable { onPickImage() }, + horizontalAlignment = Alignment.Start ) { if (image != null) { Image( painter = rememberAsyncImagePainter(model = image), contentDescription = "선택된 이미지", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop // ✅ 박스에 꽉 차도록 + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) ) } else { Column( + modifier = Modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(R.drawable.ic_transparent_logo), + painter = painterResource(R.drawable.ic_camera), contentDescription = null, - modifier = Modifier - .height(120.dp) - .padding(top = 50.dp) + modifier = Modifier.height(24.dp) ) + Text( - text = "이미지 업로드하기", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, fontFamily = LocalFontTheme.current.font), + text = "사진 추가", + fontSize = 14.sp, + fontWeight = FontWeight.Light, color = LocalColorTheme.current.gray[500], - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 7.dp) ) } } @@ -225,29 +283,28 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 27.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "메모", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp) - .height(50.dp) + .padding(top = 13.dp, start = 20.dp, end = 20.dp) .then( if (memo.isEmpty()) { Modifier.border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) @@ -258,14 +315,15 @@ fun SaveLinkScreen( ) } ) - .padding(horizontal = 22.dp), + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (memo.isEmpty()) { Text( text = "메모할 내용을 입력해주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -273,7 +331,6 @@ fun SaveLinkScreen( BasicTextField( value = memo, onValueChange = { if (it.length <= 200) onMemoChange(it) }, - singleLine = true, textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), modifier = Modifier.fillMaxWidth() ) @@ -282,41 +339,44 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(end = 32.dp, top = 12.dp), + .padding(end = 32.dp, top = 10.dp), horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = memo.length.toString(), - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[700] ) Text( text = "/200자", - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400], + modifier = Modifier.padding(start = 1.dp) ) } Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 25.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "감정", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } @@ -325,7 +385,7 @@ fun SaveLinkScreen( onEmotionSelect = onEmotionSelect ) - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(100.dp)) } Column( @@ -336,152 +396,49 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .height(50.dp) .clip(RoundedCornerShape(18.dp)) - .background( - brush = if (isButtonEnabled) { - Basic.maincolor + .noRippleClickable(enabled = isButtonEnabled) { onSaveClick() } + .then ( + if (isButtonEnabled) { + Modifier.background(Basic.maincolor) } else { - Brush.horizontalGradient( - listOf( - Color(0x1A2C6FFF), - Color(0x1AC800FF) - ) - ) + Modifier.background(LocalColorTheme.current.gray[300]) } ) - - .clickable(enabled = isButtonEnabled) { onSaveClick() }, + .padding(vertical = 15.dp), contentAlignment = Alignment.Center ) { Text( text = "저장", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - } - } -} - -@Composable -fun WarningText( - message: String, - modifier: Modifier = Modifier -) { - Text( - text = message, - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal), fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.negative, - modifier = modifier.padding(start = 32.dp) - ) -} - -@Composable -fun EmotionSelect( - selectedEmotionId: Long?, - onEmotionSelect: (Long?) -> Unit -) { - val firstRow = EMOTIONS.take(4) - val secondRow = EMOTIONS.drop(4) - - Column(modifier = Modifier.padding(top = 15.dp, start = 20.dp)) { - Row { - firstRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } - ) - Spacer(modifier = Modifier.width(10.dp)) - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row { - secondRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white, + textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.width(10.dp)) } } } } -@Composable -private fun EmotionBadgeImage( - @DrawableRes iconRes: Int, - label: String, - selected: Boolean, - onToggle: () -> Unit -) { - val boxBackground = Brush.horizontalGradient( - listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) - ) - - Row( - modifier = Modifier - .clip(RoundedCornerShape(20.dp)) - .background( - brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) - ) - .then( - if (selected) { - Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) - } else { - Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) - } - ) - .clickable { onToggle() } - .padding(horizontal = 15.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 아이콘 - Image( - painter = painterResource(id = iconRes), - contentDescription = label, - modifier = Modifier.size(20.dp) - ) - - Spacer(modifier = Modifier.width(5.dp)) - - // 라벨 - Text( - text = label, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], - fontFamily = LocalFontTheme.current.font - ) - ) - } -} - @Preview(showBackground = true) @Composable fun PreviewSaveLinkScreen() { - SaveLinkScreen( - image = null, - url = "", - memo = "", - selectedEmotionId = null, - onPickImage = {}, - onUrlChange = {}, - onMemoChange = {}, - onEmotionSelect = {}, - onSaveClick = {}, - onBack = {}, - isCheckingUrl = false, - isDuplicateUrl = null, - isInvalidLink = false - ) + ThemeProvider { + SaveLinkScreen( + image = null, + url = "", + title = "", + memo = "", + selectedEmotionId = null, + onPickImage = {}, + onUrlChange = {}, + onMemoChange = {}, + onEmotionSelect = {}, + onSaveClick = {}, + onBack = {}, + isCheckingUrl = false, + isDuplicateUrl = null, + isInvalidLink = false + ) + } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera.xml b/feature/home/src/main/res/drawable/ic_camera.xml new file mode 100644 index 00000000..7474098b --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,9 @@ + + + From cae9a16d9b361059c6538e397ab7a0fc578ed284 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:17:09 +0900 Subject: [PATCH 03/89] =?UTF-8?q?:sparkles:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/CustomToastMessage.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt diff --git a/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt new file mode 100644 index 00000000..0fa4b42c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt @@ -0,0 +1,52 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun CustomToastMessage( + backgroundColor: Color, + textColor: Color, + toastMessage: String, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background(color = backgroundColor) + .padding(horizontal = 18.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = toastMessage, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = textColor + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewCustomToastMessage() { + ThemeProvider { + CustomToastMessage( + backgroundColor = Color(0xFFE0FBEB), + textColor = LocalColorTheme.current.positive, + toastMessage = "유효한 링크입니다!" + ) + } +} From ef003df3737d87dda1c2cbb426fee4a2bf6abf40 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 03:44:42 +0900 Subject: [PATCH 04/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?Top=20Bar=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/ui/top/bar/LinkDetailTopBar.kt | 266 ++++++++++++++++++ .../src/main/res/drawable/ic_link_delete.xml | 9 + .../src/main/res/drawable/ic_link_edit.xml | 9 + .../home/src/main/res/drawable/ic_link_go.xml | 12 +- .../src/main/res/drawable/ic_link_go_gray.xml | 12 + .../src/main/res/drawable/ic_link_share.xml | 12 + .../home/src/main/res/drawable/ic_more.xml | 15 + .../res/drawable/linku_logo_transparent.xml | 16 ++ 8 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/ui/top/bar/LinkDetailTopBar.kt create mode 100644 feature/home/src/main/res/drawable/ic_link_delete.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_edit.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_go_gray.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_share.xml create mode 100644 feature/home/src/main/res/drawable/ic_more.xml create mode 100644 feature/home/src/main/res/drawable/linku_logo_transparent.xml diff --git a/feature/home/src/main/java/com/linku/home/ui/top/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/top/bar/LinkDetailTopBar.kt new file mode 100644 index 00000000..373ecc6e --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/top/bar/LinkDetailTopBar.kt @@ -0,0 +1,266 @@ +package com.linku.home.ui.top.bar + +import androidx.compose.foundation.Image +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.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.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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.AbsoluteAlignment +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailTopBar( + linkTitle: String, + category: String, + emotion: String, + onBack: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onLinkGoClick: () -> Unit, +) { + var isMenuExpanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp)) + .background(LocalColorTheme.current.blue[200]) + ) { + Image( + painter = painterResource(R.drawable.linku_logo_transparent), + contentDescription = null, + modifier = Modifier + .height(110.dp) + .align(Alignment.TopEnd) + ) + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 59.dp, start = 20.dp, end = 24.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_back_white), + contentDescription = "뒤로가기", + modifier = Modifier + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } + ) + + Text( + text = "새로운 링크", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.white, + modifier = Modifier.align(Alignment.Center) + ) + + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterEnd) + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = "더보기", + modifier = Modifier + .height(18.dp) + .align(AbsoluteAlignment.TopRight) + .noRippleClickable { + isMenuExpanded = true + } + ) + + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { + isMenuExpanded = false + } + ) { + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_edit, + text = "링크 수정하기", + onClick = { + isMenuExpanded = false + onEditClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_delete, + text = "링크 삭제하기", + onClick = { + isMenuExpanded = false + onDeleteClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_share, + text = "링크 공유하기", + onClick = { + isMenuExpanded = false + onShareClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_go_gray, + text = "링크 보러가기", + onClick = { + isMenuExpanded = false + onLinkGoClick() + } + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + ) { + Text( + text = linkTitle, + fontSize = 24.sp, // 편집모드에서는 22.sp + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + } + + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } + } + } + } + } +} + +@Composable +private fun LinkDetailDropdownItem( + iconRes: Int, + text: String, + onClick: () -> Unit, +) { + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(18.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(28.dp) + ) + + Text( + text = text, + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] + ) + } + }, + onClick = onClick, + modifier = Modifier + .height(64.dp) + .padding(horizontal = 12.dp) + ) +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailTopBar() { + ThemeProvider { + LinkDetailTopBar( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + onBack = { }, + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onLinkGoClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_link_delete.xml b/feature/home/src/main/res/drawable/ic_link_delete.xml new file mode 100644 index 00000000..2b4edfe4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_edit.xml b/feature/home/src/main/res/drawable/ic_link_edit.xml new file mode 100644 index 00000000..c643835c --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_go.xml b/feature/home/src/main/res/drawable/ic_link_go.xml index ec3d535f..f8b73ba9 100644 --- a/feature/home/src/main/res/drawable/ic_link_go.xml +++ b/feature/home/src/main/res/drawable/ic_link_go.xml @@ -1,16 +1,16 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + + + diff --git a/feature/home/src/main/res/drawable/ic_link_share.xml b/feature/home/src/main/res/drawable/ic_link_share.xml new file mode 100644 index 00000000..e8f557a3 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_share.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/home/src/main/res/drawable/ic_more.xml b/feature/home/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..7e149ac0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/linku_logo_transparent.xml b/feature/home/src/main/res/drawable/linku_logo_transparent.xml new file mode 100644 index 00000000..41ea4346 --- /dev/null +++ b/feature/home/src/main/res/drawable/linku_logo_transparent.xml @@ -0,0 +1,16 @@ + + + + From 1917332d7f6644e080a7526eb367614eca3e6cdd Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 04:35:53 +0900 Subject: [PATCH 05/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/screen/LinkDetailScreen.kt | 212 ++++++++++++++++++ .../home/ui/home/bar/LinkDetailTopBar.kt | 66 +----- .../src/main/res/drawable/ic_sparkles.xml | 10 + 3 files changed, 228 insertions(+), 60 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt create mode 100644 feature/home/src/main/res/drawable/ic_sparkles.xml diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt new file mode 100644 index 00000000..e01e9354 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -0,0 +1,212 @@ +package com.linku.home.screen + +import android.content.ClipData +import androidx.compose.foundation.Image +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.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.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalUriHandler +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R +import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.launch + +@Composable +fun LinkDetailScreen( + linkTitle: String, + category: String, + emotion: String, + linkUrl: String, + memo: String, + onBack: () -> Unit, + onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +) { + val clipboard = LocalClipboard.current + val coroutineScope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current + + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalColorTheme.current.white) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + LinkDetailTopBar( + linkTitle = linkTitle, + category = category, + emotion = emotion, + onBack = { onBack() }, + onMoreClick = { }, + onLinkGoClick = { uriHandler.openUri(linkUrl) }, + ) + + Column( + modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + ) { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + .background(LocalColorTheme.current.white) + .padding(top = 7.5.dp, start = 22.dp, end = 8.5.dp, bottom = 7.5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = linkUrl, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) + ) + } + } + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 22.dp) + ) { + Text( + text = "메모", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = memo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) + + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewLinkDetailScreen() { + ThemeProvider { + LinkDetailScreen( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + linkUrl = "https://blog.naver.com/linkU/1234", + memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + onBack = { }, + onMoreClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index 373ecc6e..c058c76a 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -1,4 +1,4 @@ -package com.linku.home.ui.top.bar +package com.linku.home.ui.home.bar import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -6,21 +6,15 @@ 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.DropdownMenu import androidx.compose.material3.DropdownMenuItem 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.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,12 +35,9 @@ fun LinkDetailTopBar( category: String, emotion: String, onBack: () -> Unit, - onEditClick: () -> Unit, - onDeleteClick: () -> Unit, - onShareClick: () -> Unit, + onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, ) { - var isMenuExpanded by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -91,6 +82,9 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) + .noRippleClickable { + onMoreClick() + } ) { Image( painter = painterResource(R.drawable.ic_more), @@ -98,53 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .height(18.dp) .align(AbsoluteAlignment.TopRight) - .noRippleClickable { - isMenuExpanded = true - } ) - - DropdownMenu( - expanded = isMenuExpanded, - onDismissRequest = { - isMenuExpanded = false - } - ) { - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_edit, - text = "링크 수정하기", - onClick = { - isMenuExpanded = false - onEditClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_delete, - text = "링크 삭제하기", - onClick = { - isMenuExpanded = false - onDeleteClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_share, - text = "링크 공유하기", - onClick = { - isMenuExpanded = false - onShareClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_go_gray, - text = "링크 보러가기", - onClick = { - isMenuExpanded = false - onLinkGoClick() - } - ) - } } } @@ -257,9 +205,7 @@ fun PreviewLinkDetailTopBar() { category = "어학", emotion = "평온", onBack = { }, - onEditClick = { }, - onDeleteClick = { }, - onShareClick = { }, + onMoreClick = { }, onLinkGoClick = { }, ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles.xml b/feature/home/src/main/res/drawable/ic_sparkles.xml new file mode 100644 index 00000000..de8029a2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles.xml @@ -0,0 +1,10 @@ + + + From b160d51fb0175224e997a8ba7089b025a2d54134 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 19:14:00 +0900 Subject: [PATCH 06/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=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 --- .../component/LinkDetailCustomDropdown.kt | 160 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 51 +++++- 2 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt new file mode 100644 index 00000000..e7e79575 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -0,0 +1,160 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailCustomDropdown( + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onGoClick: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier +) { + Column( + modifier = modifier + .width(240.dp) + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 24.dp, vertical = 13.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onEditClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_edit), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 수정하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onDeleteClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_delete), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 삭제하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onShareClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_share), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 공유하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onGoClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_go_gray), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 보러가기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCustomDropdown() { + ThemeProvider { + LinkDetailCustomDropdown( + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onGoClick = { }, + onDismiss = { }, + modifier = Modifier + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index e01e9354..01425848 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -20,7 +20,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll 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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,6 +41,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch @@ -54,6 +59,8 @@ fun LinkDetailScreen( val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + var isDropdownVisible by remember { mutableStateOf(false) } + Box( modifier = Modifier .fillMaxSize() @@ -62,19 +69,23 @@ fun LinkDetailScreen( Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) ) { LinkDetailTopBar( linkTitle = linkTitle, category = category, emotion = emotion, onBack = { onBack() }, - onMoreClick = { }, + onMoreClick = { + isDropdownVisible = !isDropdownVisible + }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, ) Column( - modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { Image( painter = painterResource(R.drawable.img_default), @@ -168,23 +179,53 @@ fun LinkDetailScreen( } } + if (isDropdownVisible) { + LinkDetailCustomDropdown( + onEditClick = { + isDropdownVisible = false + // 수정 화면 이동 로직 추가 예정 + }, + onDeleteClick = { + isDropdownVisible = false + // 삭제 로직 추가 예정 + }, + onShareClick = { + isDropdownVisible = false + // 공유 로직 추가 예정 + }, + onGoClick = { + isDropdownVisible = false + // 링크 Open 로직 추가 예정 + }, + onDismiss = { + isDropdownVisible = false + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 100.dp, end = 20.dp), + ) + } + Row( modifier = Modifier .fillMaxWidth() + .padding(horizontal = 20.dp) .align(Alignment.BottomCenter) .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) .noRippleClickable { }, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.Center ) { Image( painter = painterResource(R.drawable.ic_sparkles), contentDescription = null, modifier = Modifier.height(17.51.dp) ) - + + Spacer(modifier = Modifier.width(10.dp)) + Text( text = "AI 요약", fontSize = 16.sp, From 0c1d6cc7968509bea626faadd618cb3c42d8de5f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:09:49 +0900 Subject: [PATCH 07/89] =?UTF-8?q?:recycle:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 58 ++- .../java/com/linku/core/model/Situation.kt | 106 ++++++ .../component/LinkDetailCategoryDropdown.kt | 101 +++++ .../component/LinkDetailEmotionDropdown.kt | 99 +++++ .../component/LinkDetailSituationDropdown.kt | 76 ++++ .../com/linku/home/screen/LinkDetailScreen.kt | 357 ++++++++++++++---- .../home/ui/home/bar/LinkDetailTopBar.kt | 298 +++++++++++---- .../src/main/res/drawable/ic_camera_white.xml | 9 + .../src/main/res/drawable/ic_delete_blue.xml | 21 ++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++ 10 files changed, 1024 insertions(+), 140 deletions(-) create mode 100644 core/src/main/java/com/linku/core/model/Situation.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera_white.xml create mode 100644 feature/home/src/main/res/drawable/ic_delete_blue.xml create mode 100644 feature/home/src/main/res/drawable/ic_linku_blur.xml diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index fbb6c3b4..5bc21075 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,17 +1,59 @@ package com.linku.core.model +import androidx.annotation.DrawableRes +import com.linku.design.R + enum class EmotionType( val id: Long, - val tagName: String + val tagName: String, + @DrawableRes val imgRes: Int ) { - JOY(1, "즐거움"), - PEACE(2, "평온"), - EXCITEMENT(3, "설렘"), - SADNESS(4, "슬픔"), - ANNOYANCE(5, "짜증"), - ANGER(6, "분노"); + JOY( + id = 1L, + tagName = "즐거움", + imgRes = R.drawable.ic_joy + ), + CALM( + id = 2L, + tagName = "평온", + imgRes = R.drawable.ic_calm + ), + EXCITE( + id = 3L, + tagName = "설렘", + imgRes = R.drawable.ic_excite + ), + SAD( + id = 4L, + tagName = "슬픔", + imgRes = R.drawable.ic_sad + ), + IRRITATION( + id = 5L, + tagName = "짜증", + imgRes = R.drawable.ic_irritation + ), + ANGER( + id = 6L, + tagName = "분노", + imgRes = R.drawable.ic_anger + ); companion object { - fun fromId(id: Long): EmotionType? = values().find { it.id == id } + fun fromId(id: Long?): EmotionType? { + return entries.firstOrNull { it.id == id } + } + + fun fromTagName(tagName: String?): EmotionType? { + return entries.firstOrNull { it.tagName == tagName } + } + + fun tagNameOf(id: Long?): String? { + return fromId(id)?.tagName + } + + fun idOf(tagName: String?): Long? { + return fromTagName(tagName)?.id + } } } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt new file mode 100644 index 00000000..a0fa486f --- /dev/null +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -0,0 +1,106 @@ +package com.linku.core.model + +data class Situation( + val id: Long, + val tagName: String +) + +object SituationOptions { + val linkDetailSituations: List = listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "휴식 중"), + Situation(4L, "이동 중"), + Situation(5L, "식사 중"), + Situation(6L, "자기 전") + ) + + fun situationsFor(jobId: Long): List = when (jobId) { + 1L -> listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "식사 중"), + Situation(4L, "시험 준비"), + Situation(5L, "친구랑"), + Situation(6L, "쇼핑 중"), + Situation(7L, "휴식 중"), + Situation(8L, "자기 전") + ) + + 2L -> listOf( + Situation(9L, "과제 중"), + Situation(10L, "통학 중"), + Situation(11L, "쇼핑 중"), + Situation(12L, "알바 중"), + Situation(13L, "트렌드 확인"), + Situation(14L, "데이트 중"), + Situation(15L, "휴식 중"), + Situation(16L, "자기 전") + ) + + 3L -> listOf( + Situation(17L, "출퇴근"), + Situation(18L, "트렌드 확인"), + Situation(19L, "업무 중"), + Situation(20L, "커리어 고민"), + Situation(21L, "쇼핑 중"), + Situation(22L, "데이트 중"), + Situation(23L, "휴식 중"), + Situation(24L, "자기 전") + ) + + 4L -> listOf( + Situation(25L, "출퇴근"), + Situation(26L, "업무 준비 중"), + Situation(27L, "데이트 중"), + Situation(28L, "식사"), + Situation(29L, "쇼핑 중"), + Situation(30L, "트렌드 확인"), + Situation(31L, "휴식 중"), + Situation(32L, "자기 전") + ) + + 5L -> listOf( + Situation(33L, "작업 중"), + Situation(34L, "쇼핑 중"), + Situation(35L, "트렌드 확인"), + Situation(36L, "데이트 중"), + Situation(37L, "운동 중"), + Situation(38L, "식사"), + Situation(39L, "휴식 중"), + Situation(40L, "자기 전") + ) + + 6L -> listOf( + Situation(41L, "자소서 작성"), + Situation(42L, "면접 준비"), + Situation(43L, "요리 중"), + Situation(44L, "트렌드 확인"), + Situation(45L, "쇼핑 중"), + Situation(46L, "운동 중"), + Situation(47L, "휴식 중"), + Situation(48L, "자기 전") + ) + + else -> situationsFor(3L) + } + + fun nameOf(id: Long?): String? { + if (id == null) return null + + return (linkDetailSituations + (1L..6L).flatMap { situationsFor(it) }) + .distinctBy { it.id } + .firstOrNull { it.id == id } + ?.tagName + } + + fun idOf(tagName: String, jobId: Long? = null): Long? { + val options = if (jobId != null) { + situationsFor(jobId) + } else { + linkDetailSituations + } + + return options.firstOrNull { it.tagName == tagName }?.id + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt new file mode 100644 index 00000000..025c8779 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt @@ -0,0 +1,101 @@ +package com.linku.home.component + +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +data class LinkCategoryOption( + val id: Long, + val name: String, + val color: Color +) + +@Composable +fun LinkDetailCategoryDropdown( + categories: List, + selectedCategory: String, + onCategoryClick: (LinkCategoryOption) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 13.dp, bottom = 13.dp, end = 56.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + categories.forEach { category -> + Row( + modifier = Modifier + .noRippleClickable { + onCategoryClick(category) + } + .padding(horizontal = 6.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(25.dp) + .clip(CircleShape) + .background(category.color) + ) + + Text( + text = category.name, + fontSize = 15.sp, + fontWeight = if (category.name == selectedCategory) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (category.name == selectedCategory) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCategoryDropdown() { + ThemeProvider { + LinkDetailCategoryDropdown( + categories = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ), + selectedCategory = "카테고리2", + onCategoryClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt new file mode 100644 index 00000000..7f1050a4 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -0,0 +1,99 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.core.model.EmotionType +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailEmotionDropdown( + emotions: List, + selectedEmotion: String, + onEmotionClick: (EmotionType) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(top = 14.dp, start = 16.dp, end = 56.dp, bottom = 14.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + emotions.forEach { emotion -> + Row( + modifier = Modifier + .noRippleClickable { + onEmotionClick(emotion) + } + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(emotion.iconRes()), + contentDescription = null, + modifier = Modifier.size(29.dp) + ) + + Text( + text = emotion.tagName, + fontSize = 15.sp, + fontWeight = if (emotion.tagName == selectedEmotion) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (emotion.tagName == selectedEmotion) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +private fun EmotionType.iconRes(): Int { + return when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailEmotionDropdown() { + ThemeProvider { + LinkDetailEmotionDropdown( + emotions = EmotionType.entries.toList(), + selectedEmotion = "평온", + onEmotionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt new file mode 100644 index 00000000..e3a4990f --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -0,0 +1,76 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun LinkDetailOptionDropdown( + options: List, + selectedOption: String, + onOptionClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) + .heightIn(max = 264.dp) + ) { + options.forEach { option -> + Text( + text = option, + fontSize = 15.sp, + fontWeight = if (option == selectedOption) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (option == selectedOption) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + }, + modifier = Modifier + .noRippleClickable { + onOptionClick(option) + } + .padding(horizontal = 4.dp, vertical = 9.dp) + ) + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailOptionDropdown() { + ThemeProvider { + LinkDetailOptionDropdown( + options = listOf( + "트렌드 확인", + "통학 중", + "과제 중", + "쇼핑 중", + "데이트 중", + "알바 전" + ), + selectedOption = "통학 중", + onOptionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 01425848..eb12741d 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -14,9 +14,11 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,33 +29,49 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle 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 com.linku.core.model.EmotionType +import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkCategoryOption +import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown +import com.linku.home.component.LinkDetailEmotionDropdown +import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch +private enum class LinkDetailDropdownType { + CATEGORY, + EMOTION, + SITUATION +} + @Composable fun LinkDetailScreen( linkTitle: String, category: String, emotion: String, + situation: String, linkUrl: String, memo: String, onBack: () -> Unit, - onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() @@ -61,6 +79,31 @@ fun LinkDetailScreen( var isDropdownVisible by remember { mutableStateOf(false) } + var isEditMode by remember { mutableStateOf(false) } + var selectedTitle by remember { mutableStateOf(linkTitle) } + var selectedCategory by remember { mutableStateOf(category) } + var selectedEmotion by remember { mutableStateOf(emotion) } + var selectedSituation by remember { mutableStateOf(situation) } + var selectedMemo by remember { mutableStateOf(memo) } + + var openedDropdownType by remember { + mutableStateOf(null) + } + + val emotionOptions = EmotionType.entries.toList() + + val situationOptions = SituationOptions.linkDetailSituations + + // 카테고리 더미데이터 + val categoryOptions = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ) + Box( modifier = Modifier .fillMaxSize() @@ -72,13 +115,36 @@ fun LinkDetailScreen( ) { LinkDetailTopBar( linkTitle = linkTitle, - category = category, - emotion = emotion, + category = selectedCategory, + emotion = selectedEmotion, + situation = selectedSituation, + isEditMode = isEditMode, + isCategoryDropdownOpen = openedDropdownType == LinkDetailDropdownType.CATEGORY, + isEmotionDropdownOpen = openedDropdownType == LinkDetailDropdownType.EMOTION, + isSituationDropdownOpen = openedDropdownType == LinkDetailDropdownType.SITUATION, onBack = { onBack() }, onMoreClick = { isDropdownVisible = !isDropdownVisible }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, + onCategoryClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.CATEGORY) null + else LinkDetailDropdownType.CATEGORY + }, + onEmotionClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.EMOTION) null + else LinkDetailDropdownType.EMOTION + }, + onSituationClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.SITUATION) null + else LinkDetailDropdownType.SITUATION + }, + onTitleClearClick = { + selectedTitle = "" + } ) Column( @@ -87,20 +153,60 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Image( - painter = painterResource(R.drawable.img_default), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(18.dp)) - .border( - width = 1.dp, - color = LocalColorTheme.current.gray[200], - shape = RoundedCornerShape(18.dp) - ) - ) + Box() { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .alpha(if (isEditMode) 0.6f else 1f) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + if(isEditMode) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .noRippleClickable {}, + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .size(84.dp) + .clip(RoundedCornerShape(30.dp)) + .background(LocalColorTheme.current.gray[700]) + .alpha(0.6f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.ic_camera_white), + contentDescription = null, + modifier = Modifier + .height(24.dp) + .padding(top = 5.dp) + ) + + Spacer(modifier = Modifier.height(7.dp)) + + Text( + text = "사진 변경", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.white + ) + } + } + } + } Spacer(modifier = Modifier.height(18.dp)) @@ -123,30 +229,40 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.black + color = if (isEditMode) LocalColorTheme.current.gray[400] else LocalColorTheme.current.black, + modifier = Modifier + .then( + if (isEditMode) { + Modifier.padding(vertical = 7.5.dp) + } else { + Modifier.padding(0.dp) + } + ) ) - Spacer(modifier = Modifier.width(10.dp)) + if (!isEditMode) { + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "복사", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[200]) - .padding(horizontal = 13.5.dp, vertical = 7.dp) - .noRippleClickable { - coroutineScope.launch { - clipboard.setClipEntry( - ClipEntry( - ClipData.newPlainText("linkUrl", linkUrl) + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) ) - ) + } } - } - ) + ) + } } Column( @@ -163,18 +279,55 @@ fun LinkDetailScreen( Spacer(modifier = Modifier.height(12.dp)) - Text( - text = memo, - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - lineHeight = 20.sp, - color = LocalColorTheme.current.black, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .padding(horizontal = 22.dp, vertical = 15.5.dp) - ) + if (isEditMode) { + BasicTextField( + value = selectedMemo, + onValueChange = { + selectedMemo = it + }, + textStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth() + ) { + if (selectedMemo.isBlank()) { + Text( + text = "메모를 입력해 주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.gray[400] + ) + } + + innerTextField() + } + } + ) + } else { + Text( + text = selectedMemo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } } } } @@ -183,7 +336,7 @@ fun LinkDetailScreen( LinkDetailCustomDropdown( onEditClick = { isDropdownVisible = false - // 수정 화면 이동 로직 추가 예정 + isEditMode = true }, onDeleteClick = { isDropdownVisible = false @@ -195,7 +348,7 @@ fun LinkDetailScreen( }, onGoClick = { isDropdownVisible = false - // 링크 Open 로직 추가 예정 + uriHandler.openUri(linkUrl) }, onDismiss = { isDropdownVisible = false @@ -206,6 +359,62 @@ fun LinkDetailScreen( ) } + if (openedDropdownType != null) { + Box( + modifier = Modifier + .fillMaxSize() + .noRippleClickable { + openedDropdownType = null + } + ) + + when (openedDropdownType) { + LinkDetailDropdownType.CATEGORY -> { + LinkDetailCategoryDropdown( + categories = categoryOptions, + selectedCategory = selectedCategory, + onCategoryClick = { + selectedCategory = it.name + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 24.dp) + ) + } + + LinkDetailDropdownType.EMOTION -> { + LinkDetailEmotionDropdown( + emotions = emotionOptions, + selectedEmotion = selectedEmotion, + onEmotionClick = { + selectedEmotion = it.tagName + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 93.dp) + ) + } + + LinkDetailDropdownType.SITUATION -> { + LinkDetailOptionDropdown( + options = situationOptions.map { it.tagName }, + selectedOption = selectedSituation, + onOptionClick = { + selectedSituation = it + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 186.dp) + ) + } + + null -> Unit + } + } + Row( modifier = Modifier .fillMaxWidth() @@ -214,24 +423,41 @@ fun LinkDetailScreen( .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) - .noRippleClickable { }, + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + // AI 요약 로직 + } + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -242,12 +468,13 @@ fun PreviewLinkDetailScreen() { ThemeProvider { LinkDetailScreen( linkTitle = "3일만에 오픽 AL 꿀팁", - category = "어학", + category = "카테고리2", emotion = "평온", + situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", onBack = { }, - onMoreClick = { }, +// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index c058c76a..4a08d9a4 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -2,6 +2,7 @@ package com.linku.home.ui.home.bar import androidx.compose.foundation.Image 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.Column @@ -12,13 +13,14 @@ 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.DropdownMenuItem +//import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -34,11 +36,19 @@ fun LinkDetailTopBar( linkTitle: String, category: String, emotion: String, + situation: String, + isEditMode: Boolean, + isCategoryDropdownOpen: Boolean, + isEmotionDropdownOpen: Boolean, + isSituationDropdownOpen: Boolean, onBack: () -> Unit, onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, + onCategoryClick: () -> Unit, + onEmotionClick: () -> Unit, + onSituationClick: () -> Unit, + onTitleClearClick: () -> Unit, ) { - Box( modifier = Modifier .fillMaxWidth() @@ -71,7 +81,7 @@ fun LinkDetailTopBar( ) Text( - text = "새로운 링크", + text = if (isEditMode) "링크 수정하기" else "저장된 링크", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = LocalColorTheme.current.white, @@ -82,7 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) - .noRippleClickable { + .noRippleClickable(enabled = !isEditMode) { onMoreClick() } ) { @@ -101,17 +111,42 @@ fun LinkDetailTopBar( .fillMaxWidth() .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp ) { - Box( + Row( modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + .then( + if (isEditMode) { + Modifier + .padding(bottom = 11.dp) + .clip(RoundedCornerShape(13.dp)) + .border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(13.dp)) + .padding(horizontal = 15.dp, vertical = 4.dp) + } else { + Modifier.padding(bottom = 12.dp) + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Text( text = linkTitle, - fontSize = 24.sp, // 편집모드에서는 22.sp + fontSize = if (isEditMode) 22.sp else 24.sp, fontWeight = FontWeight.Bold, color = LocalColorTheme.current.white ) + + if (isEditMode) { + Box( + modifier = Modifier + .size(18.dp) + .noRippleClickable { onTitleClearClick() } + ) { + Image( + painter = painterResource(R.drawable.ic_delete_blue), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } } Row( @@ -123,39 +158,159 @@ fun LinkDetailTopBar( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = category, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) - - Text( - text = emotion, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + .background( + when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) // 추후 카테고리 API 연동 후 실제 색상으로 변경 예정 + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onCategoryClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.black // API 연동 후 수정 예정 + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isCategoryDropdownOpen) 180f else 0f) + ) + } + } + + Row( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background( + when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.blue[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onEmotionClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.blue[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isEmotionDropdownOpen) 180f else 0f) + ) + } + } + + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) + .background( + when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onSituationClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = situation, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.purple[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isSituationDropdownOpen) 180f else 0f) + ) + } + } } - Box( - modifier = Modifier - .size(22.dp) - .noRippleClickable { - onLinkGoClick() - }, - ) { - Image( - painter = painterResource(R.drawable.ic_link_go), - contentDescription = null, - modifier = Modifier.height(22.dp) - ) + if(!isEditMode) { + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } } } } @@ -163,38 +318,38 @@ fun LinkDetailTopBar( } } -@Composable -private fun LinkDetailDropdownItem( - iconRes: Int, - text: String, - onClick: () -> Unit, -) { - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(18.dp) - ) { - Image( - painter = painterResource(iconRes), - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - - Text( - text = text, - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] - ) - } - }, - onClick = onClick, - modifier = Modifier - .height(64.dp) - .padding(horizontal = 12.dp) - ) -} +//@Composable +//private fun LinkDetailDropdownItem( +// iconRes: Int, +// text: String, +// onClick: () -> Unit, +//) { +// DropdownMenuItem( +// text = { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// horizontalArrangement = Arrangement.spacedBy(18.dp) +// ) { +// Image( +// painter = painterResource(iconRes), +// contentDescription = null, +// modifier = Modifier.size(28.dp) +// ) +// +// Text( +// text = text, +// fontSize = 24.sp, +// fontWeight = FontWeight.Medium, +// color = LocalColorTheme.current.gray[800] +// ) +// } +// }, +// onClick = onClick, +// modifier = Modifier +// .height(64.dp) +// .padding(horizontal = 12.dp) +// ) +//} @Preview(showBackground = false) @Composable @@ -204,9 +359,18 @@ fun PreviewLinkDetailTopBar() { linkTitle = "3일만에 오픽 AL 꿀팁", category = "어학", emotion = "평온", + situation = "통학 중", + isEditMode = false, + isCategoryDropdownOpen = false, + isEmotionDropdownOpen = false, + isSituationDropdownOpen = false, onBack = { }, onMoreClick = { }, onLinkGoClick = { }, + onEmotionClick = { }, + onCategoryClick = { }, + onSituationClick = { }, + onTitleClearClick = { } ) } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera_white.xml b/feature/home/src/main/res/drawable/ic_camera_white.xml new file mode 100644 index 00000000..927b0cd0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_delete_blue.xml b/feature/home/src/main/res/drawable/ic_delete_blue.xml new file mode 100644 index 00000000..9f4c40b4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_delete_blue.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/ic_linku_blur.xml b/feature/home/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + From 522b105eea0df0095c68345774ca0bdee6ea2ece Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:19:00 +0900 Subject: [PATCH 08/89] =?UTF-8?q?:sparkles:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/DeleteLinkModal.kt | 143 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 35 ++++- 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt new file mode 100644 index 00000000..7ba646bf --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -0,0 +1,143 @@ +package com.linku.home.component + +import androidx.compose.foundation.BorderStroke +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.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.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.color.Basic +import com.linku.home.R + +@Composable +fun DeleteLinkModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column ( + modifier = Modifier + .wrapContentSize() + .padding(top = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_linku_blur), + contentDescription = null, + modifier = Modifier + .height(30.dp) + ) + } + + Text( + text = "해당 링크를 삭제하시겠습니까?", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.padding(top = 15.dp) + ) + + Text( + text = "삭제 시 해당 링크가 영구적으로 제거되며\n복구가 불가능합니다.", + fontSize = 15.sp, + lineHeight = 22.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.gray[600], + modifier = Modifier.padding(top = 13.dp) + ) + + Row( + modifier = Modifier + .padding(top = 20.dp, start = 27.dp, end = 27.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .clickable { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "취소하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + brush = Basic.maincolor, // 그라데이션 Brush 사용 + fontFamily = LocalFontTheme.current.font + ), + modifier = Modifier + .graphicsLayer(alpha = 0.99f) // brush 적용 시 필수 + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .background(brush = Basic.maincolor) + .clickable { onConfirm() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "삭제하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font + ), + color = LocalColorTheme.current.white + ) + } + } + + Spacer(modifier = Modifier.height(23.dp)) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkModal() { + DeleteLinkModal( + onDismiss = {}, + onConfirm = {} + ) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index eb12741d..2d8be644 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -42,12 +42,14 @@ 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.compose.ui.zIndex import com.linku.core.model.EmotionType import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown @@ -78,6 +80,7 @@ fun LinkDetailScreen( val uriHandler = LocalUriHandler.current var isDropdownVisible by remember { mutableStateOf(false) } + var isDeleteModalVisible by remember { mutableStateOf(false) } var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } @@ -114,7 +117,7 @@ fun LinkDetailScreen( .fillMaxWidth() ) { LinkDetailTopBar( - linkTitle = linkTitle, + linkTitle = selectedTitle, category = selectedCategory, emotion = selectedEmotion, situation = selectedSituation, @@ -340,7 +343,8 @@ fun LinkDetailScreen( }, onDeleteClick = { isDropdownVisible = false - // 삭제 로직 추가 예정 + openedDropdownType = null + isDeleteModalVisible = true }, onShareClick = { isDropdownVisible = false @@ -359,6 +363,33 @@ fun LinkDetailScreen( ) } + if (isDeleteModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(1f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier.padding(horizontal = 20.dp), + contentAlignment = Alignment.Center + ) { + DeleteLinkModal( + onDismiss = { + isDeleteModalVisible = false + }, + onConfirm = { + isDeleteModalVisible = false + // TODO: 삭제 API 호출 -> 삭제 성공 후 어디로 이동하는지 물어보기 + onBack() + } + ) + } + } + } + if (openedDropdownType != null) { Box( modifier = Modifier From cd65a0825d7b6a7331f8b90f240e6f1b78fa1903 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 13:52:35 +0900 Subject: [PATCH 09/89] =?UTF-8?q?:recycle:=20AI=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index c9b90a95..d5b392b2 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -18,83 +19,74 @@ 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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle 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 com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LinkuPreview import com.linku.design.theme.linkuColors -import com.linku.design.theme.linkuFont @Composable fun AIArticleModal( progress: Float, onCancel: () -> Unit, - modifier: Modifier = Modifier // ✅ 외부에서 전달받을 modifier + modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors - val font = MaterialTheme.linkuFont.font - + Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(colors.white), + .background(colors.white) + .padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "AI 요약 중...", - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium, fontFamily = font), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, color = colors.black, - modifier = Modifier.padding(top = 45.dp) + modifier = Modifier.padding(top = 10.dp) ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(12.dp)) // 상태바 SimpleProgressBar( progress = progress, - modifier = Modifier.padding(horizontal = 86.dp) + modifier = Modifier.width(200.dp) ) Text( - text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), + text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!\n나중에 돌아와서 확인할 수 있어요.", + fontSize = 15.sp, + fontWeight = FontWeight.Normal, color = colors.gray[600], - modifier = Modifier.padding(top = 20.dp) + lineHeight = 22.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 21.dp) ) - Text( - text = "잠시만 기다려주세요.", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), - color = colors.gray[600] - ) + Spacer(modifier = Modifier.height(27.dp)) - Column( + Box( modifier = Modifier - .padding(top = 36.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(colors.blue[200]) + .noRippleClickable { onCancel() } + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .padding(horizontal = 28.dp) - .clip(RoundedCornerShape(18.dp)) - .background(brush = MaterialTheme.linkuColors.maincolor) - .clickable { onCancel() }, - contentAlignment = Alignment.Center - ) { - Text( - text = "그만두기", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = font), - color = colors.white - ) - } - - Spacer(modifier = Modifier.height(27.92.dp)) + Text( + text = "나가기", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = colors.white + ) } } } @@ -121,12 +113,12 @@ fun SimpleProgressBar(progress: Float, modifier: Modifier = Modifier) { .fillMaxHeight() .fillMaxWidth(fraction = animated) .clip(RoundedCornerShape(4.dp)) - .background(Brush.horizontalGradient(listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)))) + .background(colors.maincolor) ) } } -@Preview(showBackground = true) +@Preview(showBackground = false) @Composable private fun PreviewAIArticleModal() { LinkuPreview { From 516e0eb3c849a6c2fb527f9f7e82c0b3e7c48cad Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 15:22:15 +0900 Subject: [PATCH 10/89] =?UTF-8?q?:sparkles:=20AI=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=AF=B8=20=EB=A7=81=ED=81=AC=20=EC=9A=94=EC=95=BD,=20AI=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20Modal=20=EC=97=B0=EB=8F=99=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 6 +- .../com/linku/home/screen/LinkDetailScreen.kt | 264 +++++++++++++++--- .../linku/home/screen/SaveLinkResultScreen.kt | 2 +- .../main/res/drawable/ic_sparkles_colored.xml | 22 ++ 4 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 feature/home/src/main/res/drawable/ic_sparkles_colored.xml diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index d5b392b2..c39ec8b6 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -31,7 +31,7 @@ import com.linku.design.theme.linkuColors @Composable fun AIArticleModal( progress: Float, - onCancel: () -> Unit, + onQuit: () -> Unit, modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors @@ -77,7 +77,7 @@ fun AIArticleModal( .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) .background(colors.blue[200]) - .noRippleClickable { onCancel() } + .noRippleClickable { onQuit() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center ) { @@ -124,7 +124,7 @@ private fun PreviewAIArticleModal() { LinkuPreview { AIArticleModal( progress = 0.5f, - onCancel = {} + onQuit = { } ) } } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 2d8be644..af21d88c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -1,6 +1,7 @@ package com.linku.home.screen import android.content.ClipData +import android.content.Intent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -22,7 +23,9 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -35,6 +38,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -49,6 +53,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.AIArticleModal import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown @@ -56,6 +61,7 @@ import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.delay import kotlinx.coroutines.launch private enum class LinkDetailDropdownType { @@ -72,17 +78,24 @@ fun LinkDetailScreen( situation: String, linkUrl: String, memo: String, + tags: List, + aiSummary: String, onBack: () -> Unit, -// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val context = LocalContext.current + + var isEditMode by remember { mutableStateOf(false) } + var isAiSummaryMode by remember { mutableStateOf(false) } var isDropdownVisible by remember { mutableStateOf(false) } var isDeleteModalVisible by remember { mutableStateOf(false) } + var isAiArticleModalVisible by remember { mutableStateOf(false) } + var isAiArticleProcessing by remember { mutableStateOf(false) } + var aiArticleProgress by remember { mutableFloatStateOf(0f) } - var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } var selectedCategory by remember { mutableStateOf(category) } var selectedEmotion by remember { mutableStateOf(emotion) } @@ -97,6 +110,13 @@ fun LinkDetailScreen( val situationOptions = SituationOptions.linkDetailSituations + val visibleTags = tags + .filter { it.isNotBlank() } + .take(4) + .map { tag -> + if (tag.startsWith("#")) tag else "#$tag" + } + // 카테고리 더미데이터 val categoryOptions = listOf( LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), @@ -107,6 +127,23 @@ fun LinkDetailScreen( LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) ) + LaunchedEffect(isAiArticleProcessing) { + if (isAiArticleProcessing) { + aiArticleProgress = 0f + + while (aiArticleProgress < 1f) { + delay(80) + aiArticleProgress = (aiArticleProgress + 0.02f).coerceAtMost(1f) + } + + delay(300) + + isAiArticleProcessing = false + isAiArticleModalVisible = false + isAiSummaryMode = true + } + } + Box( modifier = Modifier .fillMaxSize() @@ -156,7 +193,7 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Box() { + Box { Image( painter = painterResource(R.drawable.img_default), contentDescription = null, @@ -268,6 +305,96 @@ fun LinkDetailScreen( } } + if (isAiSummaryMode) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 25.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 태그", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + if (visibleTags.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + visibleTags.forEach { tag -> + Text( + text = tag, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(20.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 9.dp) + ) + } + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 28.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 링크 요약", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + Text( + text = aiSummary, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + lineHeight = 20.sp, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 22.dp, vertical = 16.dp) + ) + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -331,6 +458,10 @@ fun LinkDetailScreen( .padding(horizontal = 22.dp, vertical = 15.5.dp) ) } + + if (isAiSummaryMode) { + Spacer(modifier = Modifier.height(40.dp)) + } } } } @@ -348,7 +479,22 @@ fun LinkDetailScreen( }, onShareClick = { isDropdownVisible = false - // 공유 로직 추가 예정 + openedDropdownType = null + + val shareText = buildString { + appendLine(selectedTitle) + append(linkUrl) + } + + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" // MIME 타입 + putExtra(Intent.EXTRA_TEXT, shareText) // 공유할 내용 + putExtra(Intent.EXTRA_TITLE, selectedTitle) // 미리보기 제목 + putExtra(Intent.EXTRA_SUBJECT, selectedTitle) // 이메일 앱용 제목 + } + + val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") // ShareSheet 상단에 보이는 제목 + context.startActivity(shareIntent) }, onGoClick = { isDropdownVisible = false @@ -390,6 +536,25 @@ fun LinkDetailScreen( } } + if (isAiArticleModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(2f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + AIArticleModal( + progress = aiArticleProgress, + onQuit = { + isAiArticleModalVisible = false + }, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + if (openedDropdownType != null) { Box( modifier = Modifier @@ -446,48 +611,56 @@ fun LinkDetailScreen( } } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .align(Alignment.BottomCenter) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.maincolor) - .padding(vertical = 15.dp) - .noRippleClickable { - if (isEditMode) { - isEditMode = false - openedDropdownType = null - // 수정 API 불러오기 - } else { - // AI 요약 로직 - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - if (isEditMode) { - Text( - text = "완료", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) - } else { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (!isAiSummaryMode) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + isAiArticleModalVisible = true + openedDropdownType = null + + if (!isAiArticleProcessing) { + aiArticleProgress = 0f + isAiArticleProcessing = true + } + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -504,8 +677,9 @@ fun PreviewLinkDetailScreen() { situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + tags = listOf("오픽", "AL", "영어회화", "자격증"), + aiSummary = "오픽 시험에서는 인터뷰어 Ava와의 대화를 친구처럼 자연스럽게 임하며, 목표 점수에 맞춰 답변량과 유창성을 조절하고, MBC 구조와 콤보 유형 연습을 통해 고득점을 노리는 전략적 접근이 중요하다.", onBack = { }, -// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt index 0ecf4f9f..79b77e0c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt @@ -696,7 +696,7 @@ fun SaveLinkResultScreen( Box(modifier = Modifier.padding(horizontal = 20.dp)) { AIArticleModal( progress = aiProgress, - onCancel = onCancelAi, + onQuit = onCancelAi, modifier = Modifier.padding(horizontal = 20.dp) ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles_colored.xml b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml new file mode 100644 index 00000000..7c71d2dc --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml @@ -0,0 +1,22 @@ + + + + + + + + + + From ed304fbe33239243b77579aa08192997989284a1 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 17:08:00 +0900 Subject: [PATCH 11/89] =?UTF-8?q?:sparkles:=20LinkCardItem=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=AA=A8=EB=93=88=EC=97=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design/component/DeleteLinkItemModal.kt | 54 +++++ .../linku/design/component/LinkCardItem.kt | 208 ++++++++++++++++++ .../src/main/res/drawable/ic_ai_bookmark.xml | 25 +++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++++ design/src/main/res/drawable/ic_more.xml | 15 ++ .../src/main/res/drawable/img_genz_trend.png | Bin 0 -> 9854 bytes .../main/res/drawable/img_link_default.xml | 42 ++++ 7 files changed, 383 insertions(+) create mode 100644 design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt create mode 100644 design/src/main/java/com/linku/design/component/LinkCardItem.kt create mode 100644 design/src/main/res/drawable/ic_ai_bookmark.xml create mode 100644 design/src/main/res/drawable/ic_linku_blur.xml create mode 100644 design/src/main/res/drawable/ic_more.xml create mode 100644 design/src/main/res/drawable/img_genz_trend.png create mode 100644 design/src/main/res/drawable/img_link_default.xml diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt new file mode 100644 index 00000000..37ed8537 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -0,0 +1,54 @@ +package com.linku.design.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun DeleteLinkItemModal( + onClickModal: () -> Unit = { } +) { + Column( + modifier = Modifier + .width(120.dp) + .graphicsLayer { + shadowElevation = 10.dp.toPx() + this.shape = shape + clip = true + } + .clip(RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 10.dp) + .noRippleClickable { onClickModal() } + ) { + Text( + text = "삭제하기", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800], + modifier = Modifier.width(90.dp) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkItemModal() { + ThemeProvider { + DeleteLinkItemModal(onClickModal = { }) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt new file mode 100644 index 00000000..b925c7d5 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -0,0 +1,208 @@ +package com.linku.design.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +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.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.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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors +import com.linku.design.R + +@Composable +fun LinkCardItem( + hasAiSummary: Boolean, + linkTitle: String, + tags: List, + domainName: String? = null, + @DrawableRes linkImage: Int? = null, + @DrawableRes domainImage: Int? = null, + onClickDelete: () -> Unit +) { + var isMenuVisible by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(linkImage ?: R.drawable.img_link_default), + contentDescription = null, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + + Spacer(modifier = Modifier.width(14.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalAlignment = Alignment.Start + ) { + Text( + text = linkTitle, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.linkuColors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 13.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + tags.forEach { tag -> + Text( + text = tag, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .background( + color = LocalColorTheme.current.gray[100], + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) + + Spacer(modifier = Modifier.width(6.dp)) + } + } + + Spacer(modifier = Modifier.height(9.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (domainImage != null) { + Image( + painter = painterResource(domainImage), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + } + + Text( + text = domainName ?: "", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600] + ) + } + } + + Box( + modifier = Modifier + .height(85.dp) + .padding(end = 5.dp) + .noRippleClickable { isMenuVisible = !isMenuVisible }, + contentAlignment = Alignment.TopEnd + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier.size(17.dp) + ) + } + } + + if (hasAiSummary) { + Image( + painter = painterResource(R.drawable.ic_ai_bookmark), + contentDescription = null, + modifier = Modifier + .padding(start = 18.dp) + .size(20.dp, 26.dp) + ) + } + + if (isMenuVisible) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 36.dp, end = 12.dp) + ) { + DeleteLinkItemModal( + onClickModal = { + isMenuVisible = false + onClickDelete() + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_HasAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = true, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + linkImage = R.drawable.img_genz_trend, + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_NoAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = false, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_ai_bookmark.xml b/design/src/main/res/drawable/ic_ai_bookmark.xml new file mode 100644 index 00000000..6c38a8de --- /dev/null +++ b/design/src/main/res/drawable/ic_ai_bookmark.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_linku_blur.xml b/design/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/design/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_more.xml b/design/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..2f2f7991 --- /dev/null +++ b/design/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/design/src/main/res/drawable/img_genz_trend.png b/design/src/main/res/drawable/img_genz_trend.png new file mode 100644 index 0000000000000000000000000000000000000000..1360d8c7f0e21c0d7068324365ea5d5781a5d0b3 GIT binary patch literal 9854 zcmV-^CV|`{Bx-c_qcZME9k zqt(`r>Og5Js?=_)XssHxYtK-zfHX0V;vh>-G5P zqmNLaKmimiSP&&kmQ;W9{lvsX{7(|A|KF5~hqtn_Lg~__asU2(1t>x0dtqT==+L19 zHf`DjgTVluE{8h6OOyTlx8;=p^~{+ws8gqoT)z(a^XG%Ty}bgFS{fW2j2ky@plsQ) z3g~R@Q+`V@`TfMu`!GCyEH?SyGfF*PqG(!}neg+UfBqTP0^kpG{(~Xoroq9{2?Ye) zq@-jxIyz$I%9WTkYnDk{Gt*iEY9*h=UB3zcPF)dy^QOGXbNKIRY%YSDqO6TT#>K|M z+S(d+MvWr?F)`6FBpcx5?5w_R4t9AjFO(_nfx5M-;-fLc5fc}Wc}rHHZ0V9na&UmH zt)2QF_jBdS6;!NPF~bGpd$E7};!?FLaP+JK`!eNIH5Vz$64aTd7)<}R^!Ky(&5j}e zPDxINm0oZD1zz>@`^qiz=WY@c5^&wm4>oe(Bsl@MeEHx}v?#7$y9)Eec)j)v6oMZ1 zC<6O_{Q*}4Lh!+mesHf)8P|MH z{-V1F^!W+_3e42^b$MM8=XVV@h2=B1yhsXrX#5PYDN+~-ckUqhQ5bAXltOY;6s+v* z1VC%qM~eDV;@wcVwr`8box5RED0SafcDAr~aglvJG8uwR;X=6={RvRutpGSVIl;!( zR?OA`u`w|UP#YT?*vUz9vz5*N#&wehQ2Sh*onUQai((#y(RIlCNQ?+Yg=)2N;lkf2 z=3PetO7O>x8;5e`%Au$%lto>TeLg*U0?Cn&O@PMv-$40OhfsFg22_>(+LtbcqO)fq z@$o~rG;bpNNks7_^WoaD9V!M~LBhRIk~62kAJ{PF#8s9qHb5ALCISP+VQ_A#p5 z4}{_AQ`mWyhHKk4C_HTvk`trh+@zssaw1Coydw8NWyCfZFN7wMoSgb^Yim7~L4lZ( zy6DP(nv0`@J+9o1Li=WQv3bK!uyJw6XP6z=$ExIXlAp%0;*|Z3NF9Tc8&jXIsb}hL~97?bsfLCXIpj zxzk7p5CEfN;M%G+3~_Nd^1?lbXNw#AAH0I{aOsXa8|Drb(vp-Lr0Y}%}Vcp}Hq)9i2Gfwg;n#9Z)I zW?gXN7#PTlZEc`0=q~2%geUv
0*`fRE-96N#NdAfbkSr6D=!H&TmdU%1lP#_;lqdV+4}?zKR*LM8 zARC@MM&6#?5V3YW^d3cE#m^*$7@j^sx&6CfWp9TkyY}M#7qfGprzXgO;^jmb1qmu$ z0!=kCGD2yXtGuT(Ghb#)^to@n^%i>d>ZM*k{^V2f$I%!&cC7lG`*RU6$XV~*yN8Dl zA7bdxp&7b>h~Kv0W{)0lE>}rKY!&^rMKt~2EsR)=U&I|81CG|fVcjF3kr8UV8pwv2N;jYxg@bGYD(vO(|0Vaa)AYAqQ z`SbYn(@$|z$b8|#g@qYhK-H>MpF0Kt3C4RID8SkMN8G^Ry%cnvq`vyiA!g%C}}EDJ&Zj(29~>mG?r``ysF)$JLU8mn3@c8cK!L45|nwp1y^2i!t|DH9|z1|acHD5B4tgG;@-NC*s6 z6e3ngx>Ff%1dkqv3g?ccIRJwY%{C+?L{*^N>G_*p~$Gwa33|y2; zQ0<3{#6_T6$jEm;T)F~I_38*e^)>_a<%>RS{vt$f-iACan;}}rt+h~|#5;G9w|f^v zY}^8;+BJnLgdjO0QmD#SILk>!?K>!GStG<(<5RpMQ>S-MU#kCi!Z< z0exX5Bwo>}8~~c0wPcsYXky*(!qw?ay<*P!h*mz4tl%yTv2l2M_^1%y1jPAXL86$M z;b|0-!y}aGr%09*E8nM9$4cs#B7Qk3DGpx8|58yl{Rz?JSFc`;QsPQ1F*v?Q^C$59 zj+a!e%-bjoPIMO=Hf&IuUZqMEb&S)ePb+u8nnHRZBVoW&=aStzERzRgRqe!y6WG3e zyRsTjPfzt;cEQ!>@7wDIs^gN!OMt)s-2%Ax?GJ~VHN|e1PaP8VP_3zve0?GhMddDDWUI`<(T+E1w z2<5i20`gPy30aR&>+#TP_8TaJb8Dify0YGhyAR9!&Kd5@6# zi|2hXp!b`&9~OZ(>eaw6+xEc!_5=7`y`loz?c29i2Y_0m%f;SPmoA;LZ~sBW3lpj+ z6*30_GD*yz+r=w>D&X$lzu)wIx}Gjwx~L3~i>!%0r;#m<7@KQWwmRqtXa?O&euojA z;H_GLE2TvQm6Xg$ zF8ZJ9sX@+}=fLCpOZ~KQ-5RJV6l~_#-y-H=AlCn~3H57MLk}sQ*RNMU&2M>4q>6ks z$I(hIBqbmqKm`i=5k_iFm2*Q2G(+G4uZni<-nA1ucmJsZ0Zob*#MU&mj-Zk*5p;f@ z+S`gjv0}wi+clyw{2n9oryowmBhi!$kBjV4{1`FaCe50oQPZZVSHC{2gvA`#v>Mj( z<6vo_1m6zD=ie>E=ziT$r$b-t*uGVr=WDOMhDMDVseNb(i}tGHNiy11u3TB#!}(Qq zbL!M7bwU4Rh#|%38VKfH(X6e)LAOf({^s=?;_xHY@tAJ%M95p|Pr2ee=Jo5>m2c&H z+~_VyvH1DEef!Y!t**-7IY?@q(dH<>cTihK-!XnJom;&uEX7N3IC17Yw(Q;q8_E8- zEI<76JGN|CkE0j-U~g-!l#YQ+qkLL`Te@_q2{S#-P*Y#NNy<}SR&3IwNh%+Iks$^x zdwHCkgg>QfeoX=tX;D!LY^&C-LycOs)NyF`3_c|C^ttq@6rK~oCcRgwbTt&xBd$74Jhg|FdXmYanbFcX3ayLK%K;8rlTg~HDh;sKT$@ef2UI7-NSLe#U2j{9?R zc2R8{-^;1jTE^20y}EVB%xUA`>f)#@Y0=`vm^t%VSC>NYzW(~_G(1^v`|mVGm7RcZooR-NoYMR^L^Ti_TElP#B0di zB3+EsjSb>zwBxvo?;a*5T(fp<6*O5>ICA7LiWP5o!!8WFo%+ z#OvU77AfL^E0->zxpXm{#IG}ek{vN?VtJCmlx$?nmMxevWs1dPDNQvMJnv0-6wZtxT zRzgjX+rzUdV#-n@PD*t}g#}bU>36Xr#z09w$wD}(9<`FLhu22ACB=EtO^$TZhZd3F zlD$lxJlS$lU0sAu%4ioZVa>Wt7&@RAo=T%ES~RG1IWO${drz7la0k0~?NWrDfY20L zv}mDn`z*6FYO1VqN;(jq`;Gjakn6>HT2-u?G$E{)F8Qj$jmAW%kpz@9kKFi@Xe$HQ zwQGK;CK=Wz|Cx!c+qT22d^yEh$nP_%j_G+p)D(N^XYW3E1n-R)2zyr-l=g0fe0lT0 z=i+5qjil7^9b=Scp3A(qhxFF__wTO?B^(?h(EOVO*^7 zE#bZtH;#U)H1LSHHfE62G-5zyp~lkBlP4#LMFj+f;LN^F`0(=u@V{^zn>KDjo9^!* z=$5~7cNsM*bq}Q)6edKrTZfj|`Nz+Q5x*E7auag``XNv+2mgu0L1(gkjtGUZ6Z=OC&{AxrSF#4EuR{u zqZ=aNyf!Kr{c5Yd;qCR5iVD#)aAZv-tOE6J2$QtDi((%oehqiI)|>k`l-prL4yWm7`!R^Ilt^vYHVXzfw+iJeD>XH zT=Vn8gr;wzY^jpZndbWS>oH)!031Gc9IuIU-?4Kix_9r6&Ye5sn3&Yakt0(tg7Zi5 z393=3NrEm}%r6Fkk0Vgg#!f|4)+)L33;+}Y=_(xeu{05N@vOQVDVVB;*#a+6sZ0^Z zVJnf`N!*g1AvL)6?cD=&=FP|T6MIqp&{5Q_RSl)fmWE3Kci1`?5@6%7>ici;)3&{c zc=QMhXHCP2(`V2?iZ5CskRhm#!^0Fs;M~)-(5Kk+>(2trC{vA7(-y5LOAWnv!3R%6 z18_Mg0+ounB32weIUaeOmQ~nF3&pWkbu~Mn32D-Fk5;(6bt3{jOQEbp(KTzi5)m=K8_X4;v@FCu`?Ca4^n6S-a&$)ZT+?*eoYPt+bI;clV+>aPSYn195N7TC|3@ zXGtVQ+(LP;a`2SeX|nX?Yzh`dWaw?ooc<-gS-w_Hgtutf8ckoXj&?o!p=beD+>>CN zRV_r%8YLqhfh|<1pg7!^f9}_YE=GvVep+k3CSLX;h4*EbLR#-`!hYD*CAGSH_YNLEiNb3&8^J%)fXdz# zvOKm)kF_ch%WaJAyfKVbZLRpq&sojczI_`8_V0(%9t99`(_iY5&XSS1WqBa}p8I|K z?YC81g{dFGWQ&=8wU(3vIY>d0%^}}JaVhM3J@|Sd!hhSSW(%_GPsxMxNYJ_5uLmN< zbn{5PAu%aIEaP2Mfbo(Pbk3YP=-Yn~%9ksTTGgu}{Lw?)l&pzGdzSI2a9rqQS==(E z%ixfle|98IL4}|)U~p&LIH6Ey$YiBTmC(9%Yc+X7pQ>){Vc-!UgDPRAH564YH`!~C1N-xqJOn%QPKMw4U88b%h zn~gLw2AUds`?L;~QHd2?Z_yl16>-+=!(W`eK#YHX6Bru=X zMvv(3$Vjx2Bz8|}MOyKkibjT$W#8ZY4@O=_Z)Oy%JaOX=IY>bBrcOIG`#y;l>UoPn1(4^hN3YlaAWwuqvoLKi%jGFSlFIGg@ds{+ zKRa|_zx;g*TYuk#3qI!%Efl5G#0x>U7FUpM5qjisD+qr{r9MS`W8C8? zRL6=;c(`8AtMnj?}cvX;o8VY*=X5lxDbFKe2j9&83N2ct$^D;)^eo z`%`lfMvCck_KbSH7kbyUujfFeDbOLaTWG!l1cCJDk>tSYM{+R)pXsGJ(6JIdM+_c_ zZ8Nk{u9a%B)X6R+X(j z-QI+YSkyE*Zw`K}mh023FwK!^3e0GZA3v@@)PyucQ5~ajf`oyJsU;6;qFb$6VB_{3 zXxX41HpuH+?b=|;iq(>>j>niuld*Z@MtE1Q0(%EX{Iq(N3V89PPVzga9B@%mE;}it zKdfC9AyWS>5)&^O;Zux!|ATC4r@1hp$>@+kpDN^w8VSkC*qwR-SE}CZ}Ic$ z)#&n8cle0)xfd*qLqZRa9X^~ER0714;w!b*eEf&NYFQUS@7uSpso=tcuUPs$_8dKq zYLcn!JAM+cSE`7ud-kDA>sFY*XbGlIor2y{-8z5noZ4p<$*mVGU4oF>^;4-+4xW`_ zC0ZF@xDd{a9|7m!+jt=6*-y%GEDCAYkrPnUYpPb`)wI2&6o;rN>$m=a_lFI^=X2-b zM>+TQQklDW;XFLVx^3)i5Gu)mgH*$wvk{Vhl7G<B_jF&#)lPwyx$PsNsX&9gfOn zOX0%xo2VkMr!QSWgBsPbecyfz?%NwPzM74d-+zzVb?Pd98t~vg-uYoYo=R^&C)CMW zO&KO)&!^*HFO~T_!g1TQZ?DcPt4ME2Nb4+tYVJaA3;Q{A>I}w?co(0|o|6h_r!KfC z1*RfJiz%h@zkUPtq|W^7PivnCl{=*A^TMB-yOhtUrOb%T4@&KBR;Lzx{R2_XqYy62 zYo#)!aLngCS~hBcmB0LospH0?|IlGLdGrWehUzr*B!%W${PK`Bo<0@nAX^Du7) z?C*z(f4;4{p`H>*vqN<(iI$L95^aAaq`1<&K^@Haa<%~aeJY?A2xw6u9|EXQ%Lc*> z*8aF06F;10xnaqGMDyljHaJ?Y_4he*#o;v;rOi)-LFeG9v#4CAByQZjuU;<&1fqcy zDfS*YhJkPQ#Fz8G#j0gXQBU&xy9<}%f~0%j6fT2@DcLi716wfy@59O;mKp)|frn@)l_|jA2P$sZ*z_S86N=ZOKj3H@x^*kFUR&1z)LMdY3AW%Yh-NA;n)GzZ+=Vv@y2r z-H-93MquKn)3N9G-{G`!4ZMq&z|`n4jJKEKb4upsZ;*nG+wFTe`^h-yih00Uh`xjH z#vX6?ezB(jneW?z>(R_#tI!peo>i|mJ7fFKT{tF|RLaWDs~KeglM67EO3)A>i*%q zQH%%a8G{C@rp3$#m#$ikb5iFhDnW_E9sxJ}QNM92pfvfBLk0?6K5jcmVqD-SjL)R)m8!9)LDXC-Shy@2t%*P$o9KV#uXa;OyJ4!)>2i)@K zKJ@O|1v~Z~#5+B^VfmVM`1;sEbRIAUzZG@IYbjQ!s<%TdH6fE@^C%9|{0lEz627Ab z!c|;LtXM<^DRA8KzbQmJI*VJCvYw?bQlzl7N^Z!xJ6lWuG*F|Wo?x#FnD!wSEdCzzjvPR{_dmuOM=Lbc*}~J>#&pSedpZh8(6f>t z>B6b(yLS;H?!fd+kqmc~HK^{=V+#qnjmpxnj5RAFX%?yZTR!$_*`kFQF=9CCHEbk+ zt~3HV2x~U{rc9oImMu{n2~m$T2bBP6{!|lY9|t7ObwI=I4Bj9rJU0cHB?D86pr;;-NmdLJofcl6n-?yQyBO&c$M za}7t>Q!H4F8Zj3|(X^NOf7_0o@S1letdS^h4rm(z^pKc*ZRuS_MTR4vC}O6da>wR( z=uu0_�NEB1$R@a2BmPeewj_b?OA2yq#ZC*}y)7P>@&w*u8&iJI^psYFqd^gNXynrkYr^|HhkB*dwJ{NB(Zsenq8 zkUq3mmrm%@e;_`a{;A5)G6j`8PJi1YM}#;*l58QZq|=X)b_NR#VGEWZLK=2Jll9;3~Y`@bJdBq zA?3xM0Kr}Iatyh&abL-X6cmv9Kz`}JapPl|taR3@jF&zENY7mfyoLoW^vn6*;HfYI z`qJ3g7~$XxCGT&HSP8DgDpOE-Fs_ystoZLzrAn)liWI7ql$@mQnB&6{IU>`bNz)up zk^HwrJ@!+8Nf1-!#s`tHeCgus^4yIa3{=`3E81^0;I*f}IlH9Z_{)2;_}Q%4a}gu_ zTh$;kI}z@j#TC*RwL@d^p?2MRs9F1U^&STh_&`M7F=u0eM87IR7Z4|n!35F51bL;O zjC>p^prp=v@!HsYWSgGo!eWYftR??)|O zvZQ)C8iCgyV(^ddbA8FeN2o?eM!Cr34j z!sZ^elXDJKCbp-kg$&m8vzZBlpkmGG+bLbk;xEg6{~LtQb9EJMaue_fg1x;1 zT1#hw54k0nT0@dPl5S6X(x&+{U9yT9s(YgONzHV*(T{MYIFaY)=ck4#US-qHmWchc z)0!xz{N%}FjC}6{toivD)q>ZkppTD_8u#I766+oWl|GjDG_a7alxkVYrxZfV}U zk(XF-{GJnuY<674Y{+DprZ^Ir`bG(hv_&DAVGMxy5!i`L}RCf=y}Gzo$mZFKB8 z)zYK~VTcb4g)ZC{uYwp-65%M?H*~;2Q-U&F+SweA;ElU%Gjn)?T@T&~y?ps{g#*Fl zR3ZoS2s&Tcn&)j#8HrWTo;_2{XQwEdR+7i@7M>Au@SIA_x%28d&Q|(dN{jxXaPZBg zLtE(0OqR;TSy&F_(L!0~WB?;Iq(W(rbkZI!!RHWgu8Ozqv%Z{>Sas{xEhVGK#vSlZ z46aa4M0+g!D<|n5?)dDz>F)SsHYyo2DvmkP9gt$tr}DdeMit#*M)y!r^qEbt z81^G|zJlV-)4H5zr9JuB*8d0w&;TXb2G+J7bI kqRZ@mP4uGX|9@TnAD(ks^5^-L=Kufz07*qoM6N<$g7$PJF#rGn literal 0 HcmV?d00001 diff --git a/design/src/main/res/drawable/img_link_default.xml b/design/src/main/res/drawable/img_link_default.xml new file mode 100644 index 00000000..7539981f --- /dev/null +++ b/design/src/main/res/drawable/img_link_default.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + From 4b6f8d461f032126c8c164089cd2594502bcff56 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:16:49 +0900 Subject: [PATCH 12/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/HomeApp.kt | 1 + .../com/linku/home/component/EmotionSelect.kt | 149 +++++++ .../com/linku/home/screen/SaveLinkScreen.kt | 415 ++++++++---------- .../home/src/main/res/drawable/ic_camera.xml | 9 + 4 files changed, 345 insertions(+), 229 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera.xml diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index 6ec7b301..df2305a2 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -125,6 +125,7 @@ fun HomeApp( SaveLinkScreen( image = viewModel.image, url = viewModel.url, +// title = viewModel.title, memo = viewModel.memo, selectedEmotionId = viewModel.selectedEmotionId, onPickImage = { imagePicker.launch("image/*") }, diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt new file mode 100644 index 00000000..8526042c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -0,0 +1,149 @@ +package com.linku.home.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.color.Basic +import com.linku.home.R + +private data class EmotionUi( + val id: Long, + val label: String, + @DrawableRes val iconRes: Int +) + +private val EMOTIONS = listOf( + EmotionUi(1L, "즐거움", R.drawable.ic_joy), + EmotionUi(2L, "평온", R.drawable.ic_calm), + EmotionUi(3L, "설렘", R.drawable.ic_excite), + EmotionUi(4L, "우울", R.drawable.ic_sad), + EmotionUi(5L, "짜증", R.drawable.ic_irritation), + EmotionUi(6L, "분노", R.drawable.ic_anger), +) + +@Composable +fun EmotionSelect( + selectedEmotionId: Long?, + onEmotionSelect: (Long?) -> Unit +) { + val firstRow = EMOTIONS.take(3) + val secondRow = EMOTIONS.drop(3) + + Column( + modifier = Modifier.padding(top = 13.dp, start = 20.dp) + ) { + Row { + firstRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row { + secondRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + } +} + +@Composable +private fun EmotionBadgeImage( + @DrawableRes iconRes: Int, + label: String, + selected: Boolean, + onToggle: () -> Unit +) { + val boxBackground = Brush.horizontalGradient( + listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) + ) + + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background( + brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) + ) + .then( + if (selected) { + Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) + } + ) + .noRippleClickable { onToggle() } + .padding(horizontal = 15.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 아이콘 + Image( + painter = painterResource(id = iconRes), + contentDescription = label, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(5.dp)) + + // 라벨 + Text( + text = label, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], + fontFamily = LocalFontTheme.current.font + ) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewEmotionSelect() { + ThemeProvider { + EmotionSelect( + selectedEmotionId = 1, + onEmotionSelect = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 3fca190a..967eb32e 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -11,6 +11,7 @@ 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 @@ -36,32 +37,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import coil3.compose.rememberAsyncImagePainter +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R +import com.linku.home.component.EmotionSelect import java.io.File -private data class EmotionUi( - val id: Long, - val label: String, - @DrawableRes val iconRes: Int -) - -private val EMOTIONS = listOf( - EmotionUi(1L, "즐거움", R.drawable.ic_joy), - EmotionUi(2L, "평온", R.drawable.ic_calm), - EmotionUi(3L, "설렘", R.drawable.ic_excite), - EmotionUi(4L, "우울", R.drawable.ic_sad), - EmotionUi(5L, "짜증", R.drawable.ic_irritation), - EmotionUi(6L, "분노", R.drawable.ic_anger), -) - @Composable fun SaveLinkScreen( image: File?, url: String, + title: String? = "", memo: String, selectedEmotionId: Long?, onPickImage: () -> Unit, @@ -91,32 +82,35 @@ fun SaveLinkScreen( .padding(bottom = 70.dp) .verticalScroll(scrollState) ) { - Row( + Box( modifier = Modifier .fillMaxWidth() - .padding(top = 59.dp, start = 20.dp), - verticalAlignment = Alignment.CenterVertically + .padding(top = 59.dp, start = 20.dp, end = 20.dp) + .height(24.dp) ) { Image( painter = painterResource(R.drawable.ic_back), contentDescription = null, modifier = Modifier - .size(width = 10.dp, height = 16.25.dp) - .clickable { onBack() } + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } ) - Spacer(modifier = Modifier.width(131.dp)) - Text( text = "새로운 링크", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.black + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.align(Alignment.Center) ) } Text( - text = "URL 링크 입력", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + text = "URL 링크", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.black, modifier = Modifier.padding(top = 31.dp, start = 24.dp) ) @@ -124,16 +118,22 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp, bottom = 12.dp) - .height(50.dp) - .border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) - .padding(horizontal = 22.dp), + .padding(top = 13.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (url.isEmpty()) { Text( text = "링크를 입력하거나 붙여넣어 주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -147,76 +147,134 @@ fun SaveLinkScreen( ) } - // URL 검사 결과 메시지 - when { - url.isBlank() -> Unit - showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") - isCheckingUrl -> Text( - text = "링크를 확인 중입니다…", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[600], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 17.dp, start = 24.dp, end = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "링크 제목", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black, ) - isInvalidLink -> { - WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") - } - isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") - isDuplicateUrl == false -> Text( - text = "저장 가능한 링크예요.", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), + + Text( + text = "선택", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) ) - else -> Unit } -// if (isInvalidLink) { -// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 15.dp), + contentAlignment = Alignment.CenterStart + ) { + if (url.isEmpty()) { + Text( + text = "링크 제목을 입력해주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400] + ) + } + + BasicTextField( + value = url, // TODO: 추후 API 파라미터에 링크 제목 추가되면 바꾸기 + onValueChange = onUrlChange, + singleLine = true, + textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + modifier = Modifier.fillMaxWidth() + ) + } + +// // URL 검사 결과 메시지 +// when { +// url.isBlank() -> Unit +// showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +// isCheckingUrl -> Text( +// text = "링크를 확인 중입니다…", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.gray[600], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// isInvalidLink -> { +// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +// } +// isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") +// isDuplicateUrl == false -> Text( +// text = "저장 가능한 링크예요.", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.blue[200], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// else -> Unit // } // -// if (showVideoWarning) { -// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// if (isInvalidLink) { +//// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +//// } +//// +//// if (showVideoWarning) { +//// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// } +// +// // 둘 다 false일 때만 Spacer 추가 +// if (!isInvalidLink && !showVideoWarning) { +// Spacer(modifier = Modifier.height(12.dp)) // } - // 둘 다 false일 때만 Spacer 추가 - if (!isInvalidLink && !showVideoWarning) { - Spacer(modifier = Modifier.height(12.dp)) - } - Column( modifier = Modifier .fillMaxWidth() - .height(209.3.dp) - .padding(top = 18.dp, start = 20.dp, end = 20.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) - .clickable { onPickImage() }, - horizontalAlignment = Alignment.CenterHorizontally + .padding(start = 20.dp, end = 20.dp, top = 19.dp) + .noRippleClickable { onPickImage() }, + horizontalAlignment = Alignment.Start ) { if (image != null) { Image( painter = rememberAsyncImagePainter(model = image), contentDescription = "선택된 이미지", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop // ✅ 박스에 꽉 차도록 + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) ) } else { Column( + modifier = Modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(R.drawable.ic_transparent_logo), + painter = painterResource(R.drawable.ic_camera), contentDescription = null, - modifier = Modifier - .height(120.dp) - .padding(top = 50.dp) + modifier = Modifier.height(24.dp) ) + Text( - text = "이미지 업로드하기", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, fontFamily = LocalFontTheme.current.font), + text = "사진 추가", + fontSize = 14.sp, + fontWeight = FontWeight.Light, color = LocalColorTheme.current.gray[500], - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 7.dp) ) } } @@ -225,29 +283,28 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 27.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "메모", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp) - .height(50.dp) + .padding(top = 13.dp, start = 20.dp, end = 20.dp) .then( if (memo.isEmpty()) { Modifier.border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) @@ -258,14 +315,15 @@ fun SaveLinkScreen( ) } ) - .padding(horizontal = 22.dp), + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (memo.isEmpty()) { Text( text = "메모할 내용을 입력해주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -273,7 +331,6 @@ fun SaveLinkScreen( BasicTextField( value = memo, onValueChange = { if (it.length <= 200) onMemoChange(it) }, - singleLine = true, textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), modifier = Modifier.fillMaxWidth() ) @@ -282,41 +339,44 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(end = 32.dp, top = 12.dp), + .padding(end = 32.dp, top = 10.dp), horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = memo.length.toString(), - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[700] ) Text( text = "/200자", - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400], + modifier = Modifier.padding(start = 1.dp) ) } Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 25.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "감정", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } @@ -325,7 +385,7 @@ fun SaveLinkScreen( onEmotionSelect = onEmotionSelect ) - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(100.dp)) } Column( @@ -336,152 +396,49 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .height(50.dp) .clip(RoundedCornerShape(18.dp)) - .background( - brush = if (isButtonEnabled) { - Basic.maincolor + .noRippleClickable(enabled = isButtonEnabled) { onSaveClick() } + .then ( + if (isButtonEnabled) { + Modifier.background(Basic.maincolor) } else { - Brush.horizontalGradient( - listOf( - Color(0x1A2C6FFF), - Color(0x1AC800FF) - ) - ) + Modifier.background(LocalColorTheme.current.gray[300]) } ) - - .clickable(enabled = isButtonEnabled) { onSaveClick() }, + .padding(vertical = 15.dp), contentAlignment = Alignment.Center ) { Text( text = "저장", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - } - } -} - -@Composable -fun WarningText( - message: String, - modifier: Modifier = Modifier -) { - Text( - text = message, - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal), fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.negative, - modifier = modifier.padding(start = 32.dp) - ) -} - -@Composable -fun EmotionSelect( - selectedEmotionId: Long?, - onEmotionSelect: (Long?) -> Unit -) { - val firstRow = EMOTIONS.take(4) - val secondRow = EMOTIONS.drop(4) - - Column(modifier = Modifier.padding(top = 15.dp, start = 20.dp)) { - Row { - firstRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } - ) - Spacer(modifier = Modifier.width(10.dp)) - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row { - secondRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white, + textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.width(10.dp)) } } } } -@Composable -private fun EmotionBadgeImage( - @DrawableRes iconRes: Int, - label: String, - selected: Boolean, - onToggle: () -> Unit -) { - val boxBackground = Brush.horizontalGradient( - listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) - ) - - Row( - modifier = Modifier - .clip(RoundedCornerShape(20.dp)) - .background( - brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) - ) - .then( - if (selected) { - Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) - } else { - Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) - } - ) - .clickable { onToggle() } - .padding(horizontal = 15.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 아이콘 - Image( - painter = painterResource(id = iconRes), - contentDescription = label, - modifier = Modifier.size(20.dp) - ) - - Spacer(modifier = Modifier.width(5.dp)) - - // 라벨 - Text( - text = label, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], - fontFamily = LocalFontTheme.current.font - ) - ) - } -} - @Preview(showBackground = true) @Composable fun PreviewSaveLinkScreen() { - SaveLinkScreen( - image = null, - url = "", - memo = "", - selectedEmotionId = null, - onPickImage = {}, - onUrlChange = {}, - onMemoChange = {}, - onEmotionSelect = {}, - onSaveClick = {}, - onBack = {}, - isCheckingUrl = false, - isDuplicateUrl = null, - isInvalidLink = false - ) + ThemeProvider { + SaveLinkScreen( + image = null, + url = "", + title = "", + memo = "", + selectedEmotionId = null, + onPickImage = {}, + onUrlChange = {}, + onMemoChange = {}, + onEmotionSelect = {}, + onSaveClick = {}, + onBack = {}, + isCheckingUrl = false, + isDuplicateUrl = null, + isInvalidLink = false + ) + } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera.xml b/feature/home/src/main/res/drawable/ic_camera.xml new file mode 100644 index 00000000..7474098b --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,9 @@ + + + From eac6b65c36d213e293e8fa85745529aa171b6173 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:17:09 +0900 Subject: [PATCH 13/89] =?UTF-8?q?:sparkles:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/CustomToastMessage.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt diff --git a/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt new file mode 100644 index 00000000..0fa4b42c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt @@ -0,0 +1,52 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun CustomToastMessage( + backgroundColor: Color, + textColor: Color, + toastMessage: String, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background(color = backgroundColor) + .padding(horizontal = 18.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = toastMessage, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = textColor + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewCustomToastMessage() { + ThemeProvider { + CustomToastMessage( + backgroundColor = Color(0xFFE0FBEB), + textColor = LocalColorTheme.current.positive, + toastMessage = "유효한 링크입니다!" + ) + } +} From 9a82420231bf18ca64283256bbf9199cb386994f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 03:44:42 +0900 Subject: [PATCH 14/89] :sparkles: Rebase 'chore/#126-link' onto develop --- .../home/ui/home/bar/LinkDetailTopBar.kt | 266 ++++++++++++++++++ .../src/main/res/drawable/ic_link_delete.xml | 9 + .../src/main/res/drawable/ic_link_edit.xml | 9 + .../home/src/main/res/drawable/ic_link_go.xml | 12 +- .../src/main/res/drawable/ic_link_go_gray.xml | 12 + .../src/main/res/drawable/ic_link_share.xml | 12 + .../home/src/main/res/drawable/ic_more.xml | 15 + .../res/drawable/linku_logo_transparent.xml | 16 ++ 8 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt create mode 100644 feature/home/src/main/res/drawable/ic_link_delete.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_edit.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_go_gray.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_share.xml create mode 100644 feature/home/src/main/res/drawable/ic_more.xml create mode 100644 feature/home/src/main/res/drawable/linku_logo_transparent.xml diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt new file mode 100644 index 00000000..373ecc6e --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -0,0 +1,266 @@ +package com.linku.home.ui.top.bar + +import androidx.compose.foundation.Image +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.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.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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.AbsoluteAlignment +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailTopBar( + linkTitle: String, + category: String, + emotion: String, + onBack: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onLinkGoClick: () -> Unit, +) { + var isMenuExpanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp)) + .background(LocalColorTheme.current.blue[200]) + ) { + Image( + painter = painterResource(R.drawable.linku_logo_transparent), + contentDescription = null, + modifier = Modifier + .height(110.dp) + .align(Alignment.TopEnd) + ) + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 59.dp, start = 20.dp, end = 24.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_back_white), + contentDescription = "뒤로가기", + modifier = Modifier + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } + ) + + Text( + text = "새로운 링크", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.white, + modifier = Modifier.align(Alignment.Center) + ) + + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterEnd) + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = "더보기", + modifier = Modifier + .height(18.dp) + .align(AbsoluteAlignment.TopRight) + .noRippleClickable { + isMenuExpanded = true + } + ) + + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { + isMenuExpanded = false + } + ) { + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_edit, + text = "링크 수정하기", + onClick = { + isMenuExpanded = false + onEditClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_delete, + text = "링크 삭제하기", + onClick = { + isMenuExpanded = false + onDeleteClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_share, + text = "링크 공유하기", + onClick = { + isMenuExpanded = false + onShareClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_go_gray, + text = "링크 보러가기", + onClick = { + isMenuExpanded = false + onLinkGoClick() + } + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + ) { + Text( + text = linkTitle, + fontSize = 24.sp, // 편집모드에서는 22.sp + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + } + + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } + } + } + } + } +} + +@Composable +private fun LinkDetailDropdownItem( + iconRes: Int, + text: String, + onClick: () -> Unit, +) { + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(18.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(28.dp) + ) + + Text( + text = text, + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] + ) + } + }, + onClick = onClick, + modifier = Modifier + .height(64.dp) + .padding(horizontal = 12.dp) + ) +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailTopBar() { + ThemeProvider { + LinkDetailTopBar( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + onBack = { }, + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onLinkGoClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_link_delete.xml b/feature/home/src/main/res/drawable/ic_link_delete.xml new file mode 100644 index 00000000..2b4edfe4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_edit.xml b/feature/home/src/main/res/drawable/ic_link_edit.xml new file mode 100644 index 00000000..c643835c --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_go.xml b/feature/home/src/main/res/drawable/ic_link_go.xml index ec3d535f..f8b73ba9 100644 --- a/feature/home/src/main/res/drawable/ic_link_go.xml +++ b/feature/home/src/main/res/drawable/ic_link_go.xml @@ -1,16 +1,16 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + + + diff --git a/feature/home/src/main/res/drawable/ic_link_share.xml b/feature/home/src/main/res/drawable/ic_link_share.xml new file mode 100644 index 00000000..e8f557a3 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_share.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/home/src/main/res/drawable/ic_more.xml b/feature/home/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..7e149ac0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/linku_logo_transparent.xml b/feature/home/src/main/res/drawable/linku_logo_transparent.xml new file mode 100644 index 00000000..41ea4346 --- /dev/null +++ b/feature/home/src/main/res/drawable/linku_logo_transparent.xml @@ -0,0 +1,16 @@ + + + + From c36b3496715fcdda71cee0046d0ae4e29e11ef1c Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 04:35:53 +0900 Subject: [PATCH 15/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/screen/LinkDetailScreen.kt | 212 ++++++++++++++++++ .../home/ui/home/bar/LinkDetailTopBar.kt | 66 +----- .../src/main/res/drawable/ic_sparkles.xml | 10 + 3 files changed, 228 insertions(+), 60 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt create mode 100644 feature/home/src/main/res/drawable/ic_sparkles.xml diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt new file mode 100644 index 00000000..e01e9354 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -0,0 +1,212 @@ +package com.linku.home.screen + +import android.content.ClipData +import androidx.compose.foundation.Image +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.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.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalUriHandler +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R +import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.launch + +@Composable +fun LinkDetailScreen( + linkTitle: String, + category: String, + emotion: String, + linkUrl: String, + memo: String, + onBack: () -> Unit, + onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +) { + val clipboard = LocalClipboard.current + val coroutineScope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current + + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalColorTheme.current.white) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + LinkDetailTopBar( + linkTitle = linkTitle, + category = category, + emotion = emotion, + onBack = { onBack() }, + onMoreClick = { }, + onLinkGoClick = { uriHandler.openUri(linkUrl) }, + ) + + Column( + modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + ) { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + .background(LocalColorTheme.current.white) + .padding(top = 7.5.dp, start = 22.dp, end = 8.5.dp, bottom = 7.5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = linkUrl, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) + ) + } + } + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 22.dp) + ) { + Text( + text = "메모", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = memo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) + + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewLinkDetailScreen() { + ThemeProvider { + LinkDetailScreen( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + linkUrl = "https://blog.naver.com/linkU/1234", + memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + onBack = { }, + onMoreClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index 373ecc6e..c058c76a 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -1,4 +1,4 @@ -package com.linku.home.ui.top.bar +package com.linku.home.ui.home.bar import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -6,21 +6,15 @@ 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.DropdownMenu import androidx.compose.material3.DropdownMenuItem 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.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,12 +35,9 @@ fun LinkDetailTopBar( category: String, emotion: String, onBack: () -> Unit, - onEditClick: () -> Unit, - onDeleteClick: () -> Unit, - onShareClick: () -> Unit, + onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, ) { - var isMenuExpanded by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -91,6 +82,9 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) + .noRippleClickable { + onMoreClick() + } ) { Image( painter = painterResource(R.drawable.ic_more), @@ -98,53 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .height(18.dp) .align(AbsoluteAlignment.TopRight) - .noRippleClickable { - isMenuExpanded = true - } ) - - DropdownMenu( - expanded = isMenuExpanded, - onDismissRequest = { - isMenuExpanded = false - } - ) { - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_edit, - text = "링크 수정하기", - onClick = { - isMenuExpanded = false - onEditClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_delete, - text = "링크 삭제하기", - onClick = { - isMenuExpanded = false - onDeleteClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_share, - text = "링크 공유하기", - onClick = { - isMenuExpanded = false - onShareClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_go_gray, - text = "링크 보러가기", - onClick = { - isMenuExpanded = false - onLinkGoClick() - } - ) - } } } @@ -257,9 +205,7 @@ fun PreviewLinkDetailTopBar() { category = "어학", emotion = "평온", onBack = { }, - onEditClick = { }, - onDeleteClick = { }, - onShareClick = { }, + onMoreClick = { }, onLinkGoClick = { }, ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles.xml b/feature/home/src/main/res/drawable/ic_sparkles.xml new file mode 100644 index 00000000..de8029a2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles.xml @@ -0,0 +1,10 @@ + + + From b34cf5a37030039308f9eb48003b06223080ebc3 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 19:14:00 +0900 Subject: [PATCH 16/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=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 --- .../component/LinkDetailCustomDropdown.kt | 160 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 51 +++++- 2 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt new file mode 100644 index 00000000..e7e79575 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -0,0 +1,160 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailCustomDropdown( + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onGoClick: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier +) { + Column( + modifier = modifier + .width(240.dp) + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 24.dp, vertical = 13.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onEditClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_edit), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 수정하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onDeleteClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_delete), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 삭제하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onShareClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_share), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 공유하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onGoClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_go_gray), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 보러가기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCustomDropdown() { + ThemeProvider { + LinkDetailCustomDropdown( + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onGoClick = { }, + onDismiss = { }, + modifier = Modifier + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index e01e9354..01425848 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -20,7 +20,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll 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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,6 +41,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch @@ -54,6 +59,8 @@ fun LinkDetailScreen( val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + var isDropdownVisible by remember { mutableStateOf(false) } + Box( modifier = Modifier .fillMaxSize() @@ -62,19 +69,23 @@ fun LinkDetailScreen( Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) ) { LinkDetailTopBar( linkTitle = linkTitle, category = category, emotion = emotion, onBack = { onBack() }, - onMoreClick = { }, + onMoreClick = { + isDropdownVisible = !isDropdownVisible + }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, ) Column( - modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { Image( painter = painterResource(R.drawable.img_default), @@ -168,23 +179,53 @@ fun LinkDetailScreen( } } + if (isDropdownVisible) { + LinkDetailCustomDropdown( + onEditClick = { + isDropdownVisible = false + // 수정 화면 이동 로직 추가 예정 + }, + onDeleteClick = { + isDropdownVisible = false + // 삭제 로직 추가 예정 + }, + onShareClick = { + isDropdownVisible = false + // 공유 로직 추가 예정 + }, + onGoClick = { + isDropdownVisible = false + // 링크 Open 로직 추가 예정 + }, + onDismiss = { + isDropdownVisible = false + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 100.dp, end = 20.dp), + ) + } + Row( modifier = Modifier .fillMaxWidth() + .padding(horizontal = 20.dp) .align(Alignment.BottomCenter) .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) .noRippleClickable { }, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.Center ) { Image( painter = painterResource(R.drawable.ic_sparkles), contentDescription = null, modifier = Modifier.height(17.51.dp) ) - + + Spacer(modifier = Modifier.width(10.dp)) + Text( text = "AI 요약", fontSize = 16.sp, From 03b924b2e21e6f28e697b415a249f87a51fc9fb3 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:09:49 +0900 Subject: [PATCH 17/89] =?UTF-8?q?:recycle:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 58 ++- .../java/com/linku/core/model/Situation.kt | 106 ++++++ .../component/LinkDetailCategoryDropdown.kt | 101 +++++ .../component/LinkDetailEmotionDropdown.kt | 99 +++++ .../component/LinkDetailSituationDropdown.kt | 76 ++++ .../com/linku/home/screen/LinkDetailScreen.kt | 357 ++++++++++++++---- .../home/ui/home/bar/LinkDetailTopBar.kt | 298 +++++++++++---- .../src/main/res/drawable/ic_camera_white.xml | 9 + .../src/main/res/drawable/ic_delete_blue.xml | 21 ++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++ 10 files changed, 1024 insertions(+), 140 deletions(-) create mode 100644 core/src/main/java/com/linku/core/model/Situation.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera_white.xml create mode 100644 feature/home/src/main/res/drawable/ic_delete_blue.xml create mode 100644 feature/home/src/main/res/drawable/ic_linku_blur.xml diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index fbb6c3b4..5bc21075 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,17 +1,59 @@ package com.linku.core.model +import androidx.annotation.DrawableRes +import com.linku.design.R + enum class EmotionType( val id: Long, - val tagName: String + val tagName: String, + @DrawableRes val imgRes: Int ) { - JOY(1, "즐거움"), - PEACE(2, "평온"), - EXCITEMENT(3, "설렘"), - SADNESS(4, "슬픔"), - ANNOYANCE(5, "짜증"), - ANGER(6, "분노"); + JOY( + id = 1L, + tagName = "즐거움", + imgRes = R.drawable.ic_joy + ), + CALM( + id = 2L, + tagName = "평온", + imgRes = R.drawable.ic_calm + ), + EXCITE( + id = 3L, + tagName = "설렘", + imgRes = R.drawable.ic_excite + ), + SAD( + id = 4L, + tagName = "슬픔", + imgRes = R.drawable.ic_sad + ), + IRRITATION( + id = 5L, + tagName = "짜증", + imgRes = R.drawable.ic_irritation + ), + ANGER( + id = 6L, + tagName = "분노", + imgRes = R.drawable.ic_anger + ); companion object { - fun fromId(id: Long): EmotionType? = values().find { it.id == id } + fun fromId(id: Long?): EmotionType? { + return entries.firstOrNull { it.id == id } + } + + fun fromTagName(tagName: String?): EmotionType? { + return entries.firstOrNull { it.tagName == tagName } + } + + fun tagNameOf(id: Long?): String? { + return fromId(id)?.tagName + } + + fun idOf(tagName: String?): Long? { + return fromTagName(tagName)?.id + } } } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt new file mode 100644 index 00000000..a0fa486f --- /dev/null +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -0,0 +1,106 @@ +package com.linku.core.model + +data class Situation( + val id: Long, + val tagName: String +) + +object SituationOptions { + val linkDetailSituations: List = listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "휴식 중"), + Situation(4L, "이동 중"), + Situation(5L, "식사 중"), + Situation(6L, "자기 전") + ) + + fun situationsFor(jobId: Long): List = when (jobId) { + 1L -> listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "식사 중"), + Situation(4L, "시험 준비"), + Situation(5L, "친구랑"), + Situation(6L, "쇼핑 중"), + Situation(7L, "휴식 중"), + Situation(8L, "자기 전") + ) + + 2L -> listOf( + Situation(9L, "과제 중"), + Situation(10L, "통학 중"), + Situation(11L, "쇼핑 중"), + Situation(12L, "알바 중"), + Situation(13L, "트렌드 확인"), + Situation(14L, "데이트 중"), + Situation(15L, "휴식 중"), + Situation(16L, "자기 전") + ) + + 3L -> listOf( + Situation(17L, "출퇴근"), + Situation(18L, "트렌드 확인"), + Situation(19L, "업무 중"), + Situation(20L, "커리어 고민"), + Situation(21L, "쇼핑 중"), + Situation(22L, "데이트 중"), + Situation(23L, "휴식 중"), + Situation(24L, "자기 전") + ) + + 4L -> listOf( + Situation(25L, "출퇴근"), + Situation(26L, "업무 준비 중"), + Situation(27L, "데이트 중"), + Situation(28L, "식사"), + Situation(29L, "쇼핑 중"), + Situation(30L, "트렌드 확인"), + Situation(31L, "휴식 중"), + Situation(32L, "자기 전") + ) + + 5L -> listOf( + Situation(33L, "작업 중"), + Situation(34L, "쇼핑 중"), + Situation(35L, "트렌드 확인"), + Situation(36L, "데이트 중"), + Situation(37L, "운동 중"), + Situation(38L, "식사"), + Situation(39L, "휴식 중"), + Situation(40L, "자기 전") + ) + + 6L -> listOf( + Situation(41L, "자소서 작성"), + Situation(42L, "면접 준비"), + Situation(43L, "요리 중"), + Situation(44L, "트렌드 확인"), + Situation(45L, "쇼핑 중"), + Situation(46L, "운동 중"), + Situation(47L, "휴식 중"), + Situation(48L, "자기 전") + ) + + else -> situationsFor(3L) + } + + fun nameOf(id: Long?): String? { + if (id == null) return null + + return (linkDetailSituations + (1L..6L).flatMap { situationsFor(it) }) + .distinctBy { it.id } + .firstOrNull { it.id == id } + ?.tagName + } + + fun idOf(tagName: String, jobId: Long? = null): Long? { + val options = if (jobId != null) { + situationsFor(jobId) + } else { + linkDetailSituations + } + + return options.firstOrNull { it.tagName == tagName }?.id + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt new file mode 100644 index 00000000..025c8779 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt @@ -0,0 +1,101 @@ +package com.linku.home.component + +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +data class LinkCategoryOption( + val id: Long, + val name: String, + val color: Color +) + +@Composable +fun LinkDetailCategoryDropdown( + categories: List, + selectedCategory: String, + onCategoryClick: (LinkCategoryOption) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 13.dp, bottom = 13.dp, end = 56.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + categories.forEach { category -> + Row( + modifier = Modifier + .noRippleClickable { + onCategoryClick(category) + } + .padding(horizontal = 6.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(25.dp) + .clip(CircleShape) + .background(category.color) + ) + + Text( + text = category.name, + fontSize = 15.sp, + fontWeight = if (category.name == selectedCategory) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (category.name == selectedCategory) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCategoryDropdown() { + ThemeProvider { + LinkDetailCategoryDropdown( + categories = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ), + selectedCategory = "카테고리2", + onCategoryClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt new file mode 100644 index 00000000..7f1050a4 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -0,0 +1,99 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.core.model.EmotionType +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailEmotionDropdown( + emotions: List, + selectedEmotion: String, + onEmotionClick: (EmotionType) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(top = 14.dp, start = 16.dp, end = 56.dp, bottom = 14.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + emotions.forEach { emotion -> + Row( + modifier = Modifier + .noRippleClickable { + onEmotionClick(emotion) + } + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(emotion.iconRes()), + contentDescription = null, + modifier = Modifier.size(29.dp) + ) + + Text( + text = emotion.tagName, + fontSize = 15.sp, + fontWeight = if (emotion.tagName == selectedEmotion) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (emotion.tagName == selectedEmotion) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +private fun EmotionType.iconRes(): Int { + return when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailEmotionDropdown() { + ThemeProvider { + LinkDetailEmotionDropdown( + emotions = EmotionType.entries.toList(), + selectedEmotion = "평온", + onEmotionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt new file mode 100644 index 00000000..e3a4990f --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -0,0 +1,76 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun LinkDetailOptionDropdown( + options: List, + selectedOption: String, + onOptionClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) + .heightIn(max = 264.dp) + ) { + options.forEach { option -> + Text( + text = option, + fontSize = 15.sp, + fontWeight = if (option == selectedOption) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (option == selectedOption) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + }, + modifier = Modifier + .noRippleClickable { + onOptionClick(option) + } + .padding(horizontal = 4.dp, vertical = 9.dp) + ) + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailOptionDropdown() { + ThemeProvider { + LinkDetailOptionDropdown( + options = listOf( + "트렌드 확인", + "통학 중", + "과제 중", + "쇼핑 중", + "데이트 중", + "알바 전" + ), + selectedOption = "통학 중", + onOptionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 01425848..eb12741d 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -14,9 +14,11 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,33 +29,49 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle 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 com.linku.core.model.EmotionType +import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkCategoryOption +import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown +import com.linku.home.component.LinkDetailEmotionDropdown +import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch +private enum class LinkDetailDropdownType { + CATEGORY, + EMOTION, + SITUATION +} + @Composable fun LinkDetailScreen( linkTitle: String, category: String, emotion: String, + situation: String, linkUrl: String, memo: String, onBack: () -> Unit, - onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() @@ -61,6 +79,31 @@ fun LinkDetailScreen( var isDropdownVisible by remember { mutableStateOf(false) } + var isEditMode by remember { mutableStateOf(false) } + var selectedTitle by remember { mutableStateOf(linkTitle) } + var selectedCategory by remember { mutableStateOf(category) } + var selectedEmotion by remember { mutableStateOf(emotion) } + var selectedSituation by remember { mutableStateOf(situation) } + var selectedMemo by remember { mutableStateOf(memo) } + + var openedDropdownType by remember { + mutableStateOf(null) + } + + val emotionOptions = EmotionType.entries.toList() + + val situationOptions = SituationOptions.linkDetailSituations + + // 카테고리 더미데이터 + val categoryOptions = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ) + Box( modifier = Modifier .fillMaxSize() @@ -72,13 +115,36 @@ fun LinkDetailScreen( ) { LinkDetailTopBar( linkTitle = linkTitle, - category = category, - emotion = emotion, + category = selectedCategory, + emotion = selectedEmotion, + situation = selectedSituation, + isEditMode = isEditMode, + isCategoryDropdownOpen = openedDropdownType == LinkDetailDropdownType.CATEGORY, + isEmotionDropdownOpen = openedDropdownType == LinkDetailDropdownType.EMOTION, + isSituationDropdownOpen = openedDropdownType == LinkDetailDropdownType.SITUATION, onBack = { onBack() }, onMoreClick = { isDropdownVisible = !isDropdownVisible }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, + onCategoryClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.CATEGORY) null + else LinkDetailDropdownType.CATEGORY + }, + onEmotionClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.EMOTION) null + else LinkDetailDropdownType.EMOTION + }, + onSituationClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.SITUATION) null + else LinkDetailDropdownType.SITUATION + }, + onTitleClearClick = { + selectedTitle = "" + } ) Column( @@ -87,20 +153,60 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Image( - painter = painterResource(R.drawable.img_default), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(18.dp)) - .border( - width = 1.dp, - color = LocalColorTheme.current.gray[200], - shape = RoundedCornerShape(18.dp) - ) - ) + Box() { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .alpha(if (isEditMode) 0.6f else 1f) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + if(isEditMode) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .noRippleClickable {}, + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .size(84.dp) + .clip(RoundedCornerShape(30.dp)) + .background(LocalColorTheme.current.gray[700]) + .alpha(0.6f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.ic_camera_white), + contentDescription = null, + modifier = Modifier + .height(24.dp) + .padding(top = 5.dp) + ) + + Spacer(modifier = Modifier.height(7.dp)) + + Text( + text = "사진 변경", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.white + ) + } + } + } + } Spacer(modifier = Modifier.height(18.dp)) @@ -123,30 +229,40 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.black + color = if (isEditMode) LocalColorTheme.current.gray[400] else LocalColorTheme.current.black, + modifier = Modifier + .then( + if (isEditMode) { + Modifier.padding(vertical = 7.5.dp) + } else { + Modifier.padding(0.dp) + } + ) ) - Spacer(modifier = Modifier.width(10.dp)) + if (!isEditMode) { + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "복사", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[200]) - .padding(horizontal = 13.5.dp, vertical = 7.dp) - .noRippleClickable { - coroutineScope.launch { - clipboard.setClipEntry( - ClipEntry( - ClipData.newPlainText("linkUrl", linkUrl) + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) ) - ) + } } - } - ) + ) + } } Column( @@ -163,18 +279,55 @@ fun LinkDetailScreen( Spacer(modifier = Modifier.height(12.dp)) - Text( - text = memo, - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - lineHeight = 20.sp, - color = LocalColorTheme.current.black, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .padding(horizontal = 22.dp, vertical = 15.5.dp) - ) + if (isEditMode) { + BasicTextField( + value = selectedMemo, + onValueChange = { + selectedMemo = it + }, + textStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth() + ) { + if (selectedMemo.isBlank()) { + Text( + text = "메모를 입력해 주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.gray[400] + ) + } + + innerTextField() + } + } + ) + } else { + Text( + text = selectedMemo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } } } } @@ -183,7 +336,7 @@ fun LinkDetailScreen( LinkDetailCustomDropdown( onEditClick = { isDropdownVisible = false - // 수정 화면 이동 로직 추가 예정 + isEditMode = true }, onDeleteClick = { isDropdownVisible = false @@ -195,7 +348,7 @@ fun LinkDetailScreen( }, onGoClick = { isDropdownVisible = false - // 링크 Open 로직 추가 예정 + uriHandler.openUri(linkUrl) }, onDismiss = { isDropdownVisible = false @@ -206,6 +359,62 @@ fun LinkDetailScreen( ) } + if (openedDropdownType != null) { + Box( + modifier = Modifier + .fillMaxSize() + .noRippleClickable { + openedDropdownType = null + } + ) + + when (openedDropdownType) { + LinkDetailDropdownType.CATEGORY -> { + LinkDetailCategoryDropdown( + categories = categoryOptions, + selectedCategory = selectedCategory, + onCategoryClick = { + selectedCategory = it.name + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 24.dp) + ) + } + + LinkDetailDropdownType.EMOTION -> { + LinkDetailEmotionDropdown( + emotions = emotionOptions, + selectedEmotion = selectedEmotion, + onEmotionClick = { + selectedEmotion = it.tagName + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 93.dp) + ) + } + + LinkDetailDropdownType.SITUATION -> { + LinkDetailOptionDropdown( + options = situationOptions.map { it.tagName }, + selectedOption = selectedSituation, + onOptionClick = { + selectedSituation = it + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 186.dp) + ) + } + + null -> Unit + } + } + Row( modifier = Modifier .fillMaxWidth() @@ -214,24 +423,41 @@ fun LinkDetailScreen( .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) - .noRippleClickable { }, + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + // AI 요약 로직 + } + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -242,12 +468,13 @@ fun PreviewLinkDetailScreen() { ThemeProvider { LinkDetailScreen( linkTitle = "3일만에 오픽 AL 꿀팁", - category = "어학", + category = "카테고리2", emotion = "평온", + situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", onBack = { }, - onMoreClick = { }, +// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index c058c76a..4a08d9a4 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -2,6 +2,7 @@ package com.linku.home.ui.home.bar import androidx.compose.foundation.Image 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.Column @@ -12,13 +13,14 @@ 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.DropdownMenuItem +//import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -34,11 +36,19 @@ fun LinkDetailTopBar( linkTitle: String, category: String, emotion: String, + situation: String, + isEditMode: Boolean, + isCategoryDropdownOpen: Boolean, + isEmotionDropdownOpen: Boolean, + isSituationDropdownOpen: Boolean, onBack: () -> Unit, onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, + onCategoryClick: () -> Unit, + onEmotionClick: () -> Unit, + onSituationClick: () -> Unit, + onTitleClearClick: () -> Unit, ) { - Box( modifier = Modifier .fillMaxWidth() @@ -71,7 +81,7 @@ fun LinkDetailTopBar( ) Text( - text = "새로운 링크", + text = if (isEditMode) "링크 수정하기" else "저장된 링크", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = LocalColorTheme.current.white, @@ -82,7 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) - .noRippleClickable { + .noRippleClickable(enabled = !isEditMode) { onMoreClick() } ) { @@ -101,17 +111,42 @@ fun LinkDetailTopBar( .fillMaxWidth() .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp ) { - Box( + Row( modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + .then( + if (isEditMode) { + Modifier + .padding(bottom = 11.dp) + .clip(RoundedCornerShape(13.dp)) + .border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(13.dp)) + .padding(horizontal = 15.dp, vertical = 4.dp) + } else { + Modifier.padding(bottom = 12.dp) + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Text( text = linkTitle, - fontSize = 24.sp, // 편집모드에서는 22.sp + fontSize = if (isEditMode) 22.sp else 24.sp, fontWeight = FontWeight.Bold, color = LocalColorTheme.current.white ) + + if (isEditMode) { + Box( + modifier = Modifier + .size(18.dp) + .noRippleClickable { onTitleClearClick() } + ) { + Image( + painter = painterResource(R.drawable.ic_delete_blue), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } } Row( @@ -123,39 +158,159 @@ fun LinkDetailTopBar( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = category, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) - - Text( - text = emotion, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + .background( + when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) // 추후 카테고리 API 연동 후 실제 색상으로 변경 예정 + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onCategoryClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.black // API 연동 후 수정 예정 + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isCategoryDropdownOpen) 180f else 0f) + ) + } + } + + Row( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background( + when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.blue[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onEmotionClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.blue[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isEmotionDropdownOpen) 180f else 0f) + ) + } + } + + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) + .background( + when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onSituationClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = situation, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.purple[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isSituationDropdownOpen) 180f else 0f) + ) + } + } } - Box( - modifier = Modifier - .size(22.dp) - .noRippleClickable { - onLinkGoClick() - }, - ) { - Image( - painter = painterResource(R.drawable.ic_link_go), - contentDescription = null, - modifier = Modifier.height(22.dp) - ) + if(!isEditMode) { + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } } } } @@ -163,38 +318,38 @@ fun LinkDetailTopBar( } } -@Composable -private fun LinkDetailDropdownItem( - iconRes: Int, - text: String, - onClick: () -> Unit, -) { - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(18.dp) - ) { - Image( - painter = painterResource(iconRes), - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - - Text( - text = text, - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] - ) - } - }, - onClick = onClick, - modifier = Modifier - .height(64.dp) - .padding(horizontal = 12.dp) - ) -} +//@Composable +//private fun LinkDetailDropdownItem( +// iconRes: Int, +// text: String, +// onClick: () -> Unit, +//) { +// DropdownMenuItem( +// text = { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// horizontalArrangement = Arrangement.spacedBy(18.dp) +// ) { +// Image( +// painter = painterResource(iconRes), +// contentDescription = null, +// modifier = Modifier.size(28.dp) +// ) +// +// Text( +// text = text, +// fontSize = 24.sp, +// fontWeight = FontWeight.Medium, +// color = LocalColorTheme.current.gray[800] +// ) +// } +// }, +// onClick = onClick, +// modifier = Modifier +// .height(64.dp) +// .padding(horizontal = 12.dp) +// ) +//} @Preview(showBackground = false) @Composable @@ -204,9 +359,18 @@ fun PreviewLinkDetailTopBar() { linkTitle = "3일만에 오픽 AL 꿀팁", category = "어학", emotion = "평온", + situation = "통학 중", + isEditMode = false, + isCategoryDropdownOpen = false, + isEmotionDropdownOpen = false, + isSituationDropdownOpen = false, onBack = { }, onMoreClick = { }, onLinkGoClick = { }, + onEmotionClick = { }, + onCategoryClick = { }, + onSituationClick = { }, + onTitleClearClick = { } ) } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera_white.xml b/feature/home/src/main/res/drawable/ic_camera_white.xml new file mode 100644 index 00000000..927b0cd0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_delete_blue.xml b/feature/home/src/main/res/drawable/ic_delete_blue.xml new file mode 100644 index 00000000..9f4c40b4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_delete_blue.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/ic_linku_blur.xml b/feature/home/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + From 3a38cf754320a392ab10471f3bf17f57ff2df41f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:19:00 +0900 Subject: [PATCH 18/89] =?UTF-8?q?:sparkles:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/DeleteLinkModal.kt | 143 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 35 ++++- 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt new file mode 100644 index 00000000..7ba646bf --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -0,0 +1,143 @@ +package com.linku.home.component + +import androidx.compose.foundation.BorderStroke +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.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.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.color.Basic +import com.linku.home.R + +@Composable +fun DeleteLinkModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column ( + modifier = Modifier + .wrapContentSize() + .padding(top = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_linku_blur), + contentDescription = null, + modifier = Modifier + .height(30.dp) + ) + } + + Text( + text = "해당 링크를 삭제하시겠습니까?", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.padding(top = 15.dp) + ) + + Text( + text = "삭제 시 해당 링크가 영구적으로 제거되며\n복구가 불가능합니다.", + fontSize = 15.sp, + lineHeight = 22.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.gray[600], + modifier = Modifier.padding(top = 13.dp) + ) + + Row( + modifier = Modifier + .padding(top = 20.dp, start = 27.dp, end = 27.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .clickable { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "취소하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + brush = Basic.maincolor, // 그라데이션 Brush 사용 + fontFamily = LocalFontTheme.current.font + ), + modifier = Modifier + .graphicsLayer(alpha = 0.99f) // brush 적용 시 필수 + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .background(brush = Basic.maincolor) + .clickable { onConfirm() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "삭제하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font + ), + color = LocalColorTheme.current.white + ) + } + } + + Spacer(modifier = Modifier.height(23.dp)) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkModal() { + DeleteLinkModal( + onDismiss = {}, + onConfirm = {} + ) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index eb12741d..2d8be644 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -42,12 +42,14 @@ 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.compose.ui.zIndex import com.linku.core.model.EmotionType import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown @@ -78,6 +80,7 @@ fun LinkDetailScreen( val uriHandler = LocalUriHandler.current var isDropdownVisible by remember { mutableStateOf(false) } + var isDeleteModalVisible by remember { mutableStateOf(false) } var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } @@ -114,7 +117,7 @@ fun LinkDetailScreen( .fillMaxWidth() ) { LinkDetailTopBar( - linkTitle = linkTitle, + linkTitle = selectedTitle, category = selectedCategory, emotion = selectedEmotion, situation = selectedSituation, @@ -340,7 +343,8 @@ fun LinkDetailScreen( }, onDeleteClick = { isDropdownVisible = false - // 삭제 로직 추가 예정 + openedDropdownType = null + isDeleteModalVisible = true }, onShareClick = { isDropdownVisible = false @@ -359,6 +363,33 @@ fun LinkDetailScreen( ) } + if (isDeleteModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(1f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier.padding(horizontal = 20.dp), + contentAlignment = Alignment.Center + ) { + DeleteLinkModal( + onDismiss = { + isDeleteModalVisible = false + }, + onConfirm = { + isDeleteModalVisible = false + // TODO: 삭제 API 호출 -> 삭제 성공 후 어디로 이동하는지 물어보기 + onBack() + } + ) + } + } + } + if (openedDropdownType != null) { Box( modifier = Modifier From 82d338446e634d7ddce44b0d8c09cf0b40c03f6f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 13:52:35 +0900 Subject: [PATCH 19/89] =?UTF-8?q?:recycle:=20AI=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index c9b90a95..d5b392b2 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -18,83 +19,74 @@ 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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle 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 com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LinkuPreview import com.linku.design.theme.linkuColors -import com.linku.design.theme.linkuFont @Composable fun AIArticleModal( progress: Float, onCancel: () -> Unit, - modifier: Modifier = Modifier // ✅ 외부에서 전달받을 modifier + modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors - val font = MaterialTheme.linkuFont.font - + Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(colors.white), + .background(colors.white) + .padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "AI 요약 중...", - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium, fontFamily = font), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, color = colors.black, - modifier = Modifier.padding(top = 45.dp) + modifier = Modifier.padding(top = 10.dp) ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(12.dp)) // 상태바 SimpleProgressBar( progress = progress, - modifier = Modifier.padding(horizontal = 86.dp) + modifier = Modifier.width(200.dp) ) Text( - text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), + text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!\n나중에 돌아와서 확인할 수 있어요.", + fontSize = 15.sp, + fontWeight = FontWeight.Normal, color = colors.gray[600], - modifier = Modifier.padding(top = 20.dp) + lineHeight = 22.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 21.dp) ) - Text( - text = "잠시만 기다려주세요.", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), - color = colors.gray[600] - ) + Spacer(modifier = Modifier.height(27.dp)) - Column( + Box( modifier = Modifier - .padding(top = 36.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(colors.blue[200]) + .noRippleClickable { onCancel() } + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .padding(horizontal = 28.dp) - .clip(RoundedCornerShape(18.dp)) - .background(brush = MaterialTheme.linkuColors.maincolor) - .clickable { onCancel() }, - contentAlignment = Alignment.Center - ) { - Text( - text = "그만두기", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = font), - color = colors.white - ) - } - - Spacer(modifier = Modifier.height(27.92.dp)) + Text( + text = "나가기", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = colors.white + ) } } } @@ -121,12 +113,12 @@ fun SimpleProgressBar(progress: Float, modifier: Modifier = Modifier) { .fillMaxHeight() .fillMaxWidth(fraction = animated) .clip(RoundedCornerShape(4.dp)) - .background(Brush.horizontalGradient(listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)))) + .background(colors.maincolor) ) } } -@Preview(showBackground = true) +@Preview(showBackground = false) @Composable private fun PreviewAIArticleModal() { LinkuPreview { From d01469e2369d663ebbe95e693d131561188fff4c Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 15:22:15 +0900 Subject: [PATCH 20/89] =?UTF-8?q?:sparkles:=20AI=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=AF=B8=20=EB=A7=81=ED=81=AC=20=EC=9A=94=EC=95=BD,=20AI=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20Modal=20=EC=97=B0=EB=8F=99=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 6 +- .../com/linku/home/screen/LinkDetailScreen.kt | 264 +++++++++++++++--- .../linku/home/screen/SaveLinkResultScreen.kt | 2 +- .../main/res/drawable/ic_sparkles_colored.xml | 22 ++ 4 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 feature/home/src/main/res/drawable/ic_sparkles_colored.xml diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index d5b392b2..c39ec8b6 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -31,7 +31,7 @@ import com.linku.design.theme.linkuColors @Composable fun AIArticleModal( progress: Float, - onCancel: () -> Unit, + onQuit: () -> Unit, modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors @@ -77,7 +77,7 @@ fun AIArticleModal( .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) .background(colors.blue[200]) - .noRippleClickable { onCancel() } + .noRippleClickable { onQuit() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center ) { @@ -124,7 +124,7 @@ private fun PreviewAIArticleModal() { LinkuPreview { AIArticleModal( progress = 0.5f, - onCancel = {} + onQuit = { } ) } } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 2d8be644..af21d88c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -1,6 +1,7 @@ package com.linku.home.screen import android.content.ClipData +import android.content.Intent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -22,7 +23,9 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -35,6 +38,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -49,6 +53,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.AIArticleModal import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown @@ -56,6 +61,7 @@ import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.delay import kotlinx.coroutines.launch private enum class LinkDetailDropdownType { @@ -72,17 +78,24 @@ fun LinkDetailScreen( situation: String, linkUrl: String, memo: String, + tags: List, + aiSummary: String, onBack: () -> Unit, -// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val context = LocalContext.current + + var isEditMode by remember { mutableStateOf(false) } + var isAiSummaryMode by remember { mutableStateOf(false) } var isDropdownVisible by remember { mutableStateOf(false) } var isDeleteModalVisible by remember { mutableStateOf(false) } + var isAiArticleModalVisible by remember { mutableStateOf(false) } + var isAiArticleProcessing by remember { mutableStateOf(false) } + var aiArticleProgress by remember { mutableFloatStateOf(0f) } - var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } var selectedCategory by remember { mutableStateOf(category) } var selectedEmotion by remember { mutableStateOf(emotion) } @@ -97,6 +110,13 @@ fun LinkDetailScreen( val situationOptions = SituationOptions.linkDetailSituations + val visibleTags = tags + .filter { it.isNotBlank() } + .take(4) + .map { tag -> + if (tag.startsWith("#")) tag else "#$tag" + } + // 카테고리 더미데이터 val categoryOptions = listOf( LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), @@ -107,6 +127,23 @@ fun LinkDetailScreen( LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) ) + LaunchedEffect(isAiArticleProcessing) { + if (isAiArticleProcessing) { + aiArticleProgress = 0f + + while (aiArticleProgress < 1f) { + delay(80) + aiArticleProgress = (aiArticleProgress + 0.02f).coerceAtMost(1f) + } + + delay(300) + + isAiArticleProcessing = false + isAiArticleModalVisible = false + isAiSummaryMode = true + } + } + Box( modifier = Modifier .fillMaxSize() @@ -156,7 +193,7 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Box() { + Box { Image( painter = painterResource(R.drawable.img_default), contentDescription = null, @@ -268,6 +305,96 @@ fun LinkDetailScreen( } } + if (isAiSummaryMode) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 25.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 태그", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + if (visibleTags.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + visibleTags.forEach { tag -> + Text( + text = tag, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(20.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 9.dp) + ) + } + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 28.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 링크 요약", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + Text( + text = aiSummary, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + lineHeight = 20.sp, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 22.dp, vertical = 16.dp) + ) + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -331,6 +458,10 @@ fun LinkDetailScreen( .padding(horizontal = 22.dp, vertical = 15.5.dp) ) } + + if (isAiSummaryMode) { + Spacer(modifier = Modifier.height(40.dp)) + } } } } @@ -348,7 +479,22 @@ fun LinkDetailScreen( }, onShareClick = { isDropdownVisible = false - // 공유 로직 추가 예정 + openedDropdownType = null + + val shareText = buildString { + appendLine(selectedTitle) + append(linkUrl) + } + + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" // MIME 타입 + putExtra(Intent.EXTRA_TEXT, shareText) // 공유할 내용 + putExtra(Intent.EXTRA_TITLE, selectedTitle) // 미리보기 제목 + putExtra(Intent.EXTRA_SUBJECT, selectedTitle) // 이메일 앱용 제목 + } + + val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") // ShareSheet 상단에 보이는 제목 + context.startActivity(shareIntent) }, onGoClick = { isDropdownVisible = false @@ -390,6 +536,25 @@ fun LinkDetailScreen( } } + if (isAiArticleModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(2f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + AIArticleModal( + progress = aiArticleProgress, + onQuit = { + isAiArticleModalVisible = false + }, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + if (openedDropdownType != null) { Box( modifier = Modifier @@ -446,48 +611,56 @@ fun LinkDetailScreen( } } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .align(Alignment.BottomCenter) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.maincolor) - .padding(vertical = 15.dp) - .noRippleClickable { - if (isEditMode) { - isEditMode = false - openedDropdownType = null - // 수정 API 불러오기 - } else { - // AI 요약 로직 - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - if (isEditMode) { - Text( - text = "완료", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) - } else { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (!isAiSummaryMode) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + isAiArticleModalVisible = true + openedDropdownType = null + + if (!isAiArticleProcessing) { + aiArticleProgress = 0f + isAiArticleProcessing = true + } + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -504,8 +677,9 @@ fun PreviewLinkDetailScreen() { situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + tags = listOf("오픽", "AL", "영어회화", "자격증"), + aiSummary = "오픽 시험에서는 인터뷰어 Ava와의 대화를 친구처럼 자연스럽게 임하며, 목표 점수에 맞춰 답변량과 유창성을 조절하고, MBC 구조와 콤보 유형 연습을 통해 고득점을 노리는 전략적 접근이 중요하다.", onBack = { }, -// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt index 0ecf4f9f..79b77e0c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt @@ -696,7 +696,7 @@ fun SaveLinkResultScreen( Box(modifier = Modifier.padding(horizontal = 20.dp)) { AIArticleModal( progress = aiProgress, - onCancel = onCancelAi, + onQuit = onCancelAi, modifier = Modifier.padding(horizontal = 20.dp) ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles_colored.xml b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml new file mode 100644 index 00000000..7c71d2dc --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml @@ -0,0 +1,22 @@ + + + + + + + + + + From d33e92fa06c27753402767fb1336fcea5b1378be Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 17:08:00 +0900 Subject: [PATCH 21/89] =?UTF-8?q?:sparkles:=20LinkCardItem=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=AA=A8=EB=93=88=EC=97=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design/component/DeleteLinkItemModal.kt | 54 +++++ .../linku/design/component/LinkCardItem.kt | 208 ++++++++++++++++++ .../src/main/res/drawable/ic_ai_bookmark.xml | 25 +++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++++ design/src/main/res/drawable/ic_more.xml | 15 ++ .../src/main/res/drawable/img_genz_trend.png | Bin 0 -> 9854 bytes .../main/res/drawable/img_link_default.xml | 42 ++++ 7 files changed, 383 insertions(+) create mode 100644 design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt create mode 100644 design/src/main/java/com/linku/design/component/LinkCardItem.kt create mode 100644 design/src/main/res/drawable/ic_ai_bookmark.xml create mode 100644 design/src/main/res/drawable/ic_linku_blur.xml create mode 100644 design/src/main/res/drawable/ic_more.xml create mode 100644 design/src/main/res/drawable/img_genz_trend.png create mode 100644 design/src/main/res/drawable/img_link_default.xml diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt new file mode 100644 index 00000000..37ed8537 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -0,0 +1,54 @@ +package com.linku.design.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun DeleteLinkItemModal( + onClickModal: () -> Unit = { } +) { + Column( + modifier = Modifier + .width(120.dp) + .graphicsLayer { + shadowElevation = 10.dp.toPx() + this.shape = shape + clip = true + } + .clip(RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 10.dp) + .noRippleClickable { onClickModal() } + ) { + Text( + text = "삭제하기", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800], + modifier = Modifier.width(90.dp) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkItemModal() { + ThemeProvider { + DeleteLinkItemModal(onClickModal = { }) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt new file mode 100644 index 00000000..b925c7d5 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -0,0 +1,208 @@ +package com.linku.design.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +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.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.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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors +import com.linku.design.R + +@Composable +fun LinkCardItem( + hasAiSummary: Boolean, + linkTitle: String, + tags: List, + domainName: String? = null, + @DrawableRes linkImage: Int? = null, + @DrawableRes domainImage: Int? = null, + onClickDelete: () -> Unit +) { + var isMenuVisible by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(linkImage ?: R.drawable.img_link_default), + contentDescription = null, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + + Spacer(modifier = Modifier.width(14.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalAlignment = Alignment.Start + ) { + Text( + text = linkTitle, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.linkuColors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 13.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + tags.forEach { tag -> + Text( + text = tag, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .background( + color = LocalColorTheme.current.gray[100], + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) + + Spacer(modifier = Modifier.width(6.dp)) + } + } + + Spacer(modifier = Modifier.height(9.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (domainImage != null) { + Image( + painter = painterResource(domainImage), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + } + + Text( + text = domainName ?: "", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600] + ) + } + } + + Box( + modifier = Modifier + .height(85.dp) + .padding(end = 5.dp) + .noRippleClickable { isMenuVisible = !isMenuVisible }, + contentAlignment = Alignment.TopEnd + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier.size(17.dp) + ) + } + } + + if (hasAiSummary) { + Image( + painter = painterResource(R.drawable.ic_ai_bookmark), + contentDescription = null, + modifier = Modifier + .padding(start = 18.dp) + .size(20.dp, 26.dp) + ) + } + + if (isMenuVisible) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 36.dp, end = 12.dp) + ) { + DeleteLinkItemModal( + onClickModal = { + isMenuVisible = false + onClickDelete() + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_HasAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = true, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + linkImage = R.drawable.img_genz_trend, + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_NoAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = false, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_ai_bookmark.xml b/design/src/main/res/drawable/ic_ai_bookmark.xml new file mode 100644 index 00000000..6c38a8de --- /dev/null +++ b/design/src/main/res/drawable/ic_ai_bookmark.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_linku_blur.xml b/design/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/design/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_more.xml b/design/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..2f2f7991 --- /dev/null +++ b/design/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/design/src/main/res/drawable/img_genz_trend.png b/design/src/main/res/drawable/img_genz_trend.png new file mode 100644 index 0000000000000000000000000000000000000000..1360d8c7f0e21c0d7068324365ea5d5781a5d0b3 GIT binary patch literal 9854 zcmV-^CV|`{Bx-c_qcZME9k zqt(`r>Og5Js?=_)XssHxYtK-zfHX0V;vh>-G5P zqmNLaKmimiSP&&kmQ;W9{lvsX{7(|A|KF5~hqtn_Lg~__asU2(1t>x0dtqT==+L19 zHf`DjgTVluE{8h6OOyTlx8;=p^~{+ws8gqoT)z(a^XG%Ty}bgFS{fW2j2ky@plsQ) z3g~R@Q+`V@`TfMu`!GCyEH?SyGfF*PqG(!}neg+UfBqTP0^kpG{(~Xoroq9{2?Ye) zq@-jxIyz$I%9WTkYnDk{Gt*iEY9*h=UB3zcPF)dy^QOGXbNKIRY%YSDqO6TT#>K|M z+S(d+MvWr?F)`6FBpcx5?5w_R4t9AjFO(_nfx5M-;-fLc5fc}Wc}rHHZ0V9na&UmH zt)2QF_jBdS6;!NPF~bGpd$E7};!?FLaP+JK`!eNIH5Vz$64aTd7)<}R^!Ky(&5j}e zPDxINm0oZD1zz>@`^qiz=WY@c5^&wm4>oe(Bsl@MeEHx}v?#7$y9)Eec)j)v6oMZ1 zC<6O_{Q*}4Lh!+mesHf)8P|MH z{-V1F^!W+_3e42^b$MM8=XVV@h2=B1yhsXrX#5PYDN+~-ckUqhQ5bAXltOY;6s+v* z1VC%qM~eDV;@wcVwr`8box5RED0SafcDAr~aglvJG8uwR;X=6={RvRutpGSVIl;!( zR?OA`u`w|UP#YT?*vUz9vz5*N#&wehQ2Sh*onUQai((#y(RIlCNQ?+Yg=)2N;lkf2 z=3PetO7O>x8;5e`%Au$%lto>TeLg*U0?Cn&O@PMv-$40OhfsFg22_>(+LtbcqO)fq z@$o~rG;bpNNks7_^WoaD9V!M~LBhRIk~62kAJ{PF#8s9qHb5ALCISP+VQ_A#p5 z4}{_AQ`mWyhHKk4C_HTvk`trh+@zssaw1Coydw8NWyCfZFN7wMoSgb^Yim7~L4lZ( zy6DP(nv0`@J+9o1Li=WQv3bK!uyJw6XP6z=$ExIXlAp%0;*|Z3NF9Tc8&jXIsb}hL~97?bsfLCXIpj zxzk7p5CEfN;M%G+3~_Nd^1?lbXNw#AAH0I{aOsXa8|Drb(vp-Lr0Y}%}Vcp}Hq)9i2Gfwg;n#9Z)I zW?gXN7#PTlZEc`0=q~2%geUv
0*`fRE-96N#NdAfbkSr6D=!H&TmdU%1lP#_;lqdV+4}?zKR*LM8 zARC@MM&6#?5V3YW^d3cE#m^*$7@j^sx&6CfWp9TkyY}M#7qfGprzXgO;^jmb1qmu$ z0!=kCGD2yXtGuT(Ghb#)^to@n^%i>d>ZM*k{^V2f$I%!&cC7lG`*RU6$XV~*yN8Dl zA7bdxp&7b>h~Kv0W{)0lE>}rKY!&^rMKt~2EsR)=U&I|81CG|fVcjF3kr8UV8pwv2N;jYxg@bGYD(vO(|0Vaa)AYAqQ z`SbYn(@$|z$b8|#g@qYhK-H>MpF0Kt3C4RID8SkMN8G^Ry%cnvq`vyiA!g%C}}EDJ&Zj(29~>mG?r``ysF)$JLU8mn3@c8cK!L45|nwp1y^2i!t|DH9|z1|acHD5B4tgG;@-NC*s6 z6e3ngx>Ff%1dkqv3g?ccIRJwY%{C+?L{*^N>G_*p~$Gwa33|y2; zQ0<3{#6_T6$jEm;T)F~I_38*e^)>_a<%>RS{vt$f-iACan;}}rt+h~|#5;G9w|f^v zY}^8;+BJnLgdjO0QmD#SILk>!?K>!GStG<(<5RpMQ>S-MU#kCi!Z< z0exX5Bwo>}8~~c0wPcsYXky*(!qw?ay<*P!h*mz4tl%yTv2l2M_^1%y1jPAXL86$M z;b|0-!y}aGr%09*E8nM9$4cs#B7Qk3DGpx8|58yl{Rz?JSFc`;QsPQ1F*v?Q^C$59 zj+a!e%-bjoPIMO=Hf&IuUZqMEb&S)ePb+u8nnHRZBVoW&=aStzERzRgRqe!y6WG3e zyRsTjPfzt;cEQ!>@7wDIs^gN!OMt)s-2%Ax?GJ~VHN|e1PaP8VP_3zve0?GhMddDDWUI`<(T+E1w z2<5i20`gPy30aR&>+#TP_8TaJb8Dify0YGhyAR9!&Kd5@6# zi|2hXp!b`&9~OZ(>eaw6+xEc!_5=7`y`loz?c29i2Y_0m%f;SPmoA;LZ~sBW3lpj+ z6*30_GD*yz+r=w>D&X$lzu)wIx}Gjwx~L3~i>!%0r;#m<7@KQWwmRqtXa?O&euojA z;H_GLE2TvQm6Xg$ zF8ZJ9sX@+}=fLCpOZ~KQ-5RJV6l~_#-y-H=AlCn~3H57MLk}sQ*RNMU&2M>4q>6ks z$I(hIBqbmqKm`i=5k_iFm2*Q2G(+G4uZni<-nA1ucmJsZ0Zob*#MU&mj-Zk*5p;f@ z+S`gjv0}wi+clyw{2n9oryowmBhi!$kBjV4{1`FaCe50oQPZZVSHC{2gvA`#v>Mj( z<6vo_1m6zD=ie>E=ziT$r$b-t*uGVr=WDOMhDMDVseNb(i}tGHNiy11u3TB#!}(Qq zbL!M7bwU4Rh#|%38VKfH(X6e)LAOf({^s=?;_xHY@tAJ%M95p|Pr2ee=Jo5>m2c&H z+~_VyvH1DEef!Y!t**-7IY?@q(dH<>cTihK-!XnJom;&uEX7N3IC17Yw(Q;q8_E8- zEI<76JGN|CkE0j-U~g-!l#YQ+qkLL`Te@_q2{S#-P*Y#NNy<}SR&3IwNh%+Iks$^x zdwHCkgg>QfeoX=tX;D!LY^&C-LycOs)NyF`3_c|C^ttq@6rK~oCcRgwbTt&xBd$74Jhg|FdXmYanbFcX3ayLK%K;8rlTg~HDh;sKT$@ef2UI7-NSLe#U2j{9?R zc2R8{-^;1jTE^20y}EVB%xUA`>f)#@Y0=`vm^t%VSC>NYzW(~_G(1^v`|mVGm7RcZooR-NoYMR^L^Ti_TElP#B0di zB3+EsjSb>zwBxvo?;a*5T(fp<6*O5>ICA7LiWP5o!!8WFo%+ z#OvU77AfL^E0->zxpXm{#IG}ek{vN?VtJCmlx$?nmMxevWs1dPDNQvMJnv0-6wZtxT zRzgjX+rzUdV#-n@PD*t}g#}bU>36Xr#z09w$wD}(9<`FLhu22ACB=EtO^$TZhZd3F zlD$lxJlS$lU0sAu%4ioZVa>Wt7&@RAo=T%ES~RG1IWO${drz7la0k0~?NWrDfY20L zv}mDn`z*6FYO1VqN;(jq`;Gjakn6>HT2-u?G$E{)F8Qj$jmAW%kpz@9kKFi@Xe$HQ zwQGK;CK=Wz|Cx!c+qT22d^yEh$nP_%j_G+p)D(N^XYW3E1n-R)2zyr-l=g0fe0lT0 z=i+5qjil7^9b=Scp3A(qhxFF__wTO?B^(?h(EOVO*^7 zE#bZtH;#U)H1LSHHfE62G-5zyp~lkBlP4#LMFj+f;LN^F`0(=u@V{^zn>KDjo9^!* z=$5~7cNsM*bq}Q)6edKrTZfj|`Nz+Q5x*E7auag``XNv+2mgu0L1(gkjtGUZ6Z=OC&{AxrSF#4EuR{u zqZ=aNyf!Kr{c5Yd;qCR5iVD#)aAZv-tOE6J2$QtDi((%oehqiI)|>k`l-prL4yWm7`!R^Ilt^vYHVXzfw+iJeD>XH zT=Vn8gr;wzY^jpZndbWS>oH)!031Gc9IuIU-?4Kix_9r6&Ye5sn3&Yakt0(tg7Zi5 z393=3NrEm}%r6Fkk0Vgg#!f|4)+)L33;+}Y=_(xeu{05N@vOQVDVVB;*#a+6sZ0^Z zVJnf`N!*g1AvL)6?cD=&=FP|T6MIqp&{5Q_RSl)fmWE3Kci1`?5@6%7>ici;)3&{c zc=QMhXHCP2(`V2?iZ5CskRhm#!^0Fs;M~)-(5Kk+>(2trC{vA7(-y5LOAWnv!3R%6 z18_Mg0+ounB32weIUaeOmQ~nF3&pWkbu~Mn32D-Fk5;(6bt3{jOQEbp(KTzi5)m=K8_X4;v@FCu`?Ca4^n6S-a&$)ZT+?*eoYPt+bI;clV+>aPSYn195N7TC|3@ zXGtVQ+(LP;a`2SeX|nX?Yzh`dWaw?ooc<-gS-w_Hgtutf8ckoXj&?o!p=beD+>>CN zRV_r%8YLqhfh|<1pg7!^f9}_YE=GvVep+k3CSLX;h4*EbLR#-`!hYD*CAGSH_YNLEiNb3&8^J%)fXdz# zvOKm)kF_ch%WaJAyfKVbZLRpq&sojczI_`8_V0(%9t99`(_iY5&XSS1WqBa}p8I|K z?YC81g{dFGWQ&=8wU(3vIY>d0%^}}JaVhM3J@|Sd!hhSSW(%_GPsxMxNYJ_5uLmN< zbn{5PAu%aIEaP2Mfbo(Pbk3YP=-Yn~%9ksTTGgu}{Lw?)l&pzGdzSI2a9rqQS==(E z%ixfle|98IL4}|)U~p&LIH6Ey$YiBTmC(9%Yc+X7pQ>){Vc-!UgDPRAH564YH`!~C1N-xqJOn%QPKMw4U88b%h zn~gLw2AUds`?L;~QHd2?Z_yl16>-+=!(W`eK#YHX6Bru=X zMvv(3$Vjx2Bz8|}MOyKkibjT$W#8ZY4@O=_Z)Oy%JaOX=IY>bBrcOIG`#y;l>UoPn1(4^hN3YlaAWwuqvoLKi%jGFSlFIGg@ds{+ zKRa|_zx;g*TYuk#3qI!%Efl5G#0x>U7FUpM5qjisD+qr{r9MS`W8C8? zRL6=;c(`8AtMnj?}cvX;o8VY*=X5lxDbFKe2j9&83N2ct$^D;)^eo z`%`lfMvCck_KbSH7kbyUujfFeDbOLaTWG!l1cCJDk>tSYM{+R)pXsGJ(6JIdM+_c_ zZ8Nk{u9a%B)X6R+X(j z-QI+YSkyE*Zw`K}mh023FwK!^3e0GZA3v@@)PyucQ5~ajf`oyJsU;6;qFb$6VB_{3 zXxX41HpuH+?b=|;iq(>>j>niuld*Z@MtE1Q0(%EX{Iq(N3V89PPVzga9B@%mE;}it zKdfC9AyWS>5)&^O;Zux!|ATC4r@1hp$>@+kpDN^w8VSkC*qwR-SE}CZ}Ic$ z)#&n8cle0)xfd*qLqZRa9X^~ER0714;w!b*eEf&NYFQUS@7uSpso=tcuUPs$_8dKq zYLcn!JAM+cSE`7ud-kDA>sFY*XbGlIor2y{-8z5noZ4p<$*mVGU4oF>^;4-+4xW`_ zC0ZF@xDd{a9|7m!+jt=6*-y%GEDCAYkrPnUYpPb`)wI2&6o;rN>$m=a_lFI^=X2-b zM>+TQQklDW;XFLVx^3)i5Gu)mgH*$wvk{Vhl7G<B_jF&#)lPwyx$PsNsX&9gfOn zOX0%xo2VkMr!QSWgBsPbecyfz?%NwPzM74d-+zzVb?Pd98t~vg-uYoYo=R^&C)CMW zO&KO)&!^*HFO~T_!g1TQZ?DcPt4ME2Nb4+tYVJaA3;Q{A>I}w?co(0|o|6h_r!KfC z1*RfJiz%h@zkUPtq|W^7PivnCl{=*A^TMB-yOhtUrOb%T4@&KBR;Lzx{R2_XqYy62 zYo#)!aLngCS~hBcmB0LospH0?|IlGLdGrWehUzr*B!%W${PK`Bo<0@nAX^Du7) z?C*z(f4;4{p`H>*vqN<(iI$L95^aAaq`1<&K^@Haa<%~aeJY?A2xw6u9|EXQ%Lc*> z*8aF06F;10xnaqGMDyljHaJ?Y_4he*#o;v;rOi)-LFeG9v#4CAByQZjuU;<&1fqcy zDfS*YhJkPQ#Fz8G#j0gXQBU&xy9<}%f~0%j6fT2@DcLi716wfy@59O;mKp)|frn@)l_|jA2P$sZ*z_S86N=ZOKj3H@x^*kFUR&1z)LMdY3AW%Yh-NA;n)GzZ+=Vv@y2r z-H-93MquKn)3N9G-{G`!4ZMq&z|`n4jJKEKb4upsZ;*nG+wFTe`^h-yih00Uh`xjH z#vX6?ezB(jneW?z>(R_#tI!peo>i|mJ7fFKT{tF|RLaWDs~KeglM67EO3)A>i*%q zQH%%a8G{C@rp3$#m#$ikb5iFhDnW_E9sxJ}QNM92pfvfBLk0?6K5jcmVqD-SjL)R)m8!9)LDXC-Shy@2t%*P$o9KV#uXa;OyJ4!)>2i)@K zKJ@O|1v~Z~#5+B^VfmVM`1;sEbRIAUzZG@IYbjQ!s<%TdH6fE@^C%9|{0lEz627Ab z!c|;LtXM<^DRA8KzbQmJI*VJCvYw?bQlzl7N^Z!xJ6lWuG*F|Wo?x#FnD!wSEdCzzjvPR{_dmuOM=Lbc*}~J>#&pSedpZh8(6f>t z>B6b(yLS;H?!fd+kqmc~HK^{=V+#qnjmpxnj5RAFX%?yZTR!$_*`kFQF=9CCHEbk+ zt~3HV2x~U{rc9oImMu{n2~m$T2bBP6{!|lY9|t7ObwI=I4Bj9rJU0cHB?D86pr;;-NmdLJofcl6n-?yQyBO&c$M za}7t>Q!H4F8Zj3|(X^NOf7_0o@S1letdS^h4rm(z^pKc*ZRuS_MTR4vC}O6da>wR( z=uu0_�NEB1$R@a2BmPeewj_b?OA2yq#ZC*}y)7P>@&w*u8&iJI^psYFqd^gNXynrkYr^|HhkB*dwJ{NB(Zsenq8 zkUq3mmrm%@e;_`a{;A5)G6j`8PJi1YM}#;*l58QZq|=X)b_NR#VGEWZLK=2Jll9;3~Y`@bJdBq zA?3xM0Kr}Iatyh&abL-X6cmv9Kz`}JapPl|taR3@jF&zENY7mfyoLoW^vn6*;HfYI z`qJ3g7~$XxCGT&HSP8DgDpOE-Fs_ystoZLzrAn)liWI7ql$@mQnB&6{IU>`bNz)up zk^HwrJ@!+8Nf1-!#s`tHeCgus^4yIa3{=`3E81^0;I*f}IlH9Z_{)2;_}Q%4a}gu_ zTh$;kI}z@j#TC*RwL@d^p?2MRs9F1U^&STh_&`M7F=u0eM87IR7Z4|n!35F51bL;O zjC>p^prp=v@!HsYWSgGo!eWYftR??)|O zvZQ)C8iCgyV(^ddbA8FeN2o?eM!Cr34j z!sZ^elXDJKCbp-kg$&m8vzZBlpkmGG+bLbk;xEg6{~LtQb9EJMaue_fg1x;1 zT1#hw54k0nT0@dPl5S6X(x&+{U9yT9s(YgONzHV*(T{MYIFaY)=ck4#US-qHmWchc z)0!xz{N%}FjC}6{toivD)q>ZkppTD_8u#I766+oWl|GjDG_a7alxkVYrxZfV}U zk(XF-{GJnuY<674Y{+DprZ^Ir`bG(hv_&DAVGMxy5!i`L}RCf=y}Gzo$mZFKB8 z)zYK~VTcb4g)ZC{uYwp-65%M?H*~;2Q-U&F+SweA;ElU%Gjn)?T@T&~y?ps{g#*Fl zR3ZoS2s&Tcn&)j#8HrWTo;_2{XQwEdR+7i@7M>Au@SIA_x%28d&Q|(dN{jxXaPZBg zLtE(0OqR;TSy&F_(L!0~WB?;Iq(W(rbkZI!!RHWgu8Ozqv%Z{>Sas{xEhVGK#vSlZ z46aa4M0+g!D<|n5?)dDz>F)SsHYyo2DvmkP9gt$tr}DdeMit#*M)y!r^qEbt z81^G|zJlV-)4H5zr9JuB*8d0w&;TXb2G+J7bI kqRZ@mP4uGX|9@TnAD(ks^5^-L=Kufz07*qoM6N<$g7$PJF#rGn literal 0 HcmV?d00001 diff --git a/design/src/main/res/drawable/img_link_default.xml b/design/src/main/res/drawable/img_link_default.xml new file mode 100644 index 00000000..7539981f --- /dev/null +++ b/design/src/main/res/drawable/img_link_default.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + From 7ccb07d8e71d80de85478b2da529dacd69725eed Mon Sep 17 00:00:00 2001 From: Chea Yunzi Date: Sun, 21 Jun 2026 16:18:04 +0900 Subject: [PATCH 22/89] Merge pull request #142 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor(core): `Purpose`, `Interest` 열거형의 `serverKey` 관리 방식 및 조회 … * ♻️ refactor(core): `Purpose`, `Interest` 열거형의 `serverKey` 관리 방식 및 조회 … --- .../com/linku/core/model/auth/Interest.kt | 27 ++++++++++--------- .../java/com/linku/core/model/auth/Purpose.kt | 23 ++++++++-------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/com/linku/core/model/auth/Interest.kt b/core/src/main/java/com/linku/core/model/auth/Interest.kt index 1cd51718..3196981a 100644 --- a/core/src/main/java/com/linku/core/model/auth/Interest.kt +++ b/core/src/main/java/com/linku/core/model/auth/Interest.kt @@ -3,27 +3,28 @@ package com.linku.core.model.auth enum class Interest( override val displayName: String, - override val serverKey: String ) : SelectionItem { // 행 1 - BUSINESS("비즈니스\n& 마케팅", "BUSINESS"), - STARTUP("스타트업\n& 창업", "STARTUP"), - INSIGHTS("책 요약\n& 인사이트", "INSIGHTS"), + BUSINESS("비즈니스\n& 마케팅"), + STARTUP("스타트업\n& 창업"), + INSIGHTS("책 요약\n& 인사이트"), // 행 2 - DESIGN("디자인\n& 크리에이티브", "DESIGN"), - CURRENT_EVENTS("시사\n& 트렌드", "CURRENT_EVENTS"), - STUDY("학습 & 리포트\n참고 자료", "STUDY"), + DESIGN("디자인\n& 크리에이티브"), + CURRENT_EVENTS("시사\n& 트렌드"), + STUDY("학습 & 리포트\n참고 자료"), // 행 3 - IT("IT\n& 개발", "IT"), - CAREER("커리어\n& 채용", "CAREER"), - COLLECT("그냥\n모아두고 싶은 글", "COLLECT"), + IT("IT\n& 개발"), + CAREER("커리어\n& 채용"), + COLLECT("그냥\n모아두고 싶은 글"), // 행 4 - SOCIETY("사회 & 문화\n& 환경", "SOCIETY"), - WRITING("글쓰기\n& 콘텐츠 노하우", "WRITING"), - PSYCHOLOGY("심리\n& 자기계발", "PSYCHOLOGY"); + SOCIETY("사회 & 문화\n& 환경"), + WRITING("글쓰기\n& 콘텐츠 노하우"), + PSYCHOLOGY("심리\n& 자기계발"); + + override val serverKey: String get() = name companion object { fun fromServerKey(key: String): Interest? = selectionItemOfServerKey(key) diff --git a/core/src/main/java/com/linku/core/model/auth/Purpose.kt b/core/src/main/java/com/linku/core/model/auth/Purpose.kt index 5841f86f..f793f465 100644 --- a/core/src/main/java/com/linku/core/model/auth/Purpose.kt +++ b/core/src/main/java/com/linku/core/model/auth/Purpose.kt @@ -1,25 +1,26 @@ package com.linku.core.model.auth -enum class Purpose( +enum class Purpose( override val displayName: String, - override val serverKey: String ) : SelectionItem{ // 행 1 - CAREER("취업\n& 커리어 준비", "CAREER"), - CREATION_REFERENCE("글쓰기 \n& 콘텐츠 제작", "CREATION_REFERENCE"), - INSIGHTS("인사이트\n모으기", "INSIGHTS"), + CAREER("취업\n& 커리어 준비"), + CREATION_REFERENCE("글쓰기 \n& 콘텐츠 제작"), + INSIGHTS("인사이트\n모으기"), // 행 2 - SIDE_PROJECT("사이드 프로젝트\n& 창업", "SIDE_PROJECT"), - STUDY("학업\n& 리포트 정리", "STUDY"), - LATER_READING("나중에\n읽고 싶은 글", "LATER_READING"), + SIDE_PROJECT("사이드 프로젝트\n& 창업"), + STUDY("학업\n& 리포트 정리"), + LATER_READING("나중에\n읽고 싶은 글"), // 행 3 - SELF_DEVELOPMENT("자기계발\n& 정보 수집", "SELF_DEVELOPMENT"), - WORK("업무자료\n아카이빙", "WORK"), + SELF_DEVELOPMENT("자기계발\n& 정보 수집"), + WORK("업무자료\n아카이빙"), - OTHERS("기타", "OTHERS"); + OTHERS("기타"); + + override val serverKey: String get() = name companion object { fun fromServerKey(key: String): Purpose? = selectionItemOfServerKey(key) From 6aa03ba01be2f7b8a56396a9c7438c8c732c3462 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 17:49:38 +0900 Subject: [PATCH 23/89] =?UTF-8?q?:zap:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=9C=20=EB=A7=81=ED=81=AC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 146 +++++++++++--- .../src/main/java/com/linku/home/HomeApp.kt | 183 +----------------- .../main/java/com/linku/home/HomeViewModel.kt | 4 + .../com/linku/home/screen/LinkDetailScreen.kt | 34 ++-- .../com/linku/home/screen/SaveLinkScreen.kt | 41 ++-- 5 files changed, 172 insertions(+), 236 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 901eaac9..d6a035d2 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -49,7 +49,8 @@ import com.linku.file.FileViewModel import com.linku.file.viewmodel.folder.state.FolderStateViewModel import com.linku.home.HomeApp import com.linku.home.HomeViewModel -import com.linku.home.screen.SaveLinkResultScreen +import com.linku.home.component.LinkCategoryOption +import com.linku.home.screen.LinkDetailScreen import com.linku.home.screen.SaveLinkScreen import com.linku.linku_android.curation.curationGraph import com.linku.login.navigation.LoginApp @@ -359,7 +360,7 @@ fun MainApp( HomeApp( viewModel = homeViewModel, nickname = nickname.orEmpty().ifBlank { "링큐" }, - onNavigateToMyPage = { // TODO: 추후 알림 설정 페이지로 이동 + onNavigateToMyPage = { navigator.navigate(NavigationRoute.MyPage.route) { popUpTo(navigator.graph.findStartDestination().id) { saveState = true @@ -369,6 +370,13 @@ fun MainApp( restoreState = true } }, + onNavigateToSaveLink = { url -> + homeViewModel.setUrl(url) + navigator.navigate("savelink") + }, + onNavigateToLinkDetail = { linkuId -> + navigator.navigate("savelinkresult/$linkuId") + }, onShowNavBar = { showNavBar = it } ) } @@ -453,10 +461,12 @@ fun MainApp( SaveLinkScreen( image = vm.image, url = vm.url, + title = vm.title, memo = vm.memo, selectedEmotionId = vm.selectedEmotionId, onPickImage = { imagePicker.launch("image/*") }, onUrlChange = vm::setUrl, + onTitleChange = vm::setTitle, onMemoChange = vm::setMemo, onEmotionSelect = vm::selectEmotion, onSaveClick = { @@ -496,48 +506,130 @@ fun MainApp( vm.loadCategoryColors() } + fun emotionNameOf(id: Long?): String { + return when (id) { + 1L -> "즐거움" + 2L -> "평온" + 3L -> "설렘" + 4L -> "슬픔" + 5L -> "짜증" + 6L -> "분노" + else -> "감정" + } + } + + // TODO: 카테고리 API 연동 후 categoryId 기준 실제 카테고리명/색상 매핑으로 교체 + fun categoryNameOf(id: Long?): String { + return when (id) { + 1L -> "어학" + 2L -> "뉴스" + 3L -> "공부법" + 4L -> "IT·개발" + 5L -> "자기계발" + 6L -> "취업·이직" + 7L -> "비즈니스 인사이트" + 8L -> "생산성·툴" + 9L -> "라이프스타일" + 10L -> "심리·자기이해" + 11L -> "에세이·칼럼" + 12L -> "트렌드" + 13L -> "디자인·예술" + 14L -> "영상·뮤직" + 15L -> "맛집·여행" + 16L -> "기타" + else -> "카테고리" + } + } + + fun categoryIdOf(name: String): Long? { + return when (name) { + "어학" -> 1L + "뉴스" -> 2L + "공부법" -> 3L + "IT·개발" -> 4L + "자기계발" -> 5L + "취업·이직" -> 6L + "비즈니스 인사이트" -> 7L + "생산성·툴" -> 8L + "라이프스타일" -> 9L + "심리·자기이해" -> 10L + "에세이·칼럼" -> 11L + "트렌드" -> 12L + "디자인·예술" -> 13L + "영상·뮤직" -> 14L + "맛집·여행" -> 15L + "기타" -> 16L + else -> null + } + } + + fun keywordToTags(keyword: String?): List { + return keyword + .orEmpty() + .split(",", " ", "#") + .map { it.trim() } + .filter { it.isNotBlank() } + .take(4) + } + // 진행률/색상 맵 수집 val aiProgress = vm.aiProgress.collectAsState().value val categoryColorMap = vm.categoryColorMap.collectAsState().value + val categoryOptions = categoryColorMap.mapNotNull { (name, style) -> + val id = categoryIdOf(name) ?: return@mapNotNull null + + LinkCategoryOption( + id = id, + name = name, + color = style.color4 + ) + } + // 외부 브라우저 열기 fun openUrl(url: String) { runCatching { - val fixed = if (url.startsWith("http")) url else "https://$url" + val fixed = if ( + url.startsWith("http://") || url.startsWith("https://") + ) { + url + } else { + "https://$url" + } + val intent = Intent( Intent.ACTION_VIEW, fixed.toUri() ) + context.startActivity(intent) }.onFailure { Toast.makeText(context, "링크를 열 수 없어요.", Toast.LENGTH_SHORT).show() } } - SaveLinkResultScreen( - link = vm.linkDetail, - aiArticle = vm.aiArticleDetail, - isLoading = vm.isLoadingLinkDetail || vm.isLoadingAiArticle, - isAiLoading = vm.isLoadingAiArticle, - onBack = { navigator.popBackStack() }, - onOpenLink = { url -> openUrl(url) }, - categoryColorMap = categoryColorMap, - onSubmitEdit = { title, memo, categoryId, emotionId -> - vm.updateLink( - title = title, - memo = memo, - categoryId = categoryId, - emotionId = emotionId, - onSucceed = { Toast.makeText(context, "수정 완료", Toast.LENGTH_SHORT).show() }, - onFailed = { e -> - Log.e("SaveLinkResult", "수정 실패", e) - Toast.makeText(context, e.message ?: "수정에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - ) - }, - onRequestAiSummary = { vm.loadAiArticle(linkuId) }, - aiProgress = aiProgress, - onCancelAi = { vm.cancelAiArticleJob() } + val linkDetail = vm.linkDetail + val aiArticle = vm.aiArticleDetail + + val displayKeyword = aiArticle?.keyword?.trim().orEmpty() + .ifEmpty { linkDetail?.keyword.orEmpty() } + + val displaySummary = aiArticle?.summary?.trim().orEmpty() + .ifEmpty { linkDetail?.summary.orEmpty() } + + LinkDetailScreen( + linkTitle = linkDetail?.title.orEmpty(), + category = categoryNameOf(linkDetail?.categoryId), + emotion = emotionNameOf(linkDetail?.emotionId), + situation = "통학 중", // TODO: 상세 API에 situationId 생기면 실제 값으로 변경 + linkUrl = linkDetail?.linku.orEmpty(), + memo = linkDetail?.memo.orEmpty(), + tags = keywordToTags(displayKeyword), + aiSummary = displaySummary, + categoryOptions = categoryOptions, + onBack = { + navigator.popBackStack() + } ) } diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index df2305a2..62a388c2 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -1,17 +1,10 @@ package com.linku.home import android.net.Uri -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost @@ -19,8 +12,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.linku.home.screen.AlarmScreen import com.linku.home.screen.HomeScreen -import com.linku.home.screen.SaveLinkResultScreen -import com.linku.home.screen.SaveLinkScreen import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -28,47 +19,15 @@ import java.io.InputStream @Composable fun HomeApp( viewModel: HomeViewModel, - nickname: String, // 닉네임 호출을 위해 추가함. + nickname: String, onNavigateToMyPage: () -> Unit, + onNavigateToSaveLink: (String) -> Unit, + onNavigateToLinkDetail: (Long) -> Unit, onShowNavBar: (Boolean) -> Unit = {}, ) { val recentLinks by viewModel.recentLinks.collectAsStateWithLifecycle() - val context = LocalContext.current val navController = rememberNavController() -// // === 감정/상황 키 → 서버 ID 매핑 === -// fun emotionKeyToId(key: String): Long = when (key) { -// "joy" -> 1L -// "calm" -> 2L -// "excitement" -> 3L -// "sadness" -> 4L -// "irritation" -> 5L -// "anger" -> 6L -// else -> 0L -// } -// fun taskKeyToSituationId(key: String): Long = when (key) { -// "트렌드 확인" -> 11L -// "과제 중" -> 12L -// "쇼핑 중" -> 13L -// "데이트 중" -> 14L -// "통학 중" -> 15L -// "알바 중" -> 16L -// "휴식 중" -> 17L -// "자기 전" -> 18L -// else -> 0L -// } - - // 외부 브라우저 열기 - fun openUrl(url: String) { - runCatching { - val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url" - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(fixed)) - context.startActivity(intent) - }.onFailure { - Toast.makeText(context, "링크를 열 수 없어요.", Toast.LENGTH_SHORT).show() - } - } - // LaunchedEffect(Unit) { // viewModel.loadCategoryColors() // } @@ -97,146 +56,16 @@ fun HomeApp( needMoreForRecommendation = viewModel.needMoreForRecommendation, onClearNeedMoreNotice = viewModel::clearNeedMoreNotice, jobId = viewModel.jobId ?: 2L, - onLinkClick = { id -> // ✅ 추가 - navController.navigate("savelinkresult/$id") + onLinkClick = { id -> + onNavigateToLinkDetail(id) }, onNavigateToSaveLink = { url -> - viewModel.setUrl(url) // url 세팅 - navController.navigate("savelink") // 저장 화면 이동 + onNavigateToSaveLink(url) }, onAlarmClick = { navController.navigate("alarm") } ) } - composable("savelink") { - // 이미지 픽커: 선택 → Uri를 임시 File로 복사 → viewModel.setImage(file) - val imagePicker = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - if (uri != null) { - runCatching { uri.toTempFile(context) } - .onSuccess { file -> viewModel.setImage(file) } - .onFailure { - Toast.makeText(context, "이미지 로드에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - } - } - - SaveLinkScreen( - image = viewModel.image, - url = viewModel.url, -// title = viewModel.title, - memo = viewModel.memo, - selectedEmotionId = viewModel.selectedEmotionId, - onPickImage = { imagePicker.launch("image/*") }, - onUrlChange = viewModel::setUrl, - onMemoChange = viewModel::setMemo, - onEmotionSelect = viewModel::selectEmotion, - onSaveClick = { - viewModel.saveLink( - onSucceed = { saved -> -// Log.d("SaveLinkDebug", "저장 성공: $it") - // ✅ 넘긴 값: 저장 성공한 객체와, 네비게이션에 넘길 linkuId - Log.d("SaveLinkFlow", "넘긴 값 -> saved(LinkSimpleInfo) = $saved") - Log.d("SaveLinkFlow", "넘긴 값 -> navigate param linkuId = ${saved.linkuId}") - - viewModel.resetForm() -// navController.navigate("savelinkresult") - // 저장 직후 상세화면으로 id 전달 - navController.navigate("savelinkresult/${saved.linkuId}") - }, - onFailed = { e -> - Log.e("SaveLinkDebug", "저장 실패", e) - Toast.makeText( - context, - e.message ?: "저장에 실패했습니다.", - Toast.LENGTH_SHORT - ).show() - } - ) - }, - onBack = { navController.popBackStack() }, - isCheckingUrl = viewModel.isCheckingUrl, - isDuplicateUrl = viewModel.isDuplicateUrl, - isInvalidLink = viewModel.isInvalidUrl - ) - } - - composable("savelinkresult/{linkuId}") { backStackEntry -> - val raw = backStackEntry.arguments?.getString("linkuId") - val linkuId = backStackEntry.arguments?.getString("linkuId")?.toLongOrNull() - val currentLinkuId = rememberUpdatedState(linkuId) - val aiProgress = viewModel.aiProgress.collectAsState().value - - // ✅ 네비게이션으로 넘어온 값(문자열/파싱 결과) 확인 - Log.d("SaveLinkFlow", "넘어온 값 -> route arg (raw) = $raw") - Log.d("SaveLinkFlow", "넘어온 값 -> route arg (parsed) = $linkuId") - - if (linkuId == null) { - // 잘못 들어온 경우 안전하게 되돌리기 - Log.d("HomeAppLinkResult", "id가 null입니다.") - LaunchedEffect(Unit) { navController.popBackStack() } - } else { - // 화면 진입 시 상세 로드 - LaunchedEffect(linkuId) { - viewModel.loadLinkDetail(linkuId) // 상세 안에 있으면 이걸로 표시, 없으면 내부에서 AI 호출 - viewModel.loadCategoryColors(force = true) // 상세 들어올 때마다 최신 색상 재조회 - } - - val categoryColorMap = viewModel.categoryColorMap.collectAsState().value // 색상 맵 수집 - - // 🔹 상세 데이터 전달 (필요시 로딩 상태 활용) - SaveLinkResultScreen( - link = viewModel.linkDetail, - aiArticle = viewModel.aiArticleDetail, - isLoading = viewModel.isLoadingLinkDetail || viewModel.isLoadingAiArticle, - isAiLoading = viewModel.isLoadingAiArticle, - onBack = { navController.popBackStack() }, - onOpenLink = { url -> - Log.d("HomeApp", "onOpenLink 호출! url=$url") - val fixed = if (url.startsWith("http")) url else "https://$url" - val intent = android.content.Intent( - android.content.Intent.ACTION_VIEW, - Uri.parse(fixed) - ) - try { - context.startActivity(intent) - } catch (t: Throwable) { - Toast.makeText(context, "링크를 열 수 없어요.", android.widget.Toast.LENGTH_SHORT).show() - Log.e("HomeApp", "startActivity 실패", t) - } - }, - categoryColorMap = categoryColorMap, - onSubmitEdit = { title, memo, categoryId, emotionId -> - viewModel.updateLink( - title = title, - memo = memo, - categoryId = categoryId, - emotionId = emotionId, - onSucceed = { - Toast.makeText(context, "수정 완료", Toast.LENGTH_SHORT).show() - }, - onFailed = { e -> - Log.e("SaveLinkFlow", "수정 실패", e) - Toast.makeText(context, e.message ?: "수정에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - ) - }, - onRequestAiSummary = { - val id = currentLinkuId.value - android.util.Log.d("HomeApp", "onRequestAiSummary 호출! linkuId=$id, vm=${viewModel.hashCode()}") - if (id != null) { - viewModel.loadAiArticle(id) - } else { - android.util.Log.e("HomeApp", "linkuId가 null이라 AI 호출 불가") - } - }, - aiProgress = aiProgress, - onCancelAi = { viewModel.cancelAiArticleJob() }, - ) - } - } - composable("alarm") { DisposableEffect(Unit) { onShowNavBar(false) diff --git a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt index 5d688b84..575188b0 100644 --- a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt @@ -152,6 +152,7 @@ class HomeViewModel @Inject constructor( // 새로운 링크 저장 private val imageState = mutableStateOf(null) private val urlState = mutableStateOf("") + private val titleState = mutableStateOf("") private val memoState = mutableStateOf("") private val emotionIdState = mutableStateOf(null) private val isSavingState = mutableStateOf(false) @@ -163,6 +164,7 @@ class HomeViewModel @Inject constructor( val image get() = imageState.value val url get() = urlState.value + val title get() = titleState.value val memo get() = memoState.value val selectedEmotionId get() = emotionIdState.value val isSaving get() = isSavingState.value @@ -211,6 +213,7 @@ class HomeViewModel @Inject constructor( isCheckingUrlState.value = false } } + fun setTitle(newTitle: String) { titleState.value = newTitle } fun setMemo(newMemo: String) { memoState.value = newMemo } fun selectEmotion(id: Long?) { emotionIdState.value = id } @@ -220,6 +223,7 @@ class HomeViewModel @Inject constructor( fun resetForm() { imageState.value = null urlState.value = "" + titleState.value = "" memoState.value = "" emotionIdState.value = null } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index af21d88c..21c68861 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -80,6 +80,7 @@ fun LinkDetailScreen( memo: String, tags: List, aiSummary: String, + categoryOptions: List, onBack: () -> Unit, ) { val clipboard = LocalClipboard.current @@ -117,15 +118,15 @@ fun LinkDetailScreen( if (tag.startsWith("#")) tag else "#$tag" } - // 카테고리 더미데이터 - val categoryOptions = listOf( - LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), - LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), - LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), - LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), - LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), - LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) - ) + LaunchedEffect(linkTitle, category, emotion, situation, memo) { + if (!isEditMode) { + selectedTitle = linkTitle + selectedCategory = category + selectedEmotion = emotion + selectedSituation = situation + selectedMemo = memo + } + } LaunchedEffect(isAiArticleProcessing) { if (isAiArticleProcessing) { @@ -459,9 +460,7 @@ fun LinkDetailScreen( ) } - if (isAiSummaryMode) { - Spacer(modifier = Modifier.height(40.dp)) - } + Spacer(modifier = Modifier.height(40.dp)) } } } @@ -669,6 +668,16 @@ fun LinkDetailScreen( @Preview(showBackground = true) @Composable fun PreviewLinkDetailScreen() { + // 카테고리 더미데이터 + val categoryOptions = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ) + ThemeProvider { LinkDetailScreen( linkTitle = "3일만에 오픽 AL 꿀팁", @@ -679,6 +688,7 @@ fun PreviewLinkDetailScreen() { memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", tags = listOf("오픽", "AL", "영어회화", "자격증"), aiSummary = "오픽 시험에서는 인터뷰어 Ava와의 대화를 친구처럼 자연스럽게 임하며, 목표 점수에 맞춰 답변량과 유창성을 조절하고, MBC 구조와 콤보 유형 연습을 통해 고득점을 노리는 전략적 접근이 중요하다.", + categoryOptions = categoryOptions, onBack = { }, ) } diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 967eb32e..1035f0e0 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -1,11 +1,9 @@ package com.linku.home.screen -import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke 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 import androidx.compose.foundation.layout.Column @@ -16,7 +14,6 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -27,9 +24,6 @@ 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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -52,11 +46,12 @@ import java.io.File fun SaveLinkScreen( image: File?, url: String, - title: String? = "", + title: String = "", memo: String, selectedEmotionId: Long?, onPickImage: () -> Unit, onUrlChange: (String) -> Unit, + onTitleChange: (String) -> Unit, onMemoChange: (String) -> Unit, onEmotionSelect: (Long?) -> Unit, onSaveClick: () -> Unit, @@ -173,7 +168,7 @@ fun SaveLinkScreen( .fillMaxWidth() .padding(top = 14.dp, start = 20.dp, end = 20.dp) .then( - if (url == "") { + if (title.isEmpty()) { Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) } else { Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) @@ -182,7 +177,7 @@ fun SaveLinkScreen( .padding(horizontal = 22.dp, vertical = 15.dp), contentAlignment = Alignment.CenterStart ) { - if (url.isEmpty()) { + if (title.isEmpty()) { Text( text = "링크 제목을 입력해주세요.", fontSize = 14.sp, @@ -192,10 +187,15 @@ fun SaveLinkScreen( } BasicTextField( - value = url, // TODO: 추후 API 파라미터에 링크 제목 추가되면 바꾸기 - onValueChange = onUrlChange, + value = title, + onValueChange = onTitleChange, singleLine = true, - textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + textStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + fontFamily = LocalFontTheme.current.font + ), modifier = Modifier.fillMaxWidth() ) } @@ -239,8 +239,7 @@ fun SaveLinkScreen( Column( modifier = Modifier .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, top = 19.dp) - .noRippleClickable { onPickImage() }, + .padding(start = 20.dp, end = 20.dp, top = 19.dp), horizontalAlignment = Alignment.Start ) { if (image != null) { @@ -260,6 +259,7 @@ fun SaveLinkScreen( .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.gray[100]) .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .noRippleClickable { onPickImage() } .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -430,12 +430,13 @@ fun PreviewSaveLinkScreen() { title = "", memo = "", selectedEmotionId = null, - onPickImage = {}, - onUrlChange = {}, - onMemoChange = {}, - onEmotionSelect = {}, - onSaveClick = {}, - onBack = {}, + onPickImage = { }, + onUrlChange = { }, + onTitleChange = { }, + onMemoChange = { }, + onEmotionSelect = { }, + onSaveClick = { }, + onBack = { }, isCheckingUrl = false, isDuplicateUrl = null, isInvalidLink = false From c57bb3d097e0e3c4abad896f94e34a95314dfe09 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:01:46 +0900 Subject: [PATCH 24/89] =?UTF-8?q?:sparkles:=20LinkCardItem=EC=97=90=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/LinkCardItem.kt | 53 +++++++++++++++---- design/src/main/res/drawable/ic_out_link.xml | 37 +++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 design/src/main/res/drawable/ic_out_link.xml diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index b925c7d5..1d613532 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -41,6 +41,7 @@ fun LinkCardItem( linkTitle: String, tags: List, domainName: String? = null, + isExternalLink: Boolean, @DrawableRes linkImage: Int? = null, @DrawableRes domainImage: Int? = null, onClickDelete: () -> Unit @@ -75,17 +76,32 @@ fun LinkCardItem( .weight(1f), horizontalAlignment = Alignment.Start ) { - Text( - text = linkTitle, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.linkuColors.black, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + Row( modifier = Modifier .fillMaxWidth() - .padding(top = 13.dp) - ) + .padding(top = 13.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (isExternalLink) { + Image( + painter = painterResource(R.drawable.ic_out_link), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + } + + Text( + text = linkTitle, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.linkuColors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + } Spacer(modifier = Modifier.height(5.dp)) @@ -184,6 +200,7 @@ fun PreviewLinkCardItem_HasAiSummary() { hasAiSummary = true, linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), + isExternalLink = false, linkImage = R.drawable.img_genz_trend, domainImage = R.drawable.ic_domain_blog_naver_logo, domainName = "BLOG", @@ -200,6 +217,24 @@ fun PreviewLinkCardItem_NoAiSummary() { hasAiSummary = false, linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), + isExternalLink = false, + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_HasOutLink() { + ThemeProvider { + LinkCardItem( + hasAiSummary = true, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + isExternalLink = true, + linkImage = R.drawable.img_genz_trend, domainImage = R.drawable.ic_domain_blog_naver_logo, domainName = "BLOG", onClickDelete = { } diff --git a/design/src/main/res/drawable/ic_out_link.xml b/design/src/main/res/drawable/ic_out_link.xml new file mode 100644 index 00000000..fd39d973 --- /dev/null +++ b/design/src/main/res/drawable/ic_out_link.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + From e9faa7c3b5a4873117648da00e72608be1ee6a4d Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:13:31 +0900 Subject: [PATCH 25/89] =?UTF-8?q?:sparkles:=20core=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EC=9E=88=EB=8D=98=20design=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 42 ++++--------------- .../component/LinkDetailEmotionDropdown.kt | 3 +- .../com/linku/home/util/EmotionTypeExt.kt | 15 +++++++ 3 files changed, 24 insertions(+), 36 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index 5bc21075..ab1970ab 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,43 +1,15 @@ package com.linku.core.model -import androidx.annotation.DrawableRes -import com.linku.design.R - enum class EmotionType( val id: Long, - val tagName: String, - @DrawableRes val imgRes: Int + val tagName: String ) { - JOY( - id = 1L, - tagName = "즐거움", - imgRes = R.drawable.ic_joy - ), - CALM( - id = 2L, - tagName = "평온", - imgRes = R.drawable.ic_calm - ), - EXCITE( - id = 3L, - tagName = "설렘", - imgRes = R.drawable.ic_excite - ), - SAD( - id = 4L, - tagName = "슬픔", - imgRes = R.drawable.ic_sad - ), - IRRITATION( - id = 5L, - tagName = "짜증", - imgRes = R.drawable.ic_irritation - ), - ANGER( - id = 6L, - tagName = "분노", - imgRes = R.drawable.ic_anger - ); + JOY(id = 1L, tagName = "즐거움"), + CALM(id = 2L, tagName = "평온"), + EXCITE(id = 3L, tagName = "설렘"), + SAD(id = 4L, tagName = "슬픔"), + IRRITATION(id = 5L, tagName = "짜증"), + ANGER(id = 6L, tagName = "분노"); companion object { fun fromId(id: Long?): EmotionType? { diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 7f1050a4..0b8a4381 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -24,6 +24,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.util.imgRes @Composable fun LinkDetailEmotionDropdown( @@ -51,7 +52,7 @@ fun LinkDetailEmotionDropdown( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Image( - painter = painterResource(emotion.iconRes()), + painter = painterResource(emotion.imgRes), contentDescription = null, modifier = Modifier.size(29.dp) ) diff --git a/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt new file mode 100644 index 00000000..ff31c0bb --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt @@ -0,0 +1,15 @@ +package com.linku.home.util + +import androidx.annotation.DrawableRes +import com.linku.core.model.EmotionType +import com.linku.home.R + +val EmotionType.imgRes: Int + @DrawableRes get() = when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } \ No newline at end of file From 6f5513fd4d375e6a9f3ee347a79129181eab0b7c Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:21:22 +0900 Subject: [PATCH 26/89] =?UTF-8?q?:sparkles:=20=ED=95=98=EB=82=98=EC=9D=98?= =?UTF-8?q?=20CATEGORY=5FMAP=EC=9D=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20id/name=EC=9D=84=20=EC=96=91=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=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 --- app/src/main/java/com/linku/MainApp.kt | 61 +++++++------------ .../src/main/java/com/linku/home/HomeApp.kt | 17 ------ 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index d6a035d2..17befdab 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -519,48 +519,33 @@ fun MainApp( } // TODO: 카테고리 API 연동 후 categoryId 기준 실제 카테고리명/색상 매핑으로 교체 + val CATEGORY_MAP = linkedMapOf( + 1L to "어학", + 2L to "뉴스", + 3L to "공부법", + 4L to "IT·개발", + 5L to "자기계발", + 6L to "취업·이직", + 7L to "비즈니스 인사이트", + 8L to "생산성·툴", + 9L to "라이프스타일", + 10L to "심리·자기이해", + 11L to "에세이·칼럼", + 12L to "트렌드", + 13L to "디자인·예술", + 14L to "영상·뮤직", + 15L to "맛집·여행", + 16L to "기타" + ) + fun categoryNameOf(id: Long?): String { - return when (id) { - 1L -> "어학" - 2L -> "뉴스" - 3L -> "공부법" - 4L -> "IT·개발" - 5L -> "자기계발" - 6L -> "취업·이직" - 7L -> "비즈니스 인사이트" - 8L -> "생산성·툴" - 9L -> "라이프스타일" - 10L -> "심리·자기이해" - 11L -> "에세이·칼럼" - 12L -> "트렌드" - 13L -> "디자인·예술" - 14L -> "영상·뮤직" - 15L -> "맛집·여행" - 16L -> "기타" - else -> "카테고리" - } + return CATEGORY_MAP[id] ?: "카테고리" } fun categoryIdOf(name: String): Long? { - return when (name) { - "어학" -> 1L - "뉴스" -> 2L - "공부법" -> 3L - "IT·개발" -> 4L - "자기계발" -> 5L - "취업·이직" -> 6L - "비즈니스 인사이트" -> 7L - "생산성·툴" -> 8L - "라이프스타일" -> 9L - "심리·자기이해" -> 10L - "에세이·칼럼" -> 11L - "트렌드" -> 12L - "디자인·예술" -> 13L - "영상·뮤직" -> 14L - "맛집·여행" -> 15L - "기타" -> 16L - else -> null - } + return CATEGORY_MAP.entries + .firstOrNull { it.value == name } + ?.key } fun keywordToTags(keyword: String?): List { diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index 62a388c2..cdcb8d29 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -1,20 +1,15 @@ package com.linku.home -import android.net.Uri import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.linku.home.screen.AlarmScreen import com.linku.home.screen.HomeScreen -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream @Composable fun HomeApp( @@ -84,16 +79,4 @@ fun HomeApp( ) } } -} - -/** Uri를 앱 캐시 폴더의 임시 File로 복사 */ -private fun Uri.toTempFile(context: android.content.Context): File { - val fileName = "picked_${System.currentTimeMillis()}.jpg" - val tempFile = File(context.cacheDir, fileName) - context.contentResolver.openInputStream(this).use { input: InputStream? -> - FileOutputStream(tempFile).use { output -> - if (input != null) input.copyTo(output) - } - } - return tempFile } \ No newline at end of file From 7f70deae6de2be3652427a6f54479b25d1191c07 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:53:54 +0900 Subject: [PATCH 27/89] =?UTF-8?q?:sparkles:=20SituationId=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20Situation=20Map=EC=9C=BC=EB=A1=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B3=80=EA=B2=BD=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/Situation.kt | 78 ++++++++++--------- .../component/LinkDetailSituationDropdown.kt | 45 ++++++----- .../com/linku/home/screen/LinkDetailScreen.kt | 31 ++++---- 3 files changed, 83 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt index a0fa486f..b305d49e 100644 --- a/core/src/main/java/com/linku/core/model/Situation.kt +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -6,17 +6,10 @@ data class Situation( ) object SituationOptions { - val linkDetailSituations: List = listOf( - Situation(1L, "통학 중"), - Situation(2L, "공부 중"), - Situation(3L, "휴식 중"), - Situation(4L, "이동 중"), - Situation(5L, "식사 중"), - Situation(6L, "자기 전") - ) + private const val DEFAULT_JOB_ID = 3L - fun situationsFor(jobId: Long): List = when (jobId) { - 1L -> listOf( + private val situationsByJobId: Map> = mapOf( + 1L to listOf( Situation(1L, "통학 중"), Situation(2L, "공부 중"), Situation(3L, "식사 중"), @@ -25,9 +18,8 @@ object SituationOptions { Situation(6L, "쇼핑 중"), Situation(7L, "휴식 중"), Situation(8L, "자기 전") - ) - - 2L -> listOf( + ), + 2L to listOf( Situation(9L, "과제 중"), Situation(10L, "통학 중"), Situation(11L, "쇼핑 중"), @@ -36,9 +28,8 @@ object SituationOptions { Situation(14L, "데이트 중"), Situation(15L, "휴식 중"), Situation(16L, "자기 전") - ) - - 3L -> listOf( + ), + 3L to listOf( Situation(17L, "출퇴근"), Situation(18L, "트렌드 확인"), Situation(19L, "업무 중"), @@ -47,9 +38,8 @@ object SituationOptions { Situation(22L, "데이트 중"), Situation(23L, "휴식 중"), Situation(24L, "자기 전") - ) - - 4L -> listOf( + ), + 4L to listOf( Situation(25L, "출퇴근"), Situation(26L, "업무 준비 중"), Situation(27L, "데이트 중"), @@ -58,9 +48,8 @@ object SituationOptions { Situation(30L, "트렌드 확인"), Situation(31L, "휴식 중"), Situation(32L, "자기 전") - ) - - 5L -> listOf( + ), + 5L to listOf( Situation(33L, "작업 중"), Situation(34L, "쇼핑 중"), Situation(35L, "트렌드 확인"), @@ -69,9 +58,8 @@ object SituationOptions { Situation(38L, "식사"), Situation(39L, "휴식 중"), Situation(40L, "자기 전") - ) - - 6L -> listOf( + ), + 6L to listOf( Situation(41L, "자소서 작성"), Situation(42L, "면접 준비"), Situation(43L, "요리 중"), @@ -81,26 +69,42 @@ object SituationOptions { Situation(47L, "휴식 중"), Situation(48L, "자기 전") ) + ) - else -> situationsFor(3L) + val allSituations: List by lazy { + situationsByJobId.values.flatten() + } + + private val allSituationsById: Map by lazy { + allSituations.associateBy { it.id } + } + + private val situationsByJobAndId: Map> by lazy { + situationsByJobId.mapValues { (_, situations) -> + situations.associateBy { it.id } + } + } + + fun situationsFor(jobId: Long): List { + return situationsByJobId[jobId] ?: situationsByJobId.getValue(DEFAULT_JOB_ID) } fun nameOf(id: Long?): String? { if (id == null) return null - return (linkDetailSituations + (1L..6L).flatMap { situationsFor(it) }) - .distinctBy { it.id } - .firstOrNull { it.id == id } - ?.tagName + return allSituationsById[id]?.tagName } - fun idOf(tagName: String, jobId: Long? = null): Long? { - val options = if (jobId != null) { - situationsFor(jobId) - } else { - linkDetailSituations - } + fun nameOf(id: Long?, jobId: Long): String? { + if (id == null) return null + + return situationsByJobAndId[jobId]?.get(id)?.tagName + ?: situationsByJobAndId[DEFAULT_JOB_ID]?.get(id)?.tagName + } - return options.firstOrNull { it.tagName == tagName }?.id + fun idOf(tagName: String, jobId: Long): Long? { + return situationsFor(jobId) + .firstOrNull { it.tagName == tagName } + ?.id } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt index e3a4990f..6601642d 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -14,15 +14,16 @@ 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 com.linku.core.model.Situation import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider @Composable -fun LinkDetailOptionDropdown( - options: List, - selectedOption: String, - onOptionClick: (String) -> Unit, +fun LinkDetailSituationDropdown( + situations: List, + selectedSituation: Situation?, + onSituationClick: (Situation) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -32,23 +33,23 @@ fun LinkDetailOptionDropdown( .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) .heightIn(max = 264.dp) ) { - options.forEach { option -> + situations.forEach { situation -> Text( - text = option, + text = situation.tagName, fontSize = 15.sp, - fontWeight = if (option == selectedOption) { + fontWeight = if (situation.id == selectedSituation?.id) { FontWeight.Medium } else { FontWeight.Normal }, - color = if (option == selectedOption) { + color = if (situation.id == selectedSituation?.id) { LocalColorTheme.current.blue[200] } else { LocalColorTheme.current.gray[800] }, modifier = Modifier .noRippleClickable { - onOptionClick(option) + onSituationClick(situation) } .padding(horizontal = 4.dp, vertical = 9.dp) ) @@ -58,19 +59,21 @@ fun LinkDetailOptionDropdown( @Preview(showBackground = false) @Composable -fun PreviewLinkDetailOptionDropdown() { +fun PreviewLinkDetailSituationDropdown() { ThemeProvider { - LinkDetailOptionDropdown( - options = listOf( - "트렌드 확인", - "통학 중", - "과제 중", - "쇼핑 중", - "데이트 중", - "알바 전" - ), - selectedOption = "통학 중", - onOptionClick = { } + val situations = listOf( + Situation(18L, "트렌드 확인"), + Situation(10L, "통학 중"), + Situation(9L, "과제 중"), + Situation(11L, "쇼핑 중"), + Situation(14L, "데이트 중"), + Situation(12L, "알바 전") + ) + + LinkDetailSituationDropdown( + situations = situations, + selectedSituation = situations.firstOrNull { it.tagName == "통학 중" }, + onSituationClick = { } ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 21c68861..84a224b0 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -59,7 +59,7 @@ import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown -import com.linku.home.component.LinkDetailOptionDropdown +import com.linku.home.component.LinkDetailSituationDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -75,7 +75,7 @@ fun LinkDetailScreen( linkTitle: String, category: String, emotion: String, - situation: String, + situationId: Long?, linkUrl: String, memo: String, tags: List, @@ -97,19 +97,24 @@ fun LinkDetailScreen( var isAiArticleProcessing by remember { mutableStateOf(false) } var aiArticleProgress by remember { mutableFloatStateOf(0f) } + val emotionOptions = EmotionType.entries.toList() + val situationOptions = SituationOptions.allSituations + var selectedTitle by remember { mutableStateOf(linkTitle) } var selectedCategory by remember { mutableStateOf(category) } var selectedEmotion by remember { mutableStateOf(emotion) } - var selectedSituation by remember { mutableStateOf(situation) } + var selectedSituation by remember(situationId) { + mutableStateOf( + situationOptions.firstOrNull { it.id == situationId } + ) + } var selectedMemo by remember { mutableStateOf(memo) } var openedDropdownType by remember { mutableStateOf(null) } - val emotionOptions = EmotionType.entries.toList() - val situationOptions = SituationOptions.linkDetailSituations val visibleTags = tags .filter { it.isNotBlank() } @@ -118,12 +123,12 @@ fun LinkDetailScreen( if (tag.startsWith("#")) tag else "#$tag" } - LaunchedEffect(linkTitle, category, emotion, situation, memo) { + LaunchedEffect(linkTitle, category, emotion, situationId, memo) { if (!isEditMode) { selectedTitle = linkTitle selectedCategory = category selectedEmotion = emotion - selectedSituation = situation + selectedSituation = situationOptions.firstOrNull { it.id == situationId } selectedMemo = memo } } @@ -158,7 +163,7 @@ fun LinkDetailScreen( linkTitle = selectedTitle, category = selectedCategory, emotion = selectedEmotion, - situation = selectedSituation, + situation = selectedSituation?.tagName ?: "상황", isEditMode = isEditMode, isCategoryDropdownOpen = openedDropdownType == LinkDetailDropdownType.CATEGORY, isEmotionDropdownOpen = openedDropdownType == LinkDetailDropdownType.EMOTION, @@ -593,10 +598,10 @@ fun LinkDetailScreen( } LinkDetailDropdownType.SITUATION -> { - LinkDetailOptionDropdown( - options = situationOptions.map { it.tagName }, - selectedOption = selectedSituation, - onOptionClick = { + LinkDetailSituationDropdown( + situations = situationOptions, + selectedSituation = selectedSituation, + onSituationClick = { selectedSituation = it openedDropdownType = null }, @@ -683,7 +688,7 @@ fun PreviewLinkDetailScreen() { linkTitle = "3일만에 오픽 AL 꿀팁", category = "카테고리2", emotion = "평온", - situation = "통학 중", + situationId = 10L, linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", tags = listOf("오픽", "AL", "영어회화", "자격증"), From 71f5c0db185c048dfdabbdc9b6cbd6a64fe978c7 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:56:04 +0900 Subject: [PATCH 28/89] =?UTF-8?q?:sparkles:=20SituationId=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 17befdab..b7fe2556 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -606,7 +606,7 @@ fun MainApp( linkTitle = linkDetail?.title.orEmpty(), category = categoryNameOf(linkDetail?.categoryId), emotion = emotionNameOf(linkDetail?.emotionId), - situation = "통학 중", // TODO: 상세 API에 situationId 생기면 실제 값으로 변경 + situationId = null, // TODO: 상세 API에 situationId 생기면 linkDetail?.situationId로 변경 linkUrl = linkDetail?.linku.orEmpty(), memo = linkDetail?.memo.orEmpty(), tags = keywordToTags(displayKeyword), From f2fe87f2bab7dae6343f2b52808baa3e0eca82bc Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 20:51:53 +0900 Subject: [PATCH 29/89] =?UTF-8?q?:sparkles:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=AA=85=20=EC=A7=81=EA=B4=80=EC=A0=81=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/design/component/DeleteLinkItemModal.kt | 8 +++++--- .../main/java/com/linku/design/component/LinkCardItem.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 37ed8537..6c9ad204 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -20,7 +20,7 @@ import com.linku.design.theme.ThemeProvider @Composable fun DeleteLinkItemModal( - onClickModal: () -> Unit = { } + onDeleteClick: () -> Unit = { } ) { Column( modifier = Modifier @@ -33,7 +33,7 @@ fun DeleteLinkItemModal( .clip(RoundedCornerShape(14.dp)) .background(LocalColorTheme.current.white) .padding(horizontal = 15.dp, vertical = 10.dp) - .noRippleClickable { onClickModal() } + .noRippleClickable { onDeleteClick() } ) { Text( text = "삭제하기", @@ -49,6 +49,8 @@ fun DeleteLinkItemModal( @Composable fun PreviewDeleteLinkItemModal() { ThemeProvider { - DeleteLinkItemModal(onClickModal = { }) + DeleteLinkItemModal( + onDeleteClick = { } + ) } } \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index 1d613532..2ce89b92 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -182,7 +182,7 @@ fun LinkCardItem( .padding(top = 36.dp, end = 12.dp) ) { DeleteLinkItemModal( - onClickModal = { + onDeleteClick = { isMenuVisible = false onClickDelete() } From ceaa44360c92a8a2095bfa00cb32531ff946ef8c Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 20:55:15 +0900 Subject: [PATCH 30/89] =?UTF-8?q?:sparkles:=20graphicsLayer=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20shadow=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/DeleteLinkItemModal.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 6c9ad204..0aa98a23 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -22,15 +23,17 @@ import com.linku.design.theme.ThemeProvider fun DeleteLinkItemModal( onDeleteClick: () -> Unit = { } ) { + val shape = RoundedCornerShape(14.dp) + Column( modifier = Modifier .width(120.dp) - .graphicsLayer { - shadowElevation = 10.dp.toPx() - this.shape = shape - clip = true - } - .clip(RoundedCornerShape(14.dp)) + .shadow( + elevation = 10.dp, + shape = shape, + clip = false + ) + .clip(shape) .background(LocalColorTheme.current.white) .padding(horizontal = 15.dp, vertical = 10.dp) .noRippleClickable { onDeleteClick() } From 92cf18d3ad63c0ee67ca0e989a77ea530311aae2 Mon Sep 17 00:00:00 2001 From: Bidoof Date: Sun, 21 Jun 2026 21:10:32 +0900 Subject: [PATCH 31/89] Merge pull request #132 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor(data): 알람 데이터 레이어 구조 개선 * ♻️ refactor(repository): AlarmRepository의 반환 타입을 Flow로 변경 * ♻️ refactor(data): AlarmPagingSource 생성자 및 가시성 수정 * ♻️ refactor(ui): 알람 목록 UI 최적화 및 코드 가독성 개선 * ✨ feat(alarm): 알람 목록 조회 API 연동 및 알람 관련 기능 구조 개선 * ♻️ refactor(data): 알림 설정 로직 리팩터링 및 서버 동기화 구현 * ♻️ refactor(alarm): 알림 기능 관련 코드 정리 및 UI 상태 구조 개선 * ✨ feat(fcm): FCM 토큰 등록 설정 * ✨ feat(notification): FCM 토큰 서버 등록 로직 구현 및 권한 처리 수정 * ✨ feat(notification): 알림 설정 로드 및 토글 연동 로직 구현 * chore(notification): 알람 설정 API 응답 변경 대비 TODO 주석 추가 * ♻️ refactor(notification): 알림 설정 로직 리팩터링 및 모델 통합 * ♻️ refactor(notification): FCM 토큰 관리 프로세스 개선 및 로컬 저장 로직 구현 * ✨ feat(ui): 알림 설정 화면 시스템 알람 상태 연동 및 UI 변경사항 반영 * chore(resource): ic_info_blue 아이콘 추가 * ✨ feat(mypage): 알림 설정 동기화 로직 구현 및 스켈레톤 UI 적용 * ✨ feat(data): 상대 시간 표시 기능 구현 및 알람 매퍼에 적용 * ✨ feat(mypage): 알람 설정 화면 에러 처리 및 토스트 메시지 구현 * ✨ feat(notification): FCM 알림 수신 처리 구현 및 알림 설정 로직 고도화 * ✨ feat(ui): 알람 목록 UI 컴포넌트 분리 및 페이징 처리 구현 * ♻️ refactor(ui): 알람 리스트 관련 컴포넌트 패키지 구조 개선 * :memo: docs(fcm): FCM 메시지 서비스 내 로직 설명 주석 추가 * 🐛 fix(notification): 알림 설정 동기화 및 동시성 이슈 해결 * ♻️ refactor(mypage): 알림 설정 화면 MVI 아키텍처 적용 * ♻️ refactor(mypage): 알림 설정 Side Effect 처리 방식을 Channel로 변경 * chore: 주석 추가 * ♻️ refactor(ui): 알람 화면 UI 구조 통합 및 컴포넌트 패키지 정리 * ♻️ refactor: 알림 설정 로직 및 페이징 처리 구조 개선 * ♻️ refactor(mypage): 알림 설정 실패 시 상태 복구 로직 수정 * ✨ feat(ui): 알림 화면 스와이프 새로고침 기능 추가 및 UI 구조 개선 * ♻️ refactor(alarm): 알람 데이터 로드 및 설정 확인 로직 개선 * ♻️ refactor(fcm): ApplicationScope 도입 및 FCM 토큰 등록 로직 개선 * fix: 알람 단계 DEFAULT로 수정 * ♻️ refactor(di): AlarmRepositoryModule 코드 정리 * ♻️ refactor(data): 상대 시간 변환 로직 개선 및 단위 테스트 추가 * ♻️ refactor(data): 상대 시간 파싱 로직 개선 및 UI 코드 정리 * ♻️ refactor(ui): 알람 컴포넌트 네이밍 통일 및 레이아웃 구조 개선 * merge branch develop into feature/#131-alarmApi * ♻️ refactor(ui): 알림 설정 Intent 구조 개선 및 리팩터링 * ♻️ refactor(mypage): NotificationIntent 구조 개선 및 ViewModel 업데이트 로직 최적화 * ♻️ refactor(viewmodel): NotificationViewModel 내 알림 상태 갱신 로직 분리 * ♻️ refactor(ui): AlarmSettingScreen 내 Intent 호출 코드 간소화 * refactor(home): AlarmScreen 내 탭별 스크롤 상태 저장 로직 제거 -> 이것또한 페이징3이 다 해주기 때문 * ♻️ refactor(ui): NotificationViewModel 주입 위치 변경 및 로깅 로직 수정 * ♻️ refactor(mypage): 알림 Intent에 상태 변경 로직 위임 및 ViewModel 리팩터링 --- .../java/com/linku/ExampleInstrumentedTest.kt | 22 --- .../com/linku/link/ExampleInstrumentedTest.kt | 17 +- app/src/main/AndroidManifest.xml | 4 + .../com/linku/LinkUFireBaseMessageService.kt | 82 +++++++- app/src/main/java/com/linku/MainApp.kt | 5 +- .../main/java/com/linku/MainApplication.kt | 17 ++ app/src/main/java/com/linku/MainViewModel.kt | 35 +++- .../java/com/linku/link/ExampleUnitTest.kt | 4 - core/build.gradle.kts | 5 + .../java/com/linku/core/di/SystemModule.kt | 15 ++ .../java/com/linku/core/error/ApiError.kt | 4 +- .../java/com/linku/core/model/alarm/Alarm.kt | 10 +- .../com/linku/core/model/alarm/AlarmList.kt | 7 - .../linku/core/model/alarm/AlarmSetting.kt | 22 +++ .../linku/core/repository/AlarmRepository.kt | 20 +- .../core/repository/NotificationPreference.kt | 38 ---- .../core/system/NotificationController.kt | 94 --------- .../java/com/linku/core/ExampleUnitTest.kt | 5 +- .../java/com/linku/data/api/alarm/AlarmApi.kt | 37 ++++ .../api/dto/server/alarm/AlarmSettingDTO.kt | 18 ++ .../dto/server/alarm/AlarmSettingRequest.kt | 11 ++ .../data/api/dto/server/alarm/AlarmsDTO.kt | 21 +- .../api/dto/server/alarm/FcmTokenRequest.kt | 10 + .../java/com/linku/data/api/mapToApiError.kt | 4 +- .../com/linku/data/di/api/AlarmApiModule.kt | 23 +++ .../NotificationPreferenceModule.kt | 5 +- .../di/repository/AlarmRepositoryModule.kt | 6 +- .../preference/NotificationPreferenceImpl.kt | 122 ------------ .../repository/AlarmPagingSource.kt | 50 +++-- .../repository/AlarmRepositoryImpl.kt | 85 ++++++++ .../repository/FakeAlarmRepositoryImpl.kt | 64 ------ .../java/com/linku/data/mapper/AlarmMapper.kt | 39 ++++ .../java/com/linku/data/mapper/StringExt.kt | 48 +++++ .../data/preference/NotificationPreference.kt | 39 ++++ .../java/com/linku/data/ExampleUnitTest.kt | 4 - .../com/linku/data/mapper/StringExtTest.kt | 55 ++++++ .../com/linku/design/util/OnResumeEffect.kt | 48 +++++ .../java/com/linku/home/screen/AlarmScreen.kt | 179 +++++++++++------ .../alarm/component/AlarmAppendStateFooter.kt | 98 ++++++++++ .../ui/alarm/component/AlarmErrorContent.kt | 77 ++++++++ .../home/ui/alarm/component/AlarmItem.kt | 32 +-- .../ui/alarm/component/AlarmLoadingContent.kt | 50 +++++ .../ui/alarm/component/AlarmNothingTab.kt | 4 +- .../linku/home/viewmodel/AlarmViewModel.kt | 45 +++-- .../main/java/com/linku/mypage/MyPageApp.kt | 7 +- .../com/linku/mypage/NotificationViewModel.kt | 185 ++++++++++++++---- .../component/notification/SystemAlarmTab.kt | 99 ++++++++++ .../linku/mypage/intent/NotificationIntent.kt | 87 ++++++++ .../linku/mypage/screen/AlarmSettingScreen.kt | 134 ++++++++++--- .../src/main/res/drawable/ic_info_blue.xml | 29 +++ .../src/main/res/drawable/ic_info_red.xml | 29 +++ .../src/main/res/drawable/ic_long_right.xml | 28 +++ gradle/libs.versions.toml | 10 +- 53 files changed, 1583 insertions(+), 605 deletions(-) delete mode 100644 app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt delete mode 100644 core/src/main/java/com/linku/core/model/alarm/AlarmList.kt create mode 100644 core/src/main/java/com/linku/core/model/alarm/AlarmSetting.kt delete mode 100644 core/src/main/java/com/linku/core/repository/NotificationPreference.kt delete mode 100644 core/src/main/java/com/linku/core/system/NotificationController.kt create mode 100644 data/src/main/java/com/linku/data/api/alarm/AlarmApi.kt create mode 100644 data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingDTO.kt create mode 100644 data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingRequest.kt create mode 100644 data/src/main/java/com/linku/data/api/dto/server/alarm/FcmTokenRequest.kt create mode 100644 data/src/main/java/com/linku/data/di/api/AlarmApiModule.kt delete mode 100644 data/src/main/java/com/linku/data/implementation/preference/NotificationPreferenceImpl.kt create mode 100644 data/src/main/java/com/linku/data/implementation/repository/AlarmRepositoryImpl.kt delete mode 100644 data/src/main/java/com/linku/data/implementation/repository/FakeAlarmRepositoryImpl.kt create mode 100644 data/src/main/java/com/linku/data/mapper/AlarmMapper.kt create mode 100644 data/src/main/java/com/linku/data/mapper/StringExt.kt create mode 100644 data/src/main/java/com/linku/data/preference/NotificationPreference.kt create mode 100644 data/src/test/java/com/linku/data/mapper/StringExtTest.kt create mode 100644 design/src/main/java/com/linku/design/util/OnResumeEffect.kt create mode 100644 feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmAppendStateFooter.kt create mode 100644 feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmErrorContent.kt create mode 100644 feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmLoadingContent.kt create mode 100644 feature/mypage/src/main/java/com/linku/mypage/component/notification/SystemAlarmTab.kt create mode 100644 feature/mypage/src/main/java/com/linku/mypage/intent/NotificationIntent.kt create mode 100644 feature/mypage/src/main/res/drawable/ic_info_blue.xml create mode 100644 feature/mypage/src/main/res/drawable/ic_info_red.xml create mode 100644 feature/mypage/src/main/res/drawable/ic_long_right.xml diff --git a/app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt deleted file mode 100644 index 958eb4c3..00000000 --- a/app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.linku - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Assert.assertEquals("com.linku", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt index c63f1879..4cf0c101 100644 --- a/app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt @@ -1,32 +1,17 @@ -<<<<<<<< HEAD:app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt package com.linku.link -======== -package com.linku ->>>>>>>> fd1304faab6b86e04c17e31a0786ce151290d292:app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - import org.junit.Test import org.junit.runner.RunWith - import org.junit.Assert.* -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { + @Test fun useAppContext() { - // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext -<<<<<<<< HEAD:app/src/androidTest/java/com/linku/link/ExampleInstrumentedTest.kt - assertEquals("com.linku.link", appContext.packageName) -======== assertEquals("com.linku", appContext.packageName) ->>>>>>>> fd1304faab6b86e04c17e31a0786ce151290d292:app/src/androidTest/java/com/linku/ExampleInstrumentedTest.kt } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 989a7de0..c294b96d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,10 @@ + + // 시스템 권한 요청 결과를 로컬에 저장 + ) { isGranted -> viewModel.setNotificationEnabled(isGranted) Log.d("MainApp", "알림 권한 요청 결과: $isGranted") - Log.d("MainApp", "시스템 권한 상태: ${ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED}") } @@ -173,6 +172,8 @@ fun MainApp( // Android 13 이상에서는 POST_NOTIFICATIONS 런타임 권한이 필요하므로 조건부 요청 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + viewModel.setNotificationEnabled(true) } requestNotificationPermission = false // 한 번만 요청하도록 처리 } diff --git a/app/src/main/java/com/linku/MainApplication.kt b/app/src/main/java/com/linku/MainApplication.kt index 366b17a5..265505e8 100644 --- a/app/src/main/java/com/linku/MainApplication.kt +++ b/app/src/main/java/com/linku/MainApplication.kt @@ -1,6 +1,9 @@ package com.linku import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.os.Build import android.util.Log import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp @@ -11,6 +14,20 @@ class MainApplication: Application() { super.onCreate() Log.d("DEBUG", "✅ MainApplication 실행됨") KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) + createNotificationChannels() + + } + + // 알림 채널 생성. minSdk가 26이므로 분기처리 생략 + private fun createNotificationChannels() { + val channel = NotificationChannel( + "default_channel", + "기본 알림", + NotificationManager.IMPORTANCE_HIGH + ) + + getSystemService(NotificationManager::class.java) + .createNotificationChannel(channel) } } \ No newline at end of file diff --git a/app/src/main/java/com/linku/MainViewModel.kt b/app/src/main/java/com/linku/MainViewModel.kt index 68b7bbdd..90d6de73 100644 --- a/app/src/main/java/com/linku/MainViewModel.kt +++ b/app/src/main/java/com/linku/MainViewModel.kt @@ -8,10 +8,12 @@ import android.net.NetworkRequest import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.linku.core.model.alarm.AlarmType +import com.linku.core.repository.AlarmRepository import com.linku.core.repository.RecentSearchRepository import com.linku.core.repository.UserRepository -import com.linku.core.system.NotificationController import com.linku.data.preference.AuthPreference +import com.linku.data.preference.NotificationPreference import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow @@ -28,7 +30,8 @@ import javax.inject.Inject class MainViewModel @Inject constructor( application: Application, private val recentRepository: RecentSearchRepository, - private val notificationController: NotificationController, + private val notificationPreference: NotificationPreference, + private val alarmRepository: AlarmRepository, private val authPreference: AuthPreference, private val userRepository: UserRepository // 닉네임 호출용 ) : AndroidViewModel(application) { @@ -140,11 +143,29 @@ class MainViewModel @Inject constructor( val token = authPreference.getRefreshToken() return !token.isNullOrBlank() } + // 시스템 알람 허용 여부에 따른 초기 푸시알람설정 초기화 + fun setNotificationEnabled(isGranted: Boolean) { + if (!isGranted) return - // 알림 허용 여부 저장 - // 로그인 성공 후 시스템 권한 요청 결과를 로컬에 반영 - fun setNotificationEnabled(enabled: Boolean) { - notificationController.setNotificationEnabled(enabled) - } + viewModelScope.launch { + val token = notificationPreference.getFcmToken() + + if (token == null) { + Log.d("FCM", "token 없음 → skip") + return@launch + } + // 토큰 등록이 성공했으면 전체 푸시알림 활성화 + val registerResult = alarmRepository.registerFCMToken(token) + + if (registerResult.isSuccess) { + alarmRepository.updateAlarmSetting(AlarmType.ALL) + .onFailure { e -> + Log.e("FCM", "알람 설정 실패: ${e.message}", e) + } + } else { + Log.e("FCM", "register 실패: ${registerResult.exceptionOrNull()?.message}") + } + } + } } \ No newline at end of file diff --git a/app/src/test/java/com/linku/link/ExampleUnitTest.kt b/app/src/test/java/com/linku/link/ExampleUnitTest.kt index cd7200a6..9194691b 100644 --- a/app/src/test/java/com/linku/link/ExampleUnitTest.kt +++ b/app/src/test/java/com/linku/link/ExampleUnitTest.kt @@ -1,8 +1,4 @@ -<<<<<<<< HEAD:app/src/test/java/com/linku/link/ExampleUnitTest.kt package com.linku.link -======== -package com.linku.data ->>>>>>>> 6cfa3247fa9a751d3cefb7daf59fb3f6f6c8368c:data/src/test/java/com/linku/data/ExampleUnitTest.kt import org.junit.Test diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 2ec6a7df..4d229e9a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -90,4 +90,9 @@ dependencies { implementation(libs.androidx.datastore.preferences) + //Paging3 + implementation(libs.paging.runtime) + + // Firebase + implementation(libs.bundles.firebase) } diff --git a/core/src/main/java/com/linku/core/di/SystemModule.kt b/core/src/main/java/com/linku/core/di/SystemModule.kt index c42c968f..d1fb2a9f 100644 --- a/core/src/main/java/com/linku/core/di/SystemModule.kt +++ b/core/src/main/java/com/linku/core/di/SystemModule.kt @@ -7,8 +7,16 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier import javax.inject.Singleton +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ApplicationScope + @Module @InstallIn(SingletonComponent::class) object SystemModule { @@ -20,4 +28,11 @@ object SystemModule { ): PermissionChecker { return PermissionChecker(context) } + + @Provides + @Singleton + @ApplicationScope + fun provideApplicationScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.Main) + } } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/error/ApiError.kt b/core/src/main/java/com/linku/core/error/ApiError.kt index 40ae0b84..0ee069ab 100644 --- a/core/src/main/java/com/linku/core/error/ApiError.kt +++ b/core/src/main/java/com/linku/core/error/ApiError.kt @@ -363,10 +363,10 @@ sealed class ApiError( * 알림 조회, 권한, 전송 관련 에러를 포함한다. */ sealed class Alarm(message: String) : ApiError(message) { - /** ALARM_NOT_FOUND - 알람을 찾을 수 없음 */ + /** ALARM404 - 알람을 찾을 수 없음 */ class NotFound(message: String) : Alarm(message) - /** ALARM_PERMISSION_DENIED - 알람 권한 없음 */ + /** ALARM403 - 알람 권한 없음 */ class PermissionDenied(message: String) : Alarm(message) /** ALARM5001 - 알림 주제 구독 상태 변경 실패 */ diff --git a/core/src/main/java/com/linku/core/model/alarm/Alarm.kt b/core/src/main/java/com/linku/core/model/alarm/Alarm.kt index b38d7839..8c42b814 100644 --- a/core/src/main/java/com/linku/core/model/alarm/Alarm.kt +++ b/core/src/main/java/com/linku/core/model/alarm/Alarm.kt @@ -1,13 +1,17 @@ package com.linku.core.model.alarm -/** - * 시스템 내 알람 정보를 요약하여 나타내는 데이터 클래스입니다. - */ +data class AlarmList( + val alarms: List, + val nextCursor: Long?, + val hasNext: Boolean +) + data class AlarmSummary( val id: Long, val alarmType: AlarmType, val whenSubmitted: String, val message: String, + val targetId: Long, val isRead: Boolean ) diff --git a/core/src/main/java/com/linku/core/model/alarm/AlarmList.kt b/core/src/main/java/com/linku/core/model/alarm/AlarmList.kt deleted file mode 100644 index 54d87143..00000000 --- a/core/src/main/java/com/linku/core/model/alarm/AlarmList.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.linku.core.model.alarm - -data class AlarmList( - val alarms: List, - val nextCursor: Long?, - val hasNext: Boolean -) diff --git a/core/src/main/java/com/linku/core/model/alarm/AlarmSetting.kt b/core/src/main/java/com/linku/core/model/alarm/AlarmSetting.kt new file mode 100644 index 00000000..3f299213 --- /dev/null +++ b/core/src/main/java/com/linku/core/model/alarm/AlarmSetting.kt @@ -0,0 +1,22 @@ +package com.linku.core.model.alarm + + +/** + * 사용자의 알림 및 알람 설정 정보를 나타내는 데이터 클래스입니다. + * + * @property isAllEnabled 모든 알림 유형을 한 번에 제어하는 마스터 토글입니다. + * @property isLinkEnabled 링크 활동과 관련된 알림의 활성화 여부입니다. + * @property isFolderEnabled 폴더 변경 또는 공유와 관련된 알림의 활성화 여부입니다. + * @property isCurationEnabled 큐레이션된 콘텐츠 및 추천 알림의 활성화 여부입니다. + * @property isNoticeEnabled 시스템/공지 알람의 활성화 여부입니다. + */ +data class AlarmSetting( + val isAllEnabled: Boolean = false, + val isLinkEnabled: Boolean = false, + val isFolderEnabled: Boolean = false, + val isCurationEnabled: Boolean = false, + val isNoticeEnabled: Boolean = false +) { + fun areAllSubDisabled() = + !isLinkEnabled && !isFolderEnabled && !isCurationEnabled && !isNoticeEnabled +} diff --git a/core/src/main/java/com/linku/core/repository/AlarmRepository.kt b/core/src/main/java/com/linku/core/repository/AlarmRepository.kt index 00c196f2..defbb579 100644 --- a/core/src/main/java/com/linku/core/repository/AlarmRepository.kt +++ b/core/src/main/java/com/linku/core/repository/AlarmRepository.kt @@ -1,13 +1,21 @@ package com.linku.core.repository -import com.linku.core.model.alarm.AlarmList +import androidx.paging.PagingData +import com.linku.core.model.alarm.AlarmSetting +import com.linku.core.model.alarm.AlarmSummary import com.linku.core.model.alarm.AlarmType +import kotlinx.coroutines.flow.Flow interface AlarmRepository { + fun getAlarms( + type: AlarmType + ): Flow> - suspend fun fetchAlarms( - type: AlarmType, - cursor: Long?, - size: Int - ): Result + suspend fun updateAlarmSetting( + type: AlarmType + ): Result + + suspend fun getAlarmSetting(): Result + + suspend fun registerFCMToken(token: String): Result } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/repository/NotificationPreference.kt b/core/src/main/java/com/linku/core/repository/NotificationPreference.kt deleted file mode 100644 index 079e8b77..00000000 --- a/core/src/main/java/com/linku/core/repository/NotificationPreference.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.linku.core.repository - -/** - * 앱의 알림 활성화 여부를 저장하고 조회하기 위한 인터페이스입니다. - * - * 이 인터페이스는 사용자가 알림을 허용했는지 여부를 로컬 저장소에 - * 저장하거나 읽어오는 역할을 담당합니다. - * - * - * 기존 코드베이스와 스타일을 맞추기 위해 [AuthPreference]의 스타일을 참고하였습니다:) - */ -interface NotificationPreference { - - // 마스터 알람 - fun isMasterNotificationEnabled(): Boolean - fun setMasterNotificationEnabled(enabled: Boolean) - - // 서브 알람 전체 - fun areAllSubNotificationsEnabled(): Boolean - fun areAllSubNotificationsDisabled(): Boolean - fun setSubNotificationsEnabled(enabled: Boolean) - - // 링크 활동 알림 - fun isLinkActivityEnabled(): Boolean - fun setLinkActivityEnabled(enabled: Boolean) - - // 공유 폴더 알림 - fun isSharedFolderEnabled(): Boolean - fun setSharedFolderEnabled(enabled: Boolean) - - // AI 큐레이션 알림 - fun isAiCurationEnabled(): Boolean - fun setAiCurationEnabled(enabled: Boolean) - - // 시스템/공지 알림 - fun isSystemNoticeEnabled(): Boolean - fun setSystemNoticeEnabled(enabled: Boolean) -} diff --git a/core/src/main/java/com/linku/core/system/NotificationController.kt b/core/src/main/java/com/linku/core/system/NotificationController.kt deleted file mode 100644 index c65fd615..00000000 --- a/core/src/main/java/com/linku/core/system/NotificationController.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.linku.core.system - -import com.linku.core.repository.NotificationPreference -import com.linku.core.util.NotificationState -import javax.inject.Inject - -/** - * 알림 로직 및 설정을 관리하는 컨트롤러입니다. - * - * 이 클래스는 [com.linku.data.preference.NotificationPreference]와 상호작용하는 유틸리티 역할을 하며, - * 사용자가 정의한 알림 설정을 조회하거나 업데이트할 수 있는 인터페이스를 제공합니다. - * - * 제작 이유: "전체 알림 OFF 시 세부 항목도 함께 OFF, 전체 알림 ON시 세부 알림도 다 ON, - * 세부알림을 다 켤 시 전체알람도 ON" - * 이라는 비즈니스 로직을 캡슐화하기 위함. - * - * 또한 복잡한 비즈니스 로직을 뷰모델로부터 분리하여 뷰모델의 크기를 줄이기 위함. - * - */ -class NotificationController @Inject constructor( - private val preference: NotificationPreference, -) { - - // ===== 알람 상태 조회 ===== - - // 마스터 알림 - fun isAllNotificationEnabled(): Boolean { - return preference.isMasterNotificationEnabled() - } - - // 세부 알람 - fun isLinkActivityEnabled(): Boolean = - preference.isLinkActivityEnabled() - - fun isSharedFolderEnabled(): Boolean = - preference.isSharedFolderEnabled() - - fun isAiCurationEnabled(): Boolean = - preference.isAiCurationEnabled() - - fun isSystemNoticeEnabled(): Boolean = - preference.isSystemNoticeEnabled() - - - // ===== 알람 설정 변경 ===== - - // 전체 알림 설정 변경 시 마스터 키 저장 + 세부 알림 항목도 함께 변경 - fun setNotificationEnabled(enabled: Boolean) { - preference.setMasterNotificationEnabled(enabled) - preference.setSubNotificationsEnabled(enabled) - } - - fun setLinkActivityEnabled(enabled: Boolean) { - preference.setLinkActivityEnabled(enabled) - syncMasterWithSubState() - } - - fun setSharedFolderEnabled(enabled: Boolean) { - preference.setSharedFolderEnabled(enabled) - syncMasterWithSubState() - } - - fun setAiCurationEnabled(enabled: Boolean) { - preference.setAiCurationEnabled(enabled) - syncMasterWithSubState() - } - - fun setSystemNoticeEnabled(enabled: Boolean) { - preference.setSystemNoticeEnabled(enabled) - syncMasterWithSubState() - } - - // 세부 알림 상태에 따라 마스터 알람 상태 자동 동기화 - // 전부 ON → 마스터 ON / 전부 OFF → 마스터 OFF / 일부 혼합 → 마스터 유지 - private fun syncMasterWithSubState() { - when { - preference.areAllSubNotificationsEnabled() -> - preference.setMasterNotificationEnabled(true) - - preference.areAllSubNotificationsDisabled() -> - preference.setMasterNotificationEnabled(false) - } - } - - // 현재 저장된 모든 알림 설정 값을 하나의 상태 객체로 묶어 반환하는 메서드. - fun getState(): NotificationState = - NotificationState( - notificationEnabled = isAllNotificationEnabled(), - aiCurationEnabled = isAiCurationEnabled(), - linkActivityEnabled = isLinkActivityEnabled(), - sharedFolderEnabled = isSharedFolderEnabled(), - systemNoticeEnabled = isSystemNoticeEnabled() - ) -} \ No newline at end of file diff --git a/core/src/test/java/com/linku/core/ExampleUnitTest.kt b/core/src/test/java/com/linku/core/ExampleUnitTest.kt index 8f341319..c1a18634 100644 --- a/core/src/test/java/com/linku/core/ExampleUnitTest.kt +++ b/core/src/test/java/com/linku/core/ExampleUnitTest.kt @@ -1,8 +1,5 @@ -<<<<<<<< HEAD:core/src/test/java/com/linku/core/ExampleUnitTest.kt -package com.linku.core -======== + package com.linku.link ->>>>>>>> fd1304faab6b86e04c17e31a0786ce151290d292:app/src/test/java/com/linku/link/ExampleUnitTest.kt import org.junit.Test diff --git a/data/src/main/java/com/linku/data/api/alarm/AlarmApi.kt b/data/src/main/java/com/linku/data/api/alarm/AlarmApi.kt new file mode 100644 index 00000000..1f03870b --- /dev/null +++ b/data/src/main/java/com/linku/data/api/alarm/AlarmApi.kt @@ -0,0 +1,37 @@ +package com.linku.data.api.alarm + +import com.linku.data.api.dto.BaseResponse +import com.linku.data.api.dto.server.alarm.AlarmSettingDTO +import com.linku.data.api.dto.server.alarm.AlarmSettingRequest +import com.linku.data.api.dto.server.alarm.AlarmsDTO +import com.linku.data.api.dto.server.alarm.FcmTokenRequest +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Query + +interface AlarmApi { + + @GET("alarm/list") + suspend fun getAlarms( + @Query("alarmType") alarmType: String = "ALL", + @Query("cursor") cursor: Long? = null, + @Query("size") size: Int + ): BaseResponse + + + @PATCH("alarm/settings") + suspend fun updateAlarmSetting( + @Body body: AlarmSettingRequest + ): BaseResponse + + @GET("alarm/settings") + suspend fun getAlarmSetting(): BaseResponse + + @POST("alarm/fcmtoken") + suspend fun registerFcmToken( + @Body body: FcmTokenRequest + ): BaseResponse + +} \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingDTO.kt b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingDTO.kt new file mode 100644 index 00000000..588fc92b --- /dev/null +++ b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingDTO.kt @@ -0,0 +1,18 @@ +package com.linku.data.api.dto.server.alarm + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AlarmSettingDTO( + @field:Json(name = "isAllEnabled") + val isAllEnabled: Boolean, + @field:Json(name = "isLinkEnabled") + val isLinkEnabled: Boolean, + @field:Json(name = "isFolderEnabled") + val isFolderEnabled: Boolean, + @field:Json(name = "isCurationEnabled") + val isCurationEnabled: Boolean, + @field:Json(name = "isNoticeEnabled") + val isNoticeEnabled: Boolean +) diff --git a/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingRequest.kt b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingRequest.kt new file mode 100644 index 00000000..4bcc051a --- /dev/null +++ b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmSettingRequest.kt @@ -0,0 +1,11 @@ +package com.linku.data.api.dto.server.alarm + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AlarmSettingRequest( + + @field:Json(name = "alarmType") + val alarmType: String +) diff --git a/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmsDTO.kt b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmsDTO.kt index a052c669..44f7acc7 100644 --- a/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmsDTO.kt +++ b/data/src/main/java/com/linku/data/api/dto/server/alarm/AlarmsDTO.kt @@ -1,11 +1,30 @@ package com.linku.data.api.dto.server.alarm +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class AlarmsDTO( + @field:Json(name = "items") val alarmList: List, + @field:Json(name = "nextCursor") val nextCursor: Long?, + @field:Json(name = "hasNext") val hasNext: Boolean ) +@JsonClass(generateAdapter = true) data class AlarmSummaryDTO( - val alarmId: Int + @field:Json(name = "alarmId") + val alarmId: Long, + @field:Json(name = "alarmType") + val alarmType: String, + @field:Json(name = "message") + val message: String, + @field:Json(name = "createdAt") + val createAt: String, + @field:Json(name = "targetId") + val targetId: Long, + @field:Json(name = "isRead") + val isRead: Boolean ) diff --git a/data/src/main/java/com/linku/data/api/dto/server/alarm/FcmTokenRequest.kt b/data/src/main/java/com/linku/data/api/dto/server/alarm/FcmTokenRequest.kt new file mode 100644 index 00000000..b07038b6 --- /dev/null +++ b/data/src/main/java/com/linku/data/api/dto/server/alarm/FcmTokenRequest.kt @@ -0,0 +1,10 @@ +package com.linku.data.api.dto.server.alarm + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FcmTokenRequest( + @field:Json(name = "fcmToken") + val fcmToken: String +) diff --git a/data/src/main/java/com/linku/data/api/mapToApiError.kt b/data/src/main/java/com/linku/data/api/mapToApiError.kt index ffa2f6c9..3ebd3a23 100644 --- a/data/src/main/java/com/linku/data/api/mapToApiError.kt +++ b/data/src/main/java/com/linku/data/api/mapToApiError.kt @@ -138,8 +138,8 @@ internal fun mapToApiError(code: String, message: String): ApiError = when (code // ========================================================= // 알림 에러 (ALARM) // ========================================================= - "ALARM_NOT_FOUND" -> ApiError.Alarm.NotFound(message) - "ALARM_PERMISSION_DENIED" -> ApiError.Alarm.PermissionDenied(message) + "ALARM404" -> ApiError.Alarm.NotFound(message) + "ALARM403" -> ApiError.Alarm.PermissionDenied(message) "ALARM5001" -> ApiError.Alarm.TopicSubscriptionFailed(message) "ALARM5002" -> ApiError.Alarm.SendFailed(message) diff --git a/data/src/main/java/com/linku/data/di/api/AlarmApiModule.kt b/data/src/main/java/com/linku/data/di/api/AlarmApiModule.kt new file mode 100644 index 00000000..3efe4490 --- /dev/null +++ b/data/src/main/java/com/linku/data/di/api/AlarmApiModule.kt @@ -0,0 +1,23 @@ +package com.linku.data.di.api + +import com.linku.data.api.AuthClient +import com.linku.data.api.alarm.AlarmApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AlarmApiModule { + + @Provides + @Singleton + fun provideAlarmApi( + @AuthClient retrofit: Retrofit + ): AlarmApi = + retrofit.create(AlarmApi::class.java) + +} \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/di/preference/NotificationPreferenceModule.kt b/data/src/main/java/com/linku/data/di/preference/NotificationPreferenceModule.kt index 991c3c5c..9dea8925 100644 --- a/data/src/main/java/com/linku/data/di/preference/NotificationPreferenceModule.kt +++ b/data/src/main/java/com/linku/data/di/preference/NotificationPreferenceModule.kt @@ -1,8 +1,7 @@ package com.linku.data.di.preference import android.content.Context -import com.linku.data.implementation.preference.NotificationPreferenceImpl -import com.linku.core.repository.NotificationPreference +import com.linku.data.preference.NotificationPreference import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,6 +17,6 @@ object NotificationPreferenceModule { fun provideNotificationPreference( @ApplicationContext context: Context ): NotificationPreference { - return NotificationPreferenceImpl(context) + return NotificationPreference(context) } } \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/di/repository/AlarmRepositoryModule.kt b/data/src/main/java/com/linku/data/di/repository/AlarmRepositoryModule.kt index b1a7b84f..7e6e934f 100644 --- a/data/src/main/java/com/linku/data/di/repository/AlarmRepositoryModule.kt +++ b/data/src/main/java/com/linku/data/di/repository/AlarmRepositoryModule.kt @@ -1,7 +1,7 @@ package com.linku.data.di.repository import com.linku.core.repository.AlarmRepository -import com.linku.data.implementation.repository.FakeAlarmRepositoryImpl +import com.linku.data.implementation.repository.AlarmRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -12,11 +12,9 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) abstract class AlarmRepositoryModule { - // 페이크 레포지토리 @Binds @Singleton - @Suppress("unused") abstract fun provideAlarmRepository( - impl: FakeAlarmRepositoryImpl + impl: AlarmRepositoryImpl ): AlarmRepository } \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/implementation/preference/NotificationPreferenceImpl.kt b/data/src/main/java/com/linku/data/implementation/preference/NotificationPreferenceImpl.kt deleted file mode 100644 index d3398b9d..00000000 --- a/data/src/main/java/com/linku/data/implementation/preference/NotificationPreferenceImpl.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.linku.data.implementation.preference - -import android.content.Context -import androidx.core.content.edit -import com.linku.core.repository.NotificationPreference - - -/** - * [NotificationPreference] 인터페이스의 구현체로, [android.content.SharedPreferences]를 - * 사용하여 알림 수신 여부와 같은 사용자 설정 데이터를 저장하고 관리합니다. - * - * @param context SharedPreferences 파일에 접근하고 초기화하기 위해 사용되는 컨텍스트. - * - * 이 역시 기존 코드베이스와 스타일을 맞추기 위해 [AuthPreferenceImpl]의 스타일을 참고하였습니다:) - */ -class NotificationPreferenceImpl( - context: Context -): NotificationPreference { - - companion object { - - // SharedPreferences 파일 이름 - private const val PREF_NAME = "notification" - - // 링크 활동 알림 키 - private const val KEY_NOTIFICATION_LINK_ACTIVITY = "key_notification_link_activity" - // 공유 폴더 알림 키 - private const val KEY_NOTIFICATION_SHARED_FOLDER = "key_notification_shared_folder" - // AI 큐레이션 알림 키 - private const val KEY_NOTIFICATION_AI_CURATION = "key_notification_ai_curation" - // 시스템/공지 알림 키 - private const val KEY_NOTIFICATION_SYSTEM_NOTICE = "key_notification_system_notice" - // 마스터 알림 키 (독립 저장) - private const val KEY_NOTIFICATION_MASTER = "key_notification_master" - - - // 디버깅용 태그 상수 - private const val TAG = "NotificationPreferenceImpl" - } - - // 실제 저장소 - private val pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - - /** - * 전체 알림이 활성화된 경우에만 해당 키의 세부 알림 설정값을 반환합니다. - * 전체 알림이 비활성화된 경우 세부 설정과 관계없이 항상 false를 반환합니다. - */ - private fun isEnabledWith(key: String): Boolean = - pref.getBoolean(key, true) - - /** - * 주어진 키에 해당하는 알림 설정값을 저장합니다. - */ - private fun setEnabled(key: String, enabled: Boolean) { - pref.edit { putBoolean(key, enabled) } - } - - - // 마스터 알림 - override fun isMasterNotificationEnabled(): Boolean = - pref.getBoolean(KEY_NOTIFICATION_MASTER, true) - - override fun setMasterNotificationEnabled(enabled: Boolean) = - setEnabled(KEY_NOTIFICATION_MASTER, enabled) - - - // 서브 알림 전체 일괄 - override fun areAllSubNotificationsEnabled(): Boolean { - return isLinkActivityEnabled() && - isSharedFolderEnabled() && - isAiCurationEnabled() && - isSystemNoticeEnabled() - } - - override fun areAllSubNotificationsDisabled(): Boolean { - return !isLinkActivityEnabled() && - !isSharedFolderEnabled() && - !isAiCurationEnabled() && - !isSystemNoticeEnabled() - } - - override fun setSubNotificationsEnabled(enabled: Boolean) { - setLinkActivityEnabled(enabled) - setSharedFolderEnabled(enabled) - setAiCurationEnabled(enabled) - setSystemNoticeEnabled(enabled) - } - - - // ========= 각 서브 알림 상태 관리 =========== - // 링크 - override fun isLinkActivityEnabled() = - isEnabledWith(KEY_NOTIFICATION_LINK_ACTIVITY) - - override fun setLinkActivityEnabled(enabled: Boolean) = - setEnabled(KEY_NOTIFICATION_LINK_ACTIVITY, enabled) - - - // 폴더 공유 - override fun isSharedFolderEnabled() = - isEnabledWith(KEY_NOTIFICATION_SHARED_FOLDER) - - override fun setSharedFolderEnabled(enabled: Boolean) = - setEnabled(KEY_NOTIFICATION_SHARED_FOLDER, enabled) - - - // AI 큐레이션 - override fun isAiCurationEnabled() = - isEnabledWith(KEY_NOTIFICATION_AI_CURATION) - - override fun setAiCurationEnabled(enabled: Boolean) = - setEnabled(KEY_NOTIFICATION_AI_CURATION, enabled) - - - // 시스템/공지 - override fun isSystemNoticeEnabled() = - isEnabledWith(KEY_NOTIFICATION_SYSTEM_NOTICE) - - override fun setSystemNoticeEnabled(enabled: Boolean) = - setEnabled(KEY_NOTIFICATION_SYSTEM_NOTICE, enabled) - -} \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/implementation/repository/AlarmPagingSource.kt b/data/src/main/java/com/linku/data/implementation/repository/AlarmPagingSource.kt index ffb3ef55..e4cdbb4b 100644 --- a/data/src/main/java/com/linku/data/implementation/repository/AlarmPagingSource.kt +++ b/data/src/main/java/com/linku/data/implementation/repository/AlarmPagingSource.kt @@ -4,38 +4,46 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.linku.core.model.alarm.AlarmSummary import com.linku.core.model.alarm.AlarmType -import com.linku.core.repository.AlarmRepository -import javax.inject.Inject +import com.linku.data.api.alarm.AlarmApi +import com.linku.data.api.safeApiCall +import com.linku.data.mapper.AlarmMapper.toDomain -class AlarmPagingSource @Inject constructor( - private val alarmRepository: AlarmRepository, - private val type: AlarmType -): PagingSource() { +/** + * [AlarmApi]를 통해 [AlarmSummary] 데이터를 페이징하여 가져오기 위한 [PagingSource] 구현체입니다. + * + * 특정 [AlarmType]에 대한 커서(Cursor) 기반 페이징을 처리하며, API 응답 데이터를 + * 도메인 모델로 매핑하고 네트워크 및 서버 에러에 대한 예외 처리를 수행합니다. + * + * @property alarmApi 알람 데이터를 요청하기 위한 API 인터페이스 + * @property type 조회하고자 하는 알람의 종류 + */ +class AlarmPagingSource( + val alarmApi: AlarmApi, + val type: AlarmType +) : PagingSource() { - override suspend fun load( - params: LoadParams - ): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { + val result = safeApiCall { + alarmApi.getAlarms( + alarmType = type.name, + cursor = params.key, + size = params.loadSize + ) + } - val cursor = params.key - val size = params.loadSize - - return alarmRepository.fetchAlarms( - type = type, - cursor = cursor, - size = size - ).fold( - onSuccess = { alarmList -> + return result.fold( + onSuccess = { dto -> + val alarmList = dto.toDomain() LoadResult.Page( data = alarmList.alarms, prevKey = null, nextKey = alarmList.nextCursor ) }, - onFailure = { e -> - LoadResult.Error(e) + onFailure = { exception -> + LoadResult.Error(exception) } ) - } override fun getRefreshKey(state: PagingState): Long? = null diff --git a/data/src/main/java/com/linku/data/implementation/repository/AlarmRepositoryImpl.kt b/data/src/main/java/com/linku/data/implementation/repository/AlarmRepositoryImpl.kt new file mode 100644 index 00000000..b698976c --- /dev/null +++ b/data/src/main/java/com/linku/data/implementation/repository/AlarmRepositoryImpl.kt @@ -0,0 +1,85 @@ +package com.linku.data.implementation.repository + +import android.util.Log +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.linku.core.model.alarm.AlarmSetting +import com.linku.core.model.alarm.AlarmSummary +import com.linku.core.model.alarm.AlarmType +import com.linku.core.repository.AlarmRepository +import com.linku.data.preference.NotificationPreference +import com.linku.data.api.alarm.AlarmApi +import com.linku.data.api.dto.server.alarm.AlarmSettingRequest +import com.linku.data.api.dto.server.alarm.FcmTokenRequest +import com.linku.data.api.safeApiCall +import com.linku.data.api.safeApiCallUnit +import com.linku.data.mapper.AlarmMapper.toDomain +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + + +class AlarmRepositoryImpl @Inject constructor( + private val alarmApi: AlarmApi, + private val notificationPreference: NotificationPreference, +) : AlarmRepository { + + // 알람 타입에 따라 페이징 처리된 알람 목록을 반환 + override fun getAlarms( + type: AlarmType + ): Flow> { + return Pager( + config = PagingConfig( + pageSize = 20, + initialLoadSize = 20, + enablePlaceholders = false + ), + pagingSourceFactory = { + AlarmPagingSource( + alarmApi = alarmApi, + type = type + ) + } + ).flow + } + + override suspend fun updateAlarmSetting( + type: AlarmType + ): Result { + return safeApiCall { + alarmApi.updateAlarmSetting(AlarmSettingRequest(type.name)) + }.onSuccess { dto -> + // isAllEnabled만 캐싱 + notificationPreference.setMasterNotificationEnabled(dto.isAllEnabled) + }.map { + it.toDomain() + } + } + + override suspend fun getAlarmSetting(): Result { + return safeApiCall { + alarmApi.getAlarmSetting() + }.onSuccess { dto -> + notificationPreference.setMasterNotificationEnabled(dto.isAllEnabled) + }.map { it.toDomain() } + } + + override suspend fun registerFCMToken( + token: String + ): Result { + Log.d("FCM", "registerFCMToken 진입") + + return safeApiCallUnit { + alarmApi.registerFcmToken( + FcmTokenRequest(token) + ) + }.onSuccess { + Log.d("FCM", "fcm 토큰 서버 전송 완료") + }.onFailure { e -> + Log.e("FCM", "fcm 토큰 서버 전송 실패: ${e::class.simpleName} - ${e.message}") + } + } + +} + + diff --git a/data/src/main/java/com/linku/data/implementation/repository/FakeAlarmRepositoryImpl.kt b/data/src/main/java/com/linku/data/implementation/repository/FakeAlarmRepositoryImpl.kt deleted file mode 100644 index b3b01a6b..00000000 --- a/data/src/main/java/com/linku/data/implementation/repository/FakeAlarmRepositoryImpl.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.linku.data.implementation.repository - -import com.linku.core.model.alarm.AlarmList -import com.linku.core.model.alarm.AlarmSummary -import com.linku.core.model.alarm.AlarmType -import com.linku.core.repository.AlarmRepository -import javax.inject.Inject - -/** - * [AlarmRepository]의 가짜(Fake) 구현체입니다. - * - * 실제 API 연동 전 UI 개발 및 테스트를 위해 더미 데이터를 생성하여 제공하며, - * 전달받은 커서와 크기에 따라 가상의 알람 목록을 생성합니다. - */ -class FakeAlarmRepositoryImpl @Inject constructor( - -) : AlarmRepository { - - override suspend fun fetchAlarms( - type: AlarmType, - cursor: Long?, - size: Int - ): Result { - - return runCatching { - - val start = cursor ?: 0L - val linkMessage = "'요즘 대학생들이 진짜 쓰는 앱 TOP 10' 링크에 대한 AI 요약이 완료되었어요.'요즘 대학생들이 진짜 쓰는 앱 TOP 10' 링크에 대한 AI 요약이 완료되었어요.'요즘 대학생들이 진짜 쓰는 앱 TOP 10' 링크에 대한 AI 요약이 완료되었어요." - - val alarms = List(size) { index -> - val id = start + index - - val finalType = when (type) { - AlarmType.ALL -> { - AlarmType.entries[ - (id % (AlarmType.entries.size - 1)).toInt() + 1 - ] - } - else -> type - } - - val isLinkDummy = finalType == AlarmType.LINK && id % 3L == 0L - - AlarmSummary( - id = id, - alarmType = finalType, - whenSubmitted = "${id}분 전", - message = if (isLinkDummy) linkMessage else "더미 알림 $id", - isRead = id % 2L == 0L - ) - } - - val nextCursor = if (start >= 50L) null else start + size - - AlarmList( - alarms = alarms, - nextCursor = nextCursor, - hasNext = nextCursor != null - ) - } - } -} - - diff --git a/data/src/main/java/com/linku/data/mapper/AlarmMapper.kt b/data/src/main/java/com/linku/data/mapper/AlarmMapper.kt new file mode 100644 index 00000000..df33fa32 --- /dev/null +++ b/data/src/main/java/com/linku/data/mapper/AlarmMapper.kt @@ -0,0 +1,39 @@ +package com.linku.data.mapper + +import com.linku.core.model.alarm.AlarmList +import com.linku.core.model.alarm.AlarmSetting +import com.linku.core.model.alarm.AlarmSummary +import com.linku.core.model.alarm.AlarmType +import com.linku.data.api.dto.server.alarm.AlarmSettingDTO +import com.linku.data.api.dto.server.alarm.AlarmSummaryDTO +import com.linku.data.api.dto.server.alarm.AlarmsDTO + +/** + * 서버 계층의 알람 관련 데이터 전송 객체(DTO)를 도메인 모델로 변환하는 유틸리티 객체입니다. + * + * 이 매퍼는 DTO를 애플리케이션의 도메인 계층에서 사용할 수 있는 형식으로 매핑하는 역할을 수행합니다. + */ +object AlarmMapper { + fun AlarmsDTO.toDomain(): AlarmList = AlarmList( + alarms = alarmList.map { it.toDomain() }, + nextCursor = nextCursor, + hasNext = hasNext + ) + + private fun AlarmSummaryDTO.toDomain(): AlarmSummary = AlarmSummary( + id = alarmId, + alarmType = AlarmType.from(alarmType), + whenSubmitted = createAt.toRelativeTime(), + message = message, + targetId = targetId, + isRead = isRead + ) + + fun AlarmSettingDTO.toDomain(): AlarmSetting = AlarmSetting( + isAllEnabled = isAllEnabled, + isLinkEnabled = isLinkEnabled, + isFolderEnabled = isFolderEnabled, + isCurationEnabled = isCurationEnabled, + isNoticeEnabled = isNoticeEnabled + ) +} diff --git a/data/src/main/java/com/linku/data/mapper/StringExt.kt b/data/src/main/java/com/linku/data/mapper/StringExt.kt new file mode 100644 index 00000000..0f59dccc --- /dev/null +++ b/data/src/main/java/com/linku/data/mapper/StringExt.kt @@ -0,0 +1,48 @@ +package com.linku.data.mapper + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.temporal.ChronoUnit + +/** + * ISO 8601 형식의 UTC 타임스탬프 문자열을 사람이 읽기 쉬운 상대 시간 문자열로 변환합니다. + * + * 현재 시각과의 차이를 분 단위로 계산하여 아래 기준으로 포맷합니다. + * - 1분 미만 → "방금 전" + * - 1분 이상 60분 미만 → "n분 전" + * - 1시간 이상 24시간 미만 → "n시간 전" + * - 24시간 이상 → "n일 전" + * + * 파싱에 실패하거나 포맷이 올바르지 않은 경우 "알 수 없음"을 반환합니다. + * clock skew로 인해 diff가 음수일 경우 0으로 처리합니다. + * + * @receiver ISO 8601 형식의 UTC 타임스탬프 문자열 (예: "2024-01-01T00:00:00.000Z") + * @return 상대 시간 문자열 + */ +fun String.toRelativeTime( + now: Instant = Instant.now(), + zoneId: ZoneId = ZoneId.systemDefault() +): String { + // 파싱 + val parsed = runCatching { + LocalDateTime.parse(this) + .atZone(zoneId) + .toInstant() + }.getOrElse { + return "알 수 없음" + } + + // 현재 시간과 차이 계산 + val diff = maxOf( + 0L, + ChronoUnit.MINUTES.between(parsed, now) + ) + + return when { + diff < 1 -> "방금 전" + diff < 60 -> "${diff}분 전" + diff < 60 * 24 -> "${diff / 60}시간 전" + else -> "${diff / (60 * 24)}일 전" + } +} \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/preference/NotificationPreference.kt b/data/src/main/java/com/linku/data/preference/NotificationPreference.kt new file mode 100644 index 00000000..acc740d3 --- /dev/null +++ b/data/src/main/java/com/linku/data/preference/NotificationPreference.kt @@ -0,0 +1,39 @@ +package com.linku.data.preference + +import android.content.Context +import androidx.core.content.edit + +class NotificationPreference( + context: Context +) { + + companion object { + private const val PREF_NAME = "notification" + + //KEY + private const val KEY_NOTIFICATION_MASTER = "key_notification_master" + private const val KEY_FCM_TOKEN = "key_fcm_token" + } + + private val pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + // ===== Notification ===== + fun isMasterNotificationEnabled(): Boolean = + pref.getBoolean(KEY_NOTIFICATION_MASTER, true) + + fun setMasterNotificationEnabled(enabled: Boolean) { + pref.edit { putBoolean(KEY_NOTIFICATION_MASTER, enabled) } + } + + // ===== FCM Token ===== + fun getFcmToken(): String? = + pref.getString(KEY_FCM_TOKEN, null) + + fun setFcmToken(token: String) { + pref.edit { putString(KEY_FCM_TOKEN, token) } + } + + fun clearFcmToken() { + pref.edit { remove(KEY_FCM_TOKEN) } + } +} diff --git a/data/src/test/java/com/linku/data/ExampleUnitTest.kt b/data/src/test/java/com/linku/data/ExampleUnitTest.kt index 16052b82..7668665a 100644 --- a/data/src/test/java/com/linku/data/ExampleUnitTest.kt +++ b/data/src/test/java/com/linku/data/ExampleUnitTest.kt @@ -1,8 +1,4 @@ -<<<<<<<< HEAD:data/src/test/java/com/linku/data/ExampleUnitTest.kt package com.linku.data -======== -package com.linku.link ->>>>>>>> 6cfa3247fa9a751d3cefb7daf59fb3f6f6c8368c:app/src/test/java/com/linku/link/ExampleUnitTest.kt import org.junit.Test diff --git a/data/src/test/java/com/linku/data/mapper/StringExtTest.kt b/data/src/test/java/com/linku/data/mapper/StringExtTest.kt new file mode 100644 index 00000000..fd77b34f --- /dev/null +++ b/data/src/test/java/com/linku/data/mapper/StringExtTest.kt @@ -0,0 +1,55 @@ +package com.linku.data.mapper + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Instant + +class StringExtTest { + + @Test + fun `방금 전 테스트`() { + val now = Instant.parse("2026-01-01T00:10:00Z") + val input = "2026-01-01T00:09:30Z" + + val result = input.toRelativeTime(now) + + assertEquals("방금 전", result) + } + + @Test + fun `분 전 테스트`() { + val now = Instant.parse("2026-01-01T00:10:00Z") + val input = "2026-01-01T00:05:00Z" + + val result = input.toRelativeTime(now) + + assertEquals("5분 전", result) + } + + @Test + fun `시간 전 테스트`() { + val now = Instant.parse("2026-01-01T03:00:00Z") + val input = "2026-01-01T01:00:00Z" + + val result = input.toRelativeTime(now) + + assertEquals("2시간 전", result) + } + + @Test + fun `일 전 테스트`() { + val now = Instant.parse("2026-01-05T00:00:00Z") + val input = "2026-01-03T00:00:00Z" + + val result = input.toRelativeTime(now) + + assertEquals("2일 전", result) + } + + @Test + fun `잘못된 날짜 포맷`() { + val result = "invalid-date".toRelativeTime() + + assertEquals("알 수 없음", result) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/util/OnResumeEffect.kt b/design/src/main/java/com/linku/design/util/OnResumeEffect.kt new file mode 100644 index 00000000..7af2ae90 --- /dev/null +++ b/design/src/main/java/com/linku/design/util/OnResumeEffect.kt @@ -0,0 +1,48 @@ +package com.linku.design.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner + +/** + * Composable이 속한 LifecycleOwner의 ON_RESUME 이벤트를 감지하여 + * 지정된 콜백을 실행하는 Effect입니다. + * + * 화면이 처음 표시되거나, 외부 화면(브라우저, 시스템 설정 등)으로 + * 이동한 뒤 다시 앱으로 복귀했을 때 실행됩니다. + * + * 내부적으로 LifecycleEventObserver를 등록하며, + * Composable이 Composition에서 제거될 때 Observer를 해제하여 + * 메모리 누수를 방지합니다. + * + * 사용 예시: + * + * OnResume { + * viewModel.sendIntent(NotificationIntent.RefreshSystemAlarm) + * } + * + * + * @param onResume ON_RESUME 이벤트 발생 시 실행할 콜백 + */ +@Composable +fun OnResumeEffect( + onResume: () -> Unit // onResume시 호출할 콜백 +) { + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + onResume() + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } +} diff --git a/feature/home/src/main/java/com/linku/home/screen/AlarmScreen.kt b/feature/home/src/main/java/com/linku/home/screen/AlarmScreen.kt index 01314dda..b34162a8 100644 --- a/feature/home/src/main/java/com/linku/home/screen/AlarmScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/AlarmScreen.kt @@ -5,12 +5,16 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 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 @@ -22,17 +26,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey import com.linku.core.model.alarm.AlarmSummary import com.linku.core.model.alarm.AlarmType -import com.linku.design.theme.LinkuPreview import com.linku.design.theme.LocalColorTheme -import com.linku.home.ui.alarm.component.AlarmItem import com.linku.home.ui.alarm.component.AlarmTopBar import com.linku.home.ui.alarm.component.AlarmFilterTabs +import com.linku.home.ui.alarm.component.AlarmItem +import com.linku.home.ui.alarm.component.AlarmNothingTab import com.linku.home.ui.alarm.component.AlarmSettingTab +import com.linku.home.ui.alarm.component.AlarmAppendStateFooter +import com.linku.home.ui.alarm.component.AlarmErrorLayout +import com.linku.home.ui.alarm.component.AlarmLoadingContent import com.linku.home.viewmodel.AlarmViewModel import kotlinx.coroutines.flow.flowOf @@ -43,25 +52,42 @@ fun AlarmScreen( onNavigateToHome: () -> Unit, viewModel: AlarmViewModel = hiltViewModel() ) { - val alarmState by viewModel.alarmState.collectAsStateWithLifecycle() + val pushAlarmEnabled by viewModel.pushAlarmEnabled.collectAsStateWithLifecycle() //탭 선택 상태 var selectedTab by rememberSaveable { mutableStateOf(AlarmType.ALL) } - // 탭별 목록의 스크롤 상태를 저장하는 Map - val listStates = remember { AlarmType.entries.associateWith { LazyListState() } } + // 사용자가 직접 스와이프 새로고침을 트리거했을 때 활성화되는 state + var isUserRefreshing by remember { mutableStateOf(false) } + + // 컨테이너와 표시기가 당겨진 거리를 추적하는 state. PullToRefreshBox를 쓰려면 필수 + val pullToRefreshState = rememberPullToRefreshState() // 화면에 표시될 페이징될 알람들 데이터 - val alarms = remember(selectedTab) { - viewModel.getAlarms(selectedTab) - }.collectAsLazyPagingItems() + // Paging3에서 LazyPagingItems는 + // PagingData와 LoadState(로딩/에러 상태)를 함께 관리한다 + val alarmPagingItems = viewModel.getAlarms(selectedTab) + .collectAsLazyPagingItems() + + // 새로고침이 완료되면 인디케이터 해제 + LaunchedEffect(alarmPagingItems.loadState.refresh) { + if (alarmPagingItems.loadState.refresh is LoadState.NotLoading || + alarmPagingItems.loadState.refresh is LoadState.Error) { + isUserRefreshing = false + } + } AlarmScreenContent( - isAlarmAllowed = alarmState, + isAlarmAllowed = pushAlarmEnabled, selectedTab = selectedTab, onSelectedChange = { selectedTab = it }, - alarms = alarms, - listState = listStates.getValue(selectedTab), + alarmPagingItems = alarmPagingItems, + isUserRefreshing = isUserRefreshing, + onRefresh = { + isUserRefreshing = true + alarmPagingItems.refresh() + }, + pullToRefreshState = pullToRefreshState, onBack = onBack, onNavigateToMyPage = onNavigateToMyPage, onNavigateToHome = onNavigateToHome @@ -73,15 +99,17 @@ private fun AlarmScreenContent( isAlarmAllowed: Boolean = true, selectedTab: AlarmType, onSelectedChange: (AlarmType) -> Unit, - alarms: LazyPagingItems, - listState: LazyListState, + alarmPagingItems: LazyPagingItems, + isUserRefreshing: Boolean, + onRefresh: () -> Unit, + pullToRefreshState: PullToRefreshState, onBack: () -> Unit, onNavigateToMyPage: () -> Unit, onNavigateToHome: () -> Unit, ) { Column( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .background(LocalColorTheme.current.gray[100]) .padding(start = 20.dp, end = 20.dp, top = 56.dp, bottom = 15.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -105,16 +133,62 @@ private fun AlarmScreenContent( onClick = onNavigateToMyPage ) - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(top = 12.dp), - ) { - if (isAlarmAllowed) { - items(alarms.itemCount) { index -> - val item = alarms[index] - if (item != null) { - AlarmItem(alarm = item) + if (isAlarmAllowed) { + PullToRefreshBox( //스와이프 새로고침 기능을 내장하는 컴포저블 + modifier = Modifier.weight(1f), + isRefreshing = isUserRefreshing, + onRefresh = onRefresh, + state = pullToRefreshState, + indicator = { + Indicator( //인디케이터는 임시 구현~~ + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = isUserRefreshing, + containerColor = LocalColorTheme.current.gray[200], + color = LocalColorTheme.current.black, + state = pullToRefreshState + ) + } + ) { + /** + * 허용이 되어있는 경우. Paging3의 loadState를 사용하여 분기처리 + * 디벨로퍼 문서의 코드 스니펫을 참고해서 구현하였음 + * + * - loadState.refresh: 목록 전체를 새로 불러올 때의 상태 + * - loadState.append: 스크롤로 다음 페이지를 불러올 때의 상태. [AlarmAppendStateFooter]에서 사용 + */ + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(top = 12.dp), + modifier = Modifier.fillMaxSize() + ) { + when (val refreshState = alarmPagingItems.loadState.refresh) { + + is LoadState.Loading -> { + item { AlarmLoadingContent() } + } + + is LoadState.Error -> { + item { AlarmErrorLayout(alarmPagingItems, refreshState) } + } + + else -> { + if (alarmPagingItems.itemCount == 0) { + item { + AlarmNothingTab(isVisible = true) + } + } else { + items( + count = alarmPagingItems.itemCount, + key = alarmPagingItems.itemKey { it.id } + ) { index -> + alarmPagingItems[index]?.let { AlarmItem(it) } + } + + item { + AlarmAppendStateFooter(alarmPagingItems) + } + } + } } } } @@ -122,57 +196,42 @@ private fun AlarmScreenContent( } } -@Preview(showBackground = true, showSystemUi = true) +@Preview(showBackground = true) @Composable -private fun AlarmScreenPreview() { - val fakeAlarms = listOf( - AlarmSummary( - id = 0, - alarmType = AlarmType.LINK, - whenSubmitted = "0분 전", - message = "'요즘 대학생들이 진짜 쓰는 앱 TOP 10' 링크에 대한 AI 요약이 완료되었어요.", - isRead = false - ), +private fun AlarmScreenContentPreview() { + val sampleAlarms = listOf( AlarmSummary( id = 1, alarmType = AlarmType.CURATION, - whenSubmitted = "1분 전", + whenSubmitted = "10분 전", message = "1월 세나님을 위한 링큐레이션이 도착했어요!", - isRead = true - ), - AlarmSummary( - id = 2, - alarmType = AlarmType.FOLDER, - whenSubmitted = "2분 전", - message = "더미 알림 2", + targetId = 1L, isRead = false ), AlarmSummary( - id = 3, - alarmType = AlarmType.LINK, - whenSubmitted = "3분 전", - message = "'요즘 대학생들이 진짜 쓰는 앱 TOP 10' 링크에 대한 AI 요약이 완료되었어요.", - isRead = true - ), - AlarmSummary( - id = 4, + id = 2, alarmType = AlarmType.NOTICE, - whenSubmitted = "4분 전", - message = "더미 알림 4", - isRead = false - ), + whenSubmitted = "1시간 전", + message = "새로운 서비스 공지사항입니다.", + targetId = 2L, + isRead = true + ) ) - - val alarms = flowOf(PagingData.from(fakeAlarms)).collectAsLazyPagingItems() + val alarms = flowOf(PagingData.from(sampleAlarms)).collectAsLazyPagingItems() // LinkuPreview { // AlarmScreenContent( // selectedTab = AlarmType.ALL, // onSelectedChange = {}, -// alarms = alarms, +// alarmPagingItems = alarms, +// listState = rememberLazyListState(), // onBack = {}, // onNavigateToMyPage = {}, -// onNavigateToHome = {} +// onNavigateToHome = {}, +// isUserRefreshing = false, +// onRefresh = {}, // ) // } } + + diff --git a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmAppendStateFooter.kt b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmAppendStateFooter.kt new file mode 100644 index 00000000..6cff604c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmAppendStateFooter.kt @@ -0,0 +1,98 @@ +package com.linku.home.ui.alarm.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.linku.core.model.alarm.AlarmSummary +import com.linku.design.theme.LinkuPreview + +/** + * 알람 목록 하단에 추가 로딩 상태를 표시하는 푸터 컴포저블입니다. + * + * [alarmPagingItems]의 append [LoadState]를 감지하여 로딩 중이면 [CircularProgressIndicator], + * 에러 시 재시도 버튼을 표시합니다. + * + * @param alarmPagingItems append 상태를 제공하는 페이징 데이터 + * + * 일단 임시구현해두었습니다~~ + */ +@Composable +fun AlarmAppendStateFooter( + alarmPagingItems: LazyPagingItems +) { + AlarmAppendStateFooterContent( + appendState = alarmPagingItems.loadState.append, + onRetry = { alarmPagingItems.retry() } + ) +} + +/** + * append [LoadState]에 따라 로딩 인디케이터 또는 재시도 버튼을 렌더링합니다. + * + * @param appendState 현재 append 로드 상태 + * @param onRetry 재시도 버튼 클릭 콜백 + */ +@Composable +private fun AlarmAppendStateFooterContent( + appendState: LoadState, + onRetry: () -> Unit +) { + when (appendState) { + is LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + // 임시 구현 + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + TextButton(onClick = onRetry) { + Text("다시 시도") + } + } + } + + else -> Unit + } +} + +@Preview(showBackground = true) +@Composable +private fun AlarmAppendStateFooterLoadingPreview() { + LinkuPreview { + AlarmAppendStateFooterContent( + appendState = LoadState.Loading, + onRetry = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun AlarmAppendStateFooterErrorPreview() { + LinkuPreview { + AlarmAppendStateFooterContent( + appendState = LoadState.Error(Exception("preview error")), + onRetry = {} + ) + } +} diff --git a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmErrorContent.kt b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmErrorContent.kt new file mode 100644 index 00000000..daa34e9a --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmErrorContent.kt @@ -0,0 +1,77 @@ +package com.linku.home.ui.alarm.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.linku.core.error.AppError +import com.linku.core.error.NetworkError +import com.linku.core.model.alarm.AlarmSummary +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.LocalColorTheme + +/** + * 알람 페이징 새로고침 에러를 처리하는 컴포저블입니다. + * + * [alarmPagingItems]의 refresh 상태에서 [AppError]를 추출하여 + * 에러 메시지와 재시도 버튼을 표시합니다. + * + * @param alarmPagingItems 에러가 발생한 페이징 데이터 + * + * 일단 임시구현해두었습니다 ~~ + */ +@Composable +fun AlarmErrorLayout( + alarmPagingItems: LazyPagingItems, + errorState: LoadState.Error +) { + val message = (errorState.error as AppError).displayMessage + + AlarmErrorLayoutContent( + message = message, + onRetry = { alarmPagingItems.retry() } + ) +} + +@Composable +private fun AlarmErrorLayoutContent( + message: String, + onRetry: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = message, + color = LocalColorTheme.current.gray[600] + ) + TextButton(onClick = onRetry) { Text("다시 시도") } + } + } +} + +@Composable +@Preview(showBackground = true) +private fun AlarmErrorLayoutPreview() { + LinkuPreview { + AlarmErrorLayoutContent( + message = NetworkError.NoConnection().displayMessage, + onRetry = {} + ) + } +} diff --git a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmItem.kt b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmItem.kt index 242631c0..2276cd38 100644 --- a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmItem.kt +++ b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmItem.kt @@ -107,19 +107,19 @@ fun AlarmItem( } } -@Preview(showBackground = true) -@Composable -private fun AlarmItemPreview() { - LinkuPreview { - AlarmItem( - alarm = AlarmSummary( - id = 1, - alarmType = AlarmType.CURATION, - whenSubmitted = "10분 전", - message = "1월 세나님을 위한 링큐레이션이 도착했어요!", - isRead = false - ), - modifier = Modifier.padding(16.dp) - ) - } -} +//@Preview(showBackground = true) +//@Composable +//private fun AlarmItemPreview() { +// LinkuPreview { +// AlarmItem( +// alarm = AlarmSummary( +// id = 1, +// alarmType = AlarmType.CURATION, +// whenSubmitted = "10분 전", +// message = "1월 세나님을 위한 링큐레이션이 도착했어요!", +// isRead = false +// ), +// modifier = Modifier.padding(16.dp) +// ) +// } +//} diff --git a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmLoadingContent.kt b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmLoadingContent.kt new file mode 100644 index 00000000..7c6ec077 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmLoadingContent.kt @@ -0,0 +1,50 @@ +package com.linku.home.ui.alarm.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.linku.design.modifier.skeleton +import com.linku.design.theme.LinkuPreview + +/** + * 알람 목록의 로딩 상태를 표시하는 컴포저블입니다. + * + * 데이터를 불러오는 동안 사용자에게 시각적 피드백을 제공하기 위해 + * [Column]을 사용하여 스켈레톤(Skeleton) UI 형태의 플레이스홀더를 렌더링합니다. + */ +@Composable +fun AlarmLoadingContent() { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(top = 12.dp) + ) { + repeat(5) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .clip(RoundedCornerShape(18.dp)) + .skeleton(isLoading = true) + ) + } + } +} + +@Preview +@Composable +private fun AlarmLoadingContentPreview() { + LinkuPreview { + AlarmLoadingContent() + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmNothingTab.kt b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmNothingTab.kt index 3db9b4e4..7c8e0aeb 100644 --- a/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmNothingTab.kt +++ b/feature/home/src/main/java/com/linku/home/ui/alarm/component/AlarmNothingTab.kt @@ -23,7 +23,6 @@ import com.linku.home.R @Composable fun AlarmNothingTab( - onClick: () -> Unit, isVisible: Boolean = true ) { if (!isVisible) return @@ -31,7 +30,6 @@ fun AlarmNothingTab( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .clickable { onClick() } .background(LocalColorTheme.current.white) .padding(horizontal = 17.dp, vertical = 18.dp), verticalAlignment = Alignment.CenterVertically, @@ -63,5 +61,5 @@ fun AlarmNothingTab( @Preview @Composable fun PreviewAlarmNothingTab() { - AlarmNothingTab(onClick = {}) + AlarmNothingTab() } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/viewmodel/AlarmViewModel.kt b/feature/home/src/main/java/com/linku/home/viewmodel/AlarmViewModel.kt index 87ce3507..a7e2259c 100644 --- a/feature/home/src/main/java/com/linku/home/viewmodel/AlarmViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/viewmodel/AlarmViewModel.kt @@ -2,13 +2,10 @@ package com.linku.home.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig import androidx.paging.cachedIn import com.linku.core.model.alarm.AlarmType import com.linku.core.repository.AlarmRepository -import com.linku.core.system.NotificationController -import com.linku.data.implementation.repository.AlarmPagingSource +import com.linku.data.preference.NotificationPreference import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -17,35 +14,41 @@ import javax.inject.Inject @HiltViewModel class AlarmViewModel @Inject constructor( private val alarmRepository: AlarmRepository, - private val notificationController: NotificationController -): ViewModel(){ + private val notificationPreference: NotificationPreference +) : ViewModel() { - private val _alarmState = MutableStateFlow(notificationController.isAllNotificationEnabled()) - val alarmState = _alarmState.asStateFlow() + // 알람 활성화 여부 + private val _pushAlarmEnabled = MutableStateFlow(notificationPreference.isMasterNotificationEnabled()) + val pushAlarmEnabled = _pushAlarmEnabled.asStateFlow() /** + * Map>> + * *[AlarmType]별로 구성된 페이징된 알람 Flow *[AlarmType]을 키로 Map으로 변환 후에 *[viewModelScope]에서 캐싱하여 화면 회전 등의 상황에서도 데이터를 재사용한다. * + * 원리: + * 1. 사용자가 탭 변경을 하여 selectedTab 변경 + * 2. collectAsLazyPagingItems가 실행. Flow는 Cold Stream이므로 그제서야 Repository로부터 PagingData를 받아옴 + * 3. UI에 변경된 목록 표시 + * 4. PagingData를 viewModelScope안에서 캐싱하므로, 한번 불러온 PagingData는 뷰모델 생명주기 내에서 캐싱된다. + * 5. 따라서 불필요한 API호출을 예방하고 UX를 최적화할 수 있다. + * + * 6. 사용자가 알림함 화면에 있을 때 SSOT(백엔드 서버)에 푸시알람 데이터가 하나 더 생긴 상황은 + * 7. PullToRefreshBox를 사용해 사용자가 능동적으로 새로고침 할 수 있도록 대응함. + * */ private val alarmFlows = AlarmType.entries.associateWith { type -> - Pager( - config = PagingConfig( - pageSize = 20, - enablePlaceholders = false - ), - pagingSourceFactory = { - AlarmPagingSource( - alarmRepository = alarmRepository, - type = type - ) - } - ).flow.cachedIn(viewModelScope) + alarmRepository + .getAlarms(type) + .cachedIn(viewModelScope) } + /** - * 페이징된 알람 데이터의 타입별 게터 함수 + * 지정된 [AlarmType]에 해당하는 페이징된 알람 데이터 Flow를 반환합니다. */ fun getAlarms(type: AlarmType) = alarmFlows.getValue(type) } + diff --git a/feature/mypage/src/main/java/com/linku/mypage/MyPageApp.kt b/feature/mypage/src/main/java/com/linku/mypage/MyPageApp.kt index 89c2c19c..51e21ba0 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/MyPageApp.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/MyPageApp.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -24,6 +25,7 @@ import com.linku.mypage.screen.ServiceQuitScreen @Composable fun MyPageApp( viewModel: MyPageViewModel, + notificationViewModel: NotificationViewModel = hiltViewModel(), onLogoutToLogin: () -> Unit ) { val navController = rememberNavController() @@ -322,7 +324,10 @@ fun MyPageApp( } composable("alarmSetting") { - AlarmSettingScreen(navController = navController) + AlarmSettingScreen( + navController = navController, + viewModel = notificationViewModel + ) } composable("quit") { diff --git a/feature/mypage/src/main/java/com/linku/mypage/NotificationViewModel.kt b/feature/mypage/src/main/java/com/linku/mypage/NotificationViewModel.kt index 69e7a027..a738f184 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/NotificationViewModel.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/NotificationViewModel.kt @@ -2,74 +2,173 @@ package com.linku.mypage import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.linku.core.error.AppError +import com.linku.core.model.alarm.AlarmSetting +import com.linku.core.model.alarm.AlarmType +import com.linku.core.repository.AlarmRepository import com.linku.core.system.PermissionChecker -import com.linku.core.system.NotificationController +import com.linku.mypage.intent.LinkuNotificationIntent +import com.linku.mypage.intent.NotificationIntent +import com.linku.mypage.intent.RefreshSystemAlarm +import com.linku.mypage.intent.ToggleAll +import com.linku.mypage.intent.ToggleCuration +import com.linku.mypage.intent.ToggleFolder +import com.linku.mypage.intent.ToggleLink +import com.linku.mypage.intent.ToggleNotice import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class NotificationViewModel @Inject constructor( - private val notificationController: NotificationController, + private val alarmRepository: AlarmRepository, private val checker: PermissionChecker -): ViewModel() { - // 뷰모델 내부의 상태 변수. 컨트롤러의 getState()메서드로 초기화 - private val _notificationState = MutableStateFlow(notificationController.getState()) +) : ViewModel() { - // ui에 노출시킬 변수 + // UI 이벤트(Intent) 입력 큐로서의 채널 + // ViewModel 내부에서만 consumeAsFlow로 처리되는 내부 전용 구조 + private val intentChannel = Channel(Channel.UNLIMITED) + + //알람 활성화 상태 + private val _notificationState = MutableStateFlow(AlarmSettingUiState()) val notificationState = _notificationState.asStateFlow() - // 시스템 알림 권한 요청이 필요할 때 UI에 전달하는 이벤트 - private val _permissionEvent = MutableSharedFlow() - val permissionEvent = _permissionEvent.asSharedFlow() - - // 전체 알림 토글 - fun toggleNotification(enabled: Boolean) { - // OFF 전환은 바로 처리 - if (!enabled) { - notificationController.setNotificationEnabled(false) - _notificationState.value = notificationController.getState() - return - } + // 1회성 UI 이벤트 전달용 채널 + private val _sideEffect = Channel(Channel.BUFFERED) + val sideEffect = _sideEffect.receiveAsFlow() + + init { + loadAlarmSetting() + processIntents() + } - // ON 전환 시 시스템 권한 체크 - if (checker.isNotificationEnabled()) { - notificationController.setNotificationEnabled(true) - _notificationState.value = notificationController.getState() + /** + * UI에서 발생한 Intent를 Channel에 전달하는 진입 함수. + * + * ViewModel 외부(UI)에서 발생한 모든 사용자 액션은 + * 이 함수를 통해 Intent로 변환되어 event loop로 전달된다. + * + * 역할: + * - UI → Intent stream 연결 + * - MVI 구조에서 단일 진입점 역할 + * - [sendIntent] → [processIntents] → [handleIntent] 흐름으로 이어짐 + */ + fun sendIntent(intent: NotificationIntent) { + intentChannel.trySend(intent) + } - } else { // 시스템 권한이 없다면 - // UI에 권한 요청 요청 - viewModelScope.launch { - _permissionEvent.emit(Unit) - } + private fun processIntents() { + viewModelScope.launch { + intentChannel.consumeAsFlow() + .collect { handleIntent(it) } } } + private suspend fun handleIntent( + intent: NotificationIntent + ) = when (intent) { + is RefreshSystemAlarm -> refreshSystemAlarm() - // ======== 알림 토글 ======== - // 설정 변경 후 StateFlow를 갱신하여 UI에 반영 + is LinkuNotificationIntent -> + handleLinkuNotificationIntent(intent) + } - fun toggleAiCuration(enabled: Boolean) { - notificationController.setAiCurationEnabled(enabled) - _notificationState.value = notificationController.getState() + private fun refreshSystemAlarm() { + _notificationState.update { + it.copy( + isSystemAlarmAllowed = + checker.isNotificationEnabled() + ) + } } - fun toggleLinkActivity(enabled: Boolean) { - notificationController.setLinkActivityEnabled(enabled) - _notificationState.value = notificationController.getState() + private suspend fun handleLinkuNotificationIntent( + intent: LinkuNotificationIntent + ) { + optimisticUpdate(intent) } - fun toggleSharedFolder(enabled: Boolean) { - notificationController.setSharedFolderEnabled(enabled) - _notificationState.value = notificationController.getState() + private suspend fun optimisticUpdate( + intent: LinkuNotificationIntent + ) { + // 실패 시 롤백할 값 저장 + val previous = _notificationState.value + + // 낙관적 업데이트 수행 + _notificationState.update { state -> + val updated = intent.reduce(state.alarmToggleUiState) + + state.copy( + // updated로 모든 서브알람이 꺼진다면 전테알람도 꺼지게 한다. + alarmToggleUiState = if (updated.areAllSubDisabled()) { + updated.copy(isAllEnabled = false) + } else { + updated + } + ) + } + + // 서버 응답 최종반영 + alarmRepository.updateAlarmSetting(intent.alarmType).fold( + onSuccess = { setting -> + _notificationState.update { + it.copy(alarmToggleUiState = setting) + } + }, + onFailure = { throwable -> + _notificationState.update { currentState -> + currentState.copy( + alarmToggleUiState = previous.alarmToggleUiState + ) + } + + val message = (throwable as AppError).displayMessage + _sideEffect.send(NotificationEffect.ShowToast(message)) + } + ) } - fun toggleSystemNotice(enabled: Boolean) { - notificationController.setSystemNoticeEnabled(enabled) - _notificationState.value = notificationController.getState() + // 화면 진입 시 초기 알람 설정 상태를 load + private fun loadAlarmSetting() { + viewModelScope.launch { + _notificationState.update { + it.copy( + isLoading = true, + isSystemAlarmAllowed = checker.isNotificationEnabled() + ) + } + + alarmRepository.getAlarmSetting() + .fold( + onSuccess = { setting -> + _notificationState.update { it.copy(isLoading = false, alarmToggleUiState = setting) } + }, + onFailure = { throwable -> + _notificationState.update { it.copy(isLoading = false) } + + // 토스트 메세지 발행 + val message = (throwable as AppError).displayMessage + _sideEffect.send(NotificationEffect.ShowToast(message)) + } + ) + } } + } + +// 1회성 사이드 이펙트 +sealed interface NotificationEffect { + data class ShowToast(val message: String) : NotificationEffect +} + +data class AlarmSettingUiState( + val isLoading: Boolean = false, + val isSystemAlarmAllowed: Boolean = false, + val alarmToggleUiState: AlarmSetting = AlarmSetting(), +) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/notification/SystemAlarmTab.kt b/feature/mypage/src/main/java/com/linku/mypage/component/notification/SystemAlarmTab.kt new file mode 100644 index 00000000..518af2d1 --- /dev/null +++ b/feature/mypage/src/main/java/com/linku/mypage/component/notification/SystemAlarmTab.kt @@ -0,0 +1,99 @@ +package com.linku.mypage.component.notification + +import androidx.compose.foundation.Image +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.tooling.preview.Preview +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.LocalColorTheme +import com.linku.mypage.R + +@Composable +fun SystemAlarmTab( + onClick: () -> Unit, + isSystemAlarmAllowed: Boolean, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .clickable { onClick() } + .background(LocalColorTheme.current.white) + .padding(horizontal = 17.dp, vertical = 18.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource( + if (isSystemAlarmAllowed) R.drawable.ic_info_blue + else R.drawable.ic_info_red + ), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .offset(y = (-12).dp) + ) + + // 분기 처리 및 특정 텍스트에만 스타일을 따로 지정 + val text = buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight(400), + color = LocalColorTheme.current.black + ) + ) { + append(if (isSystemAlarmAllowed) "알림이 허용되어 있어요." else "알림이 허용되지 않았어요.") + } + append(if (isSystemAlarmAllowed) "\n기기 설정에서 알림을 변경할 수 있어요." else "\n기기 설정에서 알림을 허용해주세요.") + } + + Text( + text = text, + color = LocalColorTheme.current.gray[600], + fontSize = 14.sp, + modifier = Modifier.padding(start = 12.dp) + ) + } + + Image( + painter = painterResource(R.drawable.ic_long_right), + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SystemAlarmTabPreview() { + LinkuPreview { + SystemAlarmTab( + onClick = {}, + isSystemAlarmAllowed = false + ) + } +} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/linku/mypage/intent/NotificationIntent.kt b/feature/mypage/src/main/java/com/linku/mypage/intent/NotificationIntent.kt new file mode 100644 index 00000000..cbd8e55a --- /dev/null +++ b/feature/mypage/src/main/java/com/linku/mypage/intent/NotificationIntent.kt @@ -0,0 +1,87 @@ +package com.linku.mypage.intent + +import com.linku.core.model.alarm.AlarmSetting +import com.linku.core.model.alarm.AlarmType + +// Intent 최상위 인터페이스 +sealed interface NotificationIntent + +/** + * LinkU 서비스 내 각종 알림(전체, 링크, 폴더, 큐레이션, 공지사항)의 설정 상태를 + * 변경하기 위한 인텐트 인터페이스입니다. + * + * @property enabled 변경하고자 하는 알림의 활성화 상태값 + * @property alarmType 변경 대상이 되는 알림의 종류 ([AlarmType]) + */ +sealed interface LinkuNotificationIntent : NotificationIntent { + val enabled: Boolean + val alarmType: AlarmType + + fun reduce(setting: AlarmSetting): AlarmSetting +} + +data class ToggleAll( + override val enabled: Boolean +) : LinkuNotificationIntent { + override val alarmType = AlarmType.ALL + + override fun reduce(setting: AlarmSetting): AlarmSetting { + return setting.copy( + isAllEnabled = enabled, + isLinkEnabled = enabled, + isFolderEnabled = enabled, + isCurationEnabled = enabled, + isNoticeEnabled = enabled + ) + } +} + +data class ToggleLink( + override val enabled: Boolean +) : LinkuNotificationIntent { + override val alarmType = AlarmType.LINK + + override fun reduce(setting: AlarmSetting): AlarmSetting { + return setting.copy( + isLinkEnabled = enabled + ) + } +} + +data class ToggleFolder( + override val enabled: Boolean +) : LinkuNotificationIntent { + override val alarmType = AlarmType.FOLDER + + override fun reduce(setting: AlarmSetting): AlarmSetting { + return setting.copy( + isFolderEnabled = enabled + ) + } +} + +data class ToggleCuration( + override val enabled: Boolean +) : LinkuNotificationIntent { + override val alarmType = AlarmType.CURATION + + override fun reduce(setting: AlarmSetting): AlarmSetting { + return setting.copy( + isCurationEnabled = enabled + ) + } +} + +data class ToggleNotice( + override val enabled: Boolean +) : LinkuNotificationIntent { + override val alarmType = AlarmType.NOTICE + + override fun reduce(setting: AlarmSetting): AlarmSetting { + return setting.copy( + isNoticeEnabled = enabled + ) + } +} + +data object RefreshSystemAlarm : NotificationIntent \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/AlarmSettingScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/AlarmSettingScreen.kt index eb9b5027..c11a66d0 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/AlarmSettingScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/AlarmSettingScreen.kt @@ -1,12 +1,11 @@ package com.linku.mypage.screen -import android.Manifest -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import android.content.Intent +import android.provider.Settings +import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -14,13 +13,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer @@ -28,43 +30,77 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.linku.design.modifier.noRippleClickable +import com.linku.design.modifier.skeleton import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import androidx.compose.ui.tooling.preview.Preview +import com.linku.core.model.alarm.AlarmSetting +import com.linku.design.theme.ThemeProvider +import com.linku.design.util.OnResumeEffect +import com.linku.mypage.AlarmSettingUiState +import com.linku.mypage.NotificationEffect import com.linku.mypage.NotificationViewModel import com.linku.mypage.R import com.linku.mypage.component.notification.NotificationSwitch import com.linku.mypage.component.notification.SubNotificationSwitch +import com.linku.mypage.component.notification.SystemAlarmTab +import com.linku.mypage.intent.NotificationIntent +import com.linku.mypage.intent.RefreshSystemAlarm +import com.linku.mypage.intent.ToggleAll +import com.linku.mypage.intent.ToggleCuration +import com.linku.mypage.intent.ToggleFolder +import com.linku.mypage.intent.ToggleLink +import com.linku.mypage.intent.ToggleNotice @Composable fun AlarmSettingScreen( navController: NavController, - viewModel: NotificationViewModel = hiltViewModel() + viewModel: NotificationViewModel, ) { val state by viewModel.notificationState.collectAsStateWithLifecycle() + val context = LocalContext.current - //권한 요청 런쳐 - val permissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) viewModel.toggleNotification(true) - } - + // 에러 발생시 Toast를 띄우는 Side Effect LaunchedEffect(Unit) { - viewModel.permissionEvent.collect { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + viewModel.sideEffect.collect { effect -> + when (effect) { + is NotificationEffect.ShowToast -> + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() } } } + + // 액티비티가 ON_RESUME될 때마다 + // 시스템 알림 설정 상태를 최신 값으로 갱신 + // 사용자가 시스템 알람 화면에서 알람 설정을 바꾸고 앱으로 돌아오는 상황에 대응 + OnResumeEffect { + viewModel.sendIntent(RefreshSystemAlarm) + } + + AlarmSettingScreenContent( + state = state, + onBackClick = { navController.popBackStack() }, + onIntent = { intent -> + viewModel.sendIntent(intent) + } // Intent가 발생하면 ViewModel의 Channel로 전달 + ) +} + +@Composable +private fun AlarmSettingScreenContent( + state: AlarmSettingUiState, + onBackClick: () -> Unit, + onIntent: (NotificationIntent) -> Unit +) { Column( modifier = Modifier .fillMaxSize() .background(LocalColorTheme.current.gray[100]) + .verticalScroll(rememberScrollState()) ) { Box( modifier = Modifier @@ -78,7 +114,7 @@ fun AlarmSettingScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .noRippleClickable { navController.popBackStack() } + .noRippleClickable { onBackClick() } ) Text( @@ -93,6 +129,23 @@ fun AlarmSettingScreen( Spacer(modifier = Modifier.height(40.75.dp)) + val context = LocalContext.current + SystemAlarmTab( + onClick = { + // 기기 알람 설정 화면으로 이동 + val intent = Intent( + Settings.ACTION_APP_NOTIFICATION_SETTINGS + ).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + context.startActivity(intent) + }, + isSystemAlarmAllowed = state.isSystemAlarmAllowed, + modifier = Modifier.padding(horizontal = 20.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + + // 알림 수신 설정 토글 Column( modifier = Modifier @@ -106,47 +159,68 @@ fun AlarmSettingScreen( .clip(RoundedCornerShape(22.dp)) .background(LocalColorTheme.current.white) .padding(horizontal = 20.dp, vertical = 18.dp) + .skeleton(isLoading = state.isLoading) ) { NotificationSwitch( - title = "알림 수신 설정", - checked = state.notificationEnabled, - onCheckedChange = { viewModel.toggleNotification(it) } + title = "모든 푸시 알림", + checked = state.alarmToggleUiState.isAllEnabled, + onCheckedChange = { onIntent(ToggleAll(it)) } ) - if (state.notificationEnabled) { + if (state.alarmToggleUiState.isAllEnabled) { Spacer(modifier = Modifier.height(15.dp)) SubNotificationSwitch( title = "링크 활동 알림", - checked = state.linkActivityEnabled, - onCheckedChange = { viewModel.toggleLinkActivity(it) } + checked = state.alarmToggleUiState.isLinkEnabled, + onCheckedChange = { onIntent(ToggleLink(it)) } ) Spacer(modifier = Modifier.height(15.dp)) SubNotificationSwitch( title = "폴더 공유 및 권한 알림", - checked = state.sharedFolderEnabled, - onCheckedChange = { viewModel.toggleSharedFolder(it) } + checked = state.alarmToggleUiState.isFolderEnabled, + onCheckedChange = { onIntent(ToggleFolder(it)) } ) Spacer(modifier = Modifier.height(15.dp)) SubNotificationSwitch( title = "AI 큐레이션 알림", - checked = state.aiCurationEnabled, - onCheckedChange = { viewModel.toggleAiCuration(it) } + checked = state.alarmToggleUiState.isCurationEnabled, + onCheckedChange = { onIntent(ToggleCuration(it)) } ) Spacer(modifier = Modifier.height(15.dp)) SubNotificationSwitch( title = "공지 및 서비스 알림", - checked = state.systemNoticeEnabled, - onCheckedChange = { viewModel.toggleSystemNotice(it) } + checked = state.alarmToggleUiState.isNoticeEnabled, + onCheckedChange = { onIntent(ToggleNotice(it)) } ) } } } } +@Preview(showBackground = true) +@Composable +private fun AlarmSettingScreenPreview() { + ThemeProvider { + AlarmSettingScreenContent( + state = AlarmSettingUiState( + isSystemAlarmAllowed = true, + alarmToggleUiState = AlarmSetting( + isAllEnabled = true, + isLinkEnabled = true, + isFolderEnabled = true, + isCurationEnabled = true, + isNoticeEnabled = true + ) + ), + onBackClick = {}, + onIntent = {} + ) + } +} diff --git a/feature/mypage/src/main/res/drawable/ic_info_blue.xml b/feature/mypage/src/main/res/drawable/ic_info_blue.xml new file mode 100644 index 00000000..202a37b8 --- /dev/null +++ b/feature/mypage/src/main/res/drawable/ic_info_blue.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/feature/mypage/src/main/res/drawable/ic_info_red.xml b/feature/mypage/src/main/res/drawable/ic_info_red.xml new file mode 100644 index 00000000..973f361b --- /dev/null +++ b/feature/mypage/src/main/res/drawable/ic_info_red.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/feature/mypage/src/main/res/drawable/ic_long_right.xml b/feature/mypage/src/main/res/drawable/ic_long_right.xml new file mode 100644 index 00000000..aea753bc --- /dev/null +++ b/feature/mypage/src/main/res/drawable/ic_long_right.xml @@ -0,0 +1,28 @@ + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df8838b6..ac69c173 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,6 @@ coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coilComposeVer coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilComposeVersion" } converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } grid = { module = "com.cheonjaeung.compose.grid:grid", version.ref = "grid" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -117,7 +116,6 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio testng = { group = "org.testng", name = "testng", version.ref = "testng" } androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version = {ref = "media3"}} androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android" } -firebase-messaging-ktx = { group = "com.google.firebase", name = "firebase-messaging-ktx", version.ref = "firebaseMessagingKtx" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigation" } #androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt" } @@ -152,6 +150,14 @@ androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lif paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +firebase-messaging-ktx = { group = "com.google.firebase", name = "firebase-messaging-ktx", version.ref = "firebaseMessagingKtx" } + +[bundles] +firebase = [ + "firebase-bom", + "firebase-messaging-ktx" +] [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 155ec6813e95e12e4213ef1d6bee8c61d76725ad Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:19:09 +0900 Subject: [PATCH 32/89] =?UTF-8?q?:lipstick:=20fillMaxWidth=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EA=B0=92=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/component/AIArticleModal.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index c39ec8b6..176cb403 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -103,7 +103,6 @@ fun SimpleProgressBar(progress: Float, modifier: Modifier = Modifier) { Box( modifier = modifier - .fillMaxWidth() .height(6.dp) .clip(RoundedCornerShape(4.dp)) .background(colors.gray[200]) From 523e33709b45b0e0b599436025a7bf8af9b43360 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:20:29 +0900 Subject: [PATCH 33/89] =?UTF-8?q?:heavy=5Fplus=5Fsign:=20disign=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=97=90=20coil=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- design/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/design/build.gradle.kts b/design/build.gradle.kts index bcdff6b7..2ce2b15e 100644 --- a/design/build.gradle.kts +++ b/design/build.gradle.kts @@ -59,4 +59,7 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + // coil + implementation(libs.coil.compose) } \ No newline at end of file From b8f42c3cd7695ce0f723243ae7204709d0690d4a Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:21:20 +0900 Subject: [PATCH 34/89] =?UTF-8?q?:zap:=20modifier=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/design/component/DeleteLinkItemModal.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 0aa98a23..13638021 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -21,12 +21,13 @@ import com.linku.design.theme.ThemeProvider @Composable fun DeleteLinkItemModal( - onDeleteClick: () -> Unit = { } + onDeleteClick: () -> Unit = { }, + modifier: Modifier ) { val shape = RoundedCornerShape(14.dp) Column( - modifier = Modifier + modifier = modifier .width(120.dp) .shadow( elevation = 10.dp, @@ -53,7 +54,8 @@ fun DeleteLinkItemModal( fun PreviewDeleteLinkItemModal() { ThemeProvider { DeleteLinkItemModal( - onDeleteClick = { } + onDeleteClick = { }, + modifier = Modifier ) } } \ No newline at end of file From c7bb7b62637f61628784fb08bbf060a0d0170f8e Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:23:43 +0900 Subject: [PATCH 35/89] =?UTF-8?q?:lipstick:=20=EB=A1=9C=EA=B3=A0=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=B6=95=EC=86=8C=20=EB=B0=8F=20Preview?= =?UTF-8?q?=EC=97=90=20ThemeProvider=20=EC=B6=94=EA=B0=80,=20FontFamily=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/DeleteLinkModal.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt index 7ba646bf..00da853e 100644 --- a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R @@ -55,7 +56,7 @@ fun DeleteLinkModal( painter = painterResource(R.drawable.ic_linku_blur), contentDescription = null, modifier = Modifier - .height(30.dp) + .height(25.dp) ) } @@ -63,7 +64,6 @@ fun DeleteLinkModal( text = "해당 링크를 삭제하시겠습니까?", fontSize = 18.sp, fontWeight = FontWeight.Medium, - fontFamily = LocalFontTheme.current.font, color = LocalColorTheme.current.black, modifier = Modifier.padding(top = 15.dp) ) @@ -136,8 +136,10 @@ fun DeleteLinkModal( @Preview(showBackground = false) @Composable fun PreviewDeleteLinkModal() { - DeleteLinkModal( - onDismiss = {}, - onConfirm = {} - ) + ThemeProvider { + DeleteLinkModal( + onDismiss = {}, + onConfirm = {} + ) + } } \ No newline at end of file From 9f7a7721ef7d0ccd0e7a410bb8a9d0bcbb2056d5 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:26:44 +0900 Subject: [PATCH 36/89] =?UTF-8?q?:zap:=20DrawableRes=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EC=97=90=20=EC=95=9E=EC=97=90=20param=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=ED=8C=80=EC=9B=90=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/component/EmotionSelect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 8526042c..5305e301 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -36,7 +36,7 @@ import com.linku.home.R private data class EmotionUi( val id: Long, val label: String, - @DrawableRes val iconRes: Int + @param:DrawableRes val iconRes: Int ) private val EMOTIONS = listOf( From 97dc50159b49aa4309aa1fb88193752288452fca Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:31:41 +0900 Subject: [PATCH 37/89] =?UTF-8?q?:zap:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8C=80=EC=9B=90=20=ED=94=BC=EB=93=9C=EB=B0=B1=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 --- .../linku/design/component/LinkCardItem.kt | 109 +++++++++++------- .../main/res/drawable/ic_domain_default.xml | 9 ++ 2 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 design/src/main/res/drawable/ic_domain_default.xml diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index 2ce89b92..a48c7b23 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -1,6 +1,5 @@ package com.linku.design.component -import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -23,12 +22,14 @@ 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.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider @@ -39,12 +40,12 @@ import com.linku.design.R fun LinkCardItem( hasAiSummary: Boolean, linkTitle: String, - tags: List, + tags: List = emptyList(), domainName: String? = null, isExternalLink: Boolean, - @DrawableRes linkImage: Int? = null, - @DrawableRes domainImage: Int? = null, - onClickDelete: () -> Unit + linkImageUrl: String? = null, + domainImageUrl: String? = null, + onDeleteClick: () -> Unit ) { var isMenuVisible by remember { mutableStateOf(false) } @@ -60,13 +61,25 @@ fun LinkCardItem( .padding(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Image( - painter = painterResource(linkImage ?: R.drawable.img_link_default), - contentDescription = null, - modifier = Modifier - .size(85.dp) - .clip(RoundedCornerShape(12.dp)) - ) + if (linkImageUrl.isNullOrBlank()) { + Image( + painter = painterResource(R.drawable.img_link_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } else { + Image( + painter = rememberAsyncImagePainter(model = linkImageUrl), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } Spacer(modifier = Modifier.width(14.dp)) @@ -131,16 +144,24 @@ fun LinkCardItem( Row( verticalAlignment = Alignment.CenterVertically ) { - if (domainImage != null) { + if (!domainImageUrl.isNullOrBlank()) { Image( - painter = painterResource(domainImage), + painter = rememberAsyncImagePainter(model = domainImageUrl), contentDescription = null, + contentScale = ContentScale.Crop, modifier = Modifier.size(16.dp) ) - - Spacer(modifier = Modifier.width(4.dp)) + } else { + Image( + painter = painterResource(R.drawable.ic_domain_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(22.dp) + ) } + Spacer(modifier = Modifier.width(6.dp)) + Text( text = domainName ?: "", fontSize = 12.sp, @@ -151,17 +172,21 @@ fun LinkCardItem( } Box( - modifier = Modifier - .height(85.dp) - .padding(end = 5.dp) - .noRippleClickable { isMenuVisible = !isMenuVisible }, - contentAlignment = Alignment.TopEnd + modifier = Modifier.height(85.dp) ) { - Image( - painter = painterResource(R.drawable.ic_more), - contentDescription = null, - modifier = Modifier.size(17.dp) - ) + Box( + modifier = Modifier + .size(17.dp) + .padding(end = 5.dp) + .noRippleClickable { isMenuVisible = !isMenuVisible }, + contentAlignment = Alignment.TopEnd + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier.size(17.dp) + ) + } } } @@ -176,18 +201,15 @@ fun LinkCardItem( } if (isMenuVisible) { - Box( + DeleteLinkItemModal( + onDeleteClick = { + isMenuVisible = false + onDeleteClick() + }, modifier = Modifier .align(Alignment.TopEnd) .padding(top = 36.dp, end = 12.dp) - ) { - DeleteLinkItemModal( - onDeleteClick = { - isMenuVisible = false - onClickDelete() - } - ) - } + ) } } } @@ -201,10 +223,10 @@ fun PreviewLinkCardItem_HasAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - linkImage = R.drawable.img_genz_trend, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } @@ -218,9 +240,10 @@ fun PreviewLinkCardItem_NoAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } @@ -234,10 +257,10 @@ fun PreviewLinkCardItem_HasOutLink() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = true, - linkImage = R.drawable.img_genz_trend, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_domain_default.xml b/design/src/main/res/drawable/ic_domain_default.xml new file mode 100644 index 00000000..4d9e9edd --- /dev/null +++ b/design/src/main/res/drawable/ic_domain_default.xml @@ -0,0 +1,9 @@ + + + From ae715efceea2cae6204321b30e0d59dfd7eec054 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:32:09 +0900 Subject: [PATCH 38/89] =?UTF-8?q?:art:=20=EC=A4=91=EB=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/LinkDetailCustomDropdown.kt | 141 ++++++------------ 1 file changed, 49 insertions(+), 92 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt index e7e79575..f85fc2b4 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -1,5 +1,6 @@ package com.linku.home.component +import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -42,104 +43,60 @@ fun LinkDetailCustomDropdown( .background(LocalColorTheme.current.white) .padding(horizontal = 24.dp, vertical = 13.dp) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onEditClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_edit), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - - Text( - text = "링크 수정하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onDeleteClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_delete), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_edit, + text = "링크 수정하기", + onClick = { onEditClick() } + ) - Text( - text = "링크 삭제하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_delete, + text = "링크 삭제하기", + onClick = { onDeleteClick() } + ) - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onShareClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_share), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_share, + text = "링크 공유하기", + onClick = { onShareClick() } + ) - Text( - text = "링크 공유하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_go_gray, + text = "링크 보러가기", + onClick = { onGoClick() } + ) + } +} - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onGoClick() } +@Composable +private fun LinkDetailDropdownItem( + @DrawableRes iconRes: Int, + text: String, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_go_gray), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) - Text( - text = "링크 보러가기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) } } } From 58a76d080e349d6cd1477a816dc57fdbaedf81f2 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:34:36 +0900 Subject: [PATCH 39/89] =?UTF-8?q?:art:=20clickable=20=EC=98=81=EC=97=AD=20?= =?UTF-8?q?padding=20=EC=9C=84=EB=A1=9C=20=EC=98=AC=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/screen/LinkDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 84a224b0..63e8d8e5 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -297,7 +297,6 @@ fun LinkDetailScreen( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(LocalColorTheme.current.gray[200]) - .padding(horizontal = 13.5.dp, vertical = 7.dp) .noRippleClickable { coroutineScope.launch { clipboard.setClipEntry( @@ -307,6 +306,7 @@ fun LinkDetailScreen( ) } } + .padding(horizontal = 13.5.dp, vertical = 7.dp) ) } } From f83aa5ed83c488c06b702b463be719ff7691c369 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 16:23:44 +0900 Subject: [PATCH 40/89] =?UTF-8?q?:art:=20=EC=83=81=ED=99=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=EC=B9=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/EmotionSelect.kt | 8 +- .../linku/home/component/SituationSelect.kt | 107 ++++++++++++++++++ .../com/linku/home/screen/SaveLinkScreen.kt | 42 ++++++- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/SituationSelect.kt diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 5305e301..23798b39 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -94,15 +94,11 @@ private fun EmotionBadgeImage( selected: Boolean, onToggle: () -> Unit ) { - val boxBackground = Brush.horizontalGradient( - listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) - ) - Row( modifier = Modifier .clip(RoundedCornerShape(20.dp)) .background( - brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) + brush = if (selected) LocalColorTheme.current.inactiveColor else SolidColor(LocalColorTheme.current.white) ) .then( if (selected) { @@ -130,7 +126,7 @@ private fun EmotionBadgeImage( style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], + color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font ) ) diff --git a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt new file mode 100644 index 00000000..6e4f792b --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt @@ -0,0 +1,107 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +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 com.linku.core.model.Situation +import com.linku.core.model.SituationOptions +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.color.Basic + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SituationSelect( + jobId: Long, + selectedSituationId: Long?, + onSituationSelect: (Long?) -> Unit, + modifier: Modifier = Modifier +) { + val situations = SituationOptions.situationsFor(jobId) + + FlowRow( + modifier = modifier + .fillMaxWidth() + .padding(top = 13.dp, start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + situations.forEach { situation -> + SituationChip( + situation = situation, + selected = selectedSituationId == situation.id, + onClick = { + onSituationSelect( + if (selectedSituationId == situation.id) null else situation.id + ) + } + ) + } + } +} + +@Composable +private fun SituationChip( + situation: Situation, + selected: Boolean, + onClick: () -> Unit +) { + Text( + text = situation.tagName, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + modifier = Modifier + .background( + brush = if (selected) { + LocalColorTheme.current.inactiveColor + } else { + SolidColor(LocalColorTheme.current.white) + }, + shape = RoundedCornerShape(20.dp) + ) + .then( + if (selected) { + Modifier.border( + width = 1.dp, + brush = Basic.maincolor, + shape = RoundedCornerShape(20.dp) + ) + } else { + Modifier.border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(20.dp) + ) + } + ) + .noRippleClickable { onClick() } + .padding(horizontal = 12.dp, vertical = 9.dp) + ) +} + +@Preview(showBackground = false) +@Composable +fun PreviewSituationSelect() { + ThemeProvider { + SituationSelect( + jobId = 3L, + selectedSituationId = 18L, + onSituationSelect = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 1035f0e0..391beb26 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -40,6 +40,7 @@ import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R import com.linku.home.component.EmotionSelect +import com.linku.home.component.SituationSelect import java.io.File @Composable @@ -49,11 +50,14 @@ fun SaveLinkScreen( title: String = "", memo: String, selectedEmotionId: Long?, + selectedSituationId: Long?, + jobId: Long, onPickImage: () -> Unit, onUrlChange: (String) -> Unit, onTitleChange: (String) -> Unit, onMemoChange: (String) -> Unit, onEmotionSelect: (Long?) -> Unit, + onSituationSelect: (Long?) -> Unit, onSaveClick: () -> Unit, onBack: () -> Unit, isCheckingUrl: Boolean, @@ -70,7 +74,11 @@ fun SaveLinkScreen( !isInvalidLink && (isDuplicateUrl != true) - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalColorTheme.current.white) + ) { Column( modifier = Modifier .fillMaxSize() @@ -385,7 +393,34 @@ fun SaveLinkScreen( onEmotionSelect = onEmotionSelect ) - Spacer(modifier = Modifier.height(100.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 25.dp, start = 24.dp, end = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "상황", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] + ) + + Text( + text = "선택", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] + ) + } + + SituationSelect( + jobId = jobId, + selectedSituationId = selectedSituationId, + onSituationSelect = onSituationSelect + ) + + Spacer(modifier = Modifier.height(70.dp)) } Column( @@ -430,11 +465,14 @@ fun PreviewSaveLinkScreen() { title = "", memo = "", selectedEmotionId = null, + selectedSituationId = null, + jobId = 2L, onPickImage = { }, onUrlChange = { }, onTitleChange = { }, onMemoChange = { }, onEmotionSelect = { }, + onSituationSelect = { }, onSaveClick = { }, onBack = { }, isCheckingUrl = false, From 4177d52f70321ab8dd0617b865f7bfbb42123f48 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 16:29:48 +0900 Subject: [PATCH 41/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=97=90=20=EC=83=81=ED=99=A9=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 15 ++++++++++++--- .../src/main/java/com/linku/home/HomeViewModel.kt | 8 ++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index b7fe2556..0f032774 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -464,17 +464,26 @@ fun MainApp( title = vm.title, memo = vm.memo, selectedEmotionId = vm.selectedEmotionId, + selectedSituationId = vm.selectedSituationId, + jobId = vm.jobId ?: 3L, onPickImage = { imagePicker.launch("image/*") }, onUrlChange = vm::setUrl, onTitleChange = vm::setTitle, onMemoChange = vm::setMemo, onEmotionSelect = vm::selectEmotion, + onSituationSelect = vm::selectSituation, onSaveClick = { - // 저장 버튼 로그 + API 호출 - Log.d("SaveLink", "try save -> url=${vm.url}, memo=${vm.memo}, emotionId=${vm.selectedEmotionId}, image=${vm.image?.name}") + Log.d( + "SaveLink", + "try save -> url=${vm.url}, memo=${vm.memo}, emotionId=${vm.selectedEmotionId}, situationId=${vm.selectedSituationId}, image=${vm.image?.name}" + ) + vm.saveLink( onSucceed = { saved -> - Log.d("SaveLink", "success -> id=${saved.linkuId}, title=${saved.title}, domain=${saved.domain}") + Log.d( + "SaveLink", + "success -> id=${saved.linkuId}, title=${saved.title}, domain=${saved.domain}" + ) vm.loadLinkDetail(saved.linkuId) vm.resetForm() navigator.navigate("savelinkresult/${saved.linkuId}") diff --git a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt index 575188b0..417339a7 100644 --- a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt @@ -155,6 +155,7 @@ class HomeViewModel @Inject constructor( private val titleState = mutableStateOf("") private val memoState = mutableStateOf("") private val emotionIdState = mutableStateOf(null) + private val situationIdState = mutableStateOf(null) private val isSavingState = mutableStateOf(false) // URL 유효성 검사 @@ -167,6 +168,7 @@ class HomeViewModel @Inject constructor( val title get() = titleState.value val memo get() = memoState.value val selectedEmotionId get() = emotionIdState.value + val selectedSituationId get() = situationIdState.value val isSaving get() = isSavingState.value val isCheckingUrl get() = isCheckingUrlState.value @@ -216,7 +218,7 @@ class HomeViewModel @Inject constructor( fun setTitle(newTitle: String) { titleState.value = newTitle } fun setMemo(newMemo: String) { memoState.value = newMemo } fun selectEmotion(id: Long?) { emotionIdState.value = id } - + fun selectSituation(id: Long?) { situationIdState.value = id } // 저장 폼 초기화 @@ -226,6 +228,7 @@ class HomeViewModel @Inject constructor( titleState.value = "" memoState.value = "" emotionIdState.value = null + situationIdState.value = null } // 최근 조회 링크 상태 @@ -273,7 +276,8 @@ class HomeViewModel @Inject constructor( image = imageState.value, url = currentUrl, memo = memoState.value.ifBlank { null }, - emotionId = emotionIdState.value + emotionId = emotionIdState.value, + // situationId = situationIdState.value ) // 낙관적 업데이트: 메모리의 최근 목록 즉시 갱신 From 1f14c4fcef3628d0629461407f019c4895b95f79 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:16:49 +0900 Subject: [PATCH 42/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/HomeApp.kt | 1 + .../com/linku/home/component/EmotionSelect.kt | 149 +++++++ .../com/linku/home/screen/SaveLinkScreen.kt | 415 ++++++++---------- .../home/src/main/res/drawable/ic_camera.xml | 9 + 4 files changed, 345 insertions(+), 229 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera.xml diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index 6ec7b301..df2305a2 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -125,6 +125,7 @@ fun HomeApp( SaveLinkScreen( image = viewModel.image, url = viewModel.url, +// title = viewModel.title, memo = viewModel.memo, selectedEmotionId = viewModel.selectedEmotionId, onPickImage = { imagePicker.launch("image/*") }, diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt new file mode 100644 index 00000000..8526042c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -0,0 +1,149 @@ +package com.linku.home.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.color.Basic +import com.linku.home.R + +private data class EmotionUi( + val id: Long, + val label: String, + @DrawableRes val iconRes: Int +) + +private val EMOTIONS = listOf( + EmotionUi(1L, "즐거움", R.drawable.ic_joy), + EmotionUi(2L, "평온", R.drawable.ic_calm), + EmotionUi(3L, "설렘", R.drawable.ic_excite), + EmotionUi(4L, "우울", R.drawable.ic_sad), + EmotionUi(5L, "짜증", R.drawable.ic_irritation), + EmotionUi(6L, "분노", R.drawable.ic_anger), +) + +@Composable +fun EmotionSelect( + selectedEmotionId: Long?, + onEmotionSelect: (Long?) -> Unit +) { + val firstRow = EMOTIONS.take(3) + val secondRow = EMOTIONS.drop(3) + + Column( + modifier = Modifier.padding(top = 13.dp, start = 20.dp) + ) { + Row { + firstRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row { + secondRow.forEach { e -> + EmotionBadgeImage( + iconRes = e.iconRes, + label = e.label, + selected = selectedEmotionId == e.id, + onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + } +} + +@Composable +private fun EmotionBadgeImage( + @DrawableRes iconRes: Int, + label: String, + selected: Boolean, + onToggle: () -> Unit +) { + val boxBackground = Brush.horizontalGradient( + listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) + ) + + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background( + brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) + ) + .then( + if (selected) { + Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) + } + ) + .noRippleClickable { onToggle() } + .padding(horizontal = 15.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 아이콘 + Image( + painter = painterResource(id = iconRes), + contentDescription = label, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(5.dp)) + + // 라벨 + Text( + text = label, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], + fontFamily = LocalFontTheme.current.font + ) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewEmotionSelect() { + ThemeProvider { + EmotionSelect( + selectedEmotionId = 1, + onEmotionSelect = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 3fca190a..967eb32e 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -11,6 +11,7 @@ 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 @@ -36,32 +37,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import coil3.compose.rememberAsyncImagePainter +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R +import com.linku.home.component.EmotionSelect import java.io.File -private data class EmotionUi( - val id: Long, - val label: String, - @DrawableRes val iconRes: Int -) - -private val EMOTIONS = listOf( - EmotionUi(1L, "즐거움", R.drawable.ic_joy), - EmotionUi(2L, "평온", R.drawable.ic_calm), - EmotionUi(3L, "설렘", R.drawable.ic_excite), - EmotionUi(4L, "우울", R.drawable.ic_sad), - EmotionUi(5L, "짜증", R.drawable.ic_irritation), - EmotionUi(6L, "분노", R.drawable.ic_anger), -) - @Composable fun SaveLinkScreen( image: File?, url: String, + title: String? = "", memo: String, selectedEmotionId: Long?, onPickImage: () -> Unit, @@ -91,32 +82,35 @@ fun SaveLinkScreen( .padding(bottom = 70.dp) .verticalScroll(scrollState) ) { - Row( + Box( modifier = Modifier .fillMaxWidth() - .padding(top = 59.dp, start = 20.dp), - verticalAlignment = Alignment.CenterVertically + .padding(top = 59.dp, start = 20.dp, end = 20.dp) + .height(24.dp) ) { Image( painter = painterResource(R.drawable.ic_back), contentDescription = null, modifier = Modifier - .size(width = 10.dp, height = 16.25.dp) - .clickable { onBack() } + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } ) - Spacer(modifier = Modifier.width(131.dp)) - Text( text = "새로운 링크", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.black + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.align(Alignment.Center) ) } Text( - text = "URL 링크 입력", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + text = "URL 링크", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.black, modifier = Modifier.padding(top = 31.dp, start = 24.dp) ) @@ -124,16 +118,22 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp, bottom = 12.dp) - .height(50.dp) - .border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) - .padding(horizontal = 22.dp), + .padding(top = 13.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (url.isEmpty()) { Text( text = "링크를 입력하거나 붙여넣어 주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -147,76 +147,134 @@ fun SaveLinkScreen( ) } - // URL 검사 결과 메시지 - when { - url.isBlank() -> Unit - showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") - isCheckingUrl -> Text( - text = "링크를 확인 중입니다…", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[600], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 17.dp, start = 24.dp, end = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "링크 제목", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black, ) - isInvalidLink -> { - WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") - } - isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") - isDuplicateUrl == false -> Text( - text = "저장 가능한 링크예요.", - style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), + + Text( + text = "선택", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(start = 32.dp, top = 4.dp) ) - else -> Unit } -// if (isInvalidLink) { -// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp, start = 20.dp, end = 20.dp) + .then( + if (url == "") { + Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) + } else { + Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) + } + ) + .padding(horizontal = 22.dp, vertical = 15.dp), + contentAlignment = Alignment.CenterStart + ) { + if (url.isEmpty()) { + Text( + text = "링크 제목을 입력해주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400] + ) + } + + BasicTextField( + value = url, // TODO: 추후 API 파라미터에 링크 제목 추가되면 바꾸기 + onValueChange = onUrlChange, + singleLine = true, + textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + modifier = Modifier.fillMaxWidth() + ) + } + +// // URL 검사 결과 메시지 +// when { +// url.isBlank() -> Unit +// showVideoWarning -> WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +// isCheckingUrl -> Text( +// text = "링크를 확인 중입니다…", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.gray[600], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// isInvalidLink -> { +// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +// } +// isDuplicateUrl == true -> WarningText("이미 저장된 링크예요.") +// isDuplicateUrl == false -> Text( +// text = "저장 가능한 링크예요.", +// style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), +// color = LocalColorTheme.current.blue[200], +// modifier = Modifier.padding(start = 32.dp, top = 4.dp) +// ) +// else -> Unit // } // -// if (showVideoWarning) { -// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// if (isInvalidLink) { +//// WarningText("유효하지 않은 링크입니다! 다시 입력해주세요.") +//// } +//// +//// if (showVideoWarning) { +//// WarningText("현재 링큐에서는 영상 콘텐츠를 지원하지 않아요!") +//// } +// +// // 둘 다 false일 때만 Spacer 추가 +// if (!isInvalidLink && !showVideoWarning) { +// Spacer(modifier = Modifier.height(12.dp)) // } - // 둘 다 false일 때만 Spacer 추가 - if (!isInvalidLink && !showVideoWarning) { - Spacer(modifier = Modifier.height(12.dp)) - } - Column( modifier = Modifier .fillMaxWidth() - .height(209.3.dp) - .padding(top = 18.dp, start = 20.dp, end = 20.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) - .clickable { onPickImage() }, - horizontalAlignment = Alignment.CenterHorizontally + .padding(start = 20.dp, end = 20.dp, top = 19.dp) + .noRippleClickable { onPickImage() }, + horizontalAlignment = Alignment.Start ) { if (image != null) { Image( painter = rememberAsyncImagePainter(model = image), contentDescription = "선택된 이미지", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop // ✅ 박스에 꽉 차도록 + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) ) } else { Column( + modifier = Modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(R.drawable.ic_transparent_logo), + painter = painterResource(R.drawable.ic_camera), contentDescription = null, - modifier = Modifier - .height(120.dp) - .padding(top = 50.dp) + modifier = Modifier.height(24.dp) ) + Text( - text = "이미지 업로드하기", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, fontFamily = LocalFontTheme.current.font), + text = "사진 추가", + fontSize = 14.sp, + fontWeight = FontWeight.Light, color = LocalColorTheme.current.gray[500], - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 7.dp) ) } } @@ -225,29 +283,28 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 27.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "메모", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } Box( modifier = Modifier .fillMaxWidth() - .padding(top = 15.dp, start = 20.dp, end = 20.dp) - .height(50.dp) + .padding(top = 13.dp, start = 20.dp, end = 20.dp) .then( if (memo.isEmpty()) { Modifier.border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) @@ -258,14 +315,15 @@ fun SaveLinkScreen( ) } ) - .padding(horizontal = 22.dp), + .padding(horizontal = 22.dp, vertical = 16.dp), contentAlignment = Alignment.CenterStart ) { if (memo.isEmpty()) { Text( text = "메모할 내용을 입력해주세요.", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 14.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[400] ) } @@ -273,7 +331,6 @@ fun SaveLinkScreen( BasicTextField( value = memo, onValueChange = { if (it.length <= 200) onMemoChange(it) }, - singleLine = true, textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), modifier = Modifier.fillMaxWidth() ) @@ -282,41 +339,44 @@ fun SaveLinkScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(end = 32.dp, top = 12.dp), + .padding(end = 32.dp, top = 10.dp), horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = memo.length.toString(), - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), + fontSize = 12.sp, + fontWeight = FontWeight.Normal, color = LocalColorTheme.current.gray[700] ) Text( text = "/200자", - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.gray[400], + modifier = Modifier.padding(start = 1.dp) ) } Row( modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp, start = 20.dp, end = 20.dp), + .padding(top = 25.dp, start = 24.dp, end = 32.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "감정", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 8.dp) + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] ) Text( text = "선택", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] ) } @@ -325,7 +385,7 @@ fun SaveLinkScreen( onEmotionSelect = onEmotionSelect ) - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(100.dp)) } Column( @@ -336,152 +396,49 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxWidth() - .height(50.dp) .clip(RoundedCornerShape(18.dp)) - .background( - brush = if (isButtonEnabled) { - Basic.maincolor + .noRippleClickable(enabled = isButtonEnabled) { onSaveClick() } + .then ( + if (isButtonEnabled) { + Modifier.background(Basic.maincolor) } else { - Brush.horizontalGradient( - listOf( - Color(0x1A2C6FFF), - Color(0x1AC800FF) - ) - ) + Modifier.background(LocalColorTheme.current.gray[300]) } ) - - .clickable(enabled = isButtonEnabled) { onSaveClick() }, + .padding(vertical = 15.dp), contentAlignment = Alignment.Center ) { Text( text = "저장", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - } - } -} - -@Composable -fun WarningText( - message: String, - modifier: Modifier = Modifier -) { - Text( - text = message, - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal), fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.negative, - modifier = modifier.padding(start = 32.dp) - ) -} - -@Composable -fun EmotionSelect( - selectedEmotionId: Long?, - onEmotionSelect: (Long?) -> Unit -) { - val firstRow = EMOTIONS.take(4) - val secondRow = EMOTIONS.drop(4) - - Column(modifier = Modifier.padding(top = 15.dp, start = 20.dp)) { - Row { - firstRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } - ) - Spacer(modifier = Modifier.width(10.dp)) - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row { - secondRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white, + textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.width(10.dp)) } } } } -@Composable -private fun EmotionBadgeImage( - @DrawableRes iconRes: Int, - label: String, - selected: Boolean, - onToggle: () -> Unit -) { - val boxBackground = Brush.horizontalGradient( - listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) - ) - - Row( - modifier = Modifier - .clip(RoundedCornerShape(20.dp)) - .background( - brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) - ) - .then( - if (selected) { - Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) - } else { - Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) - } - ) - .clickable { onToggle() } - .padding(horizontal = 15.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 아이콘 - Image( - painter = painterResource(id = iconRes), - contentDescription = label, - modifier = Modifier.size(20.dp) - ) - - Spacer(modifier = Modifier.width(5.dp)) - - // 라벨 - Text( - text = label, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], - fontFamily = LocalFontTheme.current.font - ) - ) - } -} - @Preview(showBackground = true) @Composable fun PreviewSaveLinkScreen() { - SaveLinkScreen( - image = null, - url = "", - memo = "", - selectedEmotionId = null, - onPickImage = {}, - onUrlChange = {}, - onMemoChange = {}, - onEmotionSelect = {}, - onSaveClick = {}, - onBack = {}, - isCheckingUrl = false, - isDuplicateUrl = null, - isInvalidLink = false - ) + ThemeProvider { + SaveLinkScreen( + image = null, + url = "", + title = "", + memo = "", + selectedEmotionId = null, + onPickImage = {}, + onUrlChange = {}, + onMemoChange = {}, + onEmotionSelect = {}, + onSaveClick = {}, + onBack = {}, + isCheckingUrl = false, + isDuplicateUrl = null, + isInvalidLink = false + ) + } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera.xml b/feature/home/src/main/res/drawable/ic_camera.xml new file mode 100644 index 00000000..7474098b --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,9 @@ + + + From 14c80b286d42889fbf7d98b36f2a2d2c11498a7f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Mon, 11 May 2026 01:17:09 +0900 Subject: [PATCH 43/89] =?UTF-8?q?:sparkles:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/CustomToastMessage.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt diff --git a/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt new file mode 100644 index 00000000..0fa4b42c --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt @@ -0,0 +1,52 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun CustomToastMessage( + backgroundColor: Color, + textColor: Color, + toastMessage: String, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background(color = backgroundColor) + .padding(horizontal = 18.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = toastMessage, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = textColor + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewCustomToastMessage() { + ThemeProvider { + CustomToastMessage( + backgroundColor = Color(0xFFE0FBEB), + textColor = LocalColorTheme.current.positive, + toastMessage = "유효한 링크입니다!" + ) + } +} From 695615542518378a5cdd9fa4f67f4c9a48f4c8e3 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 03:44:42 +0900 Subject: [PATCH 44/89] :sparkles: Rebase 'chore/#126-link' onto develop --- .../home/ui/home/bar/LinkDetailTopBar.kt | 266 ++++++++++++++++++ .../src/main/res/drawable/ic_link_delete.xml | 9 + .../src/main/res/drawable/ic_link_edit.xml | 9 + .../home/src/main/res/drawable/ic_link_go.xml | 12 +- .../src/main/res/drawable/ic_link_go_gray.xml | 12 + .../src/main/res/drawable/ic_link_share.xml | 12 + .../home/src/main/res/drawable/ic_more.xml | 15 + .../res/drawable/linku_logo_transparent.xml | 16 ++ 8 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt create mode 100644 feature/home/src/main/res/drawable/ic_link_delete.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_edit.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_go_gray.xml create mode 100644 feature/home/src/main/res/drawable/ic_link_share.xml create mode 100644 feature/home/src/main/res/drawable/ic_more.xml create mode 100644 feature/home/src/main/res/drawable/linku_logo_transparent.xml diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt new file mode 100644 index 00000000..373ecc6e --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -0,0 +1,266 @@ +package com.linku.home.ui.top.bar + +import androidx.compose.foundation.Image +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.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.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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.AbsoluteAlignment +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailTopBar( + linkTitle: String, + category: String, + emotion: String, + onBack: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onLinkGoClick: () -> Unit, +) { + var isMenuExpanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp)) + .background(LocalColorTheme.current.blue[200]) + ) { + Image( + painter = painterResource(R.drawable.linku_logo_transparent), + contentDescription = null, + modifier = Modifier + .height(110.dp) + .align(Alignment.TopEnd) + ) + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 59.dp, start = 20.dp, end = 24.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_back_white), + contentDescription = "뒤로가기", + modifier = Modifier + .align(Alignment.CenterStart) + .width(11.dp) + .noRippleClickable { onBack() } + ) + + Text( + text = "새로운 링크", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.white, + modifier = Modifier.align(Alignment.Center) + ) + + Box( + modifier = Modifier + .size(18.dp) + .align(Alignment.CenterEnd) + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = "더보기", + modifier = Modifier + .height(18.dp) + .align(AbsoluteAlignment.TopRight) + .noRippleClickable { + isMenuExpanded = true + } + ) + + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { + isMenuExpanded = false + } + ) { + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_edit, + text = "링크 수정하기", + onClick = { + isMenuExpanded = false + onEditClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_delete, + text = "링크 삭제하기", + onClick = { + isMenuExpanded = false + onDeleteClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_share, + text = "링크 공유하기", + onClick = { + isMenuExpanded = false + onShareClick() + } + ) + + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_go_gray, + text = "링크 보러가기", + onClick = { + isMenuExpanded = false + onLinkGoClick() + } + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + ) { + Text( + text = linkTitle, + fontSize = 24.sp, // 편집모드에서는 22.sp + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(LocalColorTheme.current.purple[50]) + .padding(horizontal = 10.dp, vertical = 3.dp) + ) + } + + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } + } + } + } + } +} + +@Composable +private fun LinkDetailDropdownItem( + iconRes: Int, + text: String, + onClick: () -> Unit, +) { + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(18.dp) + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(28.dp) + ) + + Text( + text = text, + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] + ) + } + }, + onClick = onClick, + modifier = Modifier + .height(64.dp) + .padding(horizontal = 12.dp) + ) +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailTopBar() { + ThemeProvider { + LinkDetailTopBar( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + onBack = { }, + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onLinkGoClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_link_delete.xml b/feature/home/src/main/res/drawable/ic_link_delete.xml new file mode 100644 index 00000000..2b4edfe4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_edit.xml b/feature/home/src/main/res/drawable/ic_link_edit.xml new file mode 100644 index 00000000..c643835c --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_link_go.xml b/feature/home/src/main/res/drawable/ic_link_go.xml index ec3d535f..f8b73ba9 100644 --- a/feature/home/src/main/res/drawable/ic_link_go.xml +++ b/feature/home/src/main/res/drawable/ic_link_go.xml @@ -1,16 +1,16 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + + + diff --git a/feature/home/src/main/res/drawable/ic_link_share.xml b/feature/home/src/main/res/drawable/ic_link_share.xml new file mode 100644 index 00000000..e8f557a3 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_link_share.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/home/src/main/res/drawable/ic_more.xml b/feature/home/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..7e149ac0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/linku_logo_transparent.xml b/feature/home/src/main/res/drawable/linku_logo_transparent.xml new file mode 100644 index 00000000..41ea4346 --- /dev/null +++ b/feature/home/src/main/res/drawable/linku_logo_transparent.xml @@ -0,0 +1,16 @@ + + + + From a3e4141e482786f0004830c8cc624c26e9343baa Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 04:35:53 +0900 Subject: [PATCH 45/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/screen/LinkDetailScreen.kt | 212 ++++++++++++++++++ .../home/ui/home/bar/LinkDetailTopBar.kt | 66 +----- .../src/main/res/drawable/ic_sparkles.xml | 10 + 3 files changed, 228 insertions(+), 60 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt create mode 100644 feature/home/src/main/res/drawable/ic_sparkles.xml diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt new file mode 100644 index 00000000..e01e9354 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -0,0 +1,212 @@ +package com.linku.home.screen + +import android.content.ClipData +import androidx.compose.foundation.Image +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.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.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalUriHandler +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R +import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.launch + +@Composable +fun LinkDetailScreen( + linkTitle: String, + category: String, + emotion: String, + linkUrl: String, + memo: String, + onBack: () -> Unit, + onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +) { + val clipboard = LocalClipboard.current + val coroutineScope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current + + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalColorTheme.current.white) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + LinkDetailTopBar( + linkTitle = linkTitle, + category = category, + emotion = emotion, + onBack = { onBack() }, + onMoreClick = { }, + onLinkGoClick = { uriHandler.openUri(linkUrl) }, + ) + + Column( + modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + ) { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + .background(LocalColorTheme.current.white) + .padding(top = 7.5.dp, start = 22.dp, end = 8.5.dp, bottom = 7.5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = linkUrl, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) + ) + } + } + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 22.dp) + ) { + Text( + text = "메모", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = memo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) + + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewLinkDetailScreen() { + ThemeProvider { + LinkDetailScreen( + linkTitle = "3일만에 오픽 AL 꿀팁", + category = "어학", + emotion = "평온", + linkUrl = "https://blog.naver.com/linkU/1234", + memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + onBack = { }, + onMoreClick = { }, + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index 373ecc6e..c058c76a 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -1,4 +1,4 @@ -package com.linku.home.ui.top.bar +package com.linku.home.ui.home.bar import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -6,21 +6,15 @@ 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.DropdownMenu import androidx.compose.material3.DropdownMenuItem 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.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,12 +35,9 @@ fun LinkDetailTopBar( category: String, emotion: String, onBack: () -> Unit, - onEditClick: () -> Unit, - onDeleteClick: () -> Unit, - onShareClick: () -> Unit, + onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, ) { - var isMenuExpanded by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -91,6 +82,9 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) + .noRippleClickable { + onMoreClick() + } ) { Image( painter = painterResource(R.drawable.ic_more), @@ -98,53 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .height(18.dp) .align(AbsoluteAlignment.TopRight) - .noRippleClickable { - isMenuExpanded = true - } ) - - DropdownMenu( - expanded = isMenuExpanded, - onDismissRequest = { - isMenuExpanded = false - } - ) { - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_edit, - text = "링크 수정하기", - onClick = { - isMenuExpanded = false - onEditClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_delete, - text = "링크 삭제하기", - onClick = { - isMenuExpanded = false - onDeleteClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_share, - text = "링크 공유하기", - onClick = { - isMenuExpanded = false - onShareClick() - } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_go_gray, - text = "링크 보러가기", - onClick = { - isMenuExpanded = false - onLinkGoClick() - } - ) - } } } @@ -257,9 +205,7 @@ fun PreviewLinkDetailTopBar() { category = "어학", emotion = "평온", onBack = { }, - onEditClick = { }, - onDeleteClick = { }, - onShareClick = { }, + onMoreClick = { }, onLinkGoClick = { }, ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles.xml b/feature/home/src/main/res/drawable/ic_sparkles.xml new file mode 100644 index 00000000..de8029a2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles.xml @@ -0,0 +1,10 @@ + + + From 72b7beb03bb92e3331fc9292b1e716464d53b649 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 19:14:00 +0900 Subject: [PATCH 46/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=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 --- .../component/LinkDetailCustomDropdown.kt | 160 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 51 +++++- 2 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt new file mode 100644 index 00000000..e7e79575 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -0,0 +1,160 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailCustomDropdown( + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onShareClick: () -> Unit, + onGoClick: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier +) { + Column( + modifier = modifier + .width(240.dp) + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 24.dp, vertical = 13.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onEditClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_edit), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 수정하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onDeleteClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_delete), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 삭제하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onShareClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_share), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 공유하기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onGoClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_link_go_gray), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + + Text( + text = "링크 보러가기", + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCustomDropdown() { + ThemeProvider { + LinkDetailCustomDropdown( + onEditClick = { }, + onDeleteClick = { }, + onShareClick = { }, + onGoClick = { }, + onDismiss = { }, + modifier = Modifier + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index e01e9354..01425848 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -20,7 +20,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll 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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,6 +41,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch @@ -54,6 +59,8 @@ fun LinkDetailScreen( val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + var isDropdownVisible by remember { mutableStateOf(false) } + Box( modifier = Modifier .fillMaxSize() @@ -62,19 +69,23 @@ fun LinkDetailScreen( Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) ) { LinkDetailTopBar( linkTitle = linkTitle, category = category, emotion = emotion, onBack = { onBack() }, - onMoreClick = { }, + onMoreClick = { + isDropdownVisible = !isDropdownVisible + }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, ) Column( - modifier = Modifier.padding(top = 25.dp, start = 20.dp, end = 20.dp) + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { Image( painter = painterResource(R.drawable.img_default), @@ -168,23 +179,53 @@ fun LinkDetailScreen( } } + if (isDropdownVisible) { + LinkDetailCustomDropdown( + onEditClick = { + isDropdownVisible = false + // 수정 화면 이동 로직 추가 예정 + }, + onDeleteClick = { + isDropdownVisible = false + // 삭제 로직 추가 예정 + }, + onShareClick = { + isDropdownVisible = false + // 공유 로직 추가 예정 + }, + onGoClick = { + isDropdownVisible = false + // 링크 Open 로직 추가 예정 + }, + onDismiss = { + isDropdownVisible = false + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 100.dp, end = 20.dp), + ) + } + Row( modifier = Modifier .fillMaxWidth() + .padding(horizontal = 20.dp) .align(Alignment.BottomCenter) .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) .noRippleClickable { }, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.Center ) { Image( painter = painterResource(R.drawable.ic_sparkles), contentDescription = null, modifier = Modifier.height(17.51.dp) ) - + + Spacer(modifier = Modifier.width(10.dp)) + Text( text = "AI 요약", fontSize = 16.sp, From 36560309104feba3a3bd75be2f9aa3fd4556b1e2 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:09:49 +0900 Subject: [PATCH 47/89] =?UTF-8?q?:recycle:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 58 ++- .../java/com/linku/core/model/Situation.kt | 106 ++++++ .../component/LinkDetailCategoryDropdown.kt | 101 +++++ .../component/LinkDetailEmotionDropdown.kt | 99 +++++ .../component/LinkDetailSituationDropdown.kt | 76 ++++ .../com/linku/home/screen/LinkDetailScreen.kt | 357 ++++++++++++++---- .../home/ui/home/bar/LinkDetailTopBar.kt | 298 +++++++++++---- .../src/main/res/drawable/ic_camera_white.xml | 9 + .../src/main/res/drawable/ic_delete_blue.xml | 21 ++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++ 10 files changed, 1024 insertions(+), 140 deletions(-) create mode 100644 core/src/main/java/com/linku/core/model/Situation.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt create mode 100644 feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt create mode 100644 feature/home/src/main/res/drawable/ic_camera_white.xml create mode 100644 feature/home/src/main/res/drawable/ic_delete_blue.xml create mode 100644 feature/home/src/main/res/drawable/ic_linku_blur.xml diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index fbb6c3b4..5bc21075 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,17 +1,59 @@ package com.linku.core.model +import androidx.annotation.DrawableRes +import com.linku.design.R + enum class EmotionType( val id: Long, - val tagName: String + val tagName: String, + @DrawableRes val imgRes: Int ) { - JOY(1, "즐거움"), - PEACE(2, "평온"), - EXCITEMENT(3, "설렘"), - SADNESS(4, "슬픔"), - ANNOYANCE(5, "짜증"), - ANGER(6, "분노"); + JOY( + id = 1L, + tagName = "즐거움", + imgRes = R.drawable.ic_joy + ), + CALM( + id = 2L, + tagName = "평온", + imgRes = R.drawable.ic_calm + ), + EXCITE( + id = 3L, + tagName = "설렘", + imgRes = R.drawable.ic_excite + ), + SAD( + id = 4L, + tagName = "슬픔", + imgRes = R.drawable.ic_sad + ), + IRRITATION( + id = 5L, + tagName = "짜증", + imgRes = R.drawable.ic_irritation + ), + ANGER( + id = 6L, + tagName = "분노", + imgRes = R.drawable.ic_anger + ); companion object { - fun fromId(id: Long): EmotionType? = values().find { it.id == id } + fun fromId(id: Long?): EmotionType? { + return entries.firstOrNull { it.id == id } + } + + fun fromTagName(tagName: String?): EmotionType? { + return entries.firstOrNull { it.tagName == tagName } + } + + fun tagNameOf(id: Long?): String? { + return fromId(id)?.tagName + } + + fun idOf(tagName: String?): Long? { + return fromTagName(tagName)?.id + } } } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt new file mode 100644 index 00000000..a0fa486f --- /dev/null +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -0,0 +1,106 @@ +package com.linku.core.model + +data class Situation( + val id: Long, + val tagName: String +) + +object SituationOptions { + val linkDetailSituations: List = listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "휴식 중"), + Situation(4L, "이동 중"), + Situation(5L, "식사 중"), + Situation(6L, "자기 전") + ) + + fun situationsFor(jobId: Long): List = when (jobId) { + 1L -> listOf( + Situation(1L, "통학 중"), + Situation(2L, "공부 중"), + Situation(3L, "식사 중"), + Situation(4L, "시험 준비"), + Situation(5L, "친구랑"), + Situation(6L, "쇼핑 중"), + Situation(7L, "휴식 중"), + Situation(8L, "자기 전") + ) + + 2L -> listOf( + Situation(9L, "과제 중"), + Situation(10L, "통학 중"), + Situation(11L, "쇼핑 중"), + Situation(12L, "알바 중"), + Situation(13L, "트렌드 확인"), + Situation(14L, "데이트 중"), + Situation(15L, "휴식 중"), + Situation(16L, "자기 전") + ) + + 3L -> listOf( + Situation(17L, "출퇴근"), + Situation(18L, "트렌드 확인"), + Situation(19L, "업무 중"), + Situation(20L, "커리어 고민"), + Situation(21L, "쇼핑 중"), + Situation(22L, "데이트 중"), + Situation(23L, "휴식 중"), + Situation(24L, "자기 전") + ) + + 4L -> listOf( + Situation(25L, "출퇴근"), + Situation(26L, "업무 준비 중"), + Situation(27L, "데이트 중"), + Situation(28L, "식사"), + Situation(29L, "쇼핑 중"), + Situation(30L, "트렌드 확인"), + Situation(31L, "휴식 중"), + Situation(32L, "자기 전") + ) + + 5L -> listOf( + Situation(33L, "작업 중"), + Situation(34L, "쇼핑 중"), + Situation(35L, "트렌드 확인"), + Situation(36L, "데이트 중"), + Situation(37L, "운동 중"), + Situation(38L, "식사"), + Situation(39L, "휴식 중"), + Situation(40L, "자기 전") + ) + + 6L -> listOf( + Situation(41L, "자소서 작성"), + Situation(42L, "면접 준비"), + Situation(43L, "요리 중"), + Situation(44L, "트렌드 확인"), + Situation(45L, "쇼핑 중"), + Situation(46L, "운동 중"), + Situation(47L, "휴식 중"), + Situation(48L, "자기 전") + ) + + else -> situationsFor(3L) + } + + fun nameOf(id: Long?): String? { + if (id == null) return null + + return (linkDetailSituations + (1L..6L).flatMap { situationsFor(it) }) + .distinctBy { it.id } + .firstOrNull { it.id == id } + ?.tagName + } + + fun idOf(tagName: String, jobId: Long? = null): Long? { + val options = if (jobId != null) { + situationsFor(jobId) + } else { + linkDetailSituations + } + + return options.firstOrNull { it.tagName == tagName }?.id + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt new file mode 100644 index 00000000..025c8779 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt @@ -0,0 +1,101 @@ +package com.linku.home.component + +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +data class LinkCategoryOption( + val id: Long, + val name: String, + val color: Color +) + +@Composable +fun LinkDetailCategoryDropdown( + categories: List, + selectedCategory: String, + onCategoryClick: (LinkCategoryOption) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 13.dp, bottom = 13.dp, end = 56.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + categories.forEach { category -> + Row( + modifier = Modifier + .noRippleClickable { + onCategoryClick(category) + } + .padding(horizontal = 6.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(25.dp) + .clip(CircleShape) + .background(category.color) + ) + + Text( + text = category.name, + fontSize = 15.sp, + fontWeight = if (category.name == selectedCategory) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (category.name == selectedCategory) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailCategoryDropdown() { + ThemeProvider { + LinkDetailCategoryDropdown( + categories = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ), + selectedCategory = "카테고리2", + onCategoryClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt new file mode 100644 index 00000000..7f1050a4 --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -0,0 +1,99 @@ +package com.linku.home.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 com.linku.core.model.EmotionType +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.home.R + +@Composable +fun LinkDetailEmotionDropdown( + emotions: List, + selectedEmotion: String, + onEmotionClick: (EmotionType) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(top = 14.dp, start = 16.dp, end = 56.dp, bottom = 14.dp) + .heightIn(max = 264.dp), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + emotions.forEach { emotion -> + Row( + modifier = Modifier + .noRippleClickable { + onEmotionClick(emotion) + } + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painter = painterResource(emotion.iconRes()), + contentDescription = null, + modifier = Modifier.size(29.dp) + ) + + Text( + text = emotion.tagName, + fontSize = 15.sp, + fontWeight = if (emotion.tagName == selectedEmotion) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (emotion.tagName == selectedEmotion) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + } + ) + } + } + } +} + +private fun EmotionType.iconRes(): Int { + return when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailEmotionDropdown() { + ThemeProvider { + LinkDetailEmotionDropdown( + emotions = EmotionType.entries.toList(), + selectedEmotion = "평온", + onEmotionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt new file mode 100644 index 00000000..e3a4990f --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -0,0 +1,76 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun LinkDetailOptionDropdown( + options: List, + selectedOption: String, + onOptionClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) + .heightIn(max = 264.dp) + ) { + options.forEach { option -> + Text( + text = option, + fontSize = 15.sp, + fontWeight = if (option == selectedOption) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + color = if (option == selectedOption) { + LocalColorTheme.current.blue[200] + } else { + LocalColorTheme.current.gray[800] + }, + modifier = Modifier + .noRippleClickable { + onOptionClick(option) + } + .padding(horizontal = 4.dp, vertical = 9.dp) + ) + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkDetailOptionDropdown() { + ThemeProvider { + LinkDetailOptionDropdown( + options = listOf( + "트렌드 확인", + "통학 중", + "과제 중", + "쇼핑 중", + "데이트 중", + "알바 전" + ), + selectedOption = "통학 중", + onOptionClick = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 01425848..eb12741d 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -14,9 +14,11 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,33 +29,49 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle 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 com.linku.core.model.EmotionType +import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.LinkCategoryOption +import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown +import com.linku.home.component.LinkDetailEmotionDropdown +import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.launch +private enum class LinkDetailDropdownType { + CATEGORY, + EMOTION, + SITUATION +} + @Composable fun LinkDetailScreen( linkTitle: String, category: String, emotion: String, + situation: String, linkUrl: String, memo: String, onBack: () -> Unit, - onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 +// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() @@ -61,6 +79,31 @@ fun LinkDetailScreen( var isDropdownVisible by remember { mutableStateOf(false) } + var isEditMode by remember { mutableStateOf(false) } + var selectedTitle by remember { mutableStateOf(linkTitle) } + var selectedCategory by remember { mutableStateOf(category) } + var selectedEmotion by remember { mutableStateOf(emotion) } + var selectedSituation by remember { mutableStateOf(situation) } + var selectedMemo by remember { mutableStateOf(memo) } + + var openedDropdownType by remember { + mutableStateOf(null) + } + + val emotionOptions = EmotionType.entries.toList() + + val situationOptions = SituationOptions.linkDetailSituations + + // 카테고리 더미데이터 + val categoryOptions = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ) + Box( modifier = Modifier .fillMaxSize() @@ -72,13 +115,36 @@ fun LinkDetailScreen( ) { LinkDetailTopBar( linkTitle = linkTitle, - category = category, - emotion = emotion, + category = selectedCategory, + emotion = selectedEmotion, + situation = selectedSituation, + isEditMode = isEditMode, + isCategoryDropdownOpen = openedDropdownType == LinkDetailDropdownType.CATEGORY, + isEmotionDropdownOpen = openedDropdownType == LinkDetailDropdownType.EMOTION, + isSituationDropdownOpen = openedDropdownType == LinkDetailDropdownType.SITUATION, onBack = { onBack() }, onMoreClick = { isDropdownVisible = !isDropdownVisible }, onLinkGoClick = { uriHandler.openUri(linkUrl) }, + onCategoryClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.CATEGORY) null + else LinkDetailDropdownType.CATEGORY + }, + onEmotionClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.EMOTION) null + else LinkDetailDropdownType.EMOTION + }, + onSituationClick = { + openedDropdownType = + if (openedDropdownType == LinkDetailDropdownType.SITUATION) null + else LinkDetailDropdownType.SITUATION + }, + onTitleClearClick = { + selectedTitle = "" + } ) Column( @@ -87,20 +153,60 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Image( - painter = painterResource(R.drawable.img_default), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(18.dp)) - .border( - width = 1.dp, - color = LocalColorTheme.current.gray[200], - shape = RoundedCornerShape(18.dp) - ) - ) + Box() { + Image( + painter = painterResource(R.drawable.img_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .alpha(if (isEditMode) 0.6f else 1f) + .border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(18.dp) + ) + ) + + if(isEditMode) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .noRippleClickable {}, + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .size(84.dp) + .clip(RoundedCornerShape(30.dp)) + .background(LocalColorTheme.current.gray[700]) + .alpha(0.6f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.ic_camera_white), + contentDescription = null, + modifier = Modifier + .height(24.dp) + .padding(top = 5.dp) + ) + + Spacer(modifier = Modifier.height(7.dp)) + + Text( + text = "사진 변경", + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.white + ) + } + } + } + } Spacer(modifier = Modifier.height(18.dp)) @@ -123,30 +229,40 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.black + color = if (isEditMode) LocalColorTheme.current.gray[400] else LocalColorTheme.current.black, + modifier = Modifier + .then( + if (isEditMode) { + Modifier.padding(vertical = 7.5.dp) + } else { + Modifier.padding(0.dp) + } + ) ) - Spacer(modifier = Modifier.width(10.dp)) + if (!isEditMode) { + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "복사", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[200]) - .padding(horizontal = 13.5.dp, vertical = 7.dp) - .noRippleClickable { - coroutineScope.launch { - clipboard.setClipEntry( - ClipEntry( - ClipData.newPlainText("linkUrl", linkUrl) + Text( + text = "복사", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(LocalColorTheme.current.gray[200]) + .padding(horizontal = 13.5.dp, vertical = 7.dp) + .noRippleClickable { + coroutineScope.launch { + clipboard.setClipEntry( + ClipEntry( + ClipData.newPlainText("linkUrl", linkUrl) + ) ) - ) + } } - } - ) + ) + } } Column( @@ -163,18 +279,55 @@ fun LinkDetailScreen( Spacer(modifier = Modifier.height(12.dp)) - Text( - text = memo, - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - lineHeight = 20.sp, - color = LocalColorTheme.current.black, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .padding(horizontal = 22.dp, vertical = 15.5.dp) - ) + if (isEditMode) { + BasicTextField( + value = selectedMemo, + onValueChange = { + selectedMemo = it + }, + textStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth() + ) { + if (selectedMemo.isBlank()) { + Text( + text = "메모를 입력해 주세요.", + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.gray[400] + ) + } + + innerTextField() + } + } + ) + } else { + Text( + text = selectedMemo, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.sp, + color = LocalColorTheme.current.black, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.gray[100]) + .padding(horizontal = 22.dp, vertical = 15.5.dp) + ) + } } } } @@ -183,7 +336,7 @@ fun LinkDetailScreen( LinkDetailCustomDropdown( onEditClick = { isDropdownVisible = false - // 수정 화면 이동 로직 추가 예정 + isEditMode = true }, onDeleteClick = { isDropdownVisible = false @@ -195,7 +348,7 @@ fun LinkDetailScreen( }, onGoClick = { isDropdownVisible = false - // 링크 Open 로직 추가 예정 + uriHandler.openUri(linkUrl) }, onDismiss = { isDropdownVisible = false @@ -206,6 +359,62 @@ fun LinkDetailScreen( ) } + if (openedDropdownType != null) { + Box( + modifier = Modifier + .fillMaxSize() + .noRippleClickable { + openedDropdownType = null + } + ) + + when (openedDropdownType) { + LinkDetailDropdownType.CATEGORY -> { + LinkDetailCategoryDropdown( + categories = categoryOptions, + selectedCategory = selectedCategory, + onCategoryClick = { + selectedCategory = it.name + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 24.dp) + ) + } + + LinkDetailDropdownType.EMOTION -> { + LinkDetailEmotionDropdown( + emotions = emotionOptions, + selectedEmotion = selectedEmotion, + onEmotionClick = { + selectedEmotion = it.tagName + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 93.dp) + ) + } + + LinkDetailDropdownType.SITUATION -> { + LinkDetailOptionDropdown( + options = situationOptions.map { it.tagName }, + selectedOption = selectedSituation, + onOptionClick = { + selectedSituation = it + openedDropdownType = null + }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 200.dp, start = 186.dp) + ) + } + + null -> Unit + } + } + Row( modifier = Modifier .fillMaxWidth() @@ -214,24 +423,41 @@ fun LinkDetailScreen( .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.maincolor) .padding(vertical = 15.dp) - .noRippleClickable { }, + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + // AI 요약 로직 + } + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -242,12 +468,13 @@ fun PreviewLinkDetailScreen() { ThemeProvider { LinkDetailScreen( linkTitle = "3일만에 오픽 AL 꿀팁", - category = "어학", + category = "카테고리2", emotion = "평온", + situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", onBack = { }, - onMoreClick = { }, +// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index c058c76a..4a08d9a4 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -2,6 +2,7 @@ package com.linku.home.ui.home.bar import androidx.compose.foundation.Image 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.Column @@ -12,13 +13,14 @@ 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.DropdownMenuItem +//import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -34,11 +36,19 @@ fun LinkDetailTopBar( linkTitle: String, category: String, emotion: String, + situation: String, + isEditMode: Boolean, + isCategoryDropdownOpen: Boolean, + isEmotionDropdownOpen: Boolean, + isSituationDropdownOpen: Boolean, onBack: () -> Unit, onMoreClick: () -> Unit, onLinkGoClick: () -> Unit, + onCategoryClick: () -> Unit, + onEmotionClick: () -> Unit, + onSituationClick: () -> Unit, + onTitleClearClick: () -> Unit, ) { - Box( modifier = Modifier .fillMaxWidth() @@ -71,7 +81,7 @@ fun LinkDetailTopBar( ) Text( - text = "새로운 링크", + text = if (isEditMode) "링크 수정하기" else "저장된 링크", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = LocalColorTheme.current.white, @@ -82,7 +92,7 @@ fun LinkDetailTopBar( modifier = Modifier .size(18.dp) .align(Alignment.CenterEnd) - .noRippleClickable { + .noRippleClickable(enabled = !isEditMode) { onMoreClick() } ) { @@ -101,17 +111,42 @@ fun LinkDetailTopBar( .fillMaxWidth() .padding(top = 29.dp, start = 24.dp, end = 24.dp, bottom = 23.dp) // 편집 모드에서는 top = 20.dp ) { - Box( + Row( modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) // 편집 모드에서는 bottom = 11.dp + .then( + if (isEditMode) { + Modifier + .padding(bottom = 11.dp) + .clip(RoundedCornerShape(13.dp)) + .border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(13.dp)) + .padding(horizontal = 15.dp, vertical = 4.dp) + } else { + Modifier.padding(bottom = 12.dp) + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Text( text = linkTitle, - fontSize = 24.sp, // 편집모드에서는 22.sp + fontSize = if (isEditMode) 22.sp else 24.sp, fontWeight = FontWeight.Bold, color = LocalColorTheme.current.white ) + + if (isEditMode) { + Box( + modifier = Modifier + .size(18.dp) + .noRippleClickable { onTitleClearClick() } + ) { + Image( + painter = painterResource(R.drawable.ic_delete_blue), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } } Row( @@ -123,39 +158,159 @@ fun LinkDetailTopBar( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = category, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) - - Text( - text = emotion, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, + .background( + when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) // 추후 카테고리 API 연동 후 실제 색상으로 변경 예정 + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onCategoryClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = category, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.black // API 연동 후 수정 예정 + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isCategoryDropdownOpen) 180f else 0f) + ) + } + } + + Row( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background( + when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.blue[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onEmotionClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = emotion, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.blue[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isEmotionDropdownOpen) 180f else 0f) + ) + } + } + + Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(LocalColorTheme.current.purple[50]) - .padding(horizontal = 10.dp, vertical = 3.dp) - ) + .background( + when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.white + isEditMode -> LocalColorTheme.current.blue[200] + else -> LocalColorTheme.current.purple[50] + } + ) + .then( + if(isEditMode) { + Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + } else { + Modifier + } + ) + .noRippleClickable(enabled = isEditMode) { + onSituationClick() + } + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = situation, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = when { + isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.blue[300] + isEditMode -> LocalColorTheme.current.white + else -> LocalColorTheme.current.purple[300] + } + ) + + if(isEditMode) { + Image( + painter = painterResource(R.drawable.ic_toggle), + contentDescription = null, + modifier = Modifier + .width(12.dp) + .rotate(if (isSituationDropdownOpen) 180f else 0f) + ) + } + } } - Box( - modifier = Modifier - .size(22.dp) - .noRippleClickable { - onLinkGoClick() - }, - ) { - Image( - painter = painterResource(R.drawable.ic_link_go), - contentDescription = null, - modifier = Modifier.height(22.dp) - ) + if(!isEditMode) { + Box( + modifier = Modifier + .size(22.dp) + .noRippleClickable { + onLinkGoClick() + }, + ) { + Image( + painter = painterResource(R.drawable.ic_link_go), + contentDescription = null, + modifier = Modifier.height(22.dp) + ) + } } } } @@ -163,38 +318,38 @@ fun LinkDetailTopBar( } } -@Composable -private fun LinkDetailDropdownItem( - iconRes: Int, - text: String, - onClick: () -> Unit, -) { - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(18.dp) - ) { - Image( - painter = painterResource(iconRes), - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - - Text( - text = text, - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] - ) - } - }, - onClick = onClick, - modifier = Modifier - .height(64.dp) - .padding(horizontal = 12.dp) - ) -} +//@Composable +//private fun LinkDetailDropdownItem( +// iconRes: Int, +// text: String, +// onClick: () -> Unit, +//) { +// DropdownMenuItem( +// text = { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// horizontalArrangement = Arrangement.spacedBy(18.dp) +// ) { +// Image( +// painter = painterResource(iconRes), +// contentDescription = null, +// modifier = Modifier.size(28.dp) +// ) +// +// Text( +// text = text, +// fontSize = 24.sp, +// fontWeight = FontWeight.Medium, +// color = LocalColorTheme.current.gray[800] +// ) +// } +// }, +// onClick = onClick, +// modifier = Modifier +// .height(64.dp) +// .padding(horizontal = 12.dp) +// ) +//} @Preview(showBackground = false) @Composable @@ -204,9 +359,18 @@ fun PreviewLinkDetailTopBar() { linkTitle = "3일만에 오픽 AL 꿀팁", category = "어학", emotion = "평온", + situation = "통학 중", + isEditMode = false, + isCategoryDropdownOpen = false, + isEmotionDropdownOpen = false, + isSituationDropdownOpen = false, onBack = { }, onMoreClick = { }, onLinkGoClick = { }, + onEmotionClick = { }, + onCategoryClick = { }, + onSituationClick = { }, + onTitleClearClick = { } ) } } \ No newline at end of file diff --git a/feature/home/src/main/res/drawable/ic_camera_white.xml b/feature/home/src/main/res/drawable/ic_camera_white.xml new file mode 100644 index 00000000..927b0cd0 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_camera_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/home/src/main/res/drawable/ic_delete_blue.xml b/feature/home/src/main/res/drawable/ic_delete_blue.xml new file mode 100644 index 00000000..9f4c40b4 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_delete_blue.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/feature/home/src/main/res/drawable/ic_linku_blur.xml b/feature/home/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + From 488e45307103f9e4be2401f3439175674049bc94 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 03:19:00 +0900 Subject: [PATCH 48/89] =?UTF-8?q?:sparkles:=20=EC=A0=80=EC=9E=A5=EB=90=9C?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/DeleteLinkModal.kt | 143 ++++++++++++++++++ .../com/linku/home/screen/LinkDetailScreen.kt | 35 ++++- 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt new file mode 100644 index 00000000..7ba646bf --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -0,0 +1,143 @@ +package com.linku.home.component + +import androidx.compose.foundation.BorderStroke +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.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.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.color.Basic +import com.linku.home.R + +@Composable +fun DeleteLinkModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(22.dp)) + .background(LocalColorTheme.current.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column ( + modifier = Modifier + .wrapContentSize() + .padding(top = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_linku_blur), + contentDescription = null, + modifier = Modifier + .height(30.dp) + ) + } + + Text( + text = "해당 링크를 삭제하시겠습니까?", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.black, + modifier = Modifier.padding(top = 15.dp) + ) + + Text( + text = "삭제 시 해당 링크가 영구적으로 제거되며\n복구가 불가능합니다.", + fontSize = 15.sp, + lineHeight = 22.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal, + fontFamily = LocalFontTheme.current.font, + color = LocalColorTheme.current.gray[600], + modifier = Modifier.padding(top = 13.dp) + ) + + Row( + modifier = Modifier + .padding(top = 20.dp, start = 27.dp, end = 27.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .clickable { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "취소하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + brush = Basic.maincolor, // 그라데이션 Brush 사용 + fontFamily = LocalFontTheme.current.font + ), + modifier = Modifier + .graphicsLayer(alpha = 0.99f) // brush 적용 시 필수 + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .clip(RoundedCornerShape(14.dp)) + .background(brush = Basic.maincolor) + .clickable { onConfirm() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "삭제하기", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = LocalFontTheme.current.font + ), + color = LocalColorTheme.current.white + ) + } + } + + Spacer(modifier = Modifier.height(23.dp)) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkModal() { + DeleteLinkModal( + onDismiss = {}, + onConfirm = {} + ) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index eb12741d..2d8be644 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -42,12 +42,14 @@ 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.compose.ui.zIndex import com.linku.core.model.EmotionType import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown @@ -78,6 +80,7 @@ fun LinkDetailScreen( val uriHandler = LocalUriHandler.current var isDropdownVisible by remember { mutableStateOf(false) } + var isDeleteModalVisible by remember { mutableStateOf(false) } var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } @@ -114,7 +117,7 @@ fun LinkDetailScreen( .fillMaxWidth() ) { LinkDetailTopBar( - linkTitle = linkTitle, + linkTitle = selectedTitle, category = selectedCategory, emotion = selectedEmotion, situation = selectedSituation, @@ -340,7 +343,8 @@ fun LinkDetailScreen( }, onDeleteClick = { isDropdownVisible = false - // 삭제 로직 추가 예정 + openedDropdownType = null + isDeleteModalVisible = true }, onShareClick = { isDropdownVisible = false @@ -359,6 +363,33 @@ fun LinkDetailScreen( ) } + if (isDeleteModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(1f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier.padding(horizontal = 20.dp), + contentAlignment = Alignment.Center + ) { + DeleteLinkModal( + onDismiss = { + isDeleteModalVisible = false + }, + onConfirm = { + isDeleteModalVisible = false + // TODO: 삭제 API 호출 -> 삭제 성공 후 어디로 이동하는지 물어보기 + onBack() + } + ) + } + } + } + if (openedDropdownType != null) { Box( modifier = Modifier From badd798109d4b34734cd30214c812791037679d5 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 13:52:35 +0900 Subject: [PATCH 49/89] =?UTF-8?q?:recycle:=20AI=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index c9b90a95..d5b392b2 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -18,83 +19,74 @@ 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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle 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 com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LinkuPreview import com.linku.design.theme.linkuColors -import com.linku.design.theme.linkuFont @Composable fun AIArticleModal( progress: Float, onCancel: () -> Unit, - modifier: Modifier = Modifier // ✅ 외부에서 전달받을 modifier + modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors - val font = MaterialTheme.linkuFont.font - + Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(colors.white), + .background(colors.white) + .padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "AI 요약 중...", - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium, fontFamily = font), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, color = colors.black, - modifier = Modifier.padding(top = 45.dp) + modifier = Modifier.padding(top = 10.dp) ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(12.dp)) // 상태바 SimpleProgressBar( progress = progress, - modifier = Modifier.padding(horizontal = 86.dp) + modifier = Modifier.width(200.dp) ) Text( - text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), + text = "AI가 링크 추출 후 본문 내용을 요약하고 있어요!\n나중에 돌아와서 확인할 수 있어요.", + fontSize = 15.sp, + fontWeight = FontWeight.Normal, color = colors.gray[600], - modifier = Modifier.padding(top = 20.dp) + lineHeight = 22.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 21.dp) ) - Text( - text = "잠시만 기다려주세요.", - style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal, fontFamily = font), - color = colors.gray[600] - ) + Spacer(modifier = Modifier.height(27.dp)) - Column( + Box( modifier = Modifier - .padding(top = 36.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(colors.blue[200]) + .noRippleClickable { onCancel() } + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .padding(horizontal = 28.dp) - .clip(RoundedCornerShape(18.dp)) - .background(brush = MaterialTheme.linkuColors.maincolor) - .clickable { onCancel() }, - contentAlignment = Alignment.Center - ) { - Text( - text = "그만두기", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = font), - color = colors.white - ) - } - - Spacer(modifier = Modifier.height(27.92.dp)) + Text( + text = "나가기", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = colors.white + ) } } } @@ -121,12 +113,12 @@ fun SimpleProgressBar(progress: Float, modifier: Modifier = Modifier) { .fillMaxHeight() .fillMaxWidth(fraction = animated) .clip(RoundedCornerShape(4.dp)) - .background(Brush.horizontalGradient(listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)))) + .background(colors.maincolor) ) } } -@Preview(showBackground = true) +@Preview(showBackground = false) @Composable private fun PreviewAIArticleModal() { LinkuPreview { From 15a9dcd96d74c2e850615e7956645cf313cb1777 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 15:22:15 +0900 Subject: [PATCH 50/89] =?UTF-8?q?:sparkles:=20AI=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=AF=B8=20=EB=A7=81=ED=81=AC=20=EC=9A=94=EC=95=BD,=20AI=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20Modal=20=EC=97=B0=EB=8F=99=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/component/AIArticleModal.kt | 6 +- .../com/linku/home/screen/LinkDetailScreen.kt | 264 +++++++++++++++--- .../linku/home/screen/SaveLinkResultScreen.kt | 2 +- .../main/res/drawable/ic_sparkles_colored.xml | 22 ++ 4 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 feature/home/src/main/res/drawable/ic_sparkles_colored.xml diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index d5b392b2..c39ec8b6 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -31,7 +31,7 @@ import com.linku.design.theme.linkuColors @Composable fun AIArticleModal( progress: Float, - onCancel: () -> Unit, + onQuit: () -> Unit, modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors @@ -77,7 +77,7 @@ fun AIArticleModal( .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) .background(colors.blue[200]) - .noRippleClickable { onCancel() } + .noRippleClickable { onQuit() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center ) { @@ -124,7 +124,7 @@ private fun PreviewAIArticleModal() { LinkuPreview { AIArticleModal( progress = 0.5f, - onCancel = {} + onQuit = { } ) } } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 2d8be644..af21d88c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -1,6 +1,7 @@ package com.linku.home.screen import android.content.ClipData +import android.content.Intent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -22,7 +23,9 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -35,6 +38,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -49,6 +53,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.component.AIArticleModal import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown @@ -56,6 +61,7 @@ import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown import com.linku.home.component.LinkDetailOptionDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar +import kotlinx.coroutines.delay import kotlinx.coroutines.launch private enum class LinkDetailDropdownType { @@ -72,17 +78,24 @@ fun LinkDetailScreen( situation: String, linkUrl: String, memo: String, + tags: List, + aiSummary: String, onBack: () -> Unit, -// onMoreClick: () -> Unit, // 드롭다운으로 변경 예정 ) { val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val context = LocalContext.current + + var isEditMode by remember { mutableStateOf(false) } + var isAiSummaryMode by remember { mutableStateOf(false) } var isDropdownVisible by remember { mutableStateOf(false) } var isDeleteModalVisible by remember { mutableStateOf(false) } + var isAiArticleModalVisible by remember { mutableStateOf(false) } + var isAiArticleProcessing by remember { mutableStateOf(false) } + var aiArticleProgress by remember { mutableFloatStateOf(0f) } - var isEditMode by remember { mutableStateOf(false) } var selectedTitle by remember { mutableStateOf(linkTitle) } var selectedCategory by remember { mutableStateOf(category) } var selectedEmotion by remember { mutableStateOf(emotion) } @@ -97,6 +110,13 @@ fun LinkDetailScreen( val situationOptions = SituationOptions.linkDetailSituations + val visibleTags = tags + .filter { it.isNotBlank() } + .take(4) + .map { tag -> + if (tag.startsWith("#")) tag else "#$tag" + } + // 카테고리 더미데이터 val categoryOptions = listOf( LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), @@ -107,6 +127,23 @@ fun LinkDetailScreen( LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) ) + LaunchedEffect(isAiArticleProcessing) { + if (isAiArticleProcessing) { + aiArticleProgress = 0f + + while (aiArticleProgress < 1f) { + delay(80) + aiArticleProgress = (aiArticleProgress + 0.02f).coerceAtMost(1f) + } + + delay(300) + + isAiArticleProcessing = false + isAiArticleModalVisible = false + isAiSummaryMode = true + } + } + Box( modifier = Modifier .fillMaxSize() @@ -156,7 +193,7 @@ fun LinkDetailScreen( .verticalScroll(rememberScrollState()) .padding(top = 25.dp, start = 20.dp, end = 20.dp) ) { - Box() { + Box { Image( painter = painterResource(R.drawable.img_default), contentDescription = null, @@ -268,6 +305,96 @@ fun LinkDetailScreen( } } + if (isAiSummaryMode) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 25.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 태그", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + if (visibleTags.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + visibleTags.forEach { tag -> + Text( + text = tag, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(20.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 9.dp) + ) + } + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 28.dp), + verticalArrangement = Arrangement.spacedBy(13.dp) + ) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_sparkles_colored), + contentDescription = null, + modifier = Modifier.height(15.dp) + ) + + Text( + text = "AI 링크 요약", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.black + ) + } + + Text( + text = aiSummary, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + lineHeight = 20.sp, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 22.dp, vertical = 16.dp) + ) + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -331,6 +458,10 @@ fun LinkDetailScreen( .padding(horizontal = 22.dp, vertical = 15.5.dp) ) } + + if (isAiSummaryMode) { + Spacer(modifier = Modifier.height(40.dp)) + } } } } @@ -348,7 +479,22 @@ fun LinkDetailScreen( }, onShareClick = { isDropdownVisible = false - // 공유 로직 추가 예정 + openedDropdownType = null + + val shareText = buildString { + appendLine(selectedTitle) + append(linkUrl) + } + + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" // MIME 타입 + putExtra(Intent.EXTRA_TEXT, shareText) // 공유할 내용 + putExtra(Intent.EXTRA_TITLE, selectedTitle) // 미리보기 제목 + putExtra(Intent.EXTRA_SUBJECT, selectedTitle) // 이메일 앱용 제목 + } + + val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") // ShareSheet 상단에 보이는 제목 + context.startActivity(shareIntent) }, onGoClick = { isDropdownVisible = false @@ -390,6 +536,25 @@ fun LinkDetailScreen( } } + if (isAiArticleModalVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x66000000)) + .zIndex(2f) + .noRippleClickable(enabled = false) {}, + contentAlignment = Alignment.Center + ) { + AIArticleModal( + progress = aiArticleProgress, + onQuit = { + isAiArticleModalVisible = false + }, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + if (openedDropdownType != null) { Box( modifier = Modifier @@ -446,48 +611,56 @@ fun LinkDetailScreen( } } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .align(Alignment.BottomCenter) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.maincolor) - .padding(vertical = 15.dp) - .noRippleClickable { - if (isEditMode) { - isEditMode = false - openedDropdownType = null - // 수정 API 불러오기 - } else { - // AI 요약 로직 - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - if (isEditMode) { - Text( - text = "완료", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) - } else { - Image( - painter = painterResource(R.drawable.ic_sparkles), - contentDescription = null, - modifier = Modifier.height(17.51.dp) - ) + if (!isAiSummaryMode) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.maincolor) + .padding(vertical = 15.dp) + .noRippleClickable { + if (isEditMode) { + isEditMode = false + openedDropdownType = null + // 수정 API 불러오기 + } else { + isAiArticleModalVisible = true + openedDropdownType = null + + if (!isAiArticleProcessing) { + aiArticleProgress = 0f + isAiArticleProcessing = true + } + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (isEditMode) { + Text( + text = "완료", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } else { + Image( + painter = painterResource(R.drawable.ic_sparkles), + contentDescription = null, + modifier = Modifier.height(17.51.dp) + ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) - Text( - text = "AI 요약", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white - ) + Text( + text = "AI 요약", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = LocalColorTheme.current.white + ) + } } } } @@ -504,8 +677,9 @@ fun PreviewLinkDetailScreen() { situation = "통학 중", linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", + tags = listOf("오픽", "AL", "영어회화", "자격증"), + aiSummary = "오픽 시험에서는 인터뷰어 Ava와의 대화를 친구처럼 자연스럽게 임하며, 목표 점수에 맞춰 답변량과 유창성을 조절하고, MBC 구조와 콤보 유형 연습을 통해 고득점을 노리는 전략적 접근이 중요하다.", onBack = { }, -// onMoreClick = { }, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt index 0ecf4f9f..79b77e0c 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt @@ -696,7 +696,7 @@ fun SaveLinkResultScreen( Box(modifier = Modifier.padding(horizontal = 20.dp)) { AIArticleModal( progress = aiProgress, - onCancel = onCancelAi, + onQuit = onCancelAi, modifier = Modifier.padding(horizontal = 20.dp) ) } diff --git a/feature/home/src/main/res/drawable/ic_sparkles_colored.xml b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml new file mode 100644 index 00000000..7c71d2dc --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_sparkles_colored.xml @@ -0,0 +1,22 @@ + + + + + + + + + + From 3422028d1f34c589e65bc409b4aa0cd98e7302a0 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sat, 20 Jun 2026 17:08:00 +0900 Subject: [PATCH 51/89] =?UTF-8?q?:sparkles:=20LinkCardItem=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=AA=A8=EB=93=88=EC=97=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design/component/DeleteLinkItemModal.kt | 54 +++++ .../linku/design/component/LinkCardItem.kt | 208 ++++++++++++++++++ .../src/main/res/drawable/ic_ai_bookmark.xml | 25 +++ .../src/main/res/drawable/ic_linku_blur.xml | 39 ++++ design/src/main/res/drawable/ic_more.xml | 15 ++ .../src/main/res/drawable/img_genz_trend.png | Bin 0 -> 9854 bytes .../main/res/drawable/img_link_default.xml | 42 ++++ 7 files changed, 383 insertions(+) create mode 100644 design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt create mode 100644 design/src/main/java/com/linku/design/component/LinkCardItem.kt create mode 100644 design/src/main/res/drawable/ic_ai_bookmark.xml create mode 100644 design/src/main/res/drawable/ic_linku_blur.xml create mode 100644 design/src/main/res/drawable/ic_more.xml create mode 100644 design/src/main/res/drawable/img_genz_trend.png create mode 100644 design/src/main/res/drawable/img_link_default.xml diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt new file mode 100644 index 00000000..37ed8537 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -0,0 +1,54 @@ +package com.linku.design.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +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 com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider + +@Composable +fun DeleteLinkItemModal( + onClickModal: () -> Unit = { } +) { + Column( + modifier = Modifier + .width(120.dp) + .graphicsLayer { + shadowElevation = 10.dp.toPx() + this.shape = shape + clip = true + } + .clip(RoundedCornerShape(14.dp)) + .background(LocalColorTheme.current.white) + .padding(horizontal = 15.dp, vertical = 10.dp) + .noRippleClickable { onClickModal() } + ) { + Text( + text = "삭제하기", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800], + modifier = Modifier.width(90.dp) + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewDeleteLinkItemModal() { + ThemeProvider { + DeleteLinkItemModal(onClickModal = { }) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt new file mode 100644 index 00000000..b925c7d5 --- /dev/null +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -0,0 +1,208 @@ +package com.linku.design.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +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.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.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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors +import com.linku.design.R + +@Composable +fun LinkCardItem( + hasAiSummary: Boolean, + linkTitle: String, + tags: List, + domainName: String? = null, + @DrawableRes linkImage: Int? = null, + @DrawableRes domainImage: Int? = null, + onClickDelete: () -> Unit +) { + var isMenuVisible by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(LocalColorTheme.current.white) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(linkImage ?: R.drawable.img_link_default), + contentDescription = null, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + + Spacer(modifier = Modifier.width(14.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalAlignment = Alignment.Start + ) { + Text( + text = linkTitle, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.linkuColors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 13.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + tags.forEach { tag -> + Text( + text = tag, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600], + modifier = Modifier + .background( + color = LocalColorTheme.current.gray[100], + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) + + Spacer(modifier = Modifier.width(6.dp)) + } + } + + Spacer(modifier = Modifier.height(9.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (domainImage != null) { + Image( + painter = painterResource(domainImage), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + } + + Text( + text = domainName ?: "", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[600] + ) + } + } + + Box( + modifier = Modifier + .height(85.dp) + .padding(end = 5.dp) + .noRippleClickable { isMenuVisible = !isMenuVisible }, + contentAlignment = Alignment.TopEnd + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier.size(17.dp) + ) + } + } + + if (hasAiSummary) { + Image( + painter = painterResource(R.drawable.ic_ai_bookmark), + contentDescription = null, + modifier = Modifier + .padding(start = 18.dp) + .size(20.dp, 26.dp) + ) + } + + if (isMenuVisible) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 36.dp, end = 12.dp) + ) { + DeleteLinkItemModal( + onClickModal = { + isMenuVisible = false + onClickDelete() + } + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_HasAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = true, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + linkImage = R.drawable.img_genz_trend, + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_NoAiSummary() { + ThemeProvider { + LinkCardItem( + hasAiSummary = false, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_ai_bookmark.xml b/design/src/main/res/drawable/ic_ai_bookmark.xml new file mode 100644 index 00000000..6c38a8de --- /dev/null +++ b/design/src/main/res/drawable/ic_ai_bookmark.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_linku_blur.xml b/design/src/main/res/drawable/ic_linku_blur.xml new file mode 100644 index 00000000..cb7c5ec2 --- /dev/null +++ b/design/src/main/res/drawable/ic_linku_blur.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/drawable/ic_more.xml b/design/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..2f2f7991 --- /dev/null +++ b/design/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/design/src/main/res/drawable/img_genz_trend.png b/design/src/main/res/drawable/img_genz_trend.png new file mode 100644 index 0000000000000000000000000000000000000000..1360d8c7f0e21c0d7068324365ea5d5781a5d0b3 GIT binary patch literal 9854 zcmV-^CV|`{Bx-c_qcZME9k zqt(`r>Og5Js?=_)XssHxYtK-zfHX0V;vh>-G5P zqmNLaKmimiSP&&kmQ;W9{lvsX{7(|A|KF5~hqtn_Lg~__asU2(1t>x0dtqT==+L19 zHf`DjgTVluE{8h6OOyTlx8;=p^~{+ws8gqoT)z(a^XG%Ty}bgFS{fW2j2ky@plsQ) z3g~R@Q+`V@`TfMu`!GCyEH?SyGfF*PqG(!}neg+UfBqTP0^kpG{(~Xoroq9{2?Ye) zq@-jxIyz$I%9WTkYnDk{Gt*iEY9*h=UB3zcPF)dy^QOGXbNKIRY%YSDqO6TT#>K|M z+S(d+MvWr?F)`6FBpcx5?5w_R4t9AjFO(_nfx5M-;-fLc5fc}Wc}rHHZ0V9na&UmH zt)2QF_jBdS6;!NPF~bGpd$E7};!?FLaP+JK`!eNIH5Vz$64aTd7)<}R^!Ky(&5j}e zPDxINm0oZD1zz>@`^qiz=WY@c5^&wm4>oe(Bsl@MeEHx}v?#7$y9)Eec)j)v6oMZ1 zC<6O_{Q*}4Lh!+mesHf)8P|MH z{-V1F^!W+_3e42^b$MM8=XVV@h2=B1yhsXrX#5PYDN+~-ckUqhQ5bAXltOY;6s+v* z1VC%qM~eDV;@wcVwr`8box5RED0SafcDAr~aglvJG8uwR;X=6={RvRutpGSVIl;!( zR?OA`u`w|UP#YT?*vUz9vz5*N#&wehQ2Sh*onUQai((#y(RIlCNQ?+Yg=)2N;lkf2 z=3PetO7O>x8;5e`%Au$%lto>TeLg*U0?Cn&O@PMv-$40OhfsFg22_>(+LtbcqO)fq z@$o~rG;bpNNks7_^WoaD9V!M~LBhRIk~62kAJ{PF#8s9qHb5ALCISP+VQ_A#p5 z4}{_AQ`mWyhHKk4C_HTvk`trh+@zssaw1Coydw8NWyCfZFN7wMoSgb^Yim7~L4lZ( zy6DP(nv0`@J+9o1Li=WQv3bK!uyJw6XP6z=$ExIXlAp%0;*|Z3NF9Tc8&jXIsb}hL~97?bsfLCXIpj zxzk7p5CEfN;M%G+3~_Nd^1?lbXNw#AAH0I{aOsXa8|Drb(vp-Lr0Y}%}Vcp}Hq)9i2Gfwg;n#9Z)I zW?gXN7#PTlZEc`0=q~2%geUv
0*`fRE-96N#NdAfbkSr6D=!H&TmdU%1lP#_;lqdV+4}?zKR*LM8 zARC@MM&6#?5V3YW^d3cE#m^*$7@j^sx&6CfWp9TkyY}M#7qfGprzXgO;^jmb1qmu$ z0!=kCGD2yXtGuT(Ghb#)^to@n^%i>d>ZM*k{^V2f$I%!&cC7lG`*RU6$XV~*yN8Dl zA7bdxp&7b>h~Kv0W{)0lE>}rKY!&^rMKt~2EsR)=U&I|81CG|fVcjF3kr8UV8pwv2N;jYxg@bGYD(vO(|0Vaa)AYAqQ z`SbYn(@$|z$b8|#g@qYhK-H>MpF0Kt3C4RID8SkMN8G^Ry%cnvq`vyiA!g%C}}EDJ&Zj(29~>mG?r``ysF)$JLU8mn3@c8cK!L45|nwp1y^2i!t|DH9|z1|acHD5B4tgG;@-NC*s6 z6e3ngx>Ff%1dkqv3g?ccIRJwY%{C+?L{*^N>G_*p~$Gwa33|y2; zQ0<3{#6_T6$jEm;T)F~I_38*e^)>_a<%>RS{vt$f-iACan;}}rt+h~|#5;G9w|f^v zY}^8;+BJnLgdjO0QmD#SILk>!?K>!GStG<(<5RpMQ>S-MU#kCi!Z< z0exX5Bwo>}8~~c0wPcsYXky*(!qw?ay<*P!h*mz4tl%yTv2l2M_^1%y1jPAXL86$M z;b|0-!y}aGr%09*E8nM9$4cs#B7Qk3DGpx8|58yl{Rz?JSFc`;QsPQ1F*v?Q^C$59 zj+a!e%-bjoPIMO=Hf&IuUZqMEb&S)ePb+u8nnHRZBVoW&=aStzERzRgRqe!y6WG3e zyRsTjPfzt;cEQ!>@7wDIs^gN!OMt)s-2%Ax?GJ~VHN|e1PaP8VP_3zve0?GhMddDDWUI`<(T+E1w z2<5i20`gPy30aR&>+#TP_8TaJb8Dify0YGhyAR9!&Kd5@6# zi|2hXp!b`&9~OZ(>eaw6+xEc!_5=7`y`loz?c29i2Y_0m%f;SPmoA;LZ~sBW3lpj+ z6*30_GD*yz+r=w>D&X$lzu)wIx}Gjwx~L3~i>!%0r;#m<7@KQWwmRqtXa?O&euojA z;H_GLE2TvQm6Xg$ zF8ZJ9sX@+}=fLCpOZ~KQ-5RJV6l~_#-y-H=AlCn~3H57MLk}sQ*RNMU&2M>4q>6ks z$I(hIBqbmqKm`i=5k_iFm2*Q2G(+G4uZni<-nA1ucmJsZ0Zob*#MU&mj-Zk*5p;f@ z+S`gjv0}wi+clyw{2n9oryowmBhi!$kBjV4{1`FaCe50oQPZZVSHC{2gvA`#v>Mj( z<6vo_1m6zD=ie>E=ziT$r$b-t*uGVr=WDOMhDMDVseNb(i}tGHNiy11u3TB#!}(Qq zbL!M7bwU4Rh#|%38VKfH(X6e)LAOf({^s=?;_xHY@tAJ%M95p|Pr2ee=Jo5>m2c&H z+~_VyvH1DEef!Y!t**-7IY?@q(dH<>cTihK-!XnJom;&uEX7N3IC17Yw(Q;q8_E8- zEI<76JGN|CkE0j-U~g-!l#YQ+qkLL`Te@_q2{S#-P*Y#NNy<}SR&3IwNh%+Iks$^x zdwHCkgg>QfeoX=tX;D!LY^&C-LycOs)NyF`3_c|C^ttq@6rK~oCcRgwbTt&xBd$74Jhg|FdXmYanbFcX3ayLK%K;8rlTg~HDh;sKT$@ef2UI7-NSLe#U2j{9?R zc2R8{-^;1jTE^20y}EVB%xUA`>f)#@Y0=`vm^t%VSC>NYzW(~_G(1^v`|mVGm7RcZooR-NoYMR^L^Ti_TElP#B0di zB3+EsjSb>zwBxvo?;a*5T(fp<6*O5>ICA7LiWP5o!!8WFo%+ z#OvU77AfL^E0->zxpXm{#IG}ek{vN?VtJCmlx$?nmMxevWs1dPDNQvMJnv0-6wZtxT zRzgjX+rzUdV#-n@PD*t}g#}bU>36Xr#z09w$wD}(9<`FLhu22ACB=EtO^$TZhZd3F zlD$lxJlS$lU0sAu%4ioZVa>Wt7&@RAo=T%ES~RG1IWO${drz7la0k0~?NWrDfY20L zv}mDn`z*6FYO1VqN;(jq`;Gjakn6>HT2-u?G$E{)F8Qj$jmAW%kpz@9kKFi@Xe$HQ zwQGK;CK=Wz|Cx!c+qT22d^yEh$nP_%j_G+p)D(N^XYW3E1n-R)2zyr-l=g0fe0lT0 z=i+5qjil7^9b=Scp3A(qhxFF__wTO?B^(?h(EOVO*^7 zE#bZtH;#U)H1LSHHfE62G-5zyp~lkBlP4#LMFj+f;LN^F`0(=u@V{^zn>KDjo9^!* z=$5~7cNsM*bq}Q)6edKrTZfj|`Nz+Q5x*E7auag``XNv+2mgu0L1(gkjtGUZ6Z=OC&{AxrSF#4EuR{u zqZ=aNyf!Kr{c5Yd;qCR5iVD#)aAZv-tOE6J2$QtDi((%oehqiI)|>k`l-prL4yWm7`!R^Ilt^vYHVXzfw+iJeD>XH zT=Vn8gr;wzY^jpZndbWS>oH)!031Gc9IuIU-?4Kix_9r6&Ye5sn3&Yakt0(tg7Zi5 z393=3NrEm}%r6Fkk0Vgg#!f|4)+)L33;+}Y=_(xeu{05N@vOQVDVVB;*#a+6sZ0^Z zVJnf`N!*g1AvL)6?cD=&=FP|T6MIqp&{5Q_RSl)fmWE3Kci1`?5@6%7>ici;)3&{c zc=QMhXHCP2(`V2?iZ5CskRhm#!^0Fs;M~)-(5Kk+>(2trC{vA7(-y5LOAWnv!3R%6 z18_Mg0+ounB32weIUaeOmQ~nF3&pWkbu~Mn32D-Fk5;(6bt3{jOQEbp(KTzi5)m=K8_X4;v@FCu`?Ca4^n6S-a&$)ZT+?*eoYPt+bI;clV+>aPSYn195N7TC|3@ zXGtVQ+(LP;a`2SeX|nX?Yzh`dWaw?ooc<-gS-w_Hgtutf8ckoXj&?o!p=beD+>>CN zRV_r%8YLqhfh|<1pg7!^f9}_YE=GvVep+k3CSLX;h4*EbLR#-`!hYD*CAGSH_YNLEiNb3&8^J%)fXdz# zvOKm)kF_ch%WaJAyfKVbZLRpq&sojczI_`8_V0(%9t99`(_iY5&XSS1WqBa}p8I|K z?YC81g{dFGWQ&=8wU(3vIY>d0%^}}JaVhM3J@|Sd!hhSSW(%_GPsxMxNYJ_5uLmN< zbn{5PAu%aIEaP2Mfbo(Pbk3YP=-Yn~%9ksTTGgu}{Lw?)l&pzGdzSI2a9rqQS==(E z%ixfle|98IL4}|)U~p&LIH6Ey$YiBTmC(9%Yc+X7pQ>){Vc-!UgDPRAH564YH`!~C1N-xqJOn%QPKMw4U88b%h zn~gLw2AUds`?L;~QHd2?Z_yl16>-+=!(W`eK#YHX6Bru=X zMvv(3$Vjx2Bz8|}MOyKkibjT$W#8ZY4@O=_Z)Oy%JaOX=IY>bBrcOIG`#y;l>UoPn1(4^hN3YlaAWwuqvoLKi%jGFSlFIGg@ds{+ zKRa|_zx;g*TYuk#3qI!%Efl5G#0x>U7FUpM5qjisD+qr{r9MS`W8C8? zRL6=;c(`8AtMnj?}cvX;o8VY*=X5lxDbFKe2j9&83N2ct$^D;)^eo z`%`lfMvCck_KbSH7kbyUujfFeDbOLaTWG!l1cCJDk>tSYM{+R)pXsGJ(6JIdM+_c_ zZ8Nk{u9a%B)X6R+X(j z-QI+YSkyE*Zw`K}mh023FwK!^3e0GZA3v@@)PyucQ5~ajf`oyJsU;6;qFb$6VB_{3 zXxX41HpuH+?b=|;iq(>>j>niuld*Z@MtE1Q0(%EX{Iq(N3V89PPVzga9B@%mE;}it zKdfC9AyWS>5)&^O;Zux!|ATC4r@1hp$>@+kpDN^w8VSkC*qwR-SE}CZ}Ic$ z)#&n8cle0)xfd*qLqZRa9X^~ER0714;w!b*eEf&NYFQUS@7uSpso=tcuUPs$_8dKq zYLcn!JAM+cSE`7ud-kDA>sFY*XbGlIor2y{-8z5noZ4p<$*mVGU4oF>^;4-+4xW`_ zC0ZF@xDd{a9|7m!+jt=6*-y%GEDCAYkrPnUYpPb`)wI2&6o;rN>$m=a_lFI^=X2-b zM>+TQQklDW;XFLVx^3)i5Gu)mgH*$wvk{Vhl7G<B_jF&#)lPwyx$PsNsX&9gfOn zOX0%xo2VkMr!QSWgBsPbecyfz?%NwPzM74d-+zzVb?Pd98t~vg-uYoYo=R^&C)CMW zO&KO)&!^*HFO~T_!g1TQZ?DcPt4ME2Nb4+tYVJaA3;Q{A>I}w?co(0|o|6h_r!KfC z1*RfJiz%h@zkUPtq|W^7PivnCl{=*A^TMB-yOhtUrOb%T4@&KBR;Lzx{R2_XqYy62 zYo#)!aLngCS~hBcmB0LospH0?|IlGLdGrWehUzr*B!%W${PK`Bo<0@nAX^Du7) z?C*z(f4;4{p`H>*vqN<(iI$L95^aAaq`1<&K^@Haa<%~aeJY?A2xw6u9|EXQ%Lc*> z*8aF06F;10xnaqGMDyljHaJ?Y_4he*#o;v;rOi)-LFeG9v#4CAByQZjuU;<&1fqcy zDfS*YhJkPQ#Fz8G#j0gXQBU&xy9<}%f~0%j6fT2@DcLi716wfy@59O;mKp)|frn@)l_|jA2P$sZ*z_S86N=ZOKj3H@x^*kFUR&1z)LMdY3AW%Yh-NA;n)GzZ+=Vv@y2r z-H-93MquKn)3N9G-{G`!4ZMq&z|`n4jJKEKb4upsZ;*nG+wFTe`^h-yih00Uh`xjH z#vX6?ezB(jneW?z>(R_#tI!peo>i|mJ7fFKT{tF|RLaWDs~KeglM67EO3)A>i*%q zQH%%a8G{C@rp3$#m#$ikb5iFhDnW_E9sxJ}QNM92pfvfBLk0?6K5jcmVqD-SjL)R)m8!9)LDXC-Shy@2t%*P$o9KV#uXa;OyJ4!)>2i)@K zKJ@O|1v~Z~#5+B^VfmVM`1;sEbRIAUzZG@IYbjQ!s<%TdH6fE@^C%9|{0lEz627Ab z!c|;LtXM<^DRA8KzbQmJI*VJCvYw?bQlzl7N^Z!xJ6lWuG*F|Wo?x#FnD!wSEdCzzjvPR{_dmuOM=Lbc*}~J>#&pSedpZh8(6f>t z>B6b(yLS;H?!fd+kqmc~HK^{=V+#qnjmpxnj5RAFX%?yZTR!$_*`kFQF=9CCHEbk+ zt~3HV2x~U{rc9oImMu{n2~m$T2bBP6{!|lY9|t7ObwI=I4Bj9rJU0cHB?D86pr;;-NmdLJofcl6n-?yQyBO&c$M za}7t>Q!H4F8Zj3|(X^NOf7_0o@S1letdS^h4rm(z^pKc*ZRuS_MTR4vC}O6da>wR( z=uu0_�NEB1$R@a2BmPeewj_b?OA2yq#ZC*}y)7P>@&w*u8&iJI^psYFqd^gNXynrkYr^|HhkB*dwJ{NB(Zsenq8 zkUq3mmrm%@e;_`a{;A5)G6j`8PJi1YM}#;*l58QZq|=X)b_NR#VGEWZLK=2Jll9;3~Y`@bJdBq zA?3xM0Kr}Iatyh&abL-X6cmv9Kz`}JapPl|taR3@jF&zENY7mfyoLoW^vn6*;HfYI z`qJ3g7~$XxCGT&HSP8DgDpOE-Fs_ystoZLzrAn)liWI7ql$@mQnB&6{IU>`bNz)up zk^HwrJ@!+8Nf1-!#s`tHeCgus^4yIa3{=`3E81^0;I*f}IlH9Z_{)2;_}Q%4a}gu_ zTh$;kI}z@j#TC*RwL@d^p?2MRs9F1U^&STh_&`M7F=u0eM87IR7Z4|n!35F51bL;O zjC>p^prp=v@!HsYWSgGo!eWYftR??)|O zvZQ)C8iCgyV(^ddbA8FeN2o?eM!Cr34j z!sZ^elXDJKCbp-kg$&m8vzZBlpkmGG+bLbk;xEg6{~LtQb9EJMaue_fg1x;1 zT1#hw54k0nT0@dPl5S6X(x&+{U9yT9s(YgONzHV*(T{MYIFaY)=ck4#US-qHmWchc z)0!xz{N%}FjC}6{toivD)q>ZkppTD_8u#I766+oWl|GjDG_a7alxkVYrxZfV}U zk(XF-{GJnuY<674Y{+DprZ^Ir`bG(hv_&DAVGMxy5!i`L}RCf=y}Gzo$mZFKB8 z)zYK~VTcb4g)ZC{uYwp-65%M?H*~;2Q-U&F+SweA;ElU%Gjn)?T@T&~y?ps{g#*Fl zR3ZoS2s&Tcn&)j#8HrWTo;_2{XQwEdR+7i@7M>Au@SIA_x%28d&Q|(dN{jxXaPZBg zLtE(0OqR;TSy&F_(L!0~WB?;Iq(W(rbkZI!!RHWgu8Ozqv%Z{>Sas{xEhVGK#vSlZ z46aa4M0+g!D<|n5?)dDz>F)SsHYyo2DvmkP9gt$tr}DdeMit#*M)y!r^qEbt z81^G|zJlV-)4H5zr9JuB*8d0w&;TXb2G+J7bI kqRZ@mP4uGX|9@TnAD(ks^5^-L=Kufz07*qoM6N<$g7$PJF#rGn literal 0 HcmV?d00001 diff --git a/design/src/main/res/drawable/img_link_default.xml b/design/src/main/res/drawable/img_link_default.xml new file mode 100644 index 00000000..7539981f --- /dev/null +++ b/design/src/main/res/drawable/img_link_default.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + From 45b03a562546ac39e4cfc49eaa14ed39af6c3f05 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 28 May 2026 03:44:42 +0900 Subject: [PATCH 52/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?Top=20Bar=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index 4a08d9a4..e9f136e5 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -1,4 +1,4 @@ -package com.linku.home.ui.home.bar +package com.linku.home.ui.top.bar import androidx.compose.foundation.Image import androidx.compose.foundation.background From 71c41dfa219b4ba072a2ce50e3f92bd08a3ee983 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 17:49:38 +0900 Subject: [PATCH 53/89] =?UTF-8?q?:zap:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=9C=20=EB=A7=81=ED=81=AC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 146 +++++++++++--- .../src/main/java/com/linku/home/HomeApp.kt | 183 +----------------- .../main/java/com/linku/home/HomeViewModel.kt | 4 + .../com/linku/home/screen/LinkDetailScreen.kt | 34 ++-- .../com/linku/home/screen/SaveLinkScreen.kt | 41 ++-- 5 files changed, 172 insertions(+), 236 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 104e16b2..0ec0ba31 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -49,7 +49,8 @@ import com.linku.file.FileViewModel import com.linku.file.viewmodel.folder.state.FolderStateViewModel import com.linku.home.HomeApp import com.linku.home.HomeViewModel -import com.linku.home.screen.SaveLinkResultScreen +import com.linku.home.component.LinkCategoryOption +import com.linku.home.screen.LinkDetailScreen import com.linku.home.screen.SaveLinkScreen import com.linku.linku_android.curation.curationGraph import com.linku.login.navigation.LoginApp @@ -360,7 +361,7 @@ fun MainApp( HomeApp( viewModel = homeViewModel, nickname = nickname.orEmpty().ifBlank { "링큐" }, - onNavigateToMyPage = { // TODO: 추후 알림 설정 페이지로 이동 + onNavigateToMyPage = { navigator.navigate(NavigationRoute.MyPage.route) { popUpTo(navigator.graph.findStartDestination().id) { saveState = true @@ -370,6 +371,13 @@ fun MainApp( restoreState = true } }, + onNavigateToSaveLink = { url -> + homeViewModel.setUrl(url) + navigator.navigate("savelink") + }, + onNavigateToLinkDetail = { linkuId -> + navigator.navigate("savelinkresult/$linkuId") + }, onShowNavBar = { showNavBar = it } ) } @@ -454,10 +462,12 @@ fun MainApp( SaveLinkScreen( image = vm.image, url = vm.url, + title = vm.title, memo = vm.memo, selectedEmotionId = vm.selectedEmotionId, onPickImage = { imagePicker.launch("image/*") }, onUrlChange = vm::setUrl, + onTitleChange = vm::setTitle, onMemoChange = vm::setMemo, onEmotionSelect = vm::selectEmotion, onSaveClick = { @@ -497,48 +507,130 @@ fun MainApp( vm.loadCategoryColors() } + fun emotionNameOf(id: Long?): String { + return when (id) { + 1L -> "즐거움" + 2L -> "평온" + 3L -> "설렘" + 4L -> "슬픔" + 5L -> "짜증" + 6L -> "분노" + else -> "감정" + } + } + + // TODO: 카테고리 API 연동 후 categoryId 기준 실제 카테고리명/색상 매핑으로 교체 + fun categoryNameOf(id: Long?): String { + return when (id) { + 1L -> "어학" + 2L -> "뉴스" + 3L -> "공부법" + 4L -> "IT·개발" + 5L -> "자기계발" + 6L -> "취업·이직" + 7L -> "비즈니스 인사이트" + 8L -> "생산성·툴" + 9L -> "라이프스타일" + 10L -> "심리·자기이해" + 11L -> "에세이·칼럼" + 12L -> "트렌드" + 13L -> "디자인·예술" + 14L -> "영상·뮤직" + 15L -> "맛집·여행" + 16L -> "기타" + else -> "카테고리" + } + } + + fun categoryIdOf(name: String): Long? { + return when (name) { + "어학" -> 1L + "뉴스" -> 2L + "공부법" -> 3L + "IT·개발" -> 4L + "자기계발" -> 5L + "취업·이직" -> 6L + "비즈니스 인사이트" -> 7L + "생산성·툴" -> 8L + "라이프스타일" -> 9L + "심리·자기이해" -> 10L + "에세이·칼럼" -> 11L + "트렌드" -> 12L + "디자인·예술" -> 13L + "영상·뮤직" -> 14L + "맛집·여행" -> 15L + "기타" -> 16L + else -> null + } + } + + fun keywordToTags(keyword: String?): List { + return keyword + .orEmpty() + .split(",", " ", "#") + .map { it.trim() } + .filter { it.isNotBlank() } + .take(4) + } + // 진행률/색상 맵 수집 val aiProgress = vm.aiProgress.collectAsState().value val categoryColorMap = vm.categoryColorMap.collectAsState().value + val categoryOptions = categoryColorMap.mapNotNull { (name, style) -> + val id = categoryIdOf(name) ?: return@mapNotNull null + + LinkCategoryOption( + id = id, + name = name, + color = style.color4 + ) + } + // 외부 브라우저 열기 fun openUrl(url: String) { runCatching { - val fixed = if (url.startsWith("http")) url else "https://$url" + val fixed = if ( + url.startsWith("http://") || url.startsWith("https://") + ) { + url + } else { + "https://$url" + } + val intent = Intent( Intent.ACTION_VIEW, fixed.toUri() ) + context.startActivity(intent) }.onFailure { Toast.makeText(context, "링크를 열 수 없어요.", Toast.LENGTH_SHORT).show() } } - SaveLinkResultScreen( - link = vm.linkDetail, - aiArticle = vm.aiArticleDetail, - isLoading = vm.isLoadingLinkDetail || vm.isLoadingAiArticle, - isAiLoading = vm.isLoadingAiArticle, - onBack = { navigator.popBackStack() }, - onOpenLink = { url -> openUrl(url) }, - categoryColorMap = categoryColorMap, - onSubmitEdit = { title, memo, categoryId, emotionId -> - vm.updateLink( - title = title, - memo = memo, - categoryId = categoryId, - emotionId = emotionId, - onSucceed = { Toast.makeText(context, "수정 완료", Toast.LENGTH_SHORT).show() }, - onFailed = { e -> - Log.e("SaveLinkResult", "수정 실패", e) - Toast.makeText(context, e.message ?: "수정에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - ) - }, - onRequestAiSummary = { vm.loadAiArticle(linkuId) }, - aiProgress = aiProgress, - onCancelAi = { vm.cancelAiArticleJob() } + val linkDetail = vm.linkDetail + val aiArticle = vm.aiArticleDetail + + val displayKeyword = aiArticle?.keyword?.trim().orEmpty() + .ifEmpty { linkDetail?.keyword.orEmpty() } + + val displaySummary = aiArticle?.summary?.trim().orEmpty() + .ifEmpty { linkDetail?.summary.orEmpty() } + + LinkDetailScreen( + linkTitle = linkDetail?.title.orEmpty(), + category = categoryNameOf(linkDetail?.categoryId), + emotion = emotionNameOf(linkDetail?.emotionId), + situation = "통학 중", // TODO: 상세 API에 situationId 생기면 실제 값으로 변경 + linkUrl = linkDetail?.linku.orEmpty(), + memo = linkDetail?.memo.orEmpty(), + tags = keywordToTags(displayKeyword), + aiSummary = displaySummary, + categoryOptions = categoryOptions, + onBack = { + navigator.popBackStack() + } ) } diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index df2305a2..62a388c2 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -1,17 +1,10 @@ package com.linku.home import android.net.Uri -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost @@ -19,8 +12,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.linku.home.screen.AlarmScreen import com.linku.home.screen.HomeScreen -import com.linku.home.screen.SaveLinkResultScreen -import com.linku.home.screen.SaveLinkScreen import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -28,47 +19,15 @@ import java.io.InputStream @Composable fun HomeApp( viewModel: HomeViewModel, - nickname: String, // 닉네임 호출을 위해 추가함. + nickname: String, onNavigateToMyPage: () -> Unit, + onNavigateToSaveLink: (String) -> Unit, + onNavigateToLinkDetail: (Long) -> Unit, onShowNavBar: (Boolean) -> Unit = {}, ) { val recentLinks by viewModel.recentLinks.collectAsStateWithLifecycle() - val context = LocalContext.current val navController = rememberNavController() -// // === 감정/상황 키 → 서버 ID 매핑 === -// fun emotionKeyToId(key: String): Long = when (key) { -// "joy" -> 1L -// "calm" -> 2L -// "excitement" -> 3L -// "sadness" -> 4L -// "irritation" -> 5L -// "anger" -> 6L -// else -> 0L -// } -// fun taskKeyToSituationId(key: String): Long = when (key) { -// "트렌드 확인" -> 11L -// "과제 중" -> 12L -// "쇼핑 중" -> 13L -// "데이트 중" -> 14L -// "통학 중" -> 15L -// "알바 중" -> 16L -// "휴식 중" -> 17L -// "자기 전" -> 18L -// else -> 0L -// } - - // 외부 브라우저 열기 - fun openUrl(url: String) { - runCatching { - val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url" - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(fixed)) - context.startActivity(intent) - }.onFailure { - Toast.makeText(context, "링크를 열 수 없어요.", Toast.LENGTH_SHORT).show() - } - } - // LaunchedEffect(Unit) { // viewModel.loadCategoryColors() // } @@ -97,146 +56,16 @@ fun HomeApp( needMoreForRecommendation = viewModel.needMoreForRecommendation, onClearNeedMoreNotice = viewModel::clearNeedMoreNotice, jobId = viewModel.jobId ?: 2L, - onLinkClick = { id -> // ✅ 추가 - navController.navigate("savelinkresult/$id") + onLinkClick = { id -> + onNavigateToLinkDetail(id) }, onNavigateToSaveLink = { url -> - viewModel.setUrl(url) // url 세팅 - navController.navigate("savelink") // 저장 화면 이동 + onNavigateToSaveLink(url) }, onAlarmClick = { navController.navigate("alarm") } ) } - composable("savelink") { - // 이미지 픽커: 선택 → Uri를 임시 File로 복사 → viewModel.setImage(file) - val imagePicker = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - if (uri != null) { - runCatching { uri.toTempFile(context) } - .onSuccess { file -> viewModel.setImage(file) } - .onFailure { - Toast.makeText(context, "이미지 로드에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - } - } - - SaveLinkScreen( - image = viewModel.image, - url = viewModel.url, -// title = viewModel.title, - memo = viewModel.memo, - selectedEmotionId = viewModel.selectedEmotionId, - onPickImage = { imagePicker.launch("image/*") }, - onUrlChange = viewModel::setUrl, - onMemoChange = viewModel::setMemo, - onEmotionSelect = viewModel::selectEmotion, - onSaveClick = { - viewModel.saveLink( - onSucceed = { saved -> -// Log.d("SaveLinkDebug", "저장 성공: $it") - // ✅ 넘긴 값: 저장 성공한 객체와, 네비게이션에 넘길 linkuId - Log.d("SaveLinkFlow", "넘긴 값 -> saved(LinkSimpleInfo) = $saved") - Log.d("SaveLinkFlow", "넘긴 값 -> navigate param linkuId = ${saved.linkuId}") - - viewModel.resetForm() -// navController.navigate("savelinkresult") - // 저장 직후 상세화면으로 id 전달 - navController.navigate("savelinkresult/${saved.linkuId}") - }, - onFailed = { e -> - Log.e("SaveLinkDebug", "저장 실패", e) - Toast.makeText( - context, - e.message ?: "저장에 실패했습니다.", - Toast.LENGTH_SHORT - ).show() - } - ) - }, - onBack = { navController.popBackStack() }, - isCheckingUrl = viewModel.isCheckingUrl, - isDuplicateUrl = viewModel.isDuplicateUrl, - isInvalidLink = viewModel.isInvalidUrl - ) - } - - composable("savelinkresult/{linkuId}") { backStackEntry -> - val raw = backStackEntry.arguments?.getString("linkuId") - val linkuId = backStackEntry.arguments?.getString("linkuId")?.toLongOrNull() - val currentLinkuId = rememberUpdatedState(linkuId) - val aiProgress = viewModel.aiProgress.collectAsState().value - - // ✅ 네비게이션으로 넘어온 값(문자열/파싱 결과) 확인 - Log.d("SaveLinkFlow", "넘어온 값 -> route arg (raw) = $raw") - Log.d("SaveLinkFlow", "넘어온 값 -> route arg (parsed) = $linkuId") - - if (linkuId == null) { - // 잘못 들어온 경우 안전하게 되돌리기 - Log.d("HomeAppLinkResult", "id가 null입니다.") - LaunchedEffect(Unit) { navController.popBackStack() } - } else { - // 화면 진입 시 상세 로드 - LaunchedEffect(linkuId) { - viewModel.loadLinkDetail(linkuId) // 상세 안에 있으면 이걸로 표시, 없으면 내부에서 AI 호출 - viewModel.loadCategoryColors(force = true) // 상세 들어올 때마다 최신 색상 재조회 - } - - val categoryColorMap = viewModel.categoryColorMap.collectAsState().value // 색상 맵 수집 - - // 🔹 상세 데이터 전달 (필요시 로딩 상태 활용) - SaveLinkResultScreen( - link = viewModel.linkDetail, - aiArticle = viewModel.aiArticleDetail, - isLoading = viewModel.isLoadingLinkDetail || viewModel.isLoadingAiArticle, - isAiLoading = viewModel.isLoadingAiArticle, - onBack = { navController.popBackStack() }, - onOpenLink = { url -> - Log.d("HomeApp", "onOpenLink 호출! url=$url") - val fixed = if (url.startsWith("http")) url else "https://$url" - val intent = android.content.Intent( - android.content.Intent.ACTION_VIEW, - Uri.parse(fixed) - ) - try { - context.startActivity(intent) - } catch (t: Throwable) { - Toast.makeText(context, "링크를 열 수 없어요.", android.widget.Toast.LENGTH_SHORT).show() - Log.e("HomeApp", "startActivity 실패", t) - } - }, - categoryColorMap = categoryColorMap, - onSubmitEdit = { title, memo, categoryId, emotionId -> - viewModel.updateLink( - title = title, - memo = memo, - categoryId = categoryId, - emotionId = emotionId, - onSucceed = { - Toast.makeText(context, "수정 완료", Toast.LENGTH_SHORT).show() - }, - onFailed = { e -> - Log.e("SaveLinkFlow", "수정 실패", e) - Toast.makeText(context, e.message ?: "수정에 실패했습니다.", Toast.LENGTH_SHORT).show() - } - ) - }, - onRequestAiSummary = { - val id = currentLinkuId.value - android.util.Log.d("HomeApp", "onRequestAiSummary 호출! linkuId=$id, vm=${viewModel.hashCode()}") - if (id != null) { - viewModel.loadAiArticle(id) - } else { - android.util.Log.e("HomeApp", "linkuId가 null이라 AI 호출 불가") - } - }, - aiProgress = aiProgress, - onCancelAi = { viewModel.cancelAiArticleJob() }, - ) - } - } - composable("alarm") { DisposableEffect(Unit) { onShowNavBar(false) diff --git a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt index 5d688b84..575188b0 100644 --- a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt @@ -152,6 +152,7 @@ class HomeViewModel @Inject constructor( // 새로운 링크 저장 private val imageState = mutableStateOf(null) private val urlState = mutableStateOf("") + private val titleState = mutableStateOf("") private val memoState = mutableStateOf("") private val emotionIdState = mutableStateOf(null) private val isSavingState = mutableStateOf(false) @@ -163,6 +164,7 @@ class HomeViewModel @Inject constructor( val image get() = imageState.value val url get() = urlState.value + val title get() = titleState.value val memo get() = memoState.value val selectedEmotionId get() = emotionIdState.value val isSaving get() = isSavingState.value @@ -211,6 +213,7 @@ class HomeViewModel @Inject constructor( isCheckingUrlState.value = false } } + fun setTitle(newTitle: String) { titleState.value = newTitle } fun setMemo(newMemo: String) { memoState.value = newMemo } fun selectEmotion(id: Long?) { emotionIdState.value = id } @@ -220,6 +223,7 @@ class HomeViewModel @Inject constructor( fun resetForm() { imageState.value = null urlState.value = "" + titleState.value = "" memoState.value = "" emotionIdState.value = null } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index af21d88c..21c68861 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -80,6 +80,7 @@ fun LinkDetailScreen( memo: String, tags: List, aiSummary: String, + categoryOptions: List, onBack: () -> Unit, ) { val clipboard = LocalClipboard.current @@ -117,15 +118,15 @@ fun LinkDetailScreen( if (tag.startsWith("#")) tag else "#$tag" } - // 카테고리 더미데이터 - val categoryOptions = listOf( - LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), - LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), - LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), - LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), - LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), - LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) - ) + LaunchedEffect(linkTitle, category, emotion, situation, memo) { + if (!isEditMode) { + selectedTitle = linkTitle + selectedCategory = category + selectedEmotion = emotion + selectedSituation = situation + selectedMemo = memo + } + } LaunchedEffect(isAiArticleProcessing) { if (isAiArticleProcessing) { @@ -459,9 +460,7 @@ fun LinkDetailScreen( ) } - if (isAiSummaryMode) { - Spacer(modifier = Modifier.height(40.dp)) - } + Spacer(modifier = Modifier.height(40.dp)) } } } @@ -669,6 +668,16 @@ fun LinkDetailScreen( @Preview(showBackground = true) @Composable fun PreviewLinkDetailScreen() { + // 카테고리 더미데이터 + val categoryOptions = listOf( + LinkCategoryOption(1L, "카테고리2", Color(0xFF55D6C2)), + LinkCategoryOption(2L, "카테고리3", Color(0xFFFFBE3D)), + LinkCategoryOption(3L, "카테고리4", Color(0xFF2FB4E9)), + LinkCategoryOption(4L, "카테고리5", Color(0xFFFF5757)), + LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), + LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) + ) + ThemeProvider { LinkDetailScreen( linkTitle = "3일만에 오픽 AL 꿀팁", @@ -679,6 +688,7 @@ fun PreviewLinkDetailScreen() { memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", tags = listOf("오픽", "AL", "영어회화", "자격증"), aiSummary = "오픽 시험에서는 인터뷰어 Ava와의 대화를 친구처럼 자연스럽게 임하며, 목표 점수에 맞춰 답변량과 유창성을 조절하고, MBC 구조와 콤보 유형 연습을 통해 고득점을 노리는 전략적 접근이 중요하다.", + categoryOptions = categoryOptions, onBack = { }, ) } diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 967eb32e..1035f0e0 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -1,11 +1,9 @@ package com.linku.home.screen -import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke 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 import androidx.compose.foundation.layout.Column @@ -16,7 +14,6 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -27,9 +24,6 @@ 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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -52,11 +46,12 @@ import java.io.File fun SaveLinkScreen( image: File?, url: String, - title: String? = "", + title: String = "", memo: String, selectedEmotionId: Long?, onPickImage: () -> Unit, onUrlChange: (String) -> Unit, + onTitleChange: (String) -> Unit, onMemoChange: (String) -> Unit, onEmotionSelect: (Long?) -> Unit, onSaveClick: () -> Unit, @@ -173,7 +168,7 @@ fun SaveLinkScreen( .fillMaxWidth() .padding(top = 14.dp, start = 20.dp, end = 20.dp) .then( - if (url == "") { + if (title.isEmpty()) { Modifier.border(1.dp, color = Basic.gray[200], shape = RoundedCornerShape(20.dp)) } else { Modifier.border(width = 1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(18.dp)) @@ -182,7 +177,7 @@ fun SaveLinkScreen( .padding(horizontal = 22.dp, vertical = 15.dp), contentAlignment = Alignment.CenterStart ) { - if (url.isEmpty()) { + if (title.isEmpty()) { Text( text = "링크 제목을 입력해주세요.", fontSize = 14.sp, @@ -192,10 +187,15 @@ fun SaveLinkScreen( } BasicTextField( - value = url, // TODO: 추후 API 파라미터에 링크 제목 추가되면 바꾸기 - onValueChange = onUrlChange, + value = title, + onValueChange = onTitleChange, singleLine = true, - textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + textStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + fontFamily = LocalFontTheme.current.font + ), modifier = Modifier.fillMaxWidth() ) } @@ -239,8 +239,7 @@ fun SaveLinkScreen( Column( modifier = Modifier .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, top = 19.dp) - .noRippleClickable { onPickImage() }, + .padding(start = 20.dp, end = 20.dp, top = 19.dp), horizontalAlignment = Alignment.Start ) { if (image != null) { @@ -260,6 +259,7 @@ fun SaveLinkScreen( .clip(RoundedCornerShape(18.dp)) .background(LocalColorTheme.current.gray[100]) .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .noRippleClickable { onPickImage() } .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -430,12 +430,13 @@ fun PreviewSaveLinkScreen() { title = "", memo = "", selectedEmotionId = null, - onPickImage = {}, - onUrlChange = {}, - onMemoChange = {}, - onEmotionSelect = {}, - onSaveClick = {}, - onBack = {}, + onPickImage = { }, + onUrlChange = { }, + onTitleChange = { }, + onMemoChange = { }, + onEmotionSelect = { }, + onSaveClick = { }, + onBack = { }, isCheckingUrl = false, isDuplicateUrl = null, isInvalidLink = false From f3e834bc607c0857fd238dd6ff447d0b2b500308 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:01:46 +0900 Subject: [PATCH 54/89] =?UTF-8?q?:sparkles:=20LinkCardItem=EC=97=90=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/LinkCardItem.kt | 53 +++++++++++++++---- design/src/main/res/drawable/ic_out_link.xml | 37 +++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 design/src/main/res/drawable/ic_out_link.xml diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index b925c7d5..1d613532 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -41,6 +41,7 @@ fun LinkCardItem( linkTitle: String, tags: List, domainName: String? = null, + isExternalLink: Boolean, @DrawableRes linkImage: Int? = null, @DrawableRes domainImage: Int? = null, onClickDelete: () -> Unit @@ -75,17 +76,32 @@ fun LinkCardItem( .weight(1f), horizontalAlignment = Alignment.Start ) { - Text( - text = linkTitle, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.linkuColors.black, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + Row( modifier = Modifier .fillMaxWidth() - .padding(top = 13.dp) - ) + .padding(top = 13.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (isExternalLink) { + Image( + painter = painterResource(R.drawable.ic_out_link), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + } + + Text( + text = linkTitle, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.linkuColors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + } Spacer(modifier = Modifier.height(5.dp)) @@ -184,6 +200,7 @@ fun PreviewLinkCardItem_HasAiSummary() { hasAiSummary = true, linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), + isExternalLink = false, linkImage = R.drawable.img_genz_trend, domainImage = R.drawable.ic_domain_blog_naver_logo, domainName = "BLOG", @@ -200,6 +217,24 @@ fun PreviewLinkCardItem_NoAiSummary() { hasAiSummary = false, linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), + isExternalLink = false, + domainImage = R.drawable.ic_domain_blog_naver_logo, + domainName = "BLOG", + onClickDelete = { } + ) + } +} + +@Preview(showBackground = false) +@Composable +fun PreviewLinkCardItem_HasOutLink() { + ThemeProvider { + LinkCardItem( + hasAiSummary = true, + linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", + tags = listOf("생산성·툴", "평온"), + isExternalLink = true, + linkImage = R.drawable.img_genz_trend, domainImage = R.drawable.ic_domain_blog_naver_logo, domainName = "BLOG", onClickDelete = { } diff --git a/design/src/main/res/drawable/ic_out_link.xml b/design/src/main/res/drawable/ic_out_link.xml new file mode 100644 index 00000000..fd39d973 --- /dev/null +++ b/design/src/main/res/drawable/ic_out_link.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + From 6bc7736b7e608dae7eef8774f0e700f54b05c4fe Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:13:31 +0900 Subject: [PATCH 55/89] =?UTF-8?q?:sparkles:=20core=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EC=9E=88=EB=8D=98=20design=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 42 ++++--------------- .../component/LinkDetailEmotionDropdown.kt | 3 +- .../com/linku/home/util/EmotionTypeExt.kt | 15 +++++++ 3 files changed, 24 insertions(+), 36 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index 5bc21075..ab1970ab 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,43 +1,15 @@ package com.linku.core.model -import androidx.annotation.DrawableRes -import com.linku.design.R - enum class EmotionType( val id: Long, - val tagName: String, - @DrawableRes val imgRes: Int + val tagName: String ) { - JOY( - id = 1L, - tagName = "즐거움", - imgRes = R.drawable.ic_joy - ), - CALM( - id = 2L, - tagName = "평온", - imgRes = R.drawable.ic_calm - ), - EXCITE( - id = 3L, - tagName = "설렘", - imgRes = R.drawable.ic_excite - ), - SAD( - id = 4L, - tagName = "슬픔", - imgRes = R.drawable.ic_sad - ), - IRRITATION( - id = 5L, - tagName = "짜증", - imgRes = R.drawable.ic_irritation - ), - ANGER( - id = 6L, - tagName = "분노", - imgRes = R.drawable.ic_anger - ); + JOY(id = 1L, tagName = "즐거움"), + CALM(id = 2L, tagName = "평온"), + EXCITE(id = 3L, tagName = "설렘"), + SAD(id = 4L, tagName = "슬픔"), + IRRITATION(id = 5L, tagName = "짜증"), + ANGER(id = 6L, tagName = "분노"); companion object { fun fromId(id: Long?): EmotionType? { diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 7f1050a4..0b8a4381 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -24,6 +24,7 @@ import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.home.R +import com.linku.home.util.imgRes @Composable fun LinkDetailEmotionDropdown( @@ -51,7 +52,7 @@ fun LinkDetailEmotionDropdown( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Image( - painter = painterResource(emotion.iconRes()), + painter = painterResource(emotion.imgRes), contentDescription = null, modifier = Modifier.size(29.dp) ) diff --git a/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt new file mode 100644 index 00000000..ff31c0bb --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt @@ -0,0 +1,15 @@ +package com.linku.home.util + +import androidx.annotation.DrawableRes +import com.linku.core.model.EmotionType +import com.linku.home.R + +val EmotionType.imgRes: Int + @DrawableRes get() = when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } \ No newline at end of file From 02cc312adffbc1a2180e454ec9176f14933aa53c Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:21:22 +0900 Subject: [PATCH 56/89] =?UTF-8?q?:sparkles:=20=ED=95=98=EB=82=98=EC=9D=98?= =?UTF-8?q?=20CATEGORY=5FMAP=EC=9D=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20id/name=EC=9D=84=20=EC=96=91=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=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 --- app/src/main/java/com/linku/MainApp.kt | 61 +++++++------------ .../src/main/java/com/linku/home/HomeApp.kt | 17 ------ 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 0ec0ba31..deec2a1a 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -520,48 +520,33 @@ fun MainApp( } // TODO: 카테고리 API 연동 후 categoryId 기준 실제 카테고리명/색상 매핑으로 교체 + val CATEGORY_MAP = linkedMapOf( + 1L to "어학", + 2L to "뉴스", + 3L to "공부법", + 4L to "IT·개발", + 5L to "자기계발", + 6L to "취업·이직", + 7L to "비즈니스 인사이트", + 8L to "생산성·툴", + 9L to "라이프스타일", + 10L to "심리·자기이해", + 11L to "에세이·칼럼", + 12L to "트렌드", + 13L to "디자인·예술", + 14L to "영상·뮤직", + 15L to "맛집·여행", + 16L to "기타" + ) + fun categoryNameOf(id: Long?): String { - return when (id) { - 1L -> "어학" - 2L -> "뉴스" - 3L -> "공부법" - 4L -> "IT·개발" - 5L -> "자기계발" - 6L -> "취업·이직" - 7L -> "비즈니스 인사이트" - 8L -> "생산성·툴" - 9L -> "라이프스타일" - 10L -> "심리·자기이해" - 11L -> "에세이·칼럼" - 12L -> "트렌드" - 13L -> "디자인·예술" - 14L -> "영상·뮤직" - 15L -> "맛집·여행" - 16L -> "기타" - else -> "카테고리" - } + return CATEGORY_MAP[id] ?: "카테고리" } fun categoryIdOf(name: String): Long? { - return when (name) { - "어학" -> 1L - "뉴스" -> 2L - "공부법" -> 3L - "IT·개발" -> 4L - "자기계발" -> 5L - "취업·이직" -> 6L - "비즈니스 인사이트" -> 7L - "생산성·툴" -> 8L - "라이프스타일" -> 9L - "심리·자기이해" -> 10L - "에세이·칼럼" -> 11L - "트렌드" -> 12L - "디자인·예술" -> 13L - "영상·뮤직" -> 14L - "맛집·여행" -> 15L - "기타" -> 16L - else -> null - } + return CATEGORY_MAP.entries + .firstOrNull { it.value == name } + ?.key } fun keywordToTags(keyword: String?): List { diff --git a/feature/home/src/main/java/com/linku/home/HomeApp.kt b/feature/home/src/main/java/com/linku/home/HomeApp.kt index 62a388c2..cdcb8d29 100644 --- a/feature/home/src/main/java/com/linku/home/HomeApp.kt +++ b/feature/home/src/main/java/com/linku/home/HomeApp.kt @@ -1,20 +1,15 @@ package com.linku.home -import android.net.Uri import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.linku.home.screen.AlarmScreen import com.linku.home.screen.HomeScreen -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream @Composable fun HomeApp( @@ -84,16 +79,4 @@ fun HomeApp( ) } } -} - -/** Uri를 앱 캐시 폴더의 임시 File로 복사 */ -private fun Uri.toTempFile(context: android.content.Context): File { - val fileName = "picked_${System.currentTimeMillis()}.jpg" - val tempFile = File(context.cacheDir, fileName) - context.contentResolver.openInputStream(this).use { input: InputStream? -> - FileOutputStream(tempFile).use { output -> - if (input != null) input.copyTo(output) - } - } - return tempFile } \ No newline at end of file From 0c9138c49d138e6418f3ba72bab1f81ce42a958b Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:53:54 +0900 Subject: [PATCH 57/89] =?UTF-8?q?:sparkles:=20SituationId=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20Situation=20Map=EC=9C=BC=EB=A1=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B3=80=EA=B2=BD=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/Situation.kt | 78 ++++++++++--------- .../component/LinkDetailSituationDropdown.kt | 45 ++++++----- .../com/linku/home/screen/LinkDetailScreen.kt | 31 ++++---- 3 files changed, 83 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt index a0fa486f..b305d49e 100644 --- a/core/src/main/java/com/linku/core/model/Situation.kt +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -6,17 +6,10 @@ data class Situation( ) object SituationOptions { - val linkDetailSituations: List = listOf( - Situation(1L, "통학 중"), - Situation(2L, "공부 중"), - Situation(3L, "휴식 중"), - Situation(4L, "이동 중"), - Situation(5L, "식사 중"), - Situation(6L, "자기 전") - ) + private const val DEFAULT_JOB_ID = 3L - fun situationsFor(jobId: Long): List = when (jobId) { - 1L -> listOf( + private val situationsByJobId: Map> = mapOf( + 1L to listOf( Situation(1L, "통학 중"), Situation(2L, "공부 중"), Situation(3L, "식사 중"), @@ -25,9 +18,8 @@ object SituationOptions { Situation(6L, "쇼핑 중"), Situation(7L, "휴식 중"), Situation(8L, "자기 전") - ) - - 2L -> listOf( + ), + 2L to listOf( Situation(9L, "과제 중"), Situation(10L, "통학 중"), Situation(11L, "쇼핑 중"), @@ -36,9 +28,8 @@ object SituationOptions { Situation(14L, "데이트 중"), Situation(15L, "휴식 중"), Situation(16L, "자기 전") - ) - - 3L -> listOf( + ), + 3L to listOf( Situation(17L, "출퇴근"), Situation(18L, "트렌드 확인"), Situation(19L, "업무 중"), @@ -47,9 +38,8 @@ object SituationOptions { Situation(22L, "데이트 중"), Situation(23L, "휴식 중"), Situation(24L, "자기 전") - ) - - 4L -> listOf( + ), + 4L to listOf( Situation(25L, "출퇴근"), Situation(26L, "업무 준비 중"), Situation(27L, "데이트 중"), @@ -58,9 +48,8 @@ object SituationOptions { Situation(30L, "트렌드 확인"), Situation(31L, "휴식 중"), Situation(32L, "자기 전") - ) - - 5L -> listOf( + ), + 5L to listOf( Situation(33L, "작업 중"), Situation(34L, "쇼핑 중"), Situation(35L, "트렌드 확인"), @@ -69,9 +58,8 @@ object SituationOptions { Situation(38L, "식사"), Situation(39L, "휴식 중"), Situation(40L, "자기 전") - ) - - 6L -> listOf( + ), + 6L to listOf( Situation(41L, "자소서 작성"), Situation(42L, "면접 준비"), Situation(43L, "요리 중"), @@ -81,26 +69,42 @@ object SituationOptions { Situation(47L, "휴식 중"), Situation(48L, "자기 전") ) + ) - else -> situationsFor(3L) + val allSituations: List by lazy { + situationsByJobId.values.flatten() + } + + private val allSituationsById: Map by lazy { + allSituations.associateBy { it.id } + } + + private val situationsByJobAndId: Map> by lazy { + situationsByJobId.mapValues { (_, situations) -> + situations.associateBy { it.id } + } + } + + fun situationsFor(jobId: Long): List { + return situationsByJobId[jobId] ?: situationsByJobId.getValue(DEFAULT_JOB_ID) } fun nameOf(id: Long?): String? { if (id == null) return null - return (linkDetailSituations + (1L..6L).flatMap { situationsFor(it) }) - .distinctBy { it.id } - .firstOrNull { it.id == id } - ?.tagName + return allSituationsById[id]?.tagName } - fun idOf(tagName: String, jobId: Long? = null): Long? { - val options = if (jobId != null) { - situationsFor(jobId) - } else { - linkDetailSituations - } + fun nameOf(id: Long?, jobId: Long): String? { + if (id == null) return null + + return situationsByJobAndId[jobId]?.get(id)?.tagName + ?: situationsByJobAndId[DEFAULT_JOB_ID]?.get(id)?.tagName + } - return options.firstOrNull { it.tagName == tagName }?.id + fun idOf(tagName: String, jobId: Long): Long? { + return situationsFor(jobId) + .firstOrNull { it.tagName == tagName } + ?.id } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt index e3a4990f..6601642d 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -14,15 +14,16 @@ 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 com.linku.core.model.Situation import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider @Composable -fun LinkDetailOptionDropdown( - options: List, - selectedOption: String, - onOptionClick: (String) -> Unit, +fun LinkDetailSituationDropdown( + situations: List, + selectedSituation: Situation?, + onSituationClick: (Situation) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -32,23 +33,23 @@ fun LinkDetailOptionDropdown( .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) .heightIn(max = 264.dp) ) { - options.forEach { option -> + situations.forEach { situation -> Text( - text = option, + text = situation.tagName, fontSize = 15.sp, - fontWeight = if (option == selectedOption) { + fontWeight = if (situation.id == selectedSituation?.id) { FontWeight.Medium } else { FontWeight.Normal }, - color = if (option == selectedOption) { + color = if (situation.id == selectedSituation?.id) { LocalColorTheme.current.blue[200] } else { LocalColorTheme.current.gray[800] }, modifier = Modifier .noRippleClickable { - onOptionClick(option) + onSituationClick(situation) } .padding(horizontal = 4.dp, vertical = 9.dp) ) @@ -58,19 +59,21 @@ fun LinkDetailOptionDropdown( @Preview(showBackground = false) @Composable -fun PreviewLinkDetailOptionDropdown() { +fun PreviewLinkDetailSituationDropdown() { ThemeProvider { - LinkDetailOptionDropdown( - options = listOf( - "트렌드 확인", - "통학 중", - "과제 중", - "쇼핑 중", - "데이트 중", - "알바 전" - ), - selectedOption = "통학 중", - onOptionClick = { } + val situations = listOf( + Situation(18L, "트렌드 확인"), + Situation(10L, "통학 중"), + Situation(9L, "과제 중"), + Situation(11L, "쇼핑 중"), + Situation(14L, "데이트 중"), + Situation(12L, "알바 전") + ) + + LinkDetailSituationDropdown( + situations = situations, + selectedSituation = situations.firstOrNull { it.tagName == "통학 중" }, + onSituationClick = { } ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 21c68861..84a224b0 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -59,7 +59,7 @@ import com.linku.home.component.LinkCategoryOption import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown -import com.linku.home.component.LinkDetailOptionDropdown +import com.linku.home.component.LinkDetailSituationDropdown import com.linku.home.ui.home.bar.LinkDetailTopBar import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -75,7 +75,7 @@ fun LinkDetailScreen( linkTitle: String, category: String, emotion: String, - situation: String, + situationId: Long?, linkUrl: String, memo: String, tags: List, @@ -97,19 +97,24 @@ fun LinkDetailScreen( var isAiArticleProcessing by remember { mutableStateOf(false) } var aiArticleProgress by remember { mutableFloatStateOf(0f) } + val emotionOptions = EmotionType.entries.toList() + val situationOptions = SituationOptions.allSituations + var selectedTitle by remember { mutableStateOf(linkTitle) } var selectedCategory by remember { mutableStateOf(category) } var selectedEmotion by remember { mutableStateOf(emotion) } - var selectedSituation by remember { mutableStateOf(situation) } + var selectedSituation by remember(situationId) { + mutableStateOf( + situationOptions.firstOrNull { it.id == situationId } + ) + } var selectedMemo by remember { mutableStateOf(memo) } var openedDropdownType by remember { mutableStateOf(null) } - val emotionOptions = EmotionType.entries.toList() - val situationOptions = SituationOptions.linkDetailSituations val visibleTags = tags .filter { it.isNotBlank() } @@ -118,12 +123,12 @@ fun LinkDetailScreen( if (tag.startsWith("#")) tag else "#$tag" } - LaunchedEffect(linkTitle, category, emotion, situation, memo) { + LaunchedEffect(linkTitle, category, emotion, situationId, memo) { if (!isEditMode) { selectedTitle = linkTitle selectedCategory = category selectedEmotion = emotion - selectedSituation = situation + selectedSituation = situationOptions.firstOrNull { it.id == situationId } selectedMemo = memo } } @@ -158,7 +163,7 @@ fun LinkDetailScreen( linkTitle = selectedTitle, category = selectedCategory, emotion = selectedEmotion, - situation = selectedSituation, + situation = selectedSituation?.tagName ?: "상황", isEditMode = isEditMode, isCategoryDropdownOpen = openedDropdownType == LinkDetailDropdownType.CATEGORY, isEmotionDropdownOpen = openedDropdownType == LinkDetailDropdownType.EMOTION, @@ -593,10 +598,10 @@ fun LinkDetailScreen( } LinkDetailDropdownType.SITUATION -> { - LinkDetailOptionDropdown( - options = situationOptions.map { it.tagName }, - selectedOption = selectedSituation, - onOptionClick = { + LinkDetailSituationDropdown( + situations = situationOptions, + selectedSituation = selectedSituation, + onSituationClick = { selectedSituation = it openedDropdownType = null }, @@ -683,7 +688,7 @@ fun PreviewLinkDetailScreen() { linkTitle = "3일만에 오픽 AL 꿀팁", category = "카테고리2", emotion = "평온", - situation = "통학 중", + situationId = 10L, linkUrl = "https://blog.naver.com/linkU/1234", memo = "오픽 시험 준비시 도움이 되는 내용 정리, AI 활용한 공부법 정리 및 다양한 내용이 포함된 링크!!", tags = listOf("오픽", "AL", "영어회화", "자격증"), From 33e3734b4b021b9e7ca2c9b775b1869be80ed920 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 18:56:04 +0900 Subject: [PATCH 58/89] =?UTF-8?q?:sparkles:=20SituationId=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index deec2a1a..7c011235 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -607,7 +607,7 @@ fun MainApp( linkTitle = linkDetail?.title.orEmpty(), category = categoryNameOf(linkDetail?.categoryId), emotion = emotionNameOf(linkDetail?.emotionId), - situation = "통학 중", // TODO: 상세 API에 situationId 생기면 실제 값으로 변경 + situationId = null, // TODO: 상세 API에 situationId 생기면 linkDetail?.situationId로 변경 linkUrl = linkDetail?.linku.orEmpty(), memo = linkDetail?.memo.orEmpty(), tags = keywordToTags(displayKeyword), From d86b3acdc8109130a2f0fb8c8cb8b8946ba666f4 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 20:51:53 +0900 Subject: [PATCH 59/89] =?UTF-8?q?:sparkles:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=AA=85=20=EC=A7=81=EA=B4=80=EC=A0=81=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/design/component/DeleteLinkItemModal.kt | 8 +++++--- .../main/java/com/linku/design/component/LinkCardItem.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 37ed8537..6c9ad204 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -20,7 +20,7 @@ import com.linku.design.theme.ThemeProvider @Composable fun DeleteLinkItemModal( - onClickModal: () -> Unit = { } + onDeleteClick: () -> Unit = { } ) { Column( modifier = Modifier @@ -33,7 +33,7 @@ fun DeleteLinkItemModal( .clip(RoundedCornerShape(14.dp)) .background(LocalColorTheme.current.white) .padding(horizontal = 15.dp, vertical = 10.dp) - .noRippleClickable { onClickModal() } + .noRippleClickable { onDeleteClick() } ) { Text( text = "삭제하기", @@ -49,6 +49,8 @@ fun DeleteLinkItemModal( @Composable fun PreviewDeleteLinkItemModal() { ThemeProvider { - DeleteLinkItemModal(onClickModal = { }) + DeleteLinkItemModal( + onDeleteClick = { } + ) } } \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index 1d613532..2ce89b92 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -182,7 +182,7 @@ fun LinkCardItem( .padding(top = 36.dp, end = 12.dp) ) { DeleteLinkItemModal( - onClickModal = { + onDeleteClick = { isMenuVisible = false onClickDelete() } From 4cf3e1bbf6de364c0c6cf8a20f2b0daacb330dd4 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Sun, 21 Jun 2026 20:55:15 +0900 Subject: [PATCH 60/89] =?UTF-8?q?:sparkles:=20graphicsLayer=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20shadow=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/DeleteLinkItemModal.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 6c9ad204..0aa98a23 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -22,15 +23,17 @@ import com.linku.design.theme.ThemeProvider fun DeleteLinkItemModal( onDeleteClick: () -> Unit = { } ) { + val shape = RoundedCornerShape(14.dp) + Column( modifier = Modifier .width(120.dp) - .graphicsLayer { - shadowElevation = 10.dp.toPx() - this.shape = shape - clip = true - } - .clip(RoundedCornerShape(14.dp)) + .shadow( + elevation = 10.dp, + shape = shape, + clip = false + ) + .clip(shape) .background(LocalColorTheme.current.white) .padding(horizontal = 15.dp, vertical = 10.dp) .noRippleClickable { onDeleteClick() } From 742bb1ea27efc9e05c78283efbf961a3c2d03410 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:19:09 +0900 Subject: [PATCH 61/89] =?UTF-8?q?:lipstick:=20fillMaxWidth=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EA=B0=92=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/component/AIArticleModal.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index c39ec8b6..176cb403 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -103,7 +103,6 @@ fun SimpleProgressBar(progress: Float, modifier: Modifier = Modifier) { Box( modifier = modifier - .fillMaxWidth() .height(6.dp) .clip(RoundedCornerShape(4.dp)) .background(colors.gray[200]) From 26cc654c2a0d06673a69c0a019365466957cfda1 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:20:29 +0900 Subject: [PATCH 62/89] =?UTF-8?q?:heavy=5Fplus=5Fsign:=20disign=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=97=90=20coil=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- design/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/design/build.gradle.kts b/design/build.gradle.kts index bcdff6b7..2ce2b15e 100644 --- a/design/build.gradle.kts +++ b/design/build.gradle.kts @@ -59,4 +59,7 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + // coil + implementation(libs.coil.compose) } \ No newline at end of file From 1422b6a0bf91baf3827da2bc9f0dff6d54d00aa0 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:21:20 +0900 Subject: [PATCH 63/89] =?UTF-8?q?:zap:=20modifier=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/design/component/DeleteLinkItemModal.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 0aa98a23..13638021 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -21,12 +21,13 @@ import com.linku.design.theme.ThemeProvider @Composable fun DeleteLinkItemModal( - onDeleteClick: () -> Unit = { } + onDeleteClick: () -> Unit = { }, + modifier: Modifier ) { val shape = RoundedCornerShape(14.dp) Column( - modifier = Modifier + modifier = modifier .width(120.dp) .shadow( elevation = 10.dp, @@ -53,7 +54,8 @@ fun DeleteLinkItemModal( fun PreviewDeleteLinkItemModal() { ThemeProvider { DeleteLinkItemModal( - onDeleteClick = { } + onDeleteClick = { }, + modifier = Modifier ) } } \ No newline at end of file From 0b32e8c4b26ca25ef16b2e0af261874f1fa8f01e Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:23:43 +0900 Subject: [PATCH 64/89] =?UTF-8?q?:lipstick:=20=EB=A1=9C=EA=B3=A0=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=B6=95=EC=86=8C=20=EB=B0=8F=20Preview?= =?UTF-8?q?=EC=97=90=20ThemeProvider=20=EC=B6=94=EA=B0=80,=20FontFamily=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/DeleteLinkModal.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt index 7ba646bf..00da853e 100644 --- a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R @@ -55,7 +56,7 @@ fun DeleteLinkModal( painter = painterResource(R.drawable.ic_linku_blur), contentDescription = null, modifier = Modifier - .height(30.dp) + .height(25.dp) ) } @@ -63,7 +64,6 @@ fun DeleteLinkModal( text = "해당 링크를 삭제하시겠습니까?", fontSize = 18.sp, fontWeight = FontWeight.Medium, - fontFamily = LocalFontTheme.current.font, color = LocalColorTheme.current.black, modifier = Modifier.padding(top = 15.dp) ) @@ -136,8 +136,10 @@ fun DeleteLinkModal( @Preview(showBackground = false) @Composable fun PreviewDeleteLinkModal() { - DeleteLinkModal( - onDismiss = {}, - onConfirm = {} - ) + ThemeProvider { + DeleteLinkModal( + onDismiss = {}, + onConfirm = {} + ) + } } \ No newline at end of file From dd8fb768d0ea33365ef9eff639812f5875cc684e Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:26:44 +0900 Subject: [PATCH 65/89] =?UTF-8?q?:zap:=20DrawableRes=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EC=97=90=20=EC=95=9E=EC=97=90=20param=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=ED=8C=80=EC=9B=90=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/component/EmotionSelect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 8526042c..5305e301 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -36,7 +36,7 @@ import com.linku.home.R private data class EmotionUi( val id: Long, val label: String, - @DrawableRes val iconRes: Int + @param:DrawableRes val iconRes: Int ) private val EMOTIONS = listOf( From f3d00c490919ce94091b0972d22b845825ea4355 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:31:41 +0900 Subject: [PATCH 66/89] =?UTF-8?q?:zap:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8C=80=EC=9B=90=20=ED=94=BC=EB=93=9C=EB=B0=B1=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 --- .../linku/design/component/LinkCardItem.kt | 109 +++++++++++------- .../main/res/drawable/ic_domain_default.xml | 9 ++ 2 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 design/src/main/res/drawable/ic_domain_default.xml diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index 2ce89b92..a48c7b23 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -1,6 +1,5 @@ package com.linku.design.component -import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -23,12 +22,14 @@ 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.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider @@ -39,12 +40,12 @@ import com.linku.design.R fun LinkCardItem( hasAiSummary: Boolean, linkTitle: String, - tags: List, + tags: List = emptyList(), domainName: String? = null, isExternalLink: Boolean, - @DrawableRes linkImage: Int? = null, - @DrawableRes domainImage: Int? = null, - onClickDelete: () -> Unit + linkImageUrl: String? = null, + domainImageUrl: String? = null, + onDeleteClick: () -> Unit ) { var isMenuVisible by remember { mutableStateOf(false) } @@ -60,13 +61,25 @@ fun LinkCardItem( .padding(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Image( - painter = painterResource(linkImage ?: R.drawable.img_link_default), - contentDescription = null, - modifier = Modifier - .size(85.dp) - .clip(RoundedCornerShape(12.dp)) - ) + if (linkImageUrl.isNullOrBlank()) { + Image( + painter = painterResource(R.drawable.img_link_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } else { + Image( + painter = rememberAsyncImagePainter(model = linkImageUrl), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } Spacer(modifier = Modifier.width(14.dp)) @@ -131,16 +144,24 @@ fun LinkCardItem( Row( verticalAlignment = Alignment.CenterVertically ) { - if (domainImage != null) { + if (!domainImageUrl.isNullOrBlank()) { Image( - painter = painterResource(domainImage), + painter = rememberAsyncImagePainter(model = domainImageUrl), contentDescription = null, + contentScale = ContentScale.Crop, modifier = Modifier.size(16.dp) ) - - Spacer(modifier = Modifier.width(4.dp)) + } else { + Image( + painter = painterResource(R.drawable.ic_domain_default), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(22.dp) + ) } + Spacer(modifier = Modifier.width(6.dp)) + Text( text = domainName ?: "", fontSize = 12.sp, @@ -151,17 +172,21 @@ fun LinkCardItem( } Box( - modifier = Modifier - .height(85.dp) - .padding(end = 5.dp) - .noRippleClickable { isMenuVisible = !isMenuVisible }, - contentAlignment = Alignment.TopEnd + modifier = Modifier.height(85.dp) ) { - Image( - painter = painterResource(R.drawable.ic_more), - contentDescription = null, - modifier = Modifier.size(17.dp) - ) + Box( + modifier = Modifier + .size(17.dp) + .padding(end = 5.dp) + .noRippleClickable { isMenuVisible = !isMenuVisible }, + contentAlignment = Alignment.TopEnd + ) { + Image( + painter = painterResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier.size(17.dp) + ) + } } } @@ -176,18 +201,15 @@ fun LinkCardItem( } if (isMenuVisible) { - Box( + DeleteLinkItemModal( + onDeleteClick = { + isMenuVisible = false + onDeleteClick() + }, modifier = Modifier .align(Alignment.TopEnd) .padding(top = 36.dp, end = 12.dp) - ) { - DeleteLinkItemModal( - onDeleteClick = { - isMenuVisible = false - onClickDelete() - } - ) - } + ) } } } @@ -201,10 +223,10 @@ fun PreviewLinkCardItem_HasAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - linkImage = R.drawable.img_genz_trend, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } @@ -218,9 +240,10 @@ fun PreviewLinkCardItem_NoAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } @@ -234,10 +257,10 @@ fun PreviewLinkCardItem_HasOutLink() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = true, - linkImage = R.drawable.img_genz_trend, - domainImage = R.drawable.ic_domain_blog_naver_logo, + linkImageUrl = null, + domainImageUrl = null, domainName = "BLOG", - onClickDelete = { } + onDeleteClick = { } ) } } \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_domain_default.xml b/design/src/main/res/drawable/ic_domain_default.xml new file mode 100644 index 00000000..4d9e9edd --- /dev/null +++ b/design/src/main/res/drawable/ic_domain_default.xml @@ -0,0 +1,9 @@ + + + From ac2984ffc34c7598af82254efbfb6cd6907edf68 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:32:09 +0900 Subject: [PATCH 67/89] =?UTF-8?q?:art:=20=EC=A4=91=EB=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/LinkDetailCustomDropdown.kt | 141 ++++++------------ 1 file changed, 49 insertions(+), 92 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt index e7e79575..f85fc2b4 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -1,5 +1,6 @@ package com.linku.home.component +import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -42,104 +43,60 @@ fun LinkDetailCustomDropdown( .background(LocalColorTheme.current.white) .padding(horizontal = 24.dp, vertical = 13.dp) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onEditClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_edit), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - - Text( - text = "링크 수정하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onDeleteClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_delete), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_edit, + text = "링크 수정하기", + onClick = { onEditClick() } + ) - Text( - text = "링크 삭제하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_delete, + text = "링크 삭제하기", + onClick = { onDeleteClick() } + ) - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onShareClick() } - ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_share), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_share, + text = "링크 공유하기", + onClick = { onShareClick() } + ) - Text( - text = "링크 공유하기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } - } + LinkDetailDropdownItem( + iconRes = R.drawable.ic_link_go_gray, + text = "링크 보러가기", + onClick = { onGoClick() } + ) + } +} - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onGoClick() } +@Composable +private fun LinkDetailDropdownItem( + @DrawableRes iconRes: Int, + text: String, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onClick() } + ) { + Row( + modifier = Modifier.padding(vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - Row( - modifier = Modifier.padding(vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) - ) { - Image( - painter = painterResource(R.drawable.ic_link_go_gray), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + Image( + painter = painterResource(iconRes), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) - Text( - text = "링크 보러가기", - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black - ) - } + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black + ) } } } From eb9a0216c7fac3e8daec0ce971e9e1fe147d3332 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Wed, 24 Jun 2026 03:34:36 +0900 Subject: [PATCH 68/89] =?UTF-8?q?:art:=20clickable=20=EC=98=81=EC=97=AD=20?= =?UTF-8?q?padding=20=EC=9C=84=EB=A1=9C=20=EC=98=AC=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/screen/LinkDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 84a224b0..63e8d8e5 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -297,7 +297,6 @@ fun LinkDetailScreen( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(LocalColorTheme.current.gray[200]) - .padding(horizontal = 13.5.dp, vertical = 7.dp) .noRippleClickable { coroutineScope.launch { clipboard.setClipEntry( @@ -307,6 +306,7 @@ fun LinkDetailScreen( ) } } + .padding(horizontal = 13.5.dp, vertical = 7.dp) ) } } From 406cb78d0f9b07230eb05cdcd475a4538d92c75e Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 16:23:44 +0900 Subject: [PATCH 69/89] =?UTF-8?q?:art:=20=EC=83=81=ED=99=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=EC=B9=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/EmotionSelect.kt | 8 +- .../linku/home/component/SituationSelect.kt | 107 ++++++++++++++++++ .../com/linku/home/screen/SaveLinkScreen.kt | 42 ++++++- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 feature/home/src/main/java/com/linku/home/component/SituationSelect.kt diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 5305e301..23798b39 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -94,15 +94,11 @@ private fun EmotionBadgeImage( selected: Boolean, onToggle: () -> Unit ) { - val boxBackground = Brush.horizontalGradient( - listOf(Color(0x1A2C6FFF), Color(0x1AC800FF)) - ) - Row( modifier = Modifier .clip(RoundedCornerShape(20.dp)) .background( - brush = if (selected) boxBackground else SolidColor(LocalColorTheme.current.white) + brush = if (selected) LocalColorTheme.current.inactiveColor else SolidColor(LocalColorTheme.current.white) ) .then( if (selected) { @@ -130,7 +126,7 @@ private fun EmotionBadgeImage( style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.gray[800], + color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font ) ) diff --git a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt new file mode 100644 index 00000000..6e4f792b --- /dev/null +++ b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt @@ -0,0 +1,107 @@ +package com.linku.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +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 com.linku.core.model.Situation +import com.linku.core.model.SituationOptions +import com.linku.design.modifier.noRippleClickable +import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.color.Basic + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SituationSelect( + jobId: Long, + selectedSituationId: Long?, + onSituationSelect: (Long?) -> Unit, + modifier: Modifier = Modifier +) { + val situations = SituationOptions.situationsFor(jobId) + + FlowRow( + modifier = modifier + .fillMaxWidth() + .padding(top = 13.dp, start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + situations.forEach { situation -> + SituationChip( + situation = situation, + selected = selectedSituationId == situation.id, + onClick = { + onSituationSelect( + if (selectedSituationId == situation.id) null else situation.id + ) + } + ) + } + } +} + +@Composable +private fun SituationChip( + situation: Situation, + selected: Boolean, + onClick: () -> Unit +) { + Text( + text = situation.tagName, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.black, + modifier = Modifier + .background( + brush = if (selected) { + LocalColorTheme.current.inactiveColor + } else { + SolidColor(LocalColorTheme.current.white) + }, + shape = RoundedCornerShape(20.dp) + ) + .then( + if (selected) { + Modifier.border( + width = 1.dp, + brush = Basic.maincolor, + shape = RoundedCornerShape(20.dp) + ) + } else { + Modifier.border( + width = 1.dp, + color = LocalColorTheme.current.gray[200], + shape = RoundedCornerShape(20.dp) + ) + } + ) + .noRippleClickable { onClick() } + .padding(horizontal = 12.dp, vertical = 9.dp) + ) +} + +@Preview(showBackground = false) +@Composable +fun PreviewSituationSelect() { + ThemeProvider { + SituationSelect( + jobId = 3L, + selectedSituationId = 18L, + onSituationSelect = { } + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 1035f0e0..391beb26 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -40,6 +40,7 @@ import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.home.R import com.linku.home.component.EmotionSelect +import com.linku.home.component.SituationSelect import java.io.File @Composable @@ -49,11 +50,14 @@ fun SaveLinkScreen( title: String = "", memo: String, selectedEmotionId: Long?, + selectedSituationId: Long?, + jobId: Long, onPickImage: () -> Unit, onUrlChange: (String) -> Unit, onTitleChange: (String) -> Unit, onMemoChange: (String) -> Unit, onEmotionSelect: (Long?) -> Unit, + onSituationSelect: (Long?) -> Unit, onSaveClick: () -> Unit, onBack: () -> Unit, isCheckingUrl: Boolean, @@ -70,7 +74,11 @@ fun SaveLinkScreen( !isInvalidLink && (isDuplicateUrl != true) - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalColorTheme.current.white) + ) { Column( modifier = Modifier .fillMaxSize() @@ -385,7 +393,34 @@ fun SaveLinkScreen( onEmotionSelect = onEmotionSelect ) - Spacer(modifier = Modifier.height(100.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 25.dp, start = 24.dp, end = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "상황", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = LocalColorTheme.current.gray[800] + ) + + Text( + text = "선택", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = LocalColorTheme.current.blue[200] + ) + } + + SituationSelect( + jobId = jobId, + selectedSituationId = selectedSituationId, + onSituationSelect = onSituationSelect + ) + + Spacer(modifier = Modifier.height(70.dp)) } Column( @@ -430,11 +465,14 @@ fun PreviewSaveLinkScreen() { title = "", memo = "", selectedEmotionId = null, + selectedSituationId = null, + jobId = 2L, onPickImage = { }, onUrlChange = { }, onTitleChange = { }, onMemoChange = { }, onEmotionSelect = { }, + onSituationSelect = { }, onSaveClick = { }, onBack = { }, isCheckingUrl = false, From 0b5645c85677741cf92ffb57057daf0c96e05f7e Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 16:29:48 +0900 Subject: [PATCH 70/89] =?UTF-8?q?:sparkles:=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=97=90=20=EC=83=81=ED=99=A9=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/linku/MainApp.kt | 15 ++++++++++++--- .../src/main/java/com/linku/home/HomeViewModel.kt | 8 ++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 7c011235..51d7bd39 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -465,17 +465,26 @@ fun MainApp( title = vm.title, memo = vm.memo, selectedEmotionId = vm.selectedEmotionId, + selectedSituationId = vm.selectedSituationId, + jobId = vm.jobId ?: 3L, onPickImage = { imagePicker.launch("image/*") }, onUrlChange = vm::setUrl, onTitleChange = vm::setTitle, onMemoChange = vm::setMemo, onEmotionSelect = vm::selectEmotion, + onSituationSelect = vm::selectSituation, onSaveClick = { - // 저장 버튼 로그 + API 호출 - Log.d("SaveLink", "try save -> url=${vm.url}, memo=${vm.memo}, emotionId=${vm.selectedEmotionId}, image=${vm.image?.name}") + Log.d( + "SaveLink", + "try save -> url=${vm.url}, memo=${vm.memo}, emotionId=${vm.selectedEmotionId}, situationId=${vm.selectedSituationId}, image=${vm.image?.name}" + ) + vm.saveLink( onSucceed = { saved -> - Log.d("SaveLink", "success -> id=${saved.linkuId}, title=${saved.title}, domain=${saved.domain}") + Log.d( + "SaveLink", + "success -> id=${saved.linkuId}, title=${saved.title}, domain=${saved.domain}" + ) vm.loadLinkDetail(saved.linkuId) vm.resetForm() navigator.navigate("savelinkresult/${saved.linkuId}") diff --git a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt index 575188b0..417339a7 100644 --- a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt @@ -155,6 +155,7 @@ class HomeViewModel @Inject constructor( private val titleState = mutableStateOf("") private val memoState = mutableStateOf("") private val emotionIdState = mutableStateOf(null) + private val situationIdState = mutableStateOf(null) private val isSavingState = mutableStateOf(false) // URL 유효성 검사 @@ -167,6 +168,7 @@ class HomeViewModel @Inject constructor( val title get() = titleState.value val memo get() = memoState.value val selectedEmotionId get() = emotionIdState.value + val selectedSituationId get() = situationIdState.value val isSaving get() = isSavingState.value val isCheckingUrl get() = isCheckingUrlState.value @@ -216,7 +218,7 @@ class HomeViewModel @Inject constructor( fun setTitle(newTitle: String) { titleState.value = newTitle } fun setMemo(newMemo: String) { memoState.value = newMemo } fun selectEmotion(id: Long?) { emotionIdState.value = id } - + fun selectSituation(id: Long?) { situationIdState.value = id } // 저장 폼 초기화 @@ -226,6 +228,7 @@ class HomeViewModel @Inject constructor( titleState.value = "" memoState.value = "" emotionIdState.value = null + situationIdState.value = null } // 최근 조회 링크 상태 @@ -273,7 +276,8 @@ class HomeViewModel @Inject constructor( image = imageState.value, url = currentUrl, memo = memoState.value.ifBlank { null }, - emotionId = emotionIdState.value + emotionId = emotionIdState.value, + // situationId = situationIdState.value ) // 낙관적 업데이트: 메모리의 최근 목록 즉시 갱신 From 7878663d10cb1886e381ab8e7fbd897098320833 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 18:19:11 +0900 Subject: [PATCH 71/89] =?UTF-8?q?:zap:=20null=EC=9D=84=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20=EC=95=88=ED=95=98=EA=B3=A0=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EB=B9=88=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/LinkCardItem.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index a48c7b23..3c9b08db 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -41,10 +41,10 @@ fun LinkCardItem( hasAiSummary: Boolean, linkTitle: String, tags: List = emptyList(), - domainName: String? = null, + domainName: String = "", isExternalLink: Boolean, - linkImageUrl: String? = null, - domainImageUrl: String? = null, + linkImageUrl: String = "", + domainImageUrl: String = "", onDeleteClick: () -> Unit ) { var isMenuVisible by remember { mutableStateOf(false) } @@ -61,7 +61,7 @@ fun LinkCardItem( .padding(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - if (linkImageUrl.isNullOrBlank()) { + if (linkImageUrl.isBlank()) { Image( painter = painterResource(R.drawable.img_link_default), contentDescription = null, @@ -144,7 +144,7 @@ fun LinkCardItem( Row( verticalAlignment = Alignment.CenterVertically ) { - if (!domainImageUrl.isNullOrBlank()) { + if (domainImageUrl.isNotBlank()) { Image( painter = rememberAsyncImagePainter(model = domainImageUrl), contentDescription = null, @@ -163,7 +163,7 @@ fun LinkCardItem( Spacer(modifier = Modifier.width(6.dp)) Text( - text = domainName ?: "", + text = domainName, fontSize = 12.sp, fontWeight = FontWeight.Medium, color = LocalColorTheme.current.gray[600] @@ -223,8 +223,8 @@ fun PreviewLinkCardItem_HasAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - linkImageUrl = null, - domainImageUrl = null, + linkImageUrl = "", + domainImageUrl = "", domainName = "BLOG", onDeleteClick = { } ) @@ -240,8 +240,8 @@ fun PreviewLinkCardItem_NoAiSummary() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = false, - linkImageUrl = null, - domainImageUrl = null, + linkImageUrl = "", + domainImageUrl = "", domainName = "BLOG", onDeleteClick = { } ) @@ -257,8 +257,8 @@ fun PreviewLinkCardItem_HasOutLink() { linkTitle = "요즘 대학생들이 진짜 쓰는 앱 TOP10", tags = listOf("생산성·툴", "평온"), isExternalLink = true, - linkImageUrl = null, - domainImageUrl = null, + linkImageUrl = "", + domainImageUrl = "", domainName = "BLOG", onDeleteClick = { } ) From 63218e48bcc1a59ad43fcabafe90a9fd1254a3c9 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 18:56:49 +0900 Subject: [PATCH 72/89] =?UTF-8?q?:zap:=20FlowRow=EB=A1=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/EmotionSelect.kt | 119 ++++++++---------- 1 file changed, 54 insertions(+), 65 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 23798b39..58a3658b 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -1,24 +1,24 @@ package com.linku.home.component -import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +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.RoundedCornerShape +import androidx.compose.material3.MaterialTheme 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.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -26,107 +26,96 @@ 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 com.linku.core.model.EmotionType import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic -import com.linku.home.R - -private data class EmotionUi( - val id: Long, - val label: String, - @param:DrawableRes val iconRes: Int -) - -private val EMOTIONS = listOf( - EmotionUi(1L, "즐거움", R.drawable.ic_joy), - EmotionUi(2L, "평온", R.drawable.ic_calm), - EmotionUi(3L, "설렘", R.drawable.ic_excite), - EmotionUi(4L, "우울", R.drawable.ic_sad), - EmotionUi(5L, "짜증", R.drawable.ic_irritation), - EmotionUi(6L, "분노", R.drawable.ic_anger), -) +import com.linku.design.theme.linkuColors +import com.linku.home.util.imgRes +@OptIn(ExperimentalLayoutApi::class) @Composable fun EmotionSelect( selectedEmotionId: Long?, - onEmotionSelect: (Long?) -> Unit + onEmotionSelect: (Long?) -> Unit, + modifier: Modifier = Modifier ) { - val firstRow = EMOTIONS.take(3) - val secondRow = EMOTIONS.drop(3) + val emotions = EmotionType.entries.toList() - Column( - modifier = Modifier.padding(top = 13.dp, start = 20.dp) + FlowRow( + modifier = modifier + .fillMaxWidth() + .padding(top = 13.dp, start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Row { - firstRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } - ) - Spacer(modifier = Modifier.width(10.dp)) - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row { - secondRow.forEach { e -> - EmotionBadgeImage( - iconRes = e.iconRes, - label = e.label, - selected = selectedEmotionId == e.id, - onToggle = { onEmotionSelect(if (selectedEmotionId == e.id) null else e.id) } - ) - Spacer(modifier = Modifier.width(10.dp)) - } + emotions.forEach { emotion -> + EmotionBadgeImage( + emotion = emotion, + selected = selectedEmotionId == emotion.id, + onClick = { + onEmotionSelect( + if (selectedEmotionId == emotion.id) null else emotion.id + ) + } + ) } } } @Composable private fun EmotionBadgeImage( - @DrawableRes iconRes: Int, - label: String, + emotion: EmotionType, selected: Boolean, - onToggle: () -> Unit + onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Row( modifier = Modifier .clip(RoundedCornerShape(20.dp)) .background( - brush = if (selected) LocalColorTheme.current.inactiveColor else SolidColor(LocalColorTheme.current.white) + brush = if (selected) { + colors.inactiveColor + } else { + SolidColor(colors.white) + }, + shape = RoundedCornerShape(20.dp) ) .then( if (selected) { - Modifier.border(1.dp, brush = Basic.maincolor, shape = RoundedCornerShape(20.dp)) + Modifier.border( + width = 1.dp, + brush = Basic.maincolor, + shape = RoundedCornerShape(20.dp) + ) } else { - Modifier.border(1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(20.dp)) + Modifier.border( + width = 1.dp, + color = colors.gray[200], + shape = RoundedCornerShape(20.dp) + ) } ) - .noRippleClickable { onToggle() } + .noRippleClickable { onClick() } .padding(horizontal = 15.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { - // 아이콘 Image( - painter = painterResource(id = iconRes), - contentDescription = label, + painter = painterResource(id = emotion.imgRes), + contentDescription = emotion.tagName, modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(5.dp)) - // 라벨 Text( - text = label, + text = emotion.tagName, style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, fontFamily = LocalFontTheme.current.font ) ) @@ -138,7 +127,7 @@ private fun EmotionBadgeImage( fun PreviewEmotionSelect() { ThemeProvider { EmotionSelect( - selectedEmotionId = 1, + selectedEmotionId = 1L, onEmotionSelect = { } ) } From e9c8c57ddd5a6a07cb38f2f2cb7cc8fa1658b762 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 19:25:47 +0900 Subject: [PATCH 73/89] =?UTF-8?q?:zap:=20home=20=EB=AA=A8=EB=93=88=20Local?= =?UTF-8?q?ColorTheme.current=EC=97=90=EC=84=9C=20Material.linkuColors?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ClipboardLinkPasteBanner.kt | 9 +- .../home/component/CustomToastMessage.kt | 7 +- .../linku/home/component/DeleteLinkModal.kt | 39 ++++---- .../component/LinkDetailCategoryDropdown.kt | 11 ++- .../component/LinkDetailCustomDropdown.kt | 11 ++- .../component/LinkDetailEmotionDropdown.kt | 11 ++- .../component/LinkDetailSituationDropdown.kt | 12 +-- .../linku/home/component/SituationSelect.kt | 15 ++-- .../java/com/linku/home/screen/HomeScreen.kt | 70 ++++++--------- .../com/linku/home/screen/LinkDetailScreen.kt | 57 ++++++------ .../com/linku/home/screen/SaveLinkScreen.kt | 65 +++++++------- .../com/linku/home/ui/home/bar/HomeTopBar.kt | 18 ++-- .../home/ui/home/bar/LinkDetailTopBar.kt | 89 ++++++------------- ...tionSelector.kt => EmotionIconSelector.kt} | 13 +-- .../ui/home/bar/component/HomeSearchBar.kt | 8 +- .../home/bar/component/SelectedSummaryRow.kt | 9 +- .../ui/home/bar/component/TaskSelector.kt | 13 ++- 17 files changed, 226 insertions(+), 231 deletions(-) rename feature/home/src/main/java/com/linku/home/ui/home/bar/component/{EmotionSelector.kt => EmotionIconSelector.kt} (90%) diff --git a/feature/home/src/main/java/com/linku/home/component/ClipboardLinkPasteBanner.kt b/feature/home/src/main/java/com/linku/home/component/ClipboardLinkPasteBanner.kt index 6e0b648e..22db9b31 100644 --- a/feature/home/src/main/java/com/linku/home/component/ClipboardLinkPasteBanner.kt +++ b/feature/home/src/main/java/com/linku/home/component/ClipboardLinkPasteBanner.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -41,8 +42,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.linkuColors import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -54,6 +55,7 @@ fun rememberClipboardUrl( schemes: List = listOf("https://", "http://") ): State { val context = LocalContext.current + val clipboard = remember(context) { context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } @@ -109,6 +111,7 @@ fun ClipboardLinkPasteBanner( onPasteClick: (() -> Unit)? = null, ) { val density = LocalDensity.current + val colors = MaterialTheme.linkuColors val scope = rememberCoroutineScope() // 드래그 오프셋(px) @@ -165,7 +168,7 @@ fun ClipboardLinkPasteBanner( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(50)) - .background(LocalColorTheme.current.maincolor) + .background(colors.maincolor) .draggable( orientation = Orientation.Vertical, state = rememberDraggableState { delta -> @@ -187,7 +190,7 @@ fun ClipboardLinkPasteBanner( text = "$displayLink 링크를 붙여넣을까요?", fontSize = 13.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.white, + color = colors.white, fontFamily = LocalFontTheme.current.font, maxLines = 1, textAlign = TextAlign.Center, diff --git a/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt index 0fa4b42c..290b688a 100644 --- a/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt +++ b/feature/home/src/main/java/com/linku/home/component/CustomToastMessage.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -14,8 +15,8 @@ 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 com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors @Composable fun CustomToastMessage( @@ -42,10 +43,12 @@ fun CustomToastMessage( @Preview(showBackground = false) @Composable fun PreviewCustomToastMessage() { + val colors = MaterialTheme.linkuColors + ThemeProvider { CustomToastMessage( backgroundColor = Color(0xFFE0FBEB), - textColor = LocalColorTheme.current.positive, + textColor = colors.positive, toastMessage = "유효한 링크입니다!" ) } diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt index 00da853e..8661e8e9 100644 --- a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -15,12 +15,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme 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.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -28,10 +28,10 @@ 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 com.linku.design.theme.LocalColorTheme -import com.linku.design.theme.LocalFontTheme +import com.linku.design.BrushText import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.home.R @Composable @@ -39,11 +39,13 @@ fun DeleteLinkModal( onDismiss: () -> Unit, onConfirm: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white), + .background(colors.white), horizontalAlignment = Alignment.CenterHorizontally ) { Column ( @@ -64,7 +66,7 @@ fun DeleteLinkModal( text = "해당 링크를 삭제하시겠습니까?", fontSize = 18.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(top = 15.dp) ) @@ -74,8 +76,7 @@ fun DeleteLinkModal( lineHeight = 22.sp, textAlign = TextAlign.Center, fontWeight = FontWeight.Normal, - fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(top = 13.dp) ) @@ -89,20 +90,17 @@ fun DeleteLinkModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .clickable { onDismiss() }, contentAlignment = Alignment.Center ) { - Text( + BrushText( text = "취소하기", + brush = colors.maincolor, style = TextStyle( fontSize = 16.sp, - fontWeight = FontWeight.Medium, - brush = Basic.maincolor, // 그라데이션 Brush 사용 - fontFamily = LocalFontTheme.current.font - ), - modifier = Modifier - .graphicsLayer(alpha = 0.99f) // brush 적용 시 필수 + fontWeight = FontWeight.Medium + ) ) } @@ -113,18 +111,15 @@ fun DeleteLinkModal( .weight(1f) .height(50.dp) .clip(RoundedCornerShape(14.dp)) - .background(brush = Basic.maincolor) + .background(brush = colors.maincolor) .clickable { onConfirm() }, contentAlignment = Alignment.Center ) { Text( text = "삭제하기", - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - fontFamily = LocalFontTheme.current.font - ), - color = LocalColorTheme.current.white + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = colors.white ) } } diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt index 025c8779..eef47a21 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -21,8 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors data class LinkCategoryOption( val id: Long, @@ -37,10 +38,12 @@ fun LinkDetailCategoryDropdown( onCategoryClick: (LinkCategoryOption) -> Unit, modifier: Modifier = Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 12.dp, top = 13.dp, bottom = 13.dp, end = 56.dp) .heightIn(max = 264.dp), verticalArrangement = Arrangement.spacedBy(1.dp) @@ -71,9 +74,9 @@ fun LinkDetailCategoryDropdown( FontWeight.Normal }, color = if (category.name == selectedCategory) { - LocalColorTheme.current.blue[200] + colors.blue[200] } else { - LocalColorTheme.current.gray[800] + colors.gray[800] } ) } diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt index f85fc2b4..08fb6a9b 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -12,6 +12,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,8 +24,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.home.R @Composable @@ -36,11 +37,13 @@ fun LinkDetailCustomDropdown( onDismiss: () -> Unit, modifier: Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .width(240.dp) .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 24.dp, vertical = 13.dp) ) { LinkDetailDropdownItem( @@ -75,6 +78,8 @@ private fun LinkDetailDropdownItem( text: String, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .fillMaxWidth() @@ -95,7 +100,7 @@ private fun LinkDetailDropdownItem( text = text, fontSize = 16.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) } } diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 0b8a4381..2ec296e7 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -21,8 +22,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.core.model.EmotionType import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.home.R import com.linku.home.util.imgRes @@ -33,10 +34,12 @@ fun LinkDetailEmotionDropdown( onEmotionClick: (EmotionType) -> Unit, modifier: Modifier = Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(top = 14.dp, start = 16.dp, end = 56.dp, bottom = 14.dp) .heightIn(max = 264.dp), verticalArrangement = Arrangement.spacedBy(1.dp) @@ -66,9 +69,9 @@ fun LinkDetailEmotionDropdown( FontWeight.Normal }, color = if (emotion.tagName == selectedEmotion) { - LocalColorTheme.current.blue[200] + colors.blue[200] } else { - LocalColorTheme.current.gray[800] + colors.gray[800] } ) } diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt index 6601642d..32ef9bf6 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -1,11 +1,11 @@ package com.linku.home.component import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,8 +16,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.core.model.Situation import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors @Composable fun LinkDetailSituationDropdown( @@ -26,10 +26,12 @@ fun LinkDetailSituationDropdown( onSituationClick: (Situation) -> Unit, modifier: Modifier = Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 38.dp) .heightIn(max = 264.dp) ) { @@ -43,9 +45,9 @@ fun LinkDetailSituationDropdown( FontWeight.Normal }, color = if (situation.id == selectedSituation?.id) { - LocalColorTheme.current.blue[200] + colors.blue[200] } else { - LocalColorTheme.current.gray[800] + colors.gray[800] }, modifier = Modifier .noRippleClickable { diff --git a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt index 6e4f792b..b0d5e1a4 100644 --- a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt @@ -3,11 +3,12 @@ package com.linku.home.component import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -19,9 +20,9 @@ import androidx.compose.ui.unit.sp import com.linku.core.model.Situation import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors @OptIn(ExperimentalLayoutApi::class) @Composable @@ -60,17 +61,19 @@ private fun SituationChip( selected: Boolean, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Text( text = situation.tagName, fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .background( brush = if (selected) { - LocalColorTheme.current.inactiveColor + colors.inactiveColor } else { - SolidColor(LocalColorTheme.current.white) + SolidColor(colors.white) }, shape = RoundedCornerShape(20.dp) ) @@ -84,7 +87,7 @@ private fun SituationChip( } else { Modifier.border( width = 1.dp, - color = LocalColorTheme.current.gray[200], + color = colors.gray[200], shape = RoundedCornerShape(20.dp) ) } diff --git a/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt b/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt index 253d0f93..9e8a3103 100644 --- a/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -48,8 +49,8 @@ import coil3.compose.AsyncImage import com.linku.core.model.LinkSimpleInfo import com.linku.core.model.SystemBarMode import com.linku.core.system.SystemBarController -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.linkuColors import com.linku.design.top.search.SearchBarTopSheet import com.linku.home.HomeViewModel import com.linku.home.R @@ -58,15 +59,6 @@ import com.linku.home.component.rememberClipboardUrl import com.linku.home.ui.home.bar.HomeTopBar import kotlinx.coroutines.launch -data class LinkItem( - val imageResId: Int?, // 링크 대표 이미지 - val title: String, // 링크 제목 - val tags: List, // 태그 2개 - val siteIconResId: Int, // 사이트 아이콘 (예: 네이버, 유튜브 등) - val siteName: String, // 사이트 이름 - val aiSummarized: Boolean // AI 요약 여부 -) - data class Situation(val id: Long, val name: String) fun situationsFor(jobId: Long): List = when (jobId) { @@ -97,16 +89,6 @@ fun situationsFor(jobId: Long): List = when (jobId) { else -> situationsFor(3L) // 혹시 모를 기본값(직장인 세트) } -private fun emotionName(id: Long?): String? = when (id) { - 1L -> "즐거움" - 2L -> "평온" - 3L -> "설렘" - 4L -> "슬픔" - 5L -> "짜증" - 6L -> "분노" - else -> null -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( @@ -124,6 +106,8 @@ fun HomeScreen( onNavigateToSaveLink: (url: String) -> Unit, onAlarmClick: () -> Unit, ) { + val colors = MaterialTheme.linkuColors + //스플래쉬에서 숨긴 시스템 바 다시 뜨도록 val systemBarController = LocalContext.current as? SystemBarController @@ -229,12 +213,12 @@ fun HomeScreen( Box( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) ) { LazyColumn( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]), + .background(colors.gray[100]), state = listState ) { stickyHeader { @@ -280,7 +264,7 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(top = 65.dp, bottom = 195.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -299,7 +283,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[700] + color = colors.gray[700] ) } } @@ -309,7 +293,7 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(top = 65.dp, bottom = 195.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -329,7 +313,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Spacer(modifier = Modifier.height(12.dp)) @@ -342,7 +326,7 @@ fun HomeScreen( textAlign = TextAlign.Center, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -352,14 +336,14 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(top = 65.dp, bottom = 195.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Text( // text = "${userName}님이 최근에 열람한 링크", // style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Start), -// color = LocalColorTheme.current.black +// color = colors.black // ) // // Spacer(modifier = Modifier.height(65.dp)) @@ -379,7 +363,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Spacer(modifier = Modifier.height(12.dp)) @@ -391,7 +375,7 @@ fun HomeScreen( fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -405,7 +389,7 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(top = 150.dp, bottom = 217.dp), contentAlignment = Alignment.Center ) { @@ -417,7 +401,7 @@ fun HomeScreen( textAlign = TextAlign.Center, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } else { @@ -428,7 +412,7 @@ fun HomeScreen( fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.black + color = colors.black ) Spacer(modifier = Modifier.height(20.dp)) @@ -511,18 +495,20 @@ fun HomeScreen( @Composable private fun EmptyRecentBox() { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(top = 150.dp, bottom = 217.dp), contentAlignment = Alignment.Center ) { Text( text = "최근에 열람한 링크가 없어요!", style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -532,11 +518,13 @@ private fun LinkCard( link: LinkSimpleInfo, onClick: () -> Unit, ) { + val colors = MaterialTheme.linkuColors + Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(10.dp) .clickable { onClick() } ) { @@ -581,7 +569,7 @@ private fun LinkCard( Text( text = link.title, style = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.black + color = colors.black ) Spacer(modifier = Modifier.height(10.dp)) @@ -598,7 +586,7 @@ private fun LinkCard( Box( modifier = Modifier .background( - LocalColorTheme.current.gray[100], + colors.gray[100], RoundedCornerShape(6.dp) ) .padding(horizontal = 6.dp, vertical = 3.dp) @@ -608,7 +596,7 @@ private fun LinkCard( style = TextStyle( fontSize = 10.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], fontFamily = LocalFontTheme.current.font ) ) @@ -642,7 +630,7 @@ private fun LinkCard( Text( text = link.domain, - style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = LocalColorTheme.current.gray[800], fontFamily = LocalFontTheme.current.font) + style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.gray[800], fontFamily = LocalFontTheme.current.font) ) } } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 63e8d8e5..5f488fb8 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,8 +51,8 @@ import androidx.compose.ui.zIndex import com.linku.core.model.EmotionType import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.home.R import com.linku.home.component.AIArticleModal import com.linku.home.component.DeleteLinkModal @@ -83,6 +84,8 @@ fun LinkDetailScreen( categoryOptions: List, onBack: () -> Unit, ) { + val colors = MaterialTheme.linkuColors + val clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current @@ -153,7 +156,7 @@ fun LinkDetailScreen( Box( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Column( modifier = Modifier @@ -211,7 +214,7 @@ fun LinkDetailScreen( .alpha(if (isEditMode) 0.6f else 1f) .border( width = 1.dp, - color = LocalColorTheme.current.gray[200], + color = colors.gray[200], shape = RoundedCornerShape(18.dp) ) ) @@ -228,7 +231,7 @@ fun LinkDetailScreen( modifier = Modifier .size(84.dp) .clip(RoundedCornerShape(30.dp)) - .background(LocalColorTheme.current.gray[700]) + .background(colors.gray[700]) .alpha(0.6f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center @@ -247,7 +250,7 @@ fun LinkDetailScreen( text = "사진 변경", fontSize = 12.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.white + color = colors.white ) } } @@ -262,10 +265,10 @@ fun LinkDetailScreen( .clip(RoundedCornerShape(18.dp)) .border( width = 1.dp, - color = LocalColorTheme.current.gray[200], + color = colors.gray[200], shape = RoundedCornerShape(18.dp) ) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(top = 7.5.dp, start = 22.dp, end = 8.5.dp, bottom = 7.5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween @@ -275,7 +278,7 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = if (isEditMode) LocalColorTheme.current.gray[400] else LocalColorTheme.current.black, + color = if (isEditMode) colors.gray[400] else colors.black, modifier = Modifier .then( if (isEditMode) { @@ -293,10 +296,10 @@ fun LinkDetailScreen( text = "복사", fontSize = 13.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[200]) + .background(colors.gray[200]) .noRippleClickable { coroutineScope.launch { clipboard.setClipEntry( @@ -333,7 +336,7 @@ fun LinkDetailScreen( text = "AI 태그", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } @@ -348,11 +351,11 @@ fun LinkDetailScreen( text = tag, fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .clip(RoundedCornerShape(20.dp)) - .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(20.dp)) - .background(LocalColorTheme.current.white) + .border(1.dp, colors.inactiveColor, RoundedCornerShape(20.dp)) + .background(colors.white) .padding(horizontal = 15.dp, vertical = 9.dp) ) } @@ -381,7 +384,7 @@ fun LinkDetailScreen( text = "AI 링크 요약", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } @@ -389,13 +392,13 @@ fun LinkDetailScreen( text = aiSummary, fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, lineHeight = 20.sp, modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .border(1.dp, LocalColorTheme.current.inactiveColor, RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .border(1.dp, colors.inactiveColor, RoundedCornerShape(18.dp)) + .background(colors.white) .padding(horizontal = 22.dp, vertical = 16.dp) ) } @@ -410,7 +413,7 @@ fun LinkDetailScreen( text = "메모", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) Spacer(modifier = Modifier.height(12.dp)) @@ -425,12 +428,12 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.black + color = colors.black ), modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 22.dp, vertical = 15.5.dp), decorationBox = { innerTextField -> Box( @@ -442,7 +445,7 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.gray[400] + color = colors.gray[400] ) } @@ -456,11 +459,11 @@ fun LinkDetailScreen( fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 22.dp, vertical = 15.5.dp) ) } @@ -622,7 +625,7 @@ fun LinkDetailScreen( .padding(horizontal = 20.dp) .align(Alignment.BottomCenter) .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.maincolor) + .background(colors.maincolor) .padding(vertical = 15.dp) .noRippleClickable { if (isEditMode) { @@ -647,7 +650,7 @@ fun LinkDetailScreen( text = "완료", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } else { Image( @@ -662,7 +665,7 @@ fun LinkDetailScreen( text = "AI 요약", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 391beb26..29506851 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -19,25 +19,26 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme 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.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle 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.compose.ui.layout.ContentScale -import androidx.compose.ui.text.style.TextAlign import coil3.compose.rememberAsyncImagePainter import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.home.R import com.linku.home.component.EmotionSelect import com.linku.home.component.SituationSelect @@ -64,6 +65,8 @@ fun SaveLinkScreen( isDuplicateUrl: Boolean?, isInvalidLink: Boolean, ) { + val colors = MaterialTheme.linkuColors + val scrollState = rememberScrollState() val bannedDomains = listOf("youtube.com", "youtu.be") val showVideoWarning = bannedDomains.any { url.contains(it, ignoreCase = true) } @@ -77,7 +80,7 @@ fun SaveLinkScreen( Box( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Column( modifier = Modifier @@ -105,7 +108,7 @@ fun SaveLinkScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -114,7 +117,7 @@ fun SaveLinkScreen( text = "URL 링크", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(top = 31.dp, start = 24.dp) ) @@ -137,7 +140,7 @@ fun SaveLinkScreen( text = "링크를 입력하거나 붙여넣어 주세요.", fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[400] + color = colors.gray[400] ) } @@ -145,7 +148,7 @@ fun SaveLinkScreen( value = url, onValueChange = onUrlChange, singleLine = true, - textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = colors.black, fontFamily = LocalFontTheme.current.font), modifier = Modifier.fillMaxWidth() ) } @@ -160,14 +163,14 @@ fun SaveLinkScreen( text = "링크 제목", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, ) Text( text = "선택", fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.blue[200], + color = colors.blue[200], ) } @@ -190,7 +193,7 @@ fun SaveLinkScreen( text = "링크 제목을 입력해주세요.", fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[400] + color = colors.gray[400] ) } @@ -201,7 +204,7 @@ fun SaveLinkScreen( textStyle = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, fontFamily = LocalFontTheme.current.font ), modifier = Modifier.fillMaxWidth() @@ -215,7 +218,7 @@ fun SaveLinkScreen( // isCheckingUrl -> Text( // text = "링크를 확인 중입니다…", // style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), -// color = LocalColorTheme.current.gray[600], +// color = colors.gray[600], // modifier = Modifier.padding(start = 32.dp, top = 4.dp) // ) // isInvalidLink -> { @@ -225,7 +228,7 @@ fun SaveLinkScreen( // isDuplicateUrl == false -> Text( // text = "저장 가능한 링크예요.", // style = TextStyle(fontSize = 13.sp, fontFamily = LocalFontTheme.current.font), -// color = LocalColorTheme.current.blue[200], +// color = colors.blue[200], // modifier = Modifier.padding(start = 32.dp, top = 4.dp) // ) // else -> Unit @@ -259,14 +262,14 @@ fun SaveLinkScreen( .fillMaxWidth() .aspectRatio(1f) .clip(RoundedCornerShape(18.dp)) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .border(1.dp, colors.gray[200], RoundedCornerShape(18.dp)) ) } else { Column( modifier = Modifier .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) + .background(colors.gray[100]) + .border(1.dp, colors.gray[200], RoundedCornerShape(18.dp)) .noRippleClickable { onPickImage() } .padding(38.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -281,7 +284,7 @@ fun SaveLinkScreen( text = "사진 추가", fontSize = 14.sp, fontWeight = FontWeight.Light, - color = LocalColorTheme.current.gray[500], + color = colors.gray[500], modifier = Modifier.padding(top = 7.dp) ) } @@ -298,14 +301,14 @@ fun SaveLinkScreen( text = "메모", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800], + color = colors.gray[800], ) Text( text = "선택", fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.blue[200] + color = colors.blue[200] ) } @@ -315,7 +318,7 @@ fun SaveLinkScreen( .padding(top = 13.dp, start = 20.dp, end = 20.dp) .then( if (memo.isEmpty()) { - Modifier.border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) + Modifier.border(width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) } else { Modifier.border( border = BorderStroke(width = 1.dp, brush = Basic.maincolor), @@ -332,14 +335,14 @@ fun SaveLinkScreen( text = "메모할 내용을 입력해주세요.", fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[400] + color = colors.gray[400] ) } BasicTextField( value = memo, onValueChange = { if (it.length <= 200) onMemoChange(it) }, - textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font), + textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, color = colors.black, fontFamily = LocalFontTheme.current.font), modifier = Modifier.fillMaxWidth() ) } @@ -355,14 +358,14 @@ fun SaveLinkScreen( text = memo.length.toString(), fontSize = 12.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[700] + color = colors.gray[700] ) Text( text = "/200자", fontSize = 12.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[400], + color = colors.gray[400], modifier = Modifier.padding(start = 1.dp) ) } @@ -377,14 +380,14 @@ fun SaveLinkScreen( text = "감정", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Text( text = "선택", fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.blue[200] + color = colors.blue[200] ) } @@ -403,14 +406,14 @@ fun SaveLinkScreen( text = "상황", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Text( text = "선택", fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.blue[200] + color = colors.blue[200] ) } @@ -437,7 +440,7 @@ fun SaveLinkScreen( if (isButtonEnabled) { Modifier.background(Basic.maincolor) } else { - Modifier.background(LocalColorTheme.current.gray[300]) + Modifier.background(colors.gray[300]) } ) .padding(vertical = 15.dp), @@ -447,7 +450,7 @@ fun SaveLinkScreen( text = "저장", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white, + color = colors.white, textAlign = TextAlign.Center ) } diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt index 56c6cde2..dedab21d 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt @@ -18,6 +18,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,15 +40,15 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic import com.linku.design.theme.font.Taebaek +import com.linku.design.theme.linkuColors import com.linku.design.top.bar.AlarmButton import com.linku.file.ui.theme.MainColor import com.linku.home.R import com.linku.home.screen.Situation -import com.linku.home.ui.home.bar.component.EmotionSelector +import com.linku.home.ui.home.bar.component.EmotionIconSelector import com.linku.home.ui.home.bar.component.HomeSearchBar import com.linku.home.ui.home.bar.component.SelectedSummaryRow import com.linku.home.ui.home.bar.component.TaskSelector @@ -68,6 +69,7 @@ fun HomeTopBar( hasRequestedRecommend: Boolean, onAlarmClick: () -> Unit, ) { + val colors = MaterialTheme.linkuColors val buttonBrush = if (recommendEnabled) Basic.maincolor @@ -84,7 +86,7 @@ fun HomeTopBar( .clip( RoundedCornerShape(bottomStart = 30.dp, bottomEnd = 30.dp) ) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 16.dp, end = 16.dp, top = 20.dp, bottom = 13.5.dp) ) { Row( @@ -148,7 +150,7 @@ fun HomeTopBar( fontSize = 20.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(start = 8.dp, bottom = 16.dp) ) @@ -157,11 +159,11 @@ fun HomeTopBar( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], modifier = Modifier.padding(start = 8.dp, bottom = 10.dp) ) - EmotionSelector( + EmotionIconSelector( selectedEmotionId = selectedEmotionId, onEmotionChange = onEmotionChange ) @@ -173,7 +175,7 @@ fun HomeTopBar( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], modifier = Modifier.padding(start = 8.dp, bottom = 10.dp) ) @@ -205,7 +207,7 @@ fun HomeTopBar( Text( text = "링크 추천해줘!", style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white + color = colors.white ) } diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt index 4a08d9a4..3dd5c194 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/LinkDetailTopBar.kt @@ -13,7 +13,7 @@ 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.DropdownMenuItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.AbsoluteAlignment @@ -27,8 +27,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.home.R @Composable @@ -49,11 +49,13 @@ fun LinkDetailTopBar( onSituationClick: () -> Unit, onTitleClearClick: () -> Unit, ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp)) - .background(LocalColorTheme.current.blue[200]) + .background(colors.blue[200]) ) { Image( painter = painterResource(R.drawable.linku_logo_transparent), @@ -84,7 +86,7 @@ fun LinkDetailTopBar( text = if (isEditMode) "링크 수정하기" else "저장된 링크", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.white, + color = colors.white, modifier = Modifier.align(Alignment.Center) ) @@ -118,7 +120,7 @@ fun LinkDetailTopBar( Modifier .padding(bottom = 11.dp) .clip(RoundedCornerShape(13.dp)) - .border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(13.dp)) + .border(1.dp, colors.white, RoundedCornerShape(13.dp)) .padding(horizontal = 15.dp, vertical = 4.dp) } else { Modifier.padding(bottom = 12.dp) @@ -131,7 +133,7 @@ fun LinkDetailTopBar( text = linkTitle, fontSize = if (isEditMode) 22.sp else 24.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) if (isEditMode) { @@ -163,14 +165,14 @@ fun LinkDetailTopBar( .clip(RoundedCornerShape(10.dp)) .background( when { - isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.white - isEditMode -> LocalColorTheme.current.blue[200] - else -> LocalColorTheme.current.purple[50] + isEditMode && isCategoryDropdownOpen -> colors.white + isEditMode -> colors.blue[200] + else -> colors.purple[50] } ) // 추후 카테고리 API 연동 후 실제 색상으로 변경 예정 .then( if(isEditMode) { - Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + Modifier.border(1.dp, colors.white, RoundedCornerShape(10.dp)) } else { Modifier } @@ -187,9 +189,9 @@ fun LinkDetailTopBar( fontSize = 15.sp, fontWeight = FontWeight.Medium, color = when { - isEditMode && isCategoryDropdownOpen -> LocalColorTheme.current.blue[300] - isEditMode -> LocalColorTheme.current.white - else -> LocalColorTheme.current.black // API 연동 후 수정 예정 + isEditMode && isCategoryDropdownOpen -> colors.blue[300] + isEditMode -> colors.white + else -> colors.black // API 연동 후 수정 예정 } ) @@ -209,14 +211,14 @@ fun LinkDetailTopBar( .clip(RoundedCornerShape(10.dp)) .background( when { - isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.white - isEditMode -> LocalColorTheme.current.blue[200] - else -> LocalColorTheme.current.blue[50] + isEditMode && isEmotionDropdownOpen -> colors.white + isEditMode -> colors.blue[200] + else -> colors.blue[50] } ) .then( if(isEditMode) { - Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + Modifier.border(1.dp, colors.white, RoundedCornerShape(10.dp)) } else { Modifier } @@ -233,9 +235,9 @@ fun LinkDetailTopBar( fontSize = 15.sp, fontWeight = FontWeight.Medium, color = when { - isEditMode && isEmotionDropdownOpen -> LocalColorTheme.current.blue[300] - isEditMode -> LocalColorTheme.current.white - else -> LocalColorTheme.current.blue[300] + isEditMode && isEmotionDropdownOpen -> colors.blue[300] + isEditMode -> colors.white + else -> colors.blue[300] } ) @@ -255,14 +257,14 @@ fun LinkDetailTopBar( .clip(RoundedCornerShape(10.dp)) .background( when { - isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.white - isEditMode -> LocalColorTheme.current.blue[200] - else -> LocalColorTheme.current.purple[50] + isEditMode && isSituationDropdownOpen -> colors.white + isEditMode -> colors.blue[200] + else -> colors.purple[50] } ) .then( if(isEditMode) { - Modifier.border(1.dp, LocalColorTheme.current.white, RoundedCornerShape(10.dp)) + Modifier.border(1.dp, colors.white, RoundedCornerShape(10.dp)) } else { Modifier } @@ -279,9 +281,9 @@ fun LinkDetailTopBar( fontSize = 15.sp, fontWeight = FontWeight.Medium, color = when { - isEditMode && isSituationDropdownOpen -> LocalColorTheme.current.blue[300] - isEditMode -> LocalColorTheme.current.white - else -> LocalColorTheme.current.purple[300] + isEditMode && isSituationDropdownOpen -> colors.blue[300] + isEditMode -> colors.white + else -> colors.purple[300] } ) @@ -318,39 +320,6 @@ fun LinkDetailTopBar( } } -//@Composable -//private fun LinkDetailDropdownItem( -// iconRes: Int, -// text: String, -// onClick: () -> Unit, -//) { -// DropdownMenuItem( -// text = { -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.spacedBy(18.dp) -// ) { -// Image( -// painter = painterResource(iconRes), -// contentDescription = null, -// modifier = Modifier.size(28.dp) -// ) -// -// Text( -// text = text, -// fontSize = 24.sp, -// fontWeight = FontWeight.Medium, -// color = LocalColorTheme.current.gray[800] -// ) -// } -// }, -// onClick = onClick, -// modifier = Modifier -// .height(64.dp) -// .padding(horizontal = 12.dp) -// ) -//} - @Preview(showBackground = false) @Composable fun PreviewLinkDetailTopBar() { diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionSelector.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt similarity index 90% rename from feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionSelector.kt rename to feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt index ebb8de84..5113b368 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionSelector.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt @@ -13,6 +13,7 @@ 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.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,15 +25,17 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.home.R @Composable -fun EmotionSelector( +fun EmotionIconSelector( selectedEmotionId: Long?, onEmotionChange: (Long?) -> Unit ) { + val colors = MaterialTheme.linkuColors + // 컬러 아이콘 val colorIcons = listOf( R.drawable.ic_joy, @@ -75,9 +78,9 @@ fun EmotionSelector( .clip(RoundedCornerShape(18.dp)) .then( if (isSelected) { - Modifier.background(brush = LocalColorTheme.current.backgroundmaincolor, shape = RoundedCornerShape(18.dp)) + Modifier.background(brush = colors.backgroundmaincolor, shape = RoundedCornerShape(18.dp)) } else { - Modifier.background(color = LocalColorTheme.current.gray[100], shape = RoundedCornerShape(18.dp)) + Modifier.background(color = colors.gray[100], shape = RoundedCornerShape(18.dp)) } ) .then( @@ -112,7 +115,7 @@ fun EmotionSelector( fun PreviewEmotionSelector() { var selected by remember { mutableStateOf(1L) } - EmotionSelector( + EmotionIconSelector( selectedEmotionId = selected, onEmotionChange = { selected = it } ) diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt index c822e698..a0bcbb96 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/HomeSearchBar.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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.Text import androidx.compose.runtime.Composable @@ -21,13 +22,14 @@ 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 com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.linkuColors import com.linku.file.R import com.linku.file.ui.theme.White - @Composable fun HomeSearchBar() { + val colors = MaterialTheme.linkuColors + Surface( modifier = Modifier .fillMaxWidth() @@ -38,7 +40,7 @@ fun HomeSearchBar() { modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(18.dp)) - .background(brush = LocalColorTheme.current.maincolor), + .background(brush = colors.maincolor), horizontalArrangement = Arrangement.spacedBy(13.dp, Alignment.Start), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/SelectedSummaryRow.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/SelectedSummaryRow.kt index 917b2e07..150338a9 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/SelectedSummaryRow.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/SelectedSummaryRow.kt @@ -11,6 +11,7 @@ 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,9 +23,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.home.R import com.linku.home.screen.Situation @@ -34,6 +35,8 @@ fun SelectedSummaryRow( selectedTaskId: Long?, situations: List, ) { + val colors = MaterialTheme.linkuColors + val colorIcons = listOf( R.drawable.ic_joy, R.drawable.ic_calm, @@ -65,7 +68,7 @@ fun SelectedSummaryRow( modifier = Modifier .size(32.dp) .clip(RoundedCornerShape(10.dp)) - .background(brush = LocalColorTheme.current.backgroundmaincolor, shape = RoundedCornerShape(18.dp)) + .background(brush = colors.backgroundmaincolor, shape = RoundedCornerShape(18.dp)) .border( width = 1.dp, brush = Basic.maincolor, @@ -87,7 +90,7 @@ fun SelectedSummaryRow( modifier = Modifier .clip(RoundedCornerShape(10.dp)) .background( - brush = LocalColorTheme.current.backgroundmaincolor, + brush = colors.backgroundmaincolor, shape = RoundedCornerShape(10.dp) ) .border( diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt index e309f28b..ef248da6 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,9 +28,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.home.screen.Situation @Composable @@ -82,6 +83,8 @@ private fun TaskChip( selected: Boolean, onClick: () -> Unit, ) { + val colors = MaterialTheme.linkuColors + BoxChip( selected = selected, onClick = onClick @@ -102,7 +105,7 @@ private fun TaskChip( style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800], + color = colors.gray[800], fontFamily = LocalFontTheme.current.font ) ) @@ -116,14 +119,16 @@ private fun BoxChip( onClick: () -> Unit, content: @Composable () -> Unit ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .clip(RoundedCornerShape(10.dp)) .then( if (selected) { - Modifier.background(brush = LocalColorTheme.current.backgroundmaincolor, shape = RoundedCornerShape(10.dp)) + Modifier.background(brush = colors.backgroundmaincolor, shape = RoundedCornerShape(10.dp)) } else { - Modifier.background(color = LocalColorTheme.current.gray[100], shape = RoundedCornerShape(10.dp)) + Modifier.background(color = colors.gray[100], shape = RoundedCornerShape(10.dp)) } ) .then( From bf47a4dcb249cb8bd332c50f07fb181098da8e78 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 19:30:16 +0900 Subject: [PATCH 74/89] =?UTF-8?q?:zap:=20home=20=EB=AA=A8=EB=93=88=20click?= =?UTF-8?q?able=EC=97=90=EC=84=9C=20noRippleClickable=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/linku/home/component/AIArticleModal.kt | 1 - .../main/java/com/linku/home/component/DeleteLinkModal.kt | 6 +++--- .../src/main/java/com/linku/home/screen/HomeScreen.kt | 4 ++-- .../main/java/com/linku/home/ui/home/bar/HomeTopBar.kt | 8 ++++---- .../home/ui/home/bar/component/EmotionIconSelector.kt | 4 ++-- .../com/linku/home/ui/home/bar/component/TaskSelector.kt | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt index 176cb403..b45eb610 100644 --- a/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/AIArticleModal.kt @@ -3,7 +3,6 @@ package com.linku.home.component import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween 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.Spacer diff --git a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt index 8661e8e9..80dcb4ff 100644 --- a/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt +++ b/feature/home/src/main/java/com/linku/home/component/DeleteLinkModal.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors @@ -91,7 +91,7 @@ fun DeleteLinkModal( .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) .background(colors.white) - .clickable { onDismiss() }, + .noRippleClickable { onDismiss() }, contentAlignment = Alignment.Center ) { BrushText( @@ -112,7 +112,7 @@ fun DeleteLinkModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .background(brush = colors.maincolor) - .clickable { onConfirm() }, + .noRippleClickable { onConfirm() }, contentAlignment = Alignment.Center ) { Text( diff --git a/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt b/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt index 9e8a3103..46445845 100644 --- a/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/HomeScreen.kt @@ -3,7 +3,6 @@ package com.linku.home.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image 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 @@ -49,6 +48,7 @@ import coil3.compose.AsyncImage import com.linku.core.model.LinkSimpleInfo import com.linku.core.model.SystemBarMode import com.linku.core.system.SystemBarController +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.linkuColors import com.linku.design.top.search.SearchBarTopSheet @@ -526,7 +526,7 @@ private fun LinkCard( .clip(RoundedCornerShape(18.dp)) .background(colors.white) .padding(10.dp) - .clickable { onClick() } + .noRippleClickable { onClick() } ) { Box() { // Image( diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt index dedab21d..f97a4701 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/HomeTopBar.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image 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 @@ -40,6 +39,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic import com.linku.design.theme.font.Taebaek @@ -124,7 +124,7 @@ fun HomeTopBar( Box( modifier = Modifier .size(30.dp) - .clickable { onAlarmClick() } + .noRippleClickable { onAlarmClick() } ) { AlarmButton( isNoticeExist = isNoticeExist, @@ -201,7 +201,7 @@ fun HomeTopBar( .height(48.dp) .clip(RoundedCornerShape(16.dp)) .background(brush = buttonBrush) - .clickable(enabled = recommendEnabled) { onRecommendClick() }, + .noRippleClickable(enabled = recommendEnabled) { onRecommendClick() }, contentAlignment = Alignment.Center ) { Text( @@ -238,7 +238,7 @@ fun HomeTopBar( modifier = Modifier .width(44.dp) .align(Alignment.CenterHorizontally) - .clickable { onExpandClick() } + .noRippleClickable { onExpandClick() } ) } } diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt index 5113b368..48656f00 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/EmotionIconSelector.kt @@ -3,7 +3,6 @@ package com.linku.home.ui.home.bar.component 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 import androidx.compose.foundation.layout.Row @@ -25,6 +24,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors import com.linku.home.R @@ -91,7 +91,7 @@ fun EmotionIconSelector( ) else Modifier ) .padding(8.dp) - .clickable { + .noRippleClickable { onEmotionChange(if (isSelected) null else id) }, contentAlignment = Alignment.Center diff --git a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt index ef248da6..73c0cb2e 100644 --- a/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt +++ b/feature/home/src/main/java/com/linku/home/ui/home/bar/component/TaskSelector.kt @@ -2,7 +2,6 @@ package com.linku.home.ui.home.bar.component 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 import androidx.compose.foundation.layout.Column @@ -28,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors @@ -139,7 +139,7 @@ private fun BoxChip( ) else Modifier ) .padding(horizontal = 15.dp, vertical = 8.dp) - .clickable(onClick = onClick) + .noRippleClickable(onClick = onClick) ) { content() } From e953b322cd912eb9e8ac2486669c63bc40ccbcb8 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 19:54:08 +0900 Subject: [PATCH 75/89] =?UTF-8?q?:zap:=20mypage=20=EB=AA=A8=EB=93=88=20Loc?= =?UTF-8?q?alColorTheme.current=EC=97=90=EC=84=9C=20Material.linkuColors?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/mypage/component/AILinkuItem.kt | 21 ++---- .../component/CustomInfoSelectionContent.kt | 21 +++--- .../component/CustomInfoSelectionItem.kt | 13 ++-- .../mypage/component/DeleteLinkuModal.kt | 15 +++-- .../com/linku/mypage/component/FaqItem.kt | 16 ++--- .../linku/mypage/component/LinkuItemModal.kt | 9 ++- .../com/linku/mypage/component/LogoutModal.kt | 15 +++-- .../com/linku/mypage/component/NoticeItem.kt | 19 +++--- .../linku/mypage/component/QuitReasonItem.kt | 9 ++- .../mypage/component/ServiceQuitModal.kt | 15 +++-- .../linku/mypage/screen/AILinkuListScreen.kt | 33 +++++---- .../mypage/screen/AccountSettingScreen.kt | 21 +++--- .../mypage/screen/ChangePasswordScreen.kt | 53 ++++++++------- .../linku/mypage/screen/EditProfileScreen.kt | 67 +++++++++++-------- .../java/com/linku/mypage/screen/FaqScreen.kt | 33 ++++----- .../mypage/screen/InterestSelectionScreen.kt | 13 ++-- .../mypage/screen/MarketingAgreeScreen.kt | 33 ++++----- .../com/linku/mypage/screen/MyPageScreen.kt | 33 ++++----- .../com/linku/mypage/screen/NoticeScreen.kt | 9 ++- .../mypage/screen/PurposeSelectionScreen.kt | 15 +++-- .../linku/mypage/screen/ServiceAgreeScreen.kt | 17 +++-- .../linku/mypage/screen/ServiceQuitScreen.kt | 32 ++++----- .../linku/mypage/ui/top/bar/MypageTopBar.kt | 35 +++++----- 23 files changed, 304 insertions(+), 243 deletions(-) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/AILinkuItem.kt b/feature/mypage/src/main/java/com/linku/mypage/component/AILinkuItem.kt index a6a42afd..f5d5bc21 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/AILinkuItem.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/AILinkuItem.kt @@ -28,9 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors import com.linku.mypage.R @@ -44,23 +42,16 @@ fun AILinkuItem( domainName: String? = null, onClickDelete: () -> Unit = {} ) { + val colors = MaterialTheme.linkuColors + var isMenuVisible by remember { mutableStateOf(false) } Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) ) { -// Box( -// modifier = Modifier -// .fillMaxWidth() -// .size(30.dp) -// .zIndex(1f) -// ) { -// -// } - Row( modifier = Modifier .fillMaxWidth() @@ -103,10 +94,10 @@ fun AILinkuItem( text = tag, fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier .background( - color = LocalColorTheme.current.gray[100], + color = colors.gray[100], shape = RoundedCornerShape(6.dp) ) .padding(horizontal = 6.dp, vertical = 2.dp) @@ -135,7 +126,7 @@ fun AILinkuItem( text = domainName ?: "", fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionContent.kt b/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionContent.kt index 188812e9..9fcce44c 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionContent.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionContent.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -22,9 +23,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -37,10 +38,12 @@ fun CustomInfoSelectionContent( onButtonClick: () -> Unit, modifier: Modifier = Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Box( modifier = Modifier @@ -62,7 +65,7 @@ fun CustomInfoSelectionContent( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -96,12 +99,12 @@ fun CustomInfoSelectionContent( .then( if (isButtonEnabled) { Modifier.background( - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, shape = RoundedCornerShape(18.dp) ) } else { Modifier.background( - color = LocalColorTheme.current.gray[300], + color = colors.gray[300], shape = RoundedCornerShape(18.dp) ) } @@ -116,7 +119,7 @@ fun CustomInfoSelectionContent( text = buttonText, fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } @@ -125,6 +128,8 @@ fun CustomInfoSelectionContent( @Preview(showBackground = true) @Composable fun PreviewCustomInfoSelectionContent() { + val colors = MaterialTheme.linkuColors + ThemeProvider { CustomInfoSelectionContent( questionContent = { @@ -132,14 +137,14 @@ fun PreviewCustomInfoSelectionContent() { text = "어떤 정보를 수정하시겠어요?", fontSize = 18.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.black + color = colors.black ) }, selectionContent = { Text( text = "선택된 정보가 여기에 표시됩니다.", fontSize = 14.sp, - color = LocalColorTheme.current.gray[700] + color = colors.gray[700] ) }, buttonText = "저장", diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionItem.kt b/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionItem.kt index 404b2b1d..db0f019d 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionItem.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/CustomInfoSelectionItem.kt @@ -11,6 +11,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,8 +24,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -33,18 +34,20 @@ fun CustomInfoSelectionItem( isSelected: Boolean, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) .border( width = 1.dp, - color = if (isSelected) LocalColorTheme.current.blue[100] else LocalColorTheme.current.gray[200], + color = if (isSelected) colors.blue[100] else colors.gray[200], shape = RoundedCornerShape(18.dp) ) .background( if (isSelected) Color(0xFFF5F8FF) - else LocalColorTheme.current.white + else colors.white ) .noRippleClickable { onClick() } .padding(horizontal = 18.dp, vertical = 15.dp), @@ -54,7 +57,7 @@ fun CustomInfoSelectionItem( modifier = Modifier .size(18.dp) .clip(RoundedCornerShape(6.dp)) - .background(LocalColorTheme.current.gray[600]) + .background(colors.gray[600]) ) Spacer(modifier = Modifier.width(8.dp)) @@ -63,7 +66,7 @@ fun CustomInfoSelectionItem( text = text, fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.weight(1f) ) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt index 7bf5eeed..ccd1518b 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -28,9 +29,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -38,11 +39,13 @@ fun DeleteLinkuModal( onDismiss: () -> Unit, onConfirm: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white), + .background(colors.white), horizontalAlignment = Alignment.CenterHorizontally ) { Column ( @@ -63,7 +66,7 @@ fun DeleteLinkuModal( text = "해당 링크를 삭제하시겠습니까?", fontSize = 18.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(top = 22.dp) ) @@ -73,7 +76,7 @@ fun DeleteLinkuModal( lineHeight = 22.sp, textAlign = TextAlign.Center, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(top = 15.dp) ) @@ -87,7 +90,7 @@ fun DeleteLinkuModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .clickable { onDismiss() }, contentAlignment = Alignment.Center ) { @@ -116,7 +119,7 @@ fun DeleteLinkuModal( text = "삭제하기", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/FaqItem.kt b/feature/mypage/src/main/java/com/linku/mypage/component/FaqItem.kt index 8950384e..9bbb3138 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/FaqItem.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/FaqItem.kt @@ -18,12 +18,10 @@ 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.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 @@ -35,8 +33,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -46,6 +44,8 @@ fun FaqItem( expanded: Boolean, onToggle: () -> Unit ) { + val colors = MaterialTheme.linkuColors + val rotation by animateFloatAsState( targetValue = if (expanded) 180f else 0f, label = "faq_arrow_rotation" @@ -62,7 +62,7 @@ fun FaqItem( clip = true } .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -86,7 +86,7 @@ fun FaqItem( text = question, fontSize = 15.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.weight(1f) ) @@ -116,7 +116,7 @@ fun FaqItem( modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(LocalColorTheme.current.gray[200]) + .background(colors.gray[200]) ) Spacer(modifier = Modifier.height(15.dp)) @@ -125,7 +125,7 @@ fun FaqItem( text = answer, fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], lineHeight = 22.sp, modifier = Modifier.padding(horizontal = 1.dp) ) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/LinkuItemModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/LinkuItemModal.kt index 3710a4d6..9e81a3f8 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/LinkuItemModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/LinkuItemModal.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -15,13 +16,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors @Composable fun LinkuItemModal( onClickModal: () -> Unit = { } ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .width(120.dp) @@ -31,7 +34,7 @@ fun LinkuItemModal( clip = true } .clip(RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 15.dp, vertical = 10.dp) .noRippleClickable { onClickModal() } ) { @@ -39,7 +42,7 @@ fun LinkuItemModal( text = "삭제하기", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800], + color = colors.gray[800], modifier = Modifier.width(90.dp) ) } diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt index d88256e7..105d96bb 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -28,9 +29,9 @@ 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 com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -38,11 +39,13 @@ fun LogoutModal( onDismiss: () -> Unit, onConfirm: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white), + .background(colors.white), horizontalAlignment = Alignment.CenterHorizontally ) { Column ( @@ -64,7 +67,7 @@ fun LogoutModal( fontSize = 18.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(top = 15.dp) ) @@ -75,7 +78,7 @@ fun LogoutModal( textAlign = TextAlign.Center, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(top = 13.dp) ) @@ -89,7 +92,7 @@ fun LogoutModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .clickable { onDismiss() }, contentAlignment = Alignment.Center ) { @@ -124,7 +127,7 @@ fun LogoutModal( fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/NoticeItem.kt b/feature/mypage/src/main/java/com/linku/mypage/component/NoticeItem.kt index 54de39f6..61978a43 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/NoticeItem.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/NoticeItem.kt @@ -18,6 +18,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,8 +37,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -47,6 +48,8 @@ fun NoticeItem( expanded: Boolean, onToggle: () -> Unit ) { + val colors = MaterialTheme.linkuColors + var hasEverExpanded by remember { mutableStateOf(false) } var hasBeenRead by remember { mutableStateOf(false) } @@ -74,7 +77,7 @@ fun NoticeItem( clip = true } .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -104,9 +107,9 @@ fun NoticeItem( fontSize = 13.sp, fontWeight = FontWeight.Medium, color = if (hasBeenRead) { - LocalColorTheme.current.gray[300] + colors.gray[300] } else { - LocalColorTheme.current.gray[600] + colors.gray[600] } ) @@ -117,9 +120,9 @@ fun NoticeItem( fontSize = 15.sp, fontWeight = FontWeight.Medium, color = if (hasBeenRead) { - LocalColorTheme.current.gray[400] + colors.gray[400] } else { - LocalColorTheme.current.black + colors.black } ) } @@ -150,7 +153,7 @@ fun NoticeItem( modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(LocalColorTheme.current.gray[200]) + .background(colors.gray[200]) ) Spacer(modifier = Modifier.height(13.dp)) @@ -159,7 +162,7 @@ fun NoticeItem( text = contents, fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], lineHeight = 22.sp, modifier = Modifier.padding(horizontal = 1.dp) ) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/QuitReasonItem.kt b/feature/mypage/src/main/java/com/linku/mypage/component/QuitReasonItem.kt index b5a09c72..bd3190e4 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/QuitReasonItem.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/QuitReasonItem.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -17,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme +import com.linku.design.theme.linkuColors @Composable fun QuitReasonItem( @@ -25,6 +26,8 @@ fun QuitReasonItem( selected: Boolean, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -34,7 +37,7 @@ fun QuitReasonItem( .size(20.dp) .border( width = if (selected) 5.dp else 1.dp, - color = LocalColorTheme.current.blue[200], + color = colors.blue[200], shape = CircleShape ) .clip(CircleShape) @@ -47,7 +50,7 @@ fun QuitReasonItem( text = text, fontSize = 16.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) } } \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt index 2299c1d6..fd79bfca 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -28,9 +29,9 @@ 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 com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -38,11 +39,13 @@ fun ServiceQuitModal( onDismiss: () -> Unit, onConfirm: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white), + .background(colors.white), horizontalAlignment = Alignment.CenterHorizontally ) { Column ( @@ -64,7 +67,7 @@ fun ServiceQuitModal( fontSize = 18.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(top = 15.dp) ) @@ -75,7 +78,7 @@ fun ServiceQuitModal( textAlign = TextAlign.Center, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(top = 13.dp) ) @@ -89,7 +92,7 @@ fun ServiceQuitModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .clickable { onDismiss() }, contentAlignment = Alignment.Center ) { @@ -124,7 +127,7 @@ fun ServiceQuitModal( fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font ), - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/AILinkuListScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/AILinkuListScreen.kt index a5eb385c..a385f087 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/AILinkuListScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/AILinkuListScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,8 +40,8 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R import com.linku.mypage.component.AILinkuItem import com.linku.mypage.component.DeleteLinkuModal @@ -60,6 +61,8 @@ fun AILinkuListScreen( navController: NavController, links: List ) { + val colors = MaterialTheme.linkuColors + var selectedCategory by remember { mutableStateOf(null) } var isCategoryMenuExpanded by remember { mutableStateOf(false) } var deleteTarget by remember { mutableStateOf(null) } @@ -84,7 +87,7 @@ fun AILinkuListScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 20.dp) ) { Box( @@ -120,7 +123,7 @@ fun AILinkuListScreen( text = "AI 요약 링크", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } } @@ -173,7 +176,7 @@ fun AILinkuListScreen( text = "아직 생성된 AI 요약 링크가 없어요.", fontSize = 15.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Spacer(modifier = Modifier.height(5.dp)) @@ -182,7 +185,7 @@ fun AILinkuListScreen( text = "링크를 저장하고 AI 요약을 생성해보세요.", fontSize = 13.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -245,10 +248,12 @@ private fun FilterChip( selected: Boolean, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .background( - color = if (selected) LocalColorTheme.current.black else LocalColorTheme.current.white, + color = if (selected) colors.black else colors.white, shape = RoundedCornerShape(10.dp) ) .noRippleClickable { onClick() } @@ -259,7 +264,7 @@ private fun FilterChip( text = text, fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = if (selected) LocalColorTheme.current.white else LocalColorTheme.current.gray[800] + color = if (selected) colors.white else colors.gray[800] ) } } @@ -273,6 +278,8 @@ private fun CategoryDropdownChip( categories: List, onCategorySelected: (String) -> Unit ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .noRippleClickable { onClick() } @@ -280,10 +287,10 @@ private fun CategoryDropdownChip( Row( modifier = Modifier .clip(RoundedCornerShape(10.dp)) - .background(color = LocalColorTheme.current.white) + .background(color = colors.white) .border( width = 1.dp, - color = LocalColorTheme.current.gray[200], + color = colors.gray[200], shape = RoundedCornerShape(10.dp) ) .padding(horizontal = 15.dp, vertical = 10.dp), @@ -293,7 +300,7 @@ private fun CategoryDropdownChip( text = text, fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Spacer(modifier = Modifier.width(10.dp)) @@ -315,13 +322,13 @@ private fun CategoryDropdownChip( tonalElevation = 0.dp, modifier = Modifier .width(180.dp) - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 18.dp, vertical = 12.dp) ) { Column { @@ -355,7 +362,7 @@ private fun CategoryDropdownChip( text = category, fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt index fe321002..76e28ef4 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt @@ -15,6 +15,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,9 +31,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R import com.linku.design.R as Res @@ -44,10 +45,12 @@ fun AccountSettingScreen( onChangePasswordClick: () -> Unit, onCustomInfoSettingClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) ) { Box( modifier = Modifier @@ -69,7 +72,7 @@ fun AccountSettingScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -81,7 +84,7 @@ fun AccountSettingScreen( .fillMaxWidth() .padding(horizontal = 20.dp) .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .graphicsLayer { shadowElevation = 12.dp.toPx() ambientShadowColor = Color.Black.copy(alpha = 0.02f) @@ -97,7 +100,7 @@ fun AccountSettingScreen( modifier = Modifier .fillMaxWidth() .padding(end = 5.dp) - .noRippleClickable() { + .noRippleClickable { onEditProfileClick() }, horizontalArrangement = Arrangement.SpaceBetween, @@ -107,7 +110,7 @@ fun AccountSettingScreen( text = "내 정보 수정", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( @@ -129,7 +132,7 @@ fun AccountSettingScreen( text = "비밀번호 변경", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( @@ -155,7 +158,7 @@ fun AccountSettingScreen( text = "맞춤정보 설정", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( @@ -163,7 +166,7 @@ fun AccountSettingScreen( contentDescription = null, modifier = Modifier .size(8.dp, 14.dp) - .noRippleClickable() { + .noRippleClickable { onCustomInfoSettingClick() } ) diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/ChangePasswordScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/ChangePasswordScreen.kt index fbf1a287..5778fec7 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/ChangePasswordScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/ChangePasswordScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -38,9 +39,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -49,6 +50,8 @@ fun ChangePasswordScreen( userEmail: String, onClickFinish: (String) -> Unit ) { + val colors = MaterialTheme.linkuColors + var password by remember { mutableStateOf("") } var confirmPassword by remember { mutableStateOf("") } var isPasswordVisible by remember { mutableStateOf(false) } @@ -67,7 +70,7 @@ fun ChangePasswordScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Box( modifier = Modifier @@ -89,7 +92,7 @@ fun ChangePasswordScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -106,7 +109,7 @@ fun ChangePasswordScreen( text = "이메일", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(start = 4.dp) ) @@ -117,8 +120,8 @@ fun ChangePasswordScreen( .fillMaxWidth() .heightIn(min = 52.dp) .clip(RoundedCornerShape(18.dp)) - .border( width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) + .border( width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) + .background(colors.gray[100]) .padding(horizontal = 22.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -126,7 +129,7 @@ fun ChangePasswordScreen( text = userEmail, fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -143,7 +146,7 @@ fun ChangePasswordScreen( text = "새 비밀번호", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(start = 4.dp) ) @@ -154,8 +157,8 @@ fun ChangePasswordScreen( .fillMaxWidth() .heightIn(min = 52.dp) .clip(RoundedCornerShape(18.dp)) - .border( width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .border( width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) + .background(colors.white) .padding(horizontal = 22.dp, vertical = 10.dp), contentAlignment = Alignment.CenterStart ) { @@ -174,7 +177,7 @@ fun ChangePasswordScreen( text = "새로운 비밀번호를 입력해주세요", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[500] + color = colors.gray[500] ) } @@ -186,13 +189,13 @@ fun ChangePasswordScreen( TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) } else { TextStyle( fontSize = 10.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) }, visualTransformation = if (isPasswordVisible) { @@ -243,7 +246,7 @@ fun ChangePasswordScreen( text = "영문, 숫자, 특수기호 조합", fontSize = 12.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } @@ -267,7 +270,7 @@ fun ChangePasswordScreen( text = "8~20자", fontSize = 12.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } @@ -280,10 +283,10 @@ fun ChangePasswordScreen( .clip(RoundedCornerShape(18.dp)) .border( width = 1.dp, - color = LocalColorTheme.current.gray[200], + color = colors.gray[200], shape = RoundedCornerShape(18.dp) ) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 22.dp, vertical = 10.dp), contentAlignment = Alignment.CenterStart ) { @@ -302,7 +305,7 @@ fun ChangePasswordScreen( text = "비밀번호를 확인해주세요", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[500] + color = colors.gray[500] ) } @@ -314,13 +317,13 @@ fun ChangePasswordScreen( TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) } else { TextStyle( fontSize = 10.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) }, visualTransformation = if (isConfirmPasswordVisible) { @@ -374,9 +377,9 @@ fun ChangePasswordScreen( fontSize = 13.sp, fontWeight = FontWeight.Normal, color = if (isPasswordMatched) { - LocalColorTheme.current.positive + colors.positive } else { - LocalColorTheme.current.negative + colors.negative } ) } @@ -394,12 +397,12 @@ fun ChangePasswordScreen( .then( if (isChangePasswordEnabled) { Modifier.background( - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, shape = RoundedCornerShape(18.dp) ) } else { Modifier.background( - color = LocalColorTheme.current.gray[300], + color = colors.gray[300], shape = RoundedCornerShape(18.dp) ) } @@ -414,7 +417,7 @@ fun ChangePasswordScreen( text = "완료", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt index 7677d9ec..9d139b96 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,9 +44,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable @@ -62,6 +63,8 @@ fun EditProfileScreen( userGender: String, userSocialLoginType: String, ) { + val colors = MaterialTheme.linkuColors + var isProfileImageChanged by remember { mutableStateOf(false) } var name by remember(userNickname) { mutableStateOf(userNickname) } @@ -132,7 +135,7 @@ fun EditProfileScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Box( modifier = Modifier @@ -154,7 +157,7 @@ fun EditProfileScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -181,7 +184,7 @@ fun EditProfileScreen( Box( modifier = Modifier .size(30.dp) - .border(4.dp, LocalColorTheme.current.white, shape = CircleShape) + .border(4.dp, colors.white, shape = CircleShape) .align(Alignment.BottomEnd) .noRippleClickable { onPickProfileImage() @@ -207,7 +210,7 @@ fun EditProfileScreen( text = "닉네임", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(start = 4.dp) ) @@ -218,8 +221,8 @@ fun EditProfileScreen( .fillMaxWidth() .heightIn(min = 52.dp) .clip(RoundedCornerShape(18.dp)) - .border( width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .border( width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) + .background(colors.white) .padding(horizontal = 22.dp, vertical = 10.dp), contentAlignment = Alignment.CenterStart ) { @@ -234,7 +237,7 @@ fun EditProfileScreen( textStyle = TextStyle( fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ), modifier = Modifier .weight(1f) @@ -262,7 +265,7 @@ fun EditProfileScreen( text = "이미 존재하는 닉네임이에요.", fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.negative, + color = colors.negative, modifier = Modifier.padding(start = 4.dp) ) @@ -282,7 +285,7 @@ fun EditProfileScreen( text = "이메일", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(start = 4.dp) ) @@ -293,8 +296,8 @@ fun EditProfileScreen( .fillMaxWidth() .heightIn(min = 52.dp) .clip(RoundedCornerShape(18.dp)) - .border( width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) + .border( width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) + .background(colors.gray[100]) .padding(horizontal = 22.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -302,7 +305,7 @@ fun EditProfileScreen( text = userEmail, fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } @@ -313,7 +316,7 @@ fun EditProfileScreen( text = socialLoginGuideText!!, fontSize = 13.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[500], + color = colors.gray[500], modifier = Modifier.padding(start = 4.dp) ) @@ -333,7 +336,7 @@ fun EditProfileScreen( text = "성별", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], ) Spacer(modifier = Modifier.height(12.5.dp)) @@ -351,7 +354,7 @@ fun EditProfileScreen( .size(20.dp) .border( width = if (selectedGender == "남성") 5.dp else 1.dp, - color = LocalColorTheme.current.blue[200], + color = colors.blue[200], shape = CircleShape ) .clip(CircleShape) @@ -363,7 +366,7 @@ fun EditProfileScreen( text = "남성", fontSize = 16.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) } @@ -376,7 +379,7 @@ fun EditProfileScreen( .size(20.dp) .border( width = if (selectedGender == "여성") 5.dp else 1.dp, - color = LocalColorTheme.current.blue[200], + color = colors.blue[200], shape = CircleShape ) .clip(CircleShape) @@ -388,7 +391,7 @@ fun EditProfileScreen( text = "여성", fontSize = 16.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) } } @@ -406,7 +409,7 @@ fun EditProfileScreen( text = "하고 있는 일 · 활동", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier.padding(start = 4.dp) ) @@ -430,10 +433,10 @@ fun EditProfileScreen( .then( if (isSubmitEnabled) { Modifier - .background(LocalColorTheme.current.maincolor) + .background(colors.maincolor) .noRippleClickable { handleSubmit() } } else { - Modifier.background(LocalColorTheme.current.gray[300]) + Modifier.background(colors.gray[300]) } ) .padding(vertical = 15.dp), @@ -443,7 +446,7 @@ fun EditProfileScreen( text = "완료", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } @@ -456,6 +459,8 @@ fun JobDropdownMenu( onOptionSelected: (String) -> Unit, menuMaxHeight: Dp = 262.dp ) { + val colors = MaterialTheme.linkuColors + var expanded by remember { mutableStateOf(false) } Box { @@ -470,14 +475,14 @@ fun JobDropdownMenu( modifier = Modifier .width(372.dp) .heightIn(max = menuMaxHeight) - .border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) + .border(width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) .shadow( elevation = 10.dp, shape = RoundedCornerShape(18.dp), ambientColor = Color(0xFF7C7C7C).copy(alpha = 0.25f), spotColor = Color(0xFF7C7C7C).copy(alpha = 0.25f) ) - .background(LocalColorTheme.current.white, RoundedCornerShape(18.dp)) + .background(colors.white, RoundedCornerShape(18.dp)) .padding(horizontal = 22.dp, vertical = 11.dp), offset = DpOffset(x = 0.dp, y = (-302).dp) ) { @@ -500,13 +505,15 @@ private fun JobDropdownField( selectedOption: String, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Box( modifier = Modifier .fillMaxWidth() .heightIn(min = 52.dp) .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) - .border(width = 1.dp, color = LocalColorTheme.current.gray[200], shape = RoundedCornerShape(18.dp)) + .background(colors.white) + .border(width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) .clickable { onClick() } .padding(horizontal = 22.dp, vertical = 10.dp), contentAlignment = Alignment.Center @@ -520,7 +527,7 @@ private fun JobDropdownField( text = selectedOption, fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Icon( @@ -537,6 +544,8 @@ private fun JobDropdownItem( selected: Boolean, onClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + Row( modifier = Modifier .fillMaxWidth() @@ -562,7 +571,7 @@ private fun JobDropdownItem( text = text, fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt index da83ad4a..12b26293 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -32,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors @@ -63,12 +62,14 @@ data class Faq( fun FaqScreen( navController: NavController ) { + val colors = MaterialTheme.linkuColors + var keyword by remember { mutableStateOf("") } var isFocused by remember { mutableStateOf(false) } var selectedFilter by remember { mutableStateOf("전체") } var expandedFaqId by remember { mutableStateOf(null) } - var feedbackRowHeightPx by remember { mutableStateOf(0) } + var feedbackRowHeightPx by remember { mutableIntStateOf(0) } val density = LocalDensity.current val feedbackRowHeightDp = with(density) { feedbackRowHeightPx.toDp() } @@ -148,7 +149,7 @@ fun FaqScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) ) { Box( modifier = Modifier @@ -170,7 +171,7 @@ fun FaqScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -182,12 +183,12 @@ fun FaqScreen( .fillMaxWidth() .padding(horizontal = 16.dp) .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .then( if (isFocused) { Modifier.border( width = 1.dp, - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, shape = RoundedCornerShape(18.dp) ) } else { @@ -214,7 +215,7 @@ fun FaqScreen( text = "궁금한 내용을 검색해보세요.", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[400], + color = colors.gray[400], ) } @@ -225,7 +226,7 @@ fun FaqScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = MaterialTheme.linkuFont.font, - color = LocalColorTheme.current.black, + color = colors.black, ), maxLines = 1, modifier = Modifier @@ -248,14 +249,14 @@ fun FaqScreen( .then( if (selectedFilter == filter) { Modifier - .background(LocalColorTheme.current.gray[800]) + .background(colors.gray[800]) .border( 1.dp, MaterialTheme.linkuColors.gray[200], RoundedCornerShape(10.dp) ) } else { - Modifier.background(LocalColorTheme.current.white) + Modifier.background(colors.white) } ) .noRippleClickable { selectedFilter = filter } @@ -267,9 +268,9 @@ fun FaqScreen( fontSize = 14.sp, fontWeight = FontWeight.Medium, color = if (selectedFilter == filter) { - LocalColorTheme.current.white + colors.white } else { - LocalColorTheme.current.gray[800] + colors.gray[800] } ) } @@ -333,21 +334,21 @@ fun FaqScreen( text = "찾으시는 질문이 없으신가요?", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[700] + color = colors.gray[700] ) Box( modifier = Modifier .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.maincolor) + .background(colors.maincolor) .padding(top = 13.dp, start = 23.5.dp, end = 28.5.dp, bottom = 13.dp) ) { Text( text = "피드백 보내기", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/InterestSelectionScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/InterestSelectionScreen.kt index 6f8fcdc5..1fbe0242 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/InterestSelectionScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/InterestSelectionScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf @@ -20,9 +21,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme -import com.linku.mypage.component.CustomInfoSelectionContent import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors +import com.linku.mypage.component.CustomInfoSelectionContent import com.linku.mypage.component.CustomInfoSelectionItem private val interestItems = listOf( @@ -42,6 +43,8 @@ fun InterestSelectionScreen( navController: NavController, onFinishClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + val selectedItems = remember { mutableStateListOf() } CustomInfoSelectionContent( @@ -52,7 +55,7 @@ fun InterestSelectionScreen( ) { BrushText( text = "어떤 분야의 콘텐츠", - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, style = TextStyle( fontSize = 22.sp, fontWeight = FontWeight.Medium, @@ -63,7 +66,7 @@ fun InterestSelectionScreen( text = "에", fontSize = 22.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } @@ -71,7 +74,7 @@ fun InterestSelectionScreen( text = "관심 있으신가요?\n모두 선택해주세요.", fontSize = 22.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } }, diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/MarketingAgreeScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/MarketingAgreeScreen.kt index eddb4e2a..9b882bbc 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/MarketingAgreeScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/MarketingAgreeScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -38,15 +39,17 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R @Composable fun MarketingAgreeScreen( navController: NavController ) { + val colors = MaterialTheme.linkuColors + val cardShape = RoundedCornerShape(22.dp) var isChecked by remember { mutableStateOf(false) } @@ -56,7 +59,7 @@ fun MarketingAgreeScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Box( modifier = Modifier @@ -78,7 +81,7 @@ fun MarketingAgreeScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -96,7 +99,7 @@ fun MarketingAgreeScreen( .fillMaxWidth() .padding(horizontal = 20.dp) .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 24.dp, vertical = 20.dp), verticalArrangement = Arrangement.spacedBy(15.dp) ) { @@ -107,7 +110,7 @@ fun MarketingAgreeScreen( text = "링큐(LINK:U) 마케팅 수신동의", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) Spacer(modifier = Modifier.width(2.dp)) @@ -116,7 +119,7 @@ fun MarketingAgreeScreen( text = "(선택)", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } @@ -127,7 +130,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Light, fontSize = 12.sp, - color = LocalColorTheme.current.black, + color = colors.black, ) ) { append(""" @@ -140,7 +143,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Normal, fontSize = 12.sp, - color = LocalColorTheme.current.black + color = colors.black ) ) { append("선택 사항") @@ -150,7 +153,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Light, fontSize = 12.sp, - color = LocalColorTheme.current.black + color = colors.black ) ) { append(""" @@ -167,7 +170,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Light, fontSize = 12.sp, - color = LocalColorTheme.current.black + color = colors.black ) ) { append(""" @@ -196,7 +199,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Normal, fontSize = 12.sp, - color = LocalColorTheme.current.black + color = colors.black ) ) { append("설정 메뉴에서 마케팅 수신 동의 해제") @@ -206,7 +209,7 @@ fun MarketingAgreeScreen( style = SpanStyle( fontWeight = FontWeight.Light, fontSize = 12.sp, - color = LocalColorTheme.current.black + color = colors.black ) ) { append(""" @@ -240,8 +243,8 @@ fun MarketingAgreeScreen( clip = true } .clip(RoundedCornerShape(22.dp)) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white) + .border(1.dp, colors.gray[200], RoundedCornerShape(22.dp)) + .background(colors.white) .padding(horizontal = 20.dp, vertical = 18.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -249,7 +252,7 @@ fun MarketingAgreeScreen( text = "마케팅 수신에 동의합니다.", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.weight(1f) ) diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/MyPageScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/MyPageScreen.kt index e4e0147d..36dee607 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/MyPageScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/MyPageScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,8 +28,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme +import com.linku.design.theme.linkuColors import com.linku.mypage.component.LogoutModal import com.linku.mypage.ui.top.bar.MypageTopBar @@ -49,12 +50,14 @@ fun MyPageScreen( onNavigateAISummary: () -> Unit, // TODO: 윤지언니에게 세션 관련해서 한 번 더 물어보고 작업하기... onRequestLogout: () -> Unit ) { + val colors = MaterialTheme.linkuColors + var showLogoutDialog by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) ) { MypageTopBar( isNoticeExist = false, // TODO: 실제 알림 여부 연결 @@ -87,7 +90,7 @@ fun MyPageScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(15.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 25.dp, top = 24.dp, end = 25.dp, bottom = 21.dp) ) { Text( @@ -95,7 +98,7 @@ fun MyPageScreen( fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[500] + color = colors.gray[500] ) Spacer(modifier = Modifier.height(14.dp)) @@ -105,7 +108,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -119,7 +122,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -133,7 +136,7 @@ fun MyPageScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(15.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 25.dp, top = 24.dp, end = 25.dp, bottom = 21.dp) ) { Text( @@ -141,7 +144,7 @@ fun MyPageScreen( fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[500] + color = colors.gray[500] ) Spacer(modifier = Modifier.height(14.dp)) @@ -151,7 +154,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -165,7 +168,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -179,7 +182,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -193,7 +196,7 @@ fun MyPageScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(15.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(start = 25.dp, top = 24.dp, end = 25.dp, bottom = 21.dp) ) { Text( @@ -201,7 +204,7 @@ fun MyPageScreen( fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.gray[500] + color = colors.gray[500] ) Spacer(modifier = Modifier.height(14.dp)) @@ -211,7 +214,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) @@ -225,7 +228,7 @@ fun MyPageScreen( fontSize = 15.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier .fillMaxWidth() .padding(start = 6.dp) diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt index 6f23a745..d36c05fc 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,9 +29,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R import com.linku.mypage.component.NoticeItem @@ -38,6 +39,8 @@ import com.linku.mypage.component.NoticeItem fun NoticeScreen( navController: NavController ) { + val colors = MaterialTheme.linkuColors + var expandedNoticeIndex by remember { mutableStateOf(null) } val notices = listOf( @@ -75,7 +78,7 @@ fun NoticeScreen( Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) ) { Box( modifier = Modifier @@ -97,7 +100,7 @@ fun NoticeScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/PurposeSelectionScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/PurposeSelectionScreen.kt index 736e6189..493982a4 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/PurposeSelectionScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/PurposeSelectionScreen.kt @@ -3,11 +3,10 @@ package com.linku.mypage.screen 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf @@ -22,8 +21,8 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.component.CustomInfoSelectionContent import com.linku.mypage.component.CustomInfoSelectionItem @@ -44,6 +43,8 @@ fun PurposeSelectionScreen( navController: NavController, onNextClick: () -> Unit // TODO: 목적 저장 API 연결 ) { + val colors = MaterialTheme.linkuColors + val selectedItems = remember { mutableStateListOf() } CustomInfoSelectionContent( @@ -51,7 +52,7 @@ fun PurposeSelectionScreen( Column { BrushText( text = "어떤 목적으로 링크를", - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, style = TextStyle( fontSize = 22.sp, fontWeight = FontWeight.Medium, @@ -63,7 +64,7 @@ fun PurposeSelectionScreen( ) { BrushText( text = "저장", - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, style = TextStyle( fontSize = 22.sp, fontWeight = FontWeight.Medium, @@ -74,7 +75,7 @@ fun PurposeSelectionScreen( text = "하고 싶으신가요?", fontSize = 22.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } @@ -82,7 +83,7 @@ fun PurposeSelectionScreen( text = "모두 선택해주세요.", fontSize = 22.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black + color = colors.black ) } }, diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt index 2bc82334..7b935ad6 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt @@ -15,6 +15,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,9 +30,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.mypage.R import com.linku.design.R as Res @@ -40,12 +41,14 @@ fun ServiceAgreeScreen( navController: NavController, onMarketingAgreeClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + val uriHandler = LocalUriHandler.current Column( modifier = Modifier .fillMaxSize() - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 20.dp) ) { Box( @@ -68,7 +71,7 @@ fun ServiceAgreeScreen( fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -79,7 +82,7 @@ fun ServiceAgreeScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(22.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(20.dp), verticalArrangement = Arrangement.spacedBy(19.dp) ) { @@ -97,7 +100,7 @@ fun ServiceAgreeScreen( text = "서비스 이용약관", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( @@ -121,7 +124,7 @@ fun ServiceAgreeScreen( text = "개인정보 처리 방침", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( @@ -145,7 +148,7 @@ fun ServiceAgreeScreen( text = "마케팅 수신 동의", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.black + color = colors.black ) Image( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt index 5fa011cd..6c70a3d3 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt @@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -36,9 +36,9 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic +import com.linku.design.theme.linkuColors import com.linku.mypage.R import com.linku.mypage.component.QuitReasonItem import com.linku.mypage.component.ServiceQuitModal @@ -56,6 +56,8 @@ fun ServiceQuitScreen( navController: NavController, onRequestQuit: (reason: String) -> Unit ) { + val colors = MaterialTheme.linkuColors + var reasonText by remember { mutableStateOf("") } var showDialog by remember { mutableStateOf(false) } var selectedReason by remember { mutableStateOf(null) } @@ -70,7 +72,7 @@ fun ServiceQuitScreen( Column( modifier = Modifier .fillMaxWidth() - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 20.dp) ) { Box( @@ -92,7 +94,7 @@ fun ServiceQuitScreen( text = "회원 탈퇴", fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.align(Alignment.Center) ) } @@ -103,7 +105,7 @@ fun ServiceQuitScreen( text = "그동안 링큐를 이용해주셔서\n감사합니다.", fontSize = 22.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.black, + color = colors.black, modifier = Modifier.padding(horizontal = 4.dp) ) @@ -114,7 +116,7 @@ fun ServiceQuitScreen( fontSize = 15.sp, lineHeight = 22.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], modifier = Modifier.padding(horizontal = 4.dp) ) @@ -145,13 +147,13 @@ fun ServiceQuitScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 22.dp, vertical = 13.dp) ) { if (reasonText.isBlank()) { Text( text = "탈퇴 사유를 적어주세요.", - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], fontSize = 14.sp, fontWeight = FontWeight.Normal, ) @@ -165,7 +167,7 @@ fun ServiceQuitScreen( } }, textStyle = TextStyle( - color = LocalColorTheme.current.black, + color = colors.black, fontSize = 14.sp, fontWeight = FontWeight.Normal, ) @@ -178,7 +180,7 @@ fun ServiceQuitScreen( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(15.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .padding(horizontal = 21.dp, vertical = 16.dp), ) { Column( @@ -189,7 +191,7 @@ fun ServiceQuitScreen( text = "탈퇴 안내 및 유의사항", fontSize = 18.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.black + color = colors.black ) Spacer(modifier = Modifier.height(4.dp)) @@ -203,7 +205,7 @@ fun ServiceQuitScreen( fontSize = 15.sp, lineHeight = 24.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) } @@ -220,7 +222,7 @@ fun ServiceQuitScreen( text = "위 안내 사항을 확인했으며 이에 동의합니다.", fontSize = 15.sp, fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.gray[800] + color = colors.gray[800] ) Spacer(modifier = Modifier.width(12.dp)) @@ -250,7 +252,7 @@ fun ServiceQuitScreen( .clip(RoundedCornerShape(18.dp)) .then( if (isQuitEnabled) Modifier.background(Basic.maincolor) - else Modifier.background(LocalColorTheme.current.gray[300]) + else Modifier.background(colors.gray[300]) ) .noRippleClickable(enabled = isQuitEnabled) { showDialog = true } .padding(vertical = 15.dp), @@ -260,7 +262,7 @@ fun ServiceQuitScreen( text = "탈퇴하기", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = LocalColorTheme.current.white + color = colors.white ) } } diff --git a/feature/mypage/src/main/java/com/linku/mypage/ui/top/bar/MypageTopBar.kt b/feature/mypage/src/main/java/com/linku/mypage/ui/top/bar/MypageTopBar.kt index b9ec078f..fb7da464 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/ui/top/bar/MypageTopBar.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/ui/top/bar/MypageTopBar.kt @@ -16,6 +16,7 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,8 +30,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors import com.linku.design.top.bar.AlarmButton import com.linku.mypage.R import com.linku.design.R as Res @@ -48,13 +49,15 @@ fun MypageTopBar( onAISummaryClick: () -> Unit, modifier: Modifier = Modifier ) { + val colors = MaterialTheme.linkuColors + Column( modifier = modifier .fillMaxWidth() .clip( RoundedCornerShape(bottomStart = 30.dp, bottomEnd = 30.dp) ) - .background(color = LocalColorTheme.current.white) + .background(color = colors.white) ) { Column( modifier = Modifier @@ -131,7 +134,7 @@ fun MypageTopBar( Text( text = nickname, - color = LocalColorTheme.current.black, + color = colors.black, fontSize = 20.sp, fontWeight = FontWeight.Bold, ) @@ -139,7 +142,7 @@ fun MypageTopBar( Text( text = email, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], fontSize = 13.sp, fontWeight = FontWeight.Medium, ) @@ -159,10 +162,10 @@ fun MypageTopBar( .weight(1f) .fillMaxHeight() .clip(RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .border( 1.dp, - LocalColorTheme.current.gray[200], + colors.gray[200], RoundedCornerShape(14.dp) ), horizontalAlignment = Alignment.CenterHorizontally, @@ -170,7 +173,7 @@ fun MypageTopBar( ) { Text( text = "나의 링크", - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], fontSize = 13.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(bottom = 2.dp) @@ -178,7 +181,7 @@ fun MypageTopBar( Text( text = myLinku.toString(), - color = LocalColorTheme.current.black, + color = colors.black, fontSize = 16.sp, fontWeight = FontWeight.Bold, ) @@ -191,10 +194,10 @@ fun MypageTopBar( .weight(1f) .fillMaxHeight() .clip(RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.gray[100]) + .background(colors.gray[100]) .border( 1.dp, - LocalColorTheme.current.gray[200], + colors.gray[200], RoundedCornerShape(14.dp) ), horizontalAlignment = Alignment.CenterHorizontally, @@ -202,7 +205,7 @@ fun MypageTopBar( ) { Text( text = "나의 폴더", - color = LocalColorTheme.current.gray[700], + color = colors.gray[700], fontSize = 13.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(bottom = 2.dp) @@ -210,7 +213,7 @@ fun MypageTopBar( Text( text = myFolder.toString(), - color = LocalColorTheme.current.black, + color = colors.black, fontSize = 16.sp, fontWeight = FontWeight.Bold, ) @@ -222,10 +225,10 @@ fun MypageTopBar( modifier = Modifier .weight(1f) .clip(RoundedCornerShape(14.dp)) - .background(LocalColorTheme.current.backgroundmaincolor) + .background(colors.backgroundmaincolor) .border( 1.dp, - LocalColorTheme.current.inactiveColor, + colors.inactiveColor, RoundedCornerShape(14.dp) ) .padding(vertical = 14.dp) @@ -246,7 +249,7 @@ fun MypageTopBar( BrushText( text = "AI 요약 링크", - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, style = TextStyle( fontSize = 13.sp, fontWeight = FontWeight.Medium, @@ -256,7 +259,7 @@ fun MypageTopBar( BrushText( text = myAiLinku.toString(), - brush = LocalColorTheme.current.maincolor, + brush = colors.maincolor, style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Bold, From 4bf61d9747cfbc4a20e6f8ebfd97e880a92c364a Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 20:00:41 +0900 Subject: [PATCH 76/89] =?UTF-8?q?:zap:=20mypage=20=EB=AA=A8=EB=93=88=20cli?= =?UTF-8?q?ckable=EC=97=90=EC=84=9C=20noRippleClickable=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/mypage/component/DeleteLinkuModal.kt | 6 +++--- .../main/java/com/linku/mypage/component/LogoutModal.kt | 6 +++--- .../java/com/linku/mypage/component/ServiceQuitModal.kt | 6 +++--- .../java/com/linku/mypage/screen/AccountSettingScreen.kt | 3 +-- .../java/com/linku/mypage/screen/EditProfileScreen.kt | 9 ++++----- .../src/main/java/com/linku/mypage/screen/FaqScreen.kt | 3 +-- .../main/java/com/linku/mypage/screen/NoticeScreen.kt | 4 ++-- .../java/com/linku/mypage/screen/ServiceAgreeScreen.kt | 3 +-- .../java/com/linku/mypage/screen/ServiceQuitScreen.kt | 7 +++---- 9 files changed, 21 insertions(+), 26 deletions(-) diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt index ccd1518b..88238ee5 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/DeleteLinkuModal.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.design.BrushText +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.ThemeProvider import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors @@ -91,7 +91,7 @@ fun DeleteLinkuModal( .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) .background(colors.white) - .clickable { onDismiss() }, + .noRippleClickable { onDismiss() }, contentAlignment = Alignment.Center ) { BrushText( @@ -112,7 +112,7 @@ fun DeleteLinkuModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .background(brush = Basic.maincolor) - .clickable { onConfirm() }, + .noRippleClickable { onConfirm() }, contentAlignment = Alignment.Center ) { Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt index 105d96bb..a7b7d569 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/LogoutModal.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,6 +28,7 @@ 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 com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors @@ -93,7 +93,7 @@ fun LogoutModal( .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) .background(colors.white) - .clickable { onDismiss() }, + .noRippleClickable { onDismiss() }, contentAlignment = Alignment.Center ) { Text( @@ -117,7 +117,7 @@ fun LogoutModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .background(brush = Basic.maincolor) - .clickable { onConfirm() }, + .noRippleClickable { onConfirm() }, contentAlignment = Alignment.Center ) { Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt b/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt index fd79bfca..22433a0e 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/component/ServiceQuitModal.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,6 +28,7 @@ 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 com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.color.Basic import com.linku.design.theme.linkuColors @@ -93,7 +93,7 @@ fun ServiceQuitModal( .clip(RoundedCornerShape(14.dp)) .border(BorderStroke(1.dp, brush = Basic.maincolor), RoundedCornerShape(14.dp)) .background(colors.white) - .clickable { onDismiss() }, + .noRippleClickable { onDismiss() }, contentAlignment = Alignment.Center ) { Text( @@ -117,7 +117,7 @@ fun ServiceQuitModal( .height(50.dp) .clip(RoundedCornerShape(14.dp)) .background(brush = Basic.maincolor) - .clickable { onConfirm() }, + .noRippleClickable { onConfirm() }, contentAlignment = Alignment.Center ) { Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt index 76e28ef4..d762f70f 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/AccountSettingScreen.kt @@ -2,7 +2,6 @@ package com.linku.mypage.screen import androidx.compose.foundation.Image 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 @@ -64,7 +63,7 @@ fun AccountSettingScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt index 9d139b96..a5524b93 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/EditProfileScreen.kt @@ -3,7 +3,6 @@ package com.linku.mypage.screen 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 import androidx.compose.foundation.layout.Column @@ -149,7 +148,7 @@ fun EditProfileScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( @@ -251,7 +250,7 @@ fun EditProfileScreen( modifier = Modifier .size(18.dp) .then( - if (name.isNotEmpty()) Modifier.clickable { name = "" } + if (name.isNotEmpty()) Modifier.noRippleClickable { name = "" } else Modifier ) ) @@ -514,7 +513,7 @@ private fun JobDropdownField( .clip(RoundedCornerShape(18.dp)) .background(colors.white) .border(width = 1.dp, color = colors.gray[200], shape = RoundedCornerShape(18.dp)) - .clickable { onClick() } + .noRippleClickable { onClick() } .padding(horizontal = 22.dp, vertical = 10.dp), contentAlignment = Alignment.Center ) { @@ -549,7 +548,7 @@ private fun JobDropdownItem( Row( modifier = Modifier .fillMaxWidth() - .clickable { onClick() } + .noRippleClickable { onClick() } .padding(horizontal = 22.dp, vertical = 9.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt index 12b26293..7b822d1c 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/FaqScreen.kt @@ -3,7 +3,6 @@ package com.linku.mypage.screen 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 import androidx.compose.foundation.layout.Column @@ -163,7 +162,7 @@ fun FaqScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt index d36c05fc..026c4ba4 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/NoticeScreen.kt @@ -2,7 +2,6 @@ package com.linku.mypage.screen import androidx.compose.foundation.Image 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.Spacer @@ -29,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors @@ -92,7 +92,7 @@ fun NoticeScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt index 7b935ad6..f76a3b64 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceAgreeScreen.kt @@ -2,7 +2,6 @@ package com.linku.mypage.screen import androidx.compose.foundation.Image 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 @@ -63,7 +62,7 @@ fun ServiceAgreeScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( diff --git a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt index 6c70a3d3..0c69addb 100644 --- a/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt +++ b/feature/mypage/src/main/java/com/linku/mypage/screen/ServiceQuitScreen.kt @@ -2,7 +2,6 @@ package com.linku.mypage.screen import androidx.compose.foundation.Image 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 @@ -87,7 +86,7 @@ fun ServiceQuitScreen( modifier = Modifier .align(Alignment.CenterStart) .width(11.dp) - .clickable { navController.popBackStack() } + .noRippleClickable { navController.popBackStack() } ) Text( @@ -214,7 +213,7 @@ fun ServiceQuitScreen( Row( modifier = Modifier .align(Alignment.End) - .clickable { isAgreeChecked = !isAgreeChecked }, + .noRippleClickable { isAgreeChecked = !isAgreeChecked }, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -275,7 +274,7 @@ fun ServiceQuitScreen( .fillMaxSize() .background(Color(0x66000000)) // 40% 투명한 검정색 배경 .zIndex(1f) - .clickable(enabled = false) {}, // 외부 클릭 막기 + .noRippleClickable(enabled = false) {}, // 외부 클릭 막기 contentAlignment = Alignment.Center ) { Box( From 3d1dc87ebed18377a5b1e3fc09c85331a791ec7a Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 20:05:19 +0900 Subject: [PATCH 77/89] =?UTF-8?q?:zap:=20shape=EB=A5=BC=20remember?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=90=EC=8B=B8=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=A0=EB=8B=B9=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/design/component/DeleteLinkItemModal.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index 13638021..d1e78e8b 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow @@ -24,7 +25,7 @@ fun DeleteLinkItemModal( onDeleteClick: () -> Unit = { }, modifier: Modifier ) { - val shape = RoundedCornerShape(14.dp) + val shape = remember { RoundedCornerShape(14.dp) } Column( modifier = modifier From b2f49ed55758b1f3c3b7485cf4f85d6f8890a23a Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 20:07:19 +0900 Subject: [PATCH 78/89] =?UTF-8?q?:zap:=20clickable=EC=97=90=EC=84=9C=20noR?= =?UTF-8?q?ippleClickable=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/design/component/DeleteLinkItemModal.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt index d1e78e8b..470a4cd8 100644 --- a/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt +++ b/design/src/main/java/com/linku/design/component/DeleteLinkItemModal.kt @@ -5,26 +5,28 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.graphicsLayer 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 com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider +import com.linku.design.theme.linkuColors @Composable fun DeleteLinkItemModal( onDeleteClick: () -> Unit = { }, modifier: Modifier ) { + val colors = MaterialTheme.linkuColors + val shape = remember { RoundedCornerShape(14.dp) } Column( @@ -36,7 +38,7 @@ fun DeleteLinkItemModal( clip = false ) .clip(shape) - .background(LocalColorTheme.current.white) + .background(colors.white) .padding(horizontal = 15.dp, vertical = 10.dp) .noRippleClickable { onDeleteClick() } ) { @@ -44,7 +46,7 @@ fun DeleteLinkItemModal( text = "삭제하기", fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[800], + color = colors.gray[800], modifier = Modifier.width(90.dp) ) } From 11811bb526444f0d6a80218600aae15c8972e17f Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 20:12:30 +0900 Subject: [PATCH 79/89] =?UTF-8?q?:zap:=20AsyncImage=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/design/component/LinkCardItem.kt | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/design/src/main/java/com/linku/design/component/LinkCardItem.kt b/design/src/main/java/com/linku/design/component/LinkCardItem.kt index 3c9b08db..2ce84048 100644 --- a/design/src/main/java/com/linku/design/component/LinkCardItem.kt +++ b/design/src/main/java/com/linku/design/component/LinkCardItem.kt @@ -29,12 +29,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.rememberAsyncImagePainter +import coil.compose.AsyncImage +import com.linku.design.R import com.linku.design.modifier.noRippleClickable -import com.linku.design.theme.LocalColorTheme import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors -import com.linku.design.R @Composable fun LinkCardItem( @@ -47,13 +46,15 @@ fun LinkCardItem( domainImageUrl: String = "", onDeleteClick: () -> Unit ) { + val colors = MaterialTheme.linkuColors + var isMenuVisible by remember { mutableStateOf(false) } Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.white) + .background(colors.white) ) { Row( modifier = Modifier @@ -61,25 +62,16 @@ fun LinkCardItem( .padding(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - if (linkImageUrl.isBlank()) { - Image( - painter = painterResource(R.drawable.img_link_default), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(85.dp) - .clip(RoundedCornerShape(12.dp)) - ) - } else { - Image( - painter = rememberAsyncImagePainter(model = linkImageUrl), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(85.dp) - .clip(RoundedCornerShape(12.dp)) - ) - } + AsyncImage( + model = linkImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.img_link_default), + error = painterResource(R.drawable.img_link_default), + modifier = Modifier + .size(85.dp) + .clip(RoundedCornerShape(12.dp)) + ) Spacer(modifier = Modifier.width(14.dp)) @@ -126,10 +118,10 @@ fun LinkCardItem( text = tag, fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600], + color = colors.gray[600], modifier = Modifier .background( - color = LocalColorTheme.current.gray[100], + color = colors.gray[100], shape = RoundedCornerShape(6.dp) ) .padding(horizontal = 6.dp, vertical = 2.dp) @@ -144,21 +136,14 @@ fun LinkCardItem( Row( verticalAlignment = Alignment.CenterVertically ) { - if (domainImageUrl.isNotBlank()) { - Image( - painter = rememberAsyncImagePainter(model = domainImageUrl), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(16.dp) - ) - } else { - Image( - painter = painterResource(R.drawable.ic_domain_default), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(22.dp) - ) - } + AsyncImage( + model = domainImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.ic_domain_default), + error = painterResource(R.drawable.ic_domain_default), + modifier = Modifier.size(22.dp) + ) Spacer(modifier = Modifier.width(6.dp)) @@ -166,7 +151,7 @@ fun LinkCardItem( text = domainName, fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = LocalColorTheme.current.gray[600] + color = colors.gray[600] ) } } From 91512d1faa5ba573a8502826bc8cef5a30223465 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 25 Jun 2026 20:22:25 +0900 Subject: [PATCH 80/89] =?UTF-8?q?:zap:=20core=EC=97=90=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20EmotionType=EC=9D=84=20=EA=B3=B5=ED=86=B5=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/LinkDetailEmotionDropdown.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 2ec296e7..3acea2f0 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -24,7 +24,6 @@ import com.linku.core.model.EmotionType import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors -import com.linku.home.R import com.linku.home.util.imgRes @Composable @@ -79,17 +78,6 @@ fun LinkDetailEmotionDropdown( } } -private fun EmotionType.iconRes(): Int { - return when (this) { - EmotionType.JOY -> R.drawable.ic_joy - EmotionType.CALM -> R.drawable.ic_calm - EmotionType.EXCITE -> R.drawable.ic_excite - EmotionType.SAD -> R.drawable.ic_sad - EmotionType.IRRITATION -> R.drawable.ic_irritation - EmotionType.ANGER -> R.drawable.ic_anger - } -} - @Preview(showBackground = false) @Composable fun PreviewLinkDetailEmotionDropdown() { From 5ade90b19b49d2b0485ed0e3d3978a17d3c728d4 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Fri, 26 Jun 2026 01:58:33 +0900 Subject: [PATCH 81/89] =?UTF-8?q?:truck:=20=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linku/home/screen/SaveLinkResultScreen.kt | 1217 ----------------- 1 file changed, 1217 deletions(-) delete mode 100644 feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt deleted file mode 100644 index 79b77e0c..00000000 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkResultScreen.kt +++ /dev/null @@ -1,1217 +0,0 @@ -package com.linku.home.screen - -import android.util.Log -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Canvas -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 -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.heightIn -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.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DropdownMenu -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.rememberUpdatedState -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.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import coil3.compose.rememberAsyncImagePainter -import coil3.request.ImageRequest -import coil3.request.crossfade -import com.linku.core.model.AiArticle -import com.linku.core.model.LinkResultInfo -import com.linku.design.BrushText -import com.linku.design.theme.LocalColorTheme -import com.linku.design.theme.LocalFontTheme -import com.linku.design.theme.color.Basic -import com.linku.design.theme.color.CategoryColorStyle -import com.linku.home.R -import com.linku.home.component.AIArticleModal -import kotlinx.coroutines.delay -import java.time.OffsetDateTime - -private fun emotionDisplayName(id: Long?): String? = when (id) { - 1L -> "즐거움" - 2L -> "평온" - 3L -> "설렘" - 4L -> "슬픔" - 5L -> "짜증" - 6L -> "분노" - else -> null -} - -private val CATEGORY_NAMES: Map = mapOf( - 1L to "어학", - 2L to "뉴스", - 3L to "공부법", - 4L to "IT·개발", - 5L to "자기계발", - 6L to "취업·이직", - 7L to "비즈니스 인사이트", - 8L to "생산성·툴", - 9L to "라이프스타일", - 10L to "심리·자기이해", - 11L to "에세이·칼럼", - 12L to "트렌드", - 13L to "디자인·예술", - 14L to "영상·뮤직", - 15L to "맛집·여행", - 16L to "기타" -) - -private fun categoryDisplayName(id: Long?): String? = id?.let { CATEGORY_NAMES[it] } - -// label -> id -private fun categoryIdOf(label: String): Long? = - CATEGORY_NAMES.entries.firstOrNull { it.value == label }?.key - -private fun emotionIdOf(label: String): Long? = when (label) { - "즐거움" -> 1L; "평온" -> 2L; "설렘" -> 3L - "슬픔" -> 4L; "짜증" -> 5L; "분노" -> 6L - else -> null -} - -// 변경: 벡터 리소스 사용 -private data class EmotionOpt( - val id: Long, - val label: String, - @DrawableRes val iconRes: Int -) - -private val EMOTION_OPTIONS = listOf( - EmotionOpt(1, "즐거움", R.drawable.ic_joy), - EmotionOpt(2, "평온", R.drawable.ic_calm), - EmotionOpt(3, "설렘", R.drawable.ic_excite), - EmotionOpt(4, "슬픔", R.drawable.ic_sad), - EmotionOpt(5, "짜증", R.drawable.ic_irritation), - EmotionOpt(6, "분노", R.drawable.ic_anger), -) - -@Composable -fun SaveLinkResultScreen( - selectedImageUri: String? = null, // 외부에서 전달받은 URI - link: LinkResultInfo?, - aiArticle: AiArticle? = null, - isLoading: Boolean = false, - isAiLoading: Boolean = false, - onBack: () -> Unit = {}, - onOpenLink: (String) -> Unit, - categoryColorMap: Map = emptyMap(), - onSubmitEdit: (title: String, memo: String?, categoryId: Long?, emotionId: Long?) -> Unit = { _,_,_,_ -> }, - onRequestAiSummary: () -> Unit, - aiProgress: Float = 0f, - onCancelAi: () -> Unit = {} -) { - // 서버 데이터 바인딩 (널/로딩 방어) - val titleFromServer = link?.title.orEmpty() - val memoFromServer = link?.memo.orEmpty() - val imageUrl = link?.linkuImageUrl - val context = LocalContext.current - val imageRequest = remember(imageUrl, link?.linkuId) { - ImageRequest.Builder(context) - .data(imageUrl) - .memoryCacheKey("linku_image_mem_${link?.linkuId}") - .diskCacheKey("linku_image_disk_${link?.linkuId}") - .crossfade(true) - .build() - } - val linku = link?.linku.orEmpty() - - // 항상 최신 콜백을 보관 - val currentOnRequestAi = rememberUpdatedState(onRequestAiSummary) - val currentOnOpenLink = rememberUpdatedState(onOpenLink) - - // 키워드/요약 실제 표시값 결정 (우선 aiArticle, 없으면 link 값) - val displayKeyword = aiArticle?.keyword?.trim().orEmpty().ifEmpty { link?.keyword.orEmpty() } - val displaySummary = aiArticle?.summary?.trim().orEmpty().ifEmpty { link?.summary.orEmpty() } - - val isKeywordEmpty = displayKeyword.isBlank() - val isSummaryEmpty = displaySummary.isBlank() - - val keywordPlaceholder = "AI가 키워드를 추출하지 못하였습니다." - val summaryPlaceholder = "AI가 본문을 요약하지 못하였습니다." - - // 요약이 "없을 때" 요청 가능하도록 - val hasContent = (aiArticle != null) || !displaySummary.isBlank() || !displayKeyword.isBlank() - val canRequestAi = !isAiLoading // 콘텐츠가 없고, 로딩 중이 아닐 때만 생성 가능 - - var showAISummary by remember { mutableStateOf(false) } - var showAIArticleModal by remember { mutableStateOf(false) } - - // 요약이 이미 있거나(aiArticle or link.summary/keyword) 로딩 완료되면 자동 펼치기 - // (자동 펼침은 “이미 존재하는 요약/키워드가 있을 때만” 유지. 새 호출은 하지 않음) - LaunchedEffect(aiArticle, link?.summary, link?.keyword, isLoading) { - if (!isLoading) { - val hasExisting = aiArticle != null || - !link?.summary.isNullOrBlank() || - !link?.keyword.isNullOrBlank() - if (hasExisting) showAISummary = true - } - } - - // isAiLoading 값이 바뀔 때마다 로그 찍기 - LaunchedEffect(isAiLoading) { - Log.d("SaveLinkResultScreen", "isAiLoading changed -> $isAiLoading") - } - - // 서버에서 내려온 최신 카테고리 이름(순서 보장)을 사용 - val categoryLabels = remember(categoryColorMap) { categoryColorMap.keys.toList() } - - // 선택 라벨/ID 계산은 동적 라벨 리스트 기반 - // 선택된 카테고리/감정 ID (초기값은 서버 응답) - var selectedEmotionId by remember(link?.emotionId) { mutableStateOf(link?.emotionId) } - var selectedCategoryId by remember(link?.categoryId) { mutableStateOf(link?.categoryId) } - - var isEditMode by remember { mutableStateOf(false) } - - // 메모/제목은 서버값으로 초기화 - var memoText by remember { mutableStateOf(memoFromServer) } - var isMemoEditing by remember { mutableStateOf(false) } - - - // 매 렌더마다 계산 - val effectiveCategoryId = selectedCategoryId ?: link?.categoryId - val effectiveEmotionId = selectedEmotionId ?: link?.emotionId - - val categoryLabel = categoryDisplayName(effectiveCategoryId) ?: "카테고리" - val emotionLabel = emotionDisplayName(effectiveEmotionId) ?: "감정" - - LaunchedEffect(link) { - // 상세가 갱신되면 UI 상태도 동기화(사용자가 수정 중이 아닐 때만) - if (!isMemoEditing) memoText = memoFromServer - } - - // 수정 완료 시 텍스트 입력 종료 - LaunchedEffect(isEditMode) { - if (!isEditMode) { - isMemoEditing = false - } - } - - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - val readModeTags = listOfNotNull( - categoryDisplayName(effectiveCategoryId), - emotionDisplayName(effectiveEmotionId) - ) - - TopBar( - isEditMode = isEditMode, - showAISummary = showAISummary, - onEditClick = {latestTitle -> - if (isEditMode) { - // 편집 종료 시 저장 호출 - onSubmitEdit( - latestTitle, - memoText.ifBlank { null }, - selectedCategoryId, - selectedEmotionId - ) - } - isEditMode = !isEditMode - }, - onBack = onBack, - titleText = titleFromServer, - tags = readModeTags, - initialCategoryLabel = categoryLabel, - initialEmotionLabel = emotionLabel, - categoryLabels = categoryLabels, - dotColorOf = { label -> - categoryColorMap[label]?.color4 ?: LocalColorTheme.current.gray[300] - }, - onCategorySelected = { label -> - selectedCategoryId = categoryIdOf(label) - }, - onEmotionSelected = { label -> - selectedEmotionId = emotionIdOf(label) - } - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp, start = 20.dp, end = 20.dp) - ) { - Text( - text = "링크", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 4.dp) - ) - - Spacer(modifier = Modifier.height(15.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 51.dp) - .clip(RoundedCornerShape(18.dp)) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)) - .padding(horizontal = 22.dp, vertical = 15.dp), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = linku, - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, lineHeight = 20.sp), - color = LocalColorTheme.current.black - ) - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(209.3.dp) - .padding(top = 18.dp, start = 20.dp, end = 20.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .border(1.dp, LocalColorTheme.current.gray[200], RoundedCornerShape(18.dp)), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (!imageUrl.isNullOrBlank()) { - Image( - painter = rememberAsyncImagePainter(model = imageRequest), - contentDescription = "선택된 이미지", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop // 박스에 꽉 차도록 - ) - } else { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(R.drawable.ic_transparent_logo), - contentDescription = null, - modifier = Modifier - .height(120.dp) - .padding(top = 50.dp) - ) - Text( - text = "이미지 업로드하기", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[500], - modifier = Modifier.padding(top = 8.dp) - ) - } - } - } - - if (showAISummary) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 29.7.dp, start = 20.dp, end = 20.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 4.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "키워드 (AI추출 태그)", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800] - ) - - if (isEditMode) { - Text( - text = "수정 불가", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) - ) - } - } - - Spacer(modifier = Modifier.height(15.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 51.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .padding(horizontal = 22.dp, vertical = 15.dp), - contentAlignment = Alignment.CenterStart - ) { - when { - isAiLoading -> { - Text( - "키워드 추출 중...", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] - ) - } - isKeywordEmpty -> { - Text( - text = keywordPlaceholder, - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] - ) - } - else -> { -// Canvas(modifier = Modifier.fillMaxSize()) { -// drawIntoCanvas { -// val text = displayKeyword -// val textSizePx = 14.sp.toPx() -// -// val paintForWidth = android.graphics.Paint().apply { -// textSize = textSizePx -// } -// val textWidth = paintForWidth.measureText(text) -// -// val gradient = android.graphics.LinearGradient( -// 0f, 0f, textWidth, 0f, // 텍스트 길이에 맞춰 그라데이션 -// intArrayOf( -// 0xFF2C6FFF.toInt(), -// 0xFFCB59EB.toInt() -// ), -// null, -// android.graphics.Shader.TileMode.CLAMP -// ) -// -// val paint = android.graphics.Paint().apply { -// isAntiAlias = true -// textSize = textSizePx -// shader = gradient -// } -// -// it.nativeCanvas.drawText( -// text, -// 0f, -// size.height / 2 + textSizePx / 2.5f, -// paint -// ) -// } -// } - BrushText( - text = displayKeyword, - brush = Basic.maincolor, // 이미 쓰는 메인 그라데이션 - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontFamily = LocalFontTheme.current.font // Paperlogy 적용 - ), - modifier = Modifier.fillMaxWidth() // 필요시 정렬/패딩 추가 - ) - - } - } - } - } - - Column( - modifier = Modifier - .padding(top = 30.dp, start = 20.dp, end = 20.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { -// Canvas( -// modifier = Modifier -// .height(11.dp) -// .padding(start = 4.dp) -// ) { -// drawIntoCanvas { -// val text = "AI 본문 요약" -// val textSizePx = 14.sp.toPx() -// -// val paintForWidth = android.graphics.Paint().apply { -// textSize = textSizePx -// typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) -// } -// val textWidth = paintForWidth.measureText(text) -// -// val gradient = android.graphics.LinearGradient( -// 0f, 0f, textWidth, 0f, -// intArrayOf( -// 0xFF2C6FFF.toInt(), -// 0xFFCB59EB.toInt() -// ), -// null, -// android.graphics.Shader.TileMode.CLAMP -// ) -// -// val paint = android.graphics.Paint().apply { -// isAntiAlias = true -// textSize = textSizePx -// shader = gradient -// typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.NORMAL) -// } -// -// it.nativeCanvas.drawText( -// text, -// 0f, -// size.height / 2 + textSizePx / 2.5f, -// paint -// ) -// } -// } - BrushText( - text = "AI 본문 요약", - brush = Basic.maincolor, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - fontFamily = LocalFontTheme.current.font // Paperlogy 적용 - ), - modifier = Modifier.padding(start = 4.dp) - ) - - - if (isEditMode) { - Text( - text = "수정 불가", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[200], - modifier = Modifier.padding(end = 12.dp) - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 51.dp) - .padding(top = 15.dp) - .clip(RoundedCornerShape(18.dp)) - .background( - if (isSummaryEmpty) { - SolidColor(LocalColorTheme.current.gray[100]) - } else { - Brush.horizontalGradient( - listOf( - LocalColorTheme.current.blue[200].copy(alpha = 0.1f), - LocalColorTheme.current.purple[200].copy(alpha = 0.1f) - ) - ) - } - ) - .padding(horizontal = 22.dp, vertical = 20.dp), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = when { - isAiLoading -> "요약 생성 중..." - isSummaryEmpty -> summaryPlaceholder - else -> displaySummary - }, - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font, lineHeight = 20.sp), - color = when { - isAiLoading || isSummaryEmpty -> LocalColorTheme.current.gray[400] - else -> LocalColorTheme.current.black - } - ) - } - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 29.7.dp, start = 20.dp, end = 20.dp) - ) { - Text( - text = "메모", - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[800], - modifier = Modifier.padding(start = 4.dp) - ) - - Spacer(modifier = Modifier.height(15.dp)) - - // 포커스 요청자 준비 - val memoFocusRequester = remember { FocusRequester() } - - // 편집 진입하면 자동 포커스 (다음 프레임에 요청) - LaunchedEffect(isMemoEditing) { - if (isMemoEditing) { - kotlinx.coroutines.yield() - memoFocusRequester.requestFocus() - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 51.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.gray[100]) - .padding(horizontal = 22.dp, vertical = 15.dp) - .then( - if (isEditMode) Modifier.clickable { isMemoEditing = true } else Modifier - ), - verticalAlignment = Alignment.CenterVertically - ) { - if (isMemoEditing) { - BasicTextField( - value = memoText, - onValueChange = { memoText = it }, - textStyle = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, color = LocalColorTheme.current.black, fontFamily = LocalFontTheme.current.font, lineHeight = 20.sp), - modifier = Modifier - .weight(1f) - .focusRequester(memoFocusRequester) - ) - } else { - Text( - text = memoText, - style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Light, fontFamily = LocalFontTheme.current.font, lineHeight = 20.sp), - color = LocalColorTheme.current.black, - modifier = Modifier.weight(1f) - ) - } - - if (isEditMode) { - Spacer(modifier = Modifier.width(10.dp)) - - Image( - painter = painterResource(R.drawable.ic_delete_gray), - contentDescription = null, - modifier = Modifier - .size(18.dp) - .clickable { - memoText = "" - isMemoEditing = true - } - ) - } - } - } - - if (showAISummary) { - Spacer(modifier = Modifier.height(80.dp)) - } - - if (!showAISummary) { - Spacer(modifier = Modifier.height(32.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .padding(horizontal = 20.dp) - .clip(RoundedCornerShape(18.dp)) - .background(LocalColorTheme.current.blue[200]) - .clickable { -// if (hasContent) { -// // 이미 요약/키워드가 있으면 네트워크 호출 없이 즉시 펼치기 -// showAISummary = true -// } else { -// // 없으면 API 요청 → isAiLoading=true 동안 모달 표시 -// onRequestAiSummary() -// } - Log.d("SaveLinkResultScreen", "AI 버튼 클릭! hasContent=$hasContent, isAiLoading=$isAiLoading") - if (isAiLoading) return@clickable - if (hasContent) { - // 이미 키워드/요약 있으면 네트워크 없이 즉시 펼치기 - showAISummary = true - } else { - // 없으면 API 요청 → ViewModel이 isLoadingAiArticle=true로 만들고 - // 화면은 로딩 오버레이를 띄워줌 - android.util.Log.d("SaveLinkResultScreen", "AI 요청 트리거") - currentOnRequestAi.value() - } - }, - contentAlignment = Alignment.Center - ) { - Text( - text = "AI 요약 보기", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - } - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "AI가 링크 내용을 바탕으로 요약해드려요! 이용해보시겠어요?", - style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.gray[400] - ) - } - - Spacer(modifier = Modifier.height(41.dp)) - } - } - - // 모달은 로딩 동안 항상 표시 - if (isAiLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0x66000000)) - .zIndex(1f) - .clickable(enabled = false) {}, - contentAlignment = Alignment.Center - ) { - Box(modifier = Modifier.padding(horizontal = 20.dp)) { - AIArticleModal( - progress = aiProgress, - onQuit = onCancelAi, - modifier = Modifier.padding(horizontal = 20.dp) - ) - } - } - } - - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .zIndex(2f) - .padding(bottom = 14.dp, end = 19.71.dp) - ) { - Row( - modifier = Modifier - .height(50.dp) - .clip(RoundedCornerShape(18.dp)) - .background(brush = Basic.maincolor) - .padding(horizontal = 16.dp) - .clickable { -// Log.d("SaveLinkResultScreen", "링크 버튼 클릭! linku='$linku'") -// val target = linku -// if (target.isNotBlank()) { -// onOpenLink(target) -// } else { -// Log.w("SaveLinkResultScreen", "linku 비어있음(상세 아직 로드 전) -> 무시") -// } - Log.d("SaveLinkResultScreen", "링크 버튼 클릭! linku='$linku'") - // 최신 콜백 호출 - currentOnOpenLink.value(linku) - }, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "링크 바로 가기", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Image( - painter = painterResource(R.drawable.ic_link_go), - contentDescription = null, - modifier = Modifier.height(14.dp) - ) - } - } - -// if (showAIArticleModal) { -// Box( -// modifier = Modifier -// .fillMaxSize() -// .background(Color(0x66000000)) // 40% 투명한 검정색 배경 -// .zIndex(1f) -// .clickable(enabled = false) {}, // 외부 클릭 막기 -// contentAlignment = Alignment.Center -// ) { -// Box( -// modifier = Modifier.padding(horizontal = 20.dp), -// contentAlignment = Alignment.Center -// ) { -// AIArticleModal( -// modifier = Modifier -//// .align(Alignment.Center) -// .padding(horizontal = 20.dp) -// ) -// } -// } -// } - } -} - -@Composable -private fun TopBar( - isEditMode: Boolean = false, - showAISummary: Boolean = false, - onEditClick: (String) -> Unit = {}, - onBack: () -> Unit = {}, - titleText: String = "", - tags: List = emptyList(), - initialCategoryLabel: String = "카테고리", - initialEmotionLabel: String = "감정", - onCategorySelected: (String) -> Unit = {}, - onEmotionSelected: (String) -> Unit = {}, - categoryLabels: List = emptyList(), - dotColorOf: @Composable (String) -> Color = { LocalColorTheme.current.gray[300] }, -) { - // 태그 샘플 -// val tags = listOf("카테고리", "감정") - - var title by remember { mutableStateOf(titleText) } - var isTitleEditing by remember { mutableStateOf(false) } - - LaunchedEffect(titleText) { if (!isTitleEditing) title = titleText } - LaunchedEffect(isEditMode) { if (!isEditMode) isTitleEditing = false } - - LaunchedEffect(titleText) { - if (!isTitleEditing) title = titleText - } - - // 수정 완료 시 텍스트 입력 종료 - LaunchedEffect(isEditMode) { - if (!isEditMode) { - isTitleEditing = false - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .clip( - RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp) - ) - .background(LocalColorTheme.current.blue[200]) - ) { - Image( - painter = painterResource(R.drawable.ic_transparent_logo_background), - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(bottom = 9.dp) - ) - - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 59.dp, start = 20.dp, end = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box(modifier = Modifier.width(40.dp), contentAlignment = Alignment.CenterStart) { - Image( - painter = painterResource(R.drawable.ic_back_white), - contentDescription = null, - modifier = Modifier - .size(width = 10.dp, height = 16.25.dp) - .clickable { onBack() } - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = "저장된 링크", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white - ) - - Spacer(modifier = Modifier.weight(1f)) - - Box(modifier = Modifier.width(40.dp), contentAlignment = Alignment.CenterStart) { - Text( - text = if (isEditMode) "완료" else "수정", - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.blue[50], - modifier = Modifier - .clickable { - onEditClick(title) - } - ) - } - } - - Spacer(modifier = Modifier.height(34.75.dp)) - - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (showAISummary) { - Image( - painter = painterResource(R.drawable.ic_ai_summarize_save), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - - Spacer(modifier = Modifier.width(8.dp)) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .then( - if (isEditMode) { - Modifier - .clip(RoundedCornerShape(13.dp)) - .border(1.dp, LocalColorTheme.current.blue[100], RoundedCornerShape(13.dp)) - .padding(top = 4.dp, start = 15.dp, end = 15.dp, bottom = 4.dp) - .clickable { isTitleEditing = true } - } else { - Modifier - } - ), - verticalAlignment = Alignment.CenterVertically - ) { - if (isTitleEditing) { - BasicTextField( - value = title, - onValueChange = { title = it }, - textStyle = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = LocalColorTheme.current.white, fontFamily = LocalFontTheme.current.font), - modifier = Modifier.weight(1f) - ) - } else { - Text( - text = title, -// overflow = TextOverflow.Ellipsis, // 말 줄임 - style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, fontFamily = LocalFontTheme.current.font), - color = LocalColorTheme.current.white, - modifier = Modifier.weight(1f) - ) - } - - if (isEditMode) { - Spacer(modifier = Modifier.width(10.dp)) - - Image( - painter = painterResource(R.drawable.ic_delete), - contentDescription = null, - modifier = Modifier - .size(18.dp) - .clickable { - title = "" - isTitleEditing = true - } - ) - } - } - } - - Spacer(modifier = Modifier.height(19.dp)) - - // tags -// if (tags.isNotEmpty()) { -// Row( -// modifier = Modifier -// .padding(start = 24.dp) -// ) { -// tags.forEach { tag -> -// Row( -// modifier = Modifier -// .background( -// LocalColorTheme.current.blue[50], -// RoundedCornerShape(10.dp) -// ) -// .padding(horizontal = 10.dp, vertical = 8.5.dp), -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text( -// text = tag, -// style = TextStyle( -// fontSize = 12.sp, -// fontWeight = FontWeight.Normal, -// color = LocalColorTheme.current.blue[300] -// ) -// ) -// -// if (isEditMode) { -// Spacer(modifier = Modifier.width(6.dp)) -// -// Image( -// painter = painterResource(R.drawable.ic_toggle), -// contentDescription = null, -// modifier = Modifier.height(6.dp) -// ) -// } -// } -// -// Spacer(modifier = Modifier.width(6.dp)) -// } -// } -// } - // ▼ 편집 모드면: 드롭다운 2개 노출 - if (isEditMode) { - Row( - modifier = Modifier.padding(start = 24.dp, end = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - CategoryChipEditable( - label = initialCategoryLabel, - labels = categoryLabels, - dotColorOf = dotColorOf, - onSelected = onCategorySelected - ) - - Spacer(Modifier.width(8.dp)) - - EmotionChipEditable( - label = initialEmotionLabel, - onSelected = onEmotionSelected - ) - } - Spacer(Modifier.height(12.dp)) - } else if (tags.isNotEmpty()) { - // 읽기 모드: 기존 태그 보여주기 - Row(modifier = Modifier.padding(start = 24.dp)) { - tags.forEach { tag -> - Row( - modifier = Modifier - .background(LocalColorTheme.current.blue[50], RoundedCornerShape(10.dp)) - .padding(horizontal = 10.dp, vertical = 8.5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = tag, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Normal, - color = LocalColorTheme.current.blue[300], - fontFamily = LocalFontTheme.current.font - ) - ) - } - Spacer(modifier = Modifier.width(6.dp)) - } - } - } - - Spacer(modifier = Modifier.height(24.dp)) - } - } -} - -@Composable -private fun CategoryChipEditable( - label: String, - labels: List, - dotColorOf: @Composable (String) -> Color, - onSelected: (String) -> Unit, - modifier: Modifier = Modifier -) { - var expanded by remember { mutableStateOf(false) } - - Box(modifier = modifier) { - Row( - modifier = Modifier - .heightIn(min = 26.dp) - .background(LocalColorTheme.current.blue[50], RoundedCornerShape(10.dp)) - .clickable { expanded = true } - .padding(horizontal = 10.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - color = LocalColorTheme.current.blue[300], - fontSize = 12.sp, - fontWeight = FontWeight.Normal, - fontFamily = LocalFontTheme.current.font - ) - Spacer(Modifier.width(6.dp)) - Image( - painter = painterResource(R.drawable.ic_toggle), - contentDescription = null, - modifier = Modifier.height(6.dp) - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - shape = RoundedCornerShape(18.dp), - containerColor = LocalColorTheme.current.white, - offset = DpOffset(0.dp, 10.dp), - ) { - Column( - modifier = Modifier - .width(205.dp) - .heightIn(max = 264.dp) - .padding(vertical = 12.5.dp) - .verticalScroll(rememberScrollState()) - ) { - labels.forEach { name -> - val selected = (name == label) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onSelected(name) - expanded = false - } - .padding(horizontal = 18.dp, vertical = 7.5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val dot = dotColorOf(name) - Canvas(Modifier.size(25.dp)) { drawCircle(dot) } - Spacer(Modifier.width(12.dp)) - Text( - text = name, - color = if (selected) LocalColorTheme.current.blue[300] else LocalColorTheme.current.gray[800], - fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, - fontSize = 14.sp, - fontFamily = LocalFontTheme.current.font - ) - } - } - } - } - } -} - -@Composable -private fun EmotionChipEditable( - label: String, - onSelected: (String) -> Unit, - modifier: Modifier = Modifier -) { - var expanded by remember { mutableStateOf(false) } - - Box(modifier = modifier.wrapContentSize(Alignment.TopCenter)) { - Row( - modifier = Modifier - .heightIn(min = 26.dp) - .background(LocalColorTheme.current.blue[50], RoundedCornerShape(10.dp)) - .clickable { expanded = true } - .padding(horizontal = 10.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - color = LocalColorTheme.current.blue[300], - fontSize = 12.sp, - fontWeight = FontWeight.Normal, - fontFamily = LocalFontTheme.current.font - ) - Spacer(Modifier.width(6.dp)) - Image( - painter = painterResource(R.drawable.ic_toggle), - contentDescription = null, - modifier = Modifier.height(6.dp) - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - shape = RoundedCornerShape(18.dp), - containerColor = LocalColorTheme.current.white, - offset = DpOffset(0.dp, 10.dp), - ) { - Column( - modifier = Modifier - .width(151.dp) - .heightIn(max = 264.dp) - .padding(top = 12.dp, start = 16.dp, end = 56.dp) - .verticalScroll(rememberScrollState()) - ) { - EMOTION_OPTIONS.forEach { emo -> - val selected = emo.label == label - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onSelected(emo.label) - expanded = false - } - .padding(bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 이모지 원형 배지 - Box( - modifier = Modifier - .size(29.dp) - .clip(RoundedCornerShape(12.dp)) - .background(LocalColorTheme.current.gray[100]), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(emo.iconRes), - contentDescription = emo.label, - modifier = Modifier.size(29.dp) - ) - } - Spacer(Modifier.width(10.dp)) - Text( - text = emo.label, - color = if (selected) LocalColorTheme.current.blue[300] - else LocalColorTheme.current.gray[800], - fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, - fontSize = 14.sp, - fontFamily = LocalFontTheme.current.font - ) - } - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun PreviewSaveLinkResultScreen() { - SaveLinkResultScreen( - link = LinkResultInfo( - userId = 1L, - linkuId = 2L, - linkuFolderId = 2L, - categoryId = 16L, - linku = "https://blog.naver.com/s2ethan/223941554164", - memo = "프리뷰 메모", - emotionId = 3L, - domain = "blog.naver", - title = "프리뷰 제목", - domainImageUrl = null, - linkuImageUrl = null, - aiArticleExists = true, - keyword = null, - summary = null, - createdAt = OffsetDateTime.parse("2025-07-21T23:13:41.354053+09:00"), - updatedAt = OffsetDateTime.parse("2025-07-21T23:13:41.354053+09:00") - ), - isLoading = false, - onOpenLink = { /* no-op for preview */ }, - onRequestAiSummary = { /* no-op for preview */ } - ) -} \ No newline at end of file From b2ae7307ae5bf946072f75b87e63f1b0117d4058 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 02:44:27 +0900 Subject: [PATCH 82/89] =?UTF-8?q?:zap:=20Situation=EC=9D=84=20enum=20class?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/Situation.kt | 256 +++++++++++------- .../component/LinkDetailEmotionDropdown.kt | 6 +- .../component/LinkDetailSituationDropdown.kt | 19 +- .../linku/home/component/SituationSelect.kt | 23 +- .../com/linku/home/screen/LinkDetailScreen.kt | 4 +- .../com/linku/home/screen/SaveLinkScreen.kt | 4 +- 6 files changed, 194 insertions(+), 118 deletions(-) diff --git a/core/src/main/java/com/linku/core/model/Situation.kt b/core/src/main/java/com/linku/core/model/Situation.kt index b305d49e..4c6dedd0 100644 --- a/core/src/main/java/com/linku/core/model/Situation.kt +++ b/core/src/main/java/com/linku/core/model/Situation.kt @@ -1,110 +1,184 @@ package com.linku.core.model data class Situation( - val id: Long, + val id: SituationId, val tagName: String ) -object SituationOptions { - private const val DEFAULT_JOB_ID = 3L - - private val situationsByJobId: Map> = mapOf( - 1L to listOf( - Situation(1L, "통학 중"), - Situation(2L, "공부 중"), - Situation(3L, "식사 중"), - Situation(4L, "시험 준비"), - Situation(5L, "친구랑"), - Situation(6L, "쇼핑 중"), - Situation(7L, "휴식 중"), - Situation(8L, "자기 전") - ), - 2L to listOf( - Situation(9L, "과제 중"), - Situation(10L, "통학 중"), - Situation(11L, "쇼핑 중"), - Situation(12L, "알바 중"), - Situation(13L, "트렌드 확인"), - Situation(14L, "데이트 중"), - Situation(15L, "휴식 중"), - Situation(16L, "자기 전") - ), - 3L to listOf( - Situation(17L, "출퇴근"), - Situation(18L, "트렌드 확인"), - Situation(19L, "업무 중"), - Situation(20L, "커리어 고민"), - Situation(21L, "쇼핑 중"), - Situation(22L, "데이트 중"), - Situation(23L, "휴식 중"), - Situation(24L, "자기 전") - ), - 4L to listOf( - Situation(25L, "출퇴근"), - Situation(26L, "업무 준비 중"), - Situation(27L, "데이트 중"), - Situation(28L, "식사"), - Situation(29L, "쇼핑 중"), - Situation(30L, "트렌드 확인"), - Situation(31L, "휴식 중"), - Situation(32L, "자기 전") - ), - 5L to listOf( - Situation(33L, "작업 중"), - Situation(34L, "쇼핑 중"), - Situation(35L, "트렌드 확인"), - Situation(36L, "데이트 중"), - Situation(37L, "운동 중"), - Situation(38L, "식사"), - Situation(39L, "휴식 중"), - Situation(40L, "자기 전") - ), - 6L to listOf( - Situation(41L, "자소서 작성"), - Situation(42L, "면접 준비"), - Situation(43L, "요리 중"), - Situation(44L, "트렌드 확인"), - Situation(45L, "쇼핑 중"), - Situation(46L, "운동 중"), - Situation(47L, "휴식 중"), - Situation(48L, "자기 전") +enum class SituationId(val value: Long) { + HIGH_SCHOOL_COMMUTE(1L), + HIGH_SCHOOL_STUDY(2L), + HIGH_SCHOOL_MEAL(3L), + HIGH_SCHOOL_EXAM_PREP(4L), + HIGH_SCHOOL_WITH_FRIENDS(5L), + HIGH_SCHOOL_SHOPPING(6L), + HIGH_SCHOOL_REST(7L), + HIGH_SCHOOL_BEFORE_SLEEP(8L), + + UNIVERSITY_ASSIGNMENT(9L), + UNIVERSITY_COMMUTE(10L), + UNIVERSITY_SHOPPING(11L), + UNIVERSITY_PART_TIME_JOB(12L), + UNIVERSITY_TREND_CHECK(13L), + UNIVERSITY_DATE(14L), + UNIVERSITY_REST(15L), + UNIVERSITY_BEFORE_SLEEP(16L), + + OFFICE_COMMUTE(17L), + OFFICE_TREND_CHECK(18L), + OFFICE_WORKING(19L), + OFFICE_CAREER_WORRY(20L), + OFFICE_SHOPPING(21L), + OFFICE_DATE(22L), + OFFICE_REST(23L), + OFFICE_BEFORE_SLEEP(24L), + + SELF_EMPLOYED_COMMUTE(25L), + SELF_EMPLOYED_WORK_PREP(26L), + SELF_EMPLOYED_DATE(27L), + SELF_EMPLOYED_MEAL(28L), + SELF_EMPLOYED_SHOPPING(29L), + SELF_EMPLOYED_TREND_CHECK(30L), + SELF_EMPLOYED_REST(31L), + SELF_EMPLOYED_BEFORE_SLEEP(32L), + + CREATOR_WORKING(33L), + CREATOR_SHOPPING(34L), + CREATOR_TREND_CHECK(35L), + CREATOR_DATE(36L), + CREATOR_EXERCISE(37L), + CREATOR_MEAL(38L), + CREATOR_REST(39L), + CREATOR_BEFORE_SLEEP(40L), + + JOB_SEEKER_COVER_LETTER(41L), + JOB_SEEKER_INTERVIEW_PREP(42L), + JOB_SEEKER_COOKING(43L), + JOB_SEEKER_TREND_CHECK(44L), + JOB_SEEKER_SHOPPING(45L), + JOB_SEEKER_EXERCISE(46L), + JOB_SEEKER_REST(47L), + JOB_SEEKER_BEFORE_SLEEP(48L) +} + +enum class JobType( + val id: Long, + val situations: List +) { + HIGH_SCHOOL_STUDENT( + id = 1L, + situations = listOf( + Situation(SituationId.HIGH_SCHOOL_COMMUTE, "통학 중"), + Situation(SituationId.HIGH_SCHOOL_STUDY, "공부 중"), + Situation(SituationId.HIGH_SCHOOL_MEAL, "식사 중"), + Situation(SituationId.HIGH_SCHOOL_EXAM_PREP, "시험 준비"), + Situation(SituationId.HIGH_SCHOOL_WITH_FRIENDS, "친구랑"), + Situation(SituationId.HIGH_SCHOOL_SHOPPING, "쇼핑 중"), + Situation(SituationId.HIGH_SCHOOL_REST, "휴식 중"), + Situation(SituationId.HIGH_SCHOOL_BEFORE_SLEEP, "자기 전") ) - ) + ), - val allSituations: List by lazy { - situationsByJobId.values.flatten() - } + UNIVERSITY_STUDENT( + id = 2L, + situations = listOf( + Situation(SituationId.UNIVERSITY_ASSIGNMENT, "과제 중"), + Situation(SituationId.UNIVERSITY_COMMUTE, "통학 중"), + Situation(SituationId.UNIVERSITY_SHOPPING, "쇼핑 중"), + Situation(SituationId.UNIVERSITY_PART_TIME_JOB, "알바 중"), + Situation(SituationId.UNIVERSITY_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.UNIVERSITY_DATE, "데이트 중"), + Situation(SituationId.UNIVERSITY_REST, "휴식 중"), + Situation(SituationId.UNIVERSITY_BEFORE_SLEEP, "자기 전") + ) + ), - private val allSituationsById: Map by lazy { - allSituations.associateBy { it.id } - } + OFFICE_WORKER( + id = 3L, + situations = listOf( + Situation(SituationId.OFFICE_COMMUTE, "출퇴근"), + Situation(SituationId.OFFICE_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.OFFICE_WORKING, "업무 중"), + Situation(SituationId.OFFICE_CAREER_WORRY, "커리어 고민"), + Situation(SituationId.OFFICE_SHOPPING, "쇼핑 중"), + Situation(SituationId.OFFICE_DATE, "데이트 중"), + Situation(SituationId.OFFICE_REST, "휴식 중"), + Situation(SituationId.OFFICE_BEFORE_SLEEP, "자기 전") + ) + ), - private val situationsByJobAndId: Map> by lazy { - situationsByJobId.mapValues { (_, situations) -> - situations.associateBy { it.id } - } - } + SELF_EMPLOYED( + id = 4L, + situations = listOf( + Situation(SituationId.SELF_EMPLOYED_COMMUTE, "출퇴근"), + Situation(SituationId.SELF_EMPLOYED_WORK_PREP, "업무 준비 중"), + Situation(SituationId.SELF_EMPLOYED_DATE, "데이트 중"), + Situation(SituationId.SELF_EMPLOYED_MEAL, "식사"), + Situation(SituationId.SELF_EMPLOYED_SHOPPING, "쇼핑 중"), + Situation(SituationId.SELF_EMPLOYED_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.SELF_EMPLOYED_REST, "휴식 중"), + Situation(SituationId.SELF_EMPLOYED_BEFORE_SLEEP, "자기 전") + ) + ), - fun situationsFor(jobId: Long): List { - return situationsByJobId[jobId] ?: situationsByJobId.getValue(DEFAULT_JOB_ID) - } + CREATOR( + id = 5L, + situations = listOf( + Situation(SituationId.CREATOR_WORKING, "작업 중"), + Situation(SituationId.CREATOR_SHOPPING, "쇼핑 중"), + Situation(SituationId.CREATOR_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.CREATOR_DATE, "데이트 중"), + Situation(SituationId.CREATOR_EXERCISE, "운동 중"), + Situation(SituationId.CREATOR_MEAL, "식사"), + Situation(SituationId.CREATOR_REST, "휴식 중"), + Situation(SituationId.CREATOR_BEFORE_SLEEP, "자기 전") + ) + ), - fun nameOf(id: Long?): String? { - if (id == null) return null + JOB_SEEKER( + id = 6L, + situations = listOf( + Situation(SituationId.JOB_SEEKER_COVER_LETTER, "자소서 작성"), + Situation(SituationId.JOB_SEEKER_INTERVIEW_PREP, "면접 준비"), + Situation(SituationId.JOB_SEEKER_COOKING, "요리 중"), + Situation(SituationId.JOB_SEEKER_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.JOB_SEEKER_SHOPPING, "쇼핑 중"), + Situation(SituationId.JOB_SEEKER_EXERCISE, "운동 중"), + Situation(SituationId.JOB_SEEKER_REST, "휴식 중"), + Situation(SituationId.JOB_SEEKER_BEFORE_SLEEP, "자기 전") + ) + ); + + companion object { + private val DEFAULT = OFFICE_WORKER - return allSituationsById[id]?.tagName + fun fromId(id: Long?): JobType { + return entries.firstOrNull { it.id == id } ?: DEFAULT + } } +} - fun nameOf(id: Long?, jobId: Long): String? { - if (id == null) return null +object SituationOptions { + val allSituations: List + get() = JobType.entries.flatMap { it.situations } - return situationsByJobAndId[jobId]?.get(id)?.tagName - ?: situationsByJobAndId[DEFAULT_JOB_ID]?.get(id)?.tagName + fun situationsFor(jobId: Long?): List { + return JobType.fromId(jobId).situations } - fun idOf(tagName: String, jobId: Long): Long? { - return situationsFor(jobId) - .firstOrNull { it.tagName == tagName } - ?.id - } + // 선택한 상황을 서버에 보낼 때 Long id가 필요하다면 valueOf 사용 +// fun valueOf(tagName: String, jobId: Long?): Long? { +// return JobType.fromId(jobId) +// .situations +// .firstOrNull { it.tagName == tagName } +// ?.id +// ?.value +// } + + // 서버에서 받은 situationId: Long을 화면에 이름으로 보여줘야 한다면 nameOf 사용 +// fun nameOf(id: Long?): String? { +// return allSituations +// .firstOrNull { it.id.value == id } +// ?.tagName +// } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 3acea2f0..905eecd9 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -44,6 +44,8 @@ fun LinkDetailEmotionDropdown( verticalArrangement = Arrangement.spacedBy(1.dp) ) { emotions.forEach { emotion -> + val isSelected = emotion.tagName == selectedEmotion + Row( modifier = Modifier .noRippleClickable { @@ -62,12 +64,12 @@ fun LinkDetailEmotionDropdown( Text( text = emotion.tagName, fontSize = 15.sp, - fontWeight = if (emotion.tagName == selectedEmotion) { + fontWeight = if (isSelected) { FontWeight.Medium } else { FontWeight.Normal }, - color = if (emotion.tagName == selectedEmotion) { + color = if (isSelected) { colors.blue[200] } else { colors.gray[800] diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt index 32ef9bf6..ac4534e5 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailSituationDropdown.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.linku.core.model.Situation +import com.linku.core.model.SituationId import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors @@ -36,15 +37,17 @@ fun LinkDetailSituationDropdown( .heightIn(max = 264.dp) ) { situations.forEach { situation -> + val isSelected = situation.id == selectedSituation?.id + Text( text = situation.tagName, fontSize = 15.sp, - fontWeight = if (situation.id == selectedSituation?.id) { + fontWeight = if (isSelected) { FontWeight.Medium } else { FontWeight.Normal }, - color = if (situation.id == selectedSituation?.id) { + color = if (isSelected) { colors.blue[200] } else { colors.gray[800] @@ -64,12 +67,12 @@ fun LinkDetailSituationDropdown( fun PreviewLinkDetailSituationDropdown() { ThemeProvider { val situations = listOf( - Situation(18L, "트렌드 확인"), - Situation(10L, "통학 중"), - Situation(9L, "과제 중"), - Situation(11L, "쇼핑 중"), - Situation(14L, "데이트 중"), - Situation(12L, "알바 전") + Situation(SituationId.OFFICE_TREND_CHECK, "트렌드 확인"), + Situation(SituationId.UNIVERSITY_COMMUTE, "통학 중"), + Situation(SituationId.UNIVERSITY_ASSIGNMENT, "과제 중"), + Situation(SituationId.UNIVERSITY_SHOPPING, "쇼핑 중"), + Situation(SituationId.UNIVERSITY_DATE, "데이트 중"), + Situation(SituationId.UNIVERSITY_PART_TIME_JOB, "알바 전") ) LinkDetailSituationDropdown( diff --git a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt index b0d5e1a4..2aa1113e 100644 --- a/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/SituationSelect.kt @@ -17,6 +17,7 @@ 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 com.linku.core.model.JobType import com.linku.core.model.Situation import com.linku.core.model.SituationOptions import com.linku.design.modifier.noRippleClickable @@ -27,27 +28,21 @@ import com.linku.design.theme.linkuColors @OptIn(ExperimentalLayoutApi::class) @Composable fun SituationSelect( - jobId: Long, + jobType: JobType, selectedSituationId: Long?, onSituationSelect: (Long?) -> Unit, - modifier: Modifier = Modifier ) { - val situations = SituationOptions.situationsFor(jobId) + FlowRow { + jobType.situations.forEach { situation -> + val situationId = situation.id.value + val selected = selectedSituationId == situationId - FlowRow( - modifier = modifier - .fillMaxWidth() - .padding(top = 13.dp, start = 20.dp, end = 20.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - situations.forEach { situation -> SituationChip( situation = situation, - selected = selectedSituationId == situation.id, + selected = selected, onClick = { onSituationSelect( - if (selectedSituationId == situation.id) null else situation.id + if (selected) null else situationId ) } ) @@ -102,7 +97,7 @@ private fun SituationChip( fun PreviewSituationSelect() { ThemeProvider { SituationSelect( - jobId = 3L, + jobType = JobType.OFFICE_WORKER, selectedSituationId = 18L, onSituationSelect = { } ) diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 5f488fb8..0aecab58 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -108,7 +108,7 @@ fun LinkDetailScreen( var selectedEmotion by remember { mutableStateOf(emotion) } var selectedSituation by remember(situationId) { mutableStateOf( - situationOptions.firstOrNull { it.id == situationId } + situationOptions.firstOrNull { it.id.value == situationId } ) } var selectedMemo by remember { mutableStateOf(memo) } @@ -131,7 +131,7 @@ fun LinkDetailScreen( selectedTitle = linkTitle selectedCategory = category selectedEmotion = emotion - selectedSituation = situationOptions.firstOrNull { it.id == situationId } + selectedSituation = situationOptions.firstOrNull { it.id.value == situationId } selectedMemo = memo } } diff --git a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt index 29506851..efd513d2 100644 --- a/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/SaveLinkScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.rememberAsyncImagePainter +import com.linku.core.model.JobType import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalFontTheme import com.linku.design.theme.ThemeProvider @@ -66,6 +67,7 @@ fun SaveLinkScreen( isInvalidLink: Boolean, ) { val colors = MaterialTheme.linkuColors + val jobType = JobType.fromId(jobId) val scrollState = rememberScrollState() val bannedDomains = listOf("youtube.com", "youtu.be") @@ -418,7 +420,7 @@ fun SaveLinkScreen( } SituationSelect( - jobId = jobId, + jobType = jobType, selectedSituationId = selectedSituationId, onSituationSelect = onSituationSelect ) From 05095a9951c6358a1a9b0610dc995bfb3154dcfc Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:02:43 +0900 Subject: [PATCH 83/89] =?UTF-8?q?:zap:=20EmotionType=EC=9D=98=20id=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=84=20enum=20class=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/linku/core/model/EmotionType.kt | 55 ++++++++++++------- .../com/linku/core/model/LinkSimpleInfo.kt | 4 +- .../com/linku/home/component/EmotionSelect.kt | 10 ++-- .../component/LinkDetailEmotionDropdown.kt | 3 +- .../com/linku/home/util/EmotionTypeExt.kt | 24 ++++---- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/com/linku/core/model/EmotionType.kt b/core/src/main/java/com/linku/core/model/EmotionType.kt index ab1970ab..67f236f3 100644 --- a/core/src/main/java/com/linku/core/model/EmotionType.kt +++ b/core/src/main/java/com/linku/core/model/EmotionType.kt @@ -1,31 +1,48 @@ package com.linku.core.model +enum class EmotionId(val value: Long) { + JOY(1L), + CALM(2L), + EXCITE(3L), + SAD(4L), + IRRITATION(5L), + ANGER(6L); + + companion object { + fun fromValue(value: Long?): EmotionId? { + if (value == null) return null + + return entries.firstOrNull { it.value == value } + } + } +} + enum class EmotionType( - val id: Long, + val id: EmotionId, val tagName: String ) { - JOY(id = 1L, tagName = "즐거움"), - CALM(id = 2L, tagName = "평온"), - EXCITE(id = 3L, tagName = "설렘"), - SAD(id = 4L, tagName = "슬픔"), - IRRITATION(id = 5L, tagName = "짜증"), - ANGER(id = 6L, tagName = "분노"); + JOY(EmotionId.JOY, "즐거움"), + CALM(EmotionId.CALM, "평온"), + EXCITE(EmotionId.EXCITE, "설렘"), + SAD(EmotionId.SAD, "슬픔"), + IRRITATION(EmotionId.IRRITATION, "짜증"), + ANGER(EmotionId.ANGER, "분노"); - companion object { - fun fromId(id: Long?): EmotionType? { - return entries.firstOrNull { it.id == id } - } + val value: Long + get() = id.value - fun fromTagName(tagName: String?): EmotionType? { - return entries.firstOrNull { it.tagName == tagName } - } + companion object { + fun fromValue(value: Long?): EmotionType? { + val emotionId = EmotionId.fromValue(value) - fun tagNameOf(id: Long?): String? { - return fromId(id)?.tagName + return entries.firstOrNull { it.id == emotionId } } - fun idOf(tagName: String?): Long? { - return fromTagName(tagName)?.id - } + // 태그명으로 찾는 게 필요하다면 fromTagName 사용 +// fun fromTagName(tagName: String?): EmotionType? { +// if (tagName == null) return null +// +// return entries.firstOrNull { it.tagName == tagName } +// } } } \ No newline at end of file diff --git a/core/src/main/java/com/linku/core/model/LinkSimpleInfo.kt b/core/src/main/java/com/linku/core/model/LinkSimpleInfo.kt index 66a24330..1646964b 100644 --- a/core/src/main/java/com/linku/core/model/LinkSimpleInfo.kt +++ b/core/src/main/java/com/linku/core/model/LinkSimpleInfo.kt @@ -11,8 +11,6 @@ data class LinkSimpleInfo( val linkuImageUrl: String?, val aiArticleExists: Boolean ) { -// val categoryType: CategoryType? = CategoryType.fromId(categoryId) -// val emotionType: EmotionType? = EmotionType.fromId(emotionId) val categoryType: CategoryType? = categoryId?.let { CategoryType.fromId(it) } - val emotionType: EmotionType? = emotionId?.let { EmotionType.fromId(it) } + val emotionType: EmotionType? = EmotionType.fromValue(emotionId) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 58a3658b..3cd92438 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -51,12 +50,15 @@ fun EmotionSelect( verticalArrangement = Arrangement.spacedBy(10.dp) ) { emotions.forEach { emotion -> + val emotionId = emotion.value + val selected = selectedEmotionId == emotionId + EmotionBadgeImage( emotion = emotion, - selected = selectedEmotionId == emotion.id, + selected = selectedEmotionId == emotionId, onClick = { onEmotionSelect( - if (selectedEmotionId == emotion.id) null else emotion.id + if (selected) null else emotionId ) } ) @@ -103,7 +105,7 @@ private fun EmotionBadgeImage( verticalAlignment = Alignment.CenterVertically ) { Image( - painter = painterResource(id = emotion.imgRes), + painter = emotion.imgRes, contentDescription = emotion.tagName, modifier = Modifier.size(20.dp) ) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt index 905eecd9..c8ad6060 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailEmotionDropdown.kt @@ -15,7 +15,6 @@ 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.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -56,7 +55,7 @@ fun LinkDetailEmotionDropdown( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Image( - painter = painterResource(emotion.imgRes), + painter = emotion.imgRes, contentDescription = null, modifier = Modifier.size(29.dp) ) diff --git a/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt index ff31c0bb..384c61ae 100644 --- a/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt +++ b/feature/home/src/main/java/com/linku/home/util/EmotionTypeExt.kt @@ -1,15 +1,19 @@ package com.linku.home.util -import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import com.linku.core.model.EmotionType import com.linku.home.R -val EmotionType.imgRes: Int - @DrawableRes get() = when (this) { - EmotionType.JOY -> R.drawable.ic_joy - EmotionType.CALM -> R.drawable.ic_calm - EmotionType.EXCITE -> R.drawable.ic_excite - EmotionType.SAD -> R.drawable.ic_sad - EmotionType.IRRITATION -> R.drawable.ic_irritation - EmotionType.ANGER -> R.drawable.ic_anger - } \ No newline at end of file +val EmotionType.imgRes: Painter + @Composable get() = painterResource( + id = when (this) { + EmotionType.JOY -> R.drawable.ic_joy + EmotionType.CALM -> R.drawable.ic_calm + EmotionType.EXCITE -> R.drawable.ic_excite + EmotionType.SAD -> R.drawable.ic_sad + EmotionType.IRRITATION -> R.drawable.ic_irritation + EmotionType.ANGER -> R.drawable.ic_anger + } + ) \ No newline at end of file From 95a35f055ef930eaa75aa5e9b1e69f9e620b5470 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:05:55 +0900 Subject: [PATCH 84/89] =?UTF-8?q?:fire:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20EmotionType=20entries=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/linku/home/component/EmotionSelect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 3cd92438..bfe4ca4f 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -40,7 +40,7 @@ fun EmotionSelect( onEmotionSelect: (Long?) -> Unit, modifier: Modifier = Modifier ) { - val emotions = EmotionType.entries.toList() + val emotions = EmotionType.entries FlowRow( modifier = modifier From 14f60a2b131b3d250552febadc0563e1026a1a64 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:08:19 +0900 Subject: [PATCH 85/89] =?UTF-8?q?:art:=20border=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/linku/home/component/EmotionSelect.kt | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index bfe4ca4f..92d1dec2 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -85,21 +85,12 @@ private fun EmotionBadgeImage( }, shape = RoundedCornerShape(20.dp) ) - .then( - if (selected) { - Modifier.border( - width = 1.dp, - brush = Basic.maincolor, - shape = RoundedCornerShape(20.dp) - ) - } else { - Modifier.border( - width = 1.dp, - color = colors.gray[200], - shape = RoundedCornerShape(20.dp) - ) - } + .border( + width = 1.dp, + brush = if (selected) Basic.maincolor else SolidColor(colors.gray[200]), // 와! 코드가 확 줄어드러욧! + shape = RoundedCornerShape(20.dp) ) + .noRippleClickable { onClick() } .padding(horizontal = 15.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically From 67b26185b8d5bedcb2620a62b2edf9e91c2059c0 Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:15:25 +0900 Subject: [PATCH 86/89] =?UTF-8?q?:zap:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A0=ED=83=9D=20=EC=83=81=ED=83=9C=EB=A5=BC=20?= =?UTF-8?q?id=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/LinkDetailCategoryDropdown.kt | 10 ++++++---- .../java/com/linku/home/screen/LinkDetailScreen.kt | 13 ++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt index eef47a21..da5dd456 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCategoryDropdown.kt @@ -34,7 +34,7 @@ data class LinkCategoryOption( @Composable fun LinkDetailCategoryDropdown( categories: List, - selectedCategory: String, + selectedCategoryId: Long?, onCategoryClick: (LinkCategoryOption) -> Unit, modifier: Modifier = Modifier ) { @@ -49,6 +49,8 @@ fun LinkDetailCategoryDropdown( verticalArrangement = Arrangement.spacedBy(1.dp) ) { categories.forEach { category -> + val isSelected = category.id == selectedCategoryId + Row( modifier = Modifier .noRippleClickable { @@ -68,12 +70,12 @@ fun LinkDetailCategoryDropdown( Text( text = category.name, fontSize = 15.sp, - fontWeight = if (category.name == selectedCategory) { + fontWeight = if (isSelected) { FontWeight.Medium } else { FontWeight.Normal }, - color = if (category.name == selectedCategory) { + color = if (isSelected) { colors.blue[200] } else { colors.gray[800] @@ -97,7 +99,7 @@ fun PreviewLinkDetailCategoryDropdown() { LinkCategoryOption(5L, "카테고리6", Color(0xFF67D414)), LinkCategoryOption(6L, "카테고리7", Color(0xFFD9DEE6)) ), - selectedCategory = "카테고리2", + selectedCategoryId = 1L, onCategoryClick = { } ) } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 0aecab58..91e29b58 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -104,7 +104,12 @@ fun LinkDetailScreen( val situationOptions = SituationOptions.allSituations var selectedTitle by remember { mutableStateOf(linkTitle) } - var selectedCategory by remember { mutableStateOf(category) } + var selectedCategory by remember { mutableStateOf(category) } // TopBar에 보여줄 이름 + var selectedCategoryId by remember(categoryOptions, category) { // 선택 여부 비교 및 API 전달용 + mutableStateOf( + categoryOptions.firstOrNull { it.name == category }?.id + ) + } var selectedEmotion by remember { mutableStateOf(emotion) } var selectedSituation by remember(situationId) { mutableStateOf( @@ -126,10 +131,11 @@ fun LinkDetailScreen( if (tag.startsWith("#")) tag else "#$tag" } - LaunchedEffect(linkTitle, category, emotion, situationId, memo) { + LaunchedEffect(linkTitle, category, emotion, situationId, memo, categoryOptions) { if (!isEditMode) { selectedTitle = linkTitle selectedCategory = category + selectedCategoryId = categoryOptions.firstOrNull { it.name == category }?.id selectedEmotion = emotion selectedSituation = situationOptions.firstOrNull { it.id.value == situationId } selectedMemo = memo @@ -575,8 +581,9 @@ fun LinkDetailScreen( LinkDetailDropdownType.CATEGORY -> { LinkDetailCategoryDropdown( categories = categoryOptions, - selectedCategory = selectedCategory, + selectedCategoryId = selectedCategoryId, onCategoryClick = { + selectedCategoryId = it.id selectedCategory = it.name openedDropdownType = null }, From 637060780f77686d03b8c1bac4fc9b8e9a6418ca Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:19:10 +0900 Subject: [PATCH 87/89] =?UTF-8?q?:art:=20=EB=A7=81=ED=81=AC=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=95=A1=EC=85=98=20=EB=A9=94=EB=89=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/LinkDetailCustomDropdown.kt | 64 ++++++++----------- .../com/linku/home/screen/LinkDetailScreen.kt | 61 +++++++++--------- 2 files changed, 56 insertions(+), 69 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt index 08fb6a9b..0d3d7ce0 100644 --- a/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt +++ b/feature/home/src/main/java/com/linku/home/component/LinkDetailCustomDropdown.kt @@ -28,17 +28,23 @@ import com.linku.design.theme.ThemeProvider import com.linku.design.theme.linkuColors import com.linku.home.R +enum class LinkDetailAction( + @param:DrawableRes val iconRes: Int, + val label: String +) { + EDIT(R.drawable.ic_link_edit, "링크 수정하기"), + DELETE(R.drawable.ic_link_delete, "링크 삭제하기"), + SHARE(R.drawable.ic_link_share, "링크 공유하기"), + GO(R.drawable.ic_link_go_gray, "링크 보러가기") +} + @Composable fun LinkDetailCustomDropdown( - onEditClick: () -> Unit, - onDeleteClick: () -> Unit, - onShareClick: () -> Unit, - onGoClick: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier + onAction: (LinkDetailAction) -> Unit, + modifier: Modifier = Modifier ) { val colors = MaterialTheme.linkuColors - + Column( modifier = modifier .width(240.dp) @@ -46,36 +52,20 @@ fun LinkDetailCustomDropdown( .background(colors.white) .padding(horizontal = 24.dp, vertical = 13.dp) ) { - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_edit, - text = "링크 수정하기", - onClick = { onEditClick() } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_delete, - text = "링크 삭제하기", - onClick = { onDeleteClick() } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_share, - text = "링크 공유하기", - onClick = { onShareClick() } - ) - - LinkDetailDropdownItem( - iconRes = R.drawable.ic_link_go_gray, - text = "링크 보러가기", - onClick = { onGoClick() } - ) + LinkDetailAction.entries.forEach { action -> + LinkDetailDropdownItem( + action = action, + onClick = { + onAction(action) + } + ) + } } } @Composable private fun LinkDetailDropdownItem( - @DrawableRes iconRes: Int, - text: String, + action: LinkDetailAction, onClick: () -> Unit ) { val colors = MaterialTheme.linkuColors @@ -91,13 +81,13 @@ private fun LinkDetailDropdownItem( horizontalArrangement = Arrangement.spacedBy(14.dp) ) { Image( - painter = painterResource(iconRes), + painter = painterResource(action.iconRes), contentDescription = null, modifier = Modifier.size(18.dp) ) Text( - text = text, + text = action.label, fontSize = 16.sp, fontWeight = FontWeight.Normal, color = colors.black @@ -111,11 +101,7 @@ private fun LinkDetailDropdownItem( fun PreviewLinkDetailCustomDropdown() { ThemeProvider { LinkDetailCustomDropdown( - onEditClick = { }, - onDeleteClick = { }, - onShareClick = { }, - onGoClick = { }, - onDismiss = { }, + onAction = { }, modifier = Modifier ) } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 91e29b58..21d51258 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -57,6 +57,7 @@ import com.linku.home.R import com.linku.home.component.AIArticleModal import com.linku.home.component.DeleteLinkModal import com.linku.home.component.LinkCategoryOption +import com.linku.home.component.LinkDetailAction import com.linku.home.component.LinkDetailCategoryDropdown import com.linku.home.component.LinkDetailCustomDropdown import com.linku.home.component.LinkDetailEmotionDropdown @@ -481,44 +482,44 @@ fun LinkDetailScreen( if (isDropdownVisible) { LinkDetailCustomDropdown( - onEditClick = { - isDropdownVisible = false - isEditMode = true - }, - onDeleteClick = { - isDropdownVisible = false - openedDropdownType = null - isDeleteModalVisible = true - }, - onShareClick = { + onAction = { action -> isDropdownVisible = false openedDropdownType = null - val shareText = buildString { - appendLine(selectedTitle) - append(linkUrl) - } + when (action) { + LinkDetailAction.EDIT -> { + isEditMode = true + } - val sendIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" // MIME 타입 - putExtra(Intent.EXTRA_TEXT, shareText) // 공유할 내용 - putExtra(Intent.EXTRA_TITLE, selectedTitle) // 미리보기 제목 - putExtra(Intent.EXTRA_SUBJECT, selectedTitle) // 이메일 앱용 제목 - } + LinkDetailAction.DELETE -> { + isDeleteModalVisible = true + } - val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") // ShareSheet 상단에 보이는 제목 - context.startActivity(shareIntent) - }, - onGoClick = { - isDropdownVisible = false - uriHandler.openUri(linkUrl) - }, - onDismiss = { - isDropdownVisible = false + LinkDetailAction.SHARE -> { + val shareText = buildString { + appendLine(selectedTitle) + append(linkUrl) + } + + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, shareText) + putExtra(Intent.EXTRA_TITLE, selectedTitle) + putExtra(Intent.EXTRA_SUBJECT, selectedTitle) + } + + val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") + context.startActivity(shareIntent) + } + + LinkDetailAction.GO -> { + uriHandler.openUri(linkUrl) + } + } }, modifier = Modifier .align(Alignment.TopEnd) - .padding(top = 100.dp, end = 20.dp), + .padding(top = 100.dp, end = 20.dp) ) } From c8d5bf7b833a966ce602bb23d9663d37093391be Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 16:27:47 +0900 Subject: [PATCH 88/89] =?UTF-8?q?:art:=20=EB=A7=81=ED=81=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20ViewModel=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=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 --- .../main/java/com/linku/home/HomeViewModel.kt | 26 ++++++----- .../com/linku/home/screen/LinkDetailScreen.kt | 43 +++++++++---------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt index 417339a7..84234db0 100644 --- a/feature/home/src/main/java/com/linku/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/linku/home/HomeViewModel.kt @@ -514,6 +514,7 @@ class HomeViewModel @Inject constructor( memo: String?, categoryId: Long?, emotionId: Long?, + situationId: Long?, onSucceed: (LinkResultInfo) -> Unit = {}, onFailed: (Throwable) -> Unit = {}, ) { @@ -521,13 +522,12 @@ class HomeViewModel @Inject constructor( onFailed(IllegalStateException("링크 상세가 없습니다.")) return } + if (isUpdatingLinkState.value) return - // 서버에서 내려준 값으로 고정 val fixedLinkuId = current.linkuId - val fixedLinku = current.linku + val fixedLinku = current.linku - // domainId 매핑 val computedDomainId = DomainIdMapper.resolve( url = fixedLinku, domain = current.domain @@ -535,23 +535,29 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { isUpdatingLinkState.value = true + runCatching { linkuRepository.updateLink( - linkuId = fixedLinkuId, - categoryId= categoryId ?: current.categoryId ?: 0L, - linku = fixedLinku, // 고정 - memo = memo, // null/"" 그대로 전달 + linkuId = fixedLinkuId, + categoryId = categoryId ?: current.categoryId ?: 0L, + linku = fixedLinku, + memo = memo, emotionId = emotionId ?: current.emotionId ?: 0L, - domainId = computedDomainId, - title = title.ifBlank { current.title } + domainId = computedDomainId, + title = title.ifBlank { current.title } + + // TODO: API 연동 시 아래 값도 함께 전달 + // situationId = situationId ?: current.situationId ?: 0L ) }.onSuccess { updated -> linkDetailState.value = updated - loadRecentLinks() // 최근 조회 목록 갱신 + linkCache[fixedLinkuId] = Cached(updated) + loadRecentLinks() onSucceed(updated) }.onFailure { e -> onFailed(e) } + isUpdatingLinkState.value = false } } diff --git a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt index 21d51258..7d0701ef 100644 --- a/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt +++ b/feature/home/src/main/java/com/linku/home/screen/LinkDetailScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -92,39 +93,37 @@ fun LinkDetailScreen( val uriHandler = LocalUriHandler.current val context = LocalContext.current - var isEditMode by remember { mutableStateOf(false) } - var isAiSummaryMode by remember { mutableStateOf(false) } + var isEditMode by rememberSaveable { mutableStateOf(false) } + var isAiSummaryMode by rememberSaveable { mutableStateOf(false) } - var isDropdownVisible by remember { mutableStateOf(false) } - var isDeleteModalVisible by remember { mutableStateOf(false) } - var isAiArticleModalVisible by remember { mutableStateOf(false) } - var isAiArticleProcessing by remember { mutableStateOf(false) } - var aiArticleProgress by remember { mutableFloatStateOf(0f) } + var isDropdownVisible by rememberSaveable { mutableStateOf(false) } + var isDeleteModalVisible by rememberSaveable { mutableStateOf(false) } + var isAiArticleModalVisible by rememberSaveable { mutableStateOf(false) } + var isAiArticleProcessing by rememberSaveable { mutableStateOf(false) } + var aiArticleProgress by rememberSaveable { mutableFloatStateOf(0f) } - val emotionOptions = EmotionType.entries.toList() + val emotionOptions = EmotionType.entries val situationOptions = SituationOptions.allSituations - var selectedTitle by remember { mutableStateOf(linkTitle) } - var selectedCategory by remember { mutableStateOf(category) } // TopBar에 보여줄 이름 - var selectedCategoryId by remember(categoryOptions, category) { // 선택 여부 비교 및 API 전달용 + var selectedTitle by rememberSaveable { mutableStateOf(linkTitle) } + var selectedCategory by rememberSaveable { mutableStateOf(category) } + var selectedCategoryId by rememberSaveable { mutableStateOf( categoryOptions.firstOrNull { it.name == category }?.id ) } - var selectedEmotion by remember { mutableStateOf(emotion) } - var selectedSituation by remember(situationId) { - mutableStateOf( - situationOptions.firstOrNull { it.id.value == situationId } - ) + var selectedEmotion by rememberSaveable { mutableStateOf(emotion) } + var selectedSituationId by rememberSaveable { mutableStateOf(situationId) } + var selectedMemo by rememberSaveable { mutableStateOf(memo) } + + val selectedSituation = situationOptions.firstOrNull { + it.id.value == selectedSituationId } - var selectedMemo by remember { mutableStateOf(memo) } - var openedDropdownType by remember { + var openedDropdownType by rememberSaveable { mutableStateOf(null) } - - val visibleTags = tags .filter { it.isNotBlank() } .take(4) @@ -138,7 +137,7 @@ fun LinkDetailScreen( selectedCategory = category selectedCategoryId = categoryOptions.firstOrNull { it.name == category }?.id selectedEmotion = emotion - selectedSituation = situationOptions.firstOrNull { it.id.value == situationId } + selectedSituationId = situationId selectedMemo = memo } } @@ -613,7 +612,7 @@ fun LinkDetailScreen( situations = situationOptions, selectedSituation = selectedSituation, onSituationClick = { - selectedSituation = it + selectedSituationId = it.id.value openedDropdownType = null }, modifier = Modifier From bfa63f98d6c75223c311d0e01459642b2843a4fd Mon Sep 17 00:00:00 2001 From: Jihyun Date: Thu, 2 Jul 2026 17:25:51 +0900 Subject: [PATCH 89/89] =?UTF-8?q?:fire:=20=EC=A3=BC=EC=84=9D=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 --- .../src/main/java/com/linku/home/component/EmotionSelect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt index 92d1dec2..8ef1219d 100644 --- a/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt +++ b/feature/home/src/main/java/com/linku/home/component/EmotionSelect.kt @@ -87,7 +87,7 @@ private fun EmotionBadgeImage( ) .border( width = 1.dp, - brush = if (selected) Basic.maincolor else SolidColor(colors.gray[200]), // 와! 코드가 확 줄어드러욧! + brush = if (selected) Basic.maincolor else SolidColor(colors.gray[200]), shape = RoundedCornerShape(20.dp) )