From bba67d710d6d696807a3a45593abd4227dd47336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=84=EC=A0=95?= Date: Sun, 29 Jun 2025 02:03:38 +0900 Subject: [PATCH 1/4] build: Add search module --- search/data/.gitignore | 1 + search/data/build.gradle.kts | 16 +++++++++++++ search/data/consumer-rules.pro | 0 search/data/proguard-rules.pro | 21 ++++++++++++++++ .../search/data/ExampleInstrumentedTest.kt | 24 +++++++++++++++++++ search/data/src/main/AndroidManifest.xml | 4 ++++ .../hyunjung/search/data/ExampleUnitTest.kt | 17 +++++++++++++ search/domain/.gitignore | 1 + search/domain/build.gradle.kts | 7 ++++++ .../com/hyunjung/search/domain/MyClass.kt | 4 ++++ search/presentation/.gitignore | 1 + search/presentation/build.gradle.kts | 14 +++++++++++ search/presentation/consumer-rules.pro | 0 search/presentation/proguard-rules.pro | 21 ++++++++++++++++ .../presentation/ExampleInstrumentedTest.kt | 24 +++++++++++++++++++ .../presentation/src/main/AndroidManifest.xml | 4 ++++ .../search/presentation/ExampleUnitTest.kt | 17 +++++++++++++ settings.gradle.kts | 5 +++- 18 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 search/data/.gitignore create mode 100644 search/data/build.gradle.kts create mode 100644 search/data/consumer-rules.pro create mode 100644 search/data/proguard-rules.pro create mode 100644 search/data/src/androidTest/java/com/hyunjung/search/data/ExampleInstrumentedTest.kt create mode 100644 search/data/src/main/AndroidManifest.xml create mode 100644 search/data/src/test/java/com/hyunjung/search/data/ExampleUnitTest.kt create mode 100644 search/domain/.gitignore create mode 100644 search/domain/build.gradle.kts create mode 100644 search/domain/src/main/java/com/hyunjung/search/domain/MyClass.kt create mode 100644 search/presentation/.gitignore create mode 100644 search/presentation/build.gradle.kts create mode 100644 search/presentation/consumer-rules.pro create mode 100644 search/presentation/proguard-rules.pro create mode 100644 search/presentation/src/androidTest/java/com/hyunjung/search/presentation/ExampleInstrumentedTest.kt create mode 100644 search/presentation/src/main/AndroidManifest.xml create mode 100644 search/presentation/src/test/java/com/hyunjung/search/presentation/ExampleUnitTest.kt diff --git a/search/data/.gitignore b/search/data/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/search/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/search/data/build.gradle.kts b/search/data/build.gradle.kts new file mode 100644 index 0000000..85d90ce --- /dev/null +++ b/search/data/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.hyunjung.cherrydan.android.library) + alias(libs.plugins.hyunjung.cherrydan.jvm.ktor) +} + +android { + namespace = "com.hyunjung.search.data" +} + +dependencies { + implementation(libs.bundles.koin) + + implementation(projects.search.domain) + implementation(projects.core.domain) + implementation(projects.core.data) +} \ No newline at end of file diff --git a/search/data/consumer-rules.pro b/search/data/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/search/data/proguard-rules.pro b/search/data/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/search/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/search/data/src/androidTest/java/com/hyunjung/search/data/ExampleInstrumentedTest.kt b/search/data/src/androidTest/java/com/hyunjung/search/data/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..0f2284d --- /dev/null +++ b/search/data/src/androidTest/java/com/hyunjung/search/data/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.hyunjung.search.data + +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 + assertEquals("com.hyunjung.search.data.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/search/data/src/main/AndroidManifest.xml b/search/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/search/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/search/data/src/test/java/com/hyunjung/search/data/ExampleUnitTest.kt b/search/data/src/test/java/com/hyunjung/search/data/ExampleUnitTest.kt new file mode 100644 index 0000000..b97b7c5 --- /dev/null +++ b/search/data/src/test/java/com/hyunjung/search/data/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.hyunjung.search.data + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/search/domain/.gitignore b/search/domain/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/search/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/search/domain/build.gradle.kts b/search/domain/build.gradle.kts new file mode 100644 index 0000000..d7c49a4 --- /dev/null +++ b/search/domain/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.hyunjung.cherrydan.jvm.library) +} + +dependencies { + implementation(projects.core.domain) +} \ No newline at end of file diff --git a/search/domain/src/main/java/com/hyunjung/search/domain/MyClass.kt b/search/domain/src/main/java/com/hyunjung/search/domain/MyClass.kt new file mode 100644 index 0000000..4bc7a84 --- /dev/null +++ b/search/domain/src/main/java/com/hyunjung/search/domain/MyClass.kt @@ -0,0 +1,4 @@ +package com.hyunjung.search.domain + +class MyClass { +} \ No newline at end of file diff --git a/search/presentation/.gitignore b/search/presentation/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/search/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/search/presentation/build.gradle.kts b/search/presentation/build.gradle.kts new file mode 100644 index 0000000..be1bb61 --- /dev/null +++ b/search/presentation/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.hyunjung.cherrydan.android.feature.ui) +} + +android { + namespace = "com.hyunjung.search.presentation" +} + +dependencies { + implementation(libs.androidx.navigation.compose) + + implementation(projects.core.domain) + implementation(projects.search.domain) +} \ No newline at end of file diff --git a/search/presentation/consumer-rules.pro b/search/presentation/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/search/presentation/proguard-rules.pro b/search/presentation/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/search/presentation/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/search/presentation/src/androidTest/java/com/hyunjung/search/presentation/ExampleInstrumentedTest.kt b/search/presentation/src/androidTest/java/com/hyunjung/search/presentation/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..497b390 --- /dev/null +++ b/search/presentation/src/androidTest/java/com/hyunjung/search/presentation/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.hyunjung.search.presentation + +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 + assertEquals("com.hyunjung.search.presentation.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/search/presentation/src/main/AndroidManifest.xml b/search/presentation/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/search/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/search/presentation/src/test/java/com/hyunjung/search/presentation/ExampleUnitTest.kt b/search/presentation/src/test/java/com/hyunjung/search/presentation/ExampleUnitTest.kt new file mode 100644 index 0000000..4e98569 --- /dev/null +++ b/search/presentation/src/test/java/com/hyunjung/search/presentation/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.hyunjung.search.presentation + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 440f18b..cef6780 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,4 +38,7 @@ include(":home:domain") include(":home:presentation") include(":notification:data") include(":notification:domain") -include(":notification:presentation") \ No newline at end of file +include(":notification:presentation") +include(":search:presentation") +include(":search:domain") +include(":search:data") From 6e2eef8d567cd1c148281b59a6655a3022edd166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=84=EC=A0=95?= Date: Sun, 29 Jun 2025 03:49:50 +0900 Subject: [PATCH 2/4] chore: Add resources --- .../core/presentation/designsystem/Icons.kt | 18 +++++++++++++++++- .../src/main/res/drawable/ic_arrow_down.xml | 11 +++++++++++ .../src/main/res/drawable/ic_close.xml | 16 ++++++++++++++++ .../res/drawable/ic_notification_small.xml | 12 ++++++++++++ .../main/res/drawable/ic_search_cancel.xml | 19 +++++++++++++++++++ .../ui/src/main/res/values/strings.xml | 11 +++++++++++ 6 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 core/presentation/designsystem/src/main/res/drawable/ic_arrow_down.xml create mode 100644 core/presentation/designsystem/src/main/res/drawable/ic_close.xml create mode 100644 core/presentation/designsystem/src/main/res/drawable/ic_notification_small.xml create mode 100644 core/presentation/designsystem/src/main/res/drawable/ic_search_cancel.xml diff --git a/core/presentation/designsystem/src/main/java/com/hyunjung/core/presentation/designsystem/Icons.kt b/core/presentation/designsystem/src/main/java/com/hyunjung/core/presentation/designsystem/Icons.kt index f0345d4..c701a06 100644 --- a/core/presentation/designsystem/src/main/java/com/hyunjung/core/presentation/designsystem/Icons.kt +++ b/core/presentation/designsystem/src/main/java/com/hyunjung/core/presentation/designsystem/Icons.kt @@ -12,6 +12,10 @@ val ArrowRightIcon: ImageVector @Composable get() = ImageVector.vectorResource(id = R.drawable.ic_arrow_right) +val ArrowDownIcon: ImageVector + @Composable + get() = ImageVector.vectorResource(id = R.drawable.ic_arrow_down) + val CalendarIcon: ImageVector @Composable get() = ImageVector.vectorResource(id = R.drawable.ic_calendar) @@ -36,6 +40,10 @@ val NotificationIcon: ImageVector @Composable get() = ImageVector.vectorResource(id = R.drawable.ic_notification) +val NotificationSmallIcon: ImageVector + @Composable + get() = ImageVector.vectorResource(id = R.drawable.ic_notification_small) + val SearchIcon: ImageVector @Composable get() = ImageVector.vectorResource(id = R.drawable.ic_search) @@ -118,4 +126,12 @@ val CircleUnselectedIcon: ImageVector val CircleCheckedIcon: ImageVector @Composable - get() = ImageVector.vectorResource(id = R.drawable.ic_circle_checked) \ No newline at end of file + get() = ImageVector.vectorResource(id = R.drawable.ic_circle_checked) + +val SearchCancelIcon: ImageVector + @Composable + get() = ImageVector.vectorResource(id = R.drawable.ic_search_cancel) + +val CloseIcon: ImageVector + @Composable + get() = ImageVector.vectorResource(id = R.drawable.ic_close) \ No newline at end of file diff --git a/core/presentation/designsystem/src/main/res/drawable/ic_arrow_down.xml b/core/presentation/designsystem/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000..9bd207e --- /dev/null +++ b/core/presentation/designsystem/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/presentation/designsystem/src/main/res/drawable/ic_close.xml b/core/presentation/designsystem/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..04e9227 --- /dev/null +++ b/core/presentation/designsystem/src/main/res/drawable/ic_close.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/presentation/designsystem/src/main/res/drawable/ic_notification_small.xml b/core/presentation/designsystem/src/main/res/drawable/ic_notification_small.xml new file mode 100644 index 0000000..9a15d60 --- /dev/null +++ b/core/presentation/designsystem/src/main/res/drawable/ic_notification_small.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/presentation/designsystem/src/main/res/drawable/ic_search_cancel.xml b/core/presentation/designsystem/src/main/res/drawable/ic_search_cancel.xml new file mode 100644 index 0000000..4d195db --- /dev/null +++ b/core/presentation/designsystem/src/main/res/drawable/ic_search_cancel.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/core/presentation/ui/src/main/res/values/strings.xml b/core/presentation/ui/src/main/res/values/strings.xml index 4e7fed0..0a843a6 100644 --- a/core/presentation/ui/src/main/res/values/strings.xml +++ b/core/presentation/ui/src/main/res/values/strings.xml @@ -35,4 +35,15 @@ 읽음 삭제 취소 + + + 검색어를 입력해 주세요 + 닫기 + 최근 검색 + 전체 삭제 + 지역 + 제품 + 키워드 알림을 추가했어요. + 키워드 알림을 더이상 추가할 수 없어요. + (마이페이지 > 내 정보 > 키워드 알림)에서 확인해 주세요. \ No newline at end of file From 9452d40245de6a132a7c35d2aee895df9a6e62ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=84=EC=A0=95?= Date: Sun, 29 Jun 2025 06:15:57 +0900 Subject: [PATCH 3/4] feat: Add SearchScreen --- .../search/presentation/SearchScreen.kt | 235 ++++++++++++++++++ .../component/CherrydanSearchTextField.kt | 143 +++++++++++ .../presentation/component/SearchToolBar.kt | 166 +++++++++++++ 3 files changed, 544 insertions(+) create mode 100644 search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt create mode 100644 search/presentation/src/main/java/com/hyunjung/search/presentation/component/CherrydanSearchTextField.kt create mode 100644 search/presentation/src/main/java/com/hyunjung/search/presentation/component/SearchToolBar.kt diff --git a/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt b/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt new file mode 100644 index 0000000..1096189 --- /dev/null +++ b/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt @@ -0,0 +1,235 @@ +package com.hyunjung.auth.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hyunjung.core.presentation.designsystem.CherrydanColors +import com.hyunjung.core.presentation.designsystem.CherrydanTheme +import com.hyunjung.core.presentation.designsystem.CherrydanTypography +import com.hyunjung.core.presentation.designsystem.ClockIcon +import com.hyunjung.core.presentation.designsystem.CloseIcon +import com.hyunjung.core.presentation.designsystem.SearchIcon +import com.hyunjung.search.presentation.component.SearchToolbar + +@Composable +fun SearchScreen(modifier: Modifier = Modifier) { + // todo : viewmodel 사용 해서 만들면 됩니다. +} + +@Composable +fun SearchContent( + searchQuery: String, + recentSearchQueries: List, + suggestions: List, + onSearchQueryChanged: (String) -> Unit, + onSearchTriggered: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(CherrydanColors.White) + ) { + SearchToolbar( + searchQuery = searchQuery, + onSearchQueryChanged = onSearchQueryChanged, + onSearchTriggered = onSearchTriggered, + onBackClick = onBackClick, + modifier = modifier + ) + if (searchQuery.isEmpty()) { + RecentSearch(recentSearchQueries = recentSearchQueries) + } else { + SuggestedSearchList( + searchQuery = searchQuery, + suggestions = suggestions, + onSuggestionClick = { query -> + onSearchQueryChanged(query) + onSearchTriggered(query) + }, + modifier = modifier + ) + } + } +} + +@Composable +private fun RecentSearch( + recentSearchQueries: List, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + .fillMaxSize(1f) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "최근 검색", + style = CherrydanTypography.Main5_B, + color = CherrydanColors.Black + ) + Text( + text = "전체 삭제", + style = CherrydanTypography.Main5_R, + color = CherrydanColors.Gray4 + ) + } + } + items(recentSearchQueries) { query -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ClockIcon, + contentDescription = null, + tint = CherrydanColors.Gray4, + ) + Spacer(Modifier.width(4.dp)) + Text( + text = query, + style = CherrydanTypography.Main5_R, + color = CherrydanColors.Black, + modifier = Modifier.weight(1f) + ) + Icon( + imageVector = CloseIcon, + contentDescription = null, + tint = CherrydanColors.Black, + ) + } + } + } +} + +@Composable +private fun SuggestedSearchList( + searchQuery: String, + suggestions: List, + onSuggestionClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 22.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(suggestions) { suggestion -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onSuggestionClick(suggestion) }), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = SearchIcon, + contentDescription = null, + tint = CherrydanColors.Gray4, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = highlightQuery( + text = suggestion, + query = searchQuery, + highlightColor = CherrydanColors.MainPink2, + ), + style = CherrydanTypography.Main5_R, + color = CherrydanColors.Black, + modifier = Modifier.weight(1f) + ) + } + } + } +} + + +@Preview(showBackground = true, name = "Empty Query") +@Composable +private fun SearchScreenPreview() { + val recentSearchQueries = listOf( + "강남 맛집", + "효소 체험", + "맛있는 피자 분당점", + ) + CherrydanTheme { + SearchContent( + searchQuery = "", + recentSearchQueries = recentSearchQueries, + suggestions = emptyList(), + onSearchQueryChanged = {}, + onSearchTriggered = {}, + onBackClick = {} + ) + } +} + +@Preview(showBackground = true, name = "With Query") +@Composable +private fun SearchScreenWithQueryPreview() { + val recentSearchQueries = emptyList() + val suggestions = listOf( + "강남 맛집", + "강남역 카페", + "강남 데이트 코스", + "강남신세계" + ) + + CherrydanTheme { + SearchContent( + searchQuery = "강남", + recentSearchQueries = recentSearchQueries, + suggestions = suggestions, + onSearchQueryChanged = {}, + onSearchTriggered = {}, + onBackClick = {} + ) + } +} + +@Composable +fun highlightQuery(text: String, query: String, highlightColor: Color): AnnotatedString { + if (query.isBlank()) return AnnotatedString(text) + + val startIndex = text.indexOf(query, ignoreCase = true) + if (startIndex == -1) return AnnotatedString(text) + + val endIndex = startIndex + query.length + return buildAnnotatedString { + append(text.substring(0, startIndex)) + withStyle(SpanStyle(color = highlightColor)) { + append(text.substring(startIndex, endIndex)) + } + append(text.substring(endIndex)) + } +} \ No newline at end of file diff --git a/search/presentation/src/main/java/com/hyunjung/search/presentation/component/CherrydanSearchTextField.kt b/search/presentation/src/main/java/com/hyunjung/search/presentation/component/CherrydanSearchTextField.kt new file mode 100644 index 0000000..b3dbb77 --- /dev/null +++ b/search/presentation/src/main/java/com/hyunjung/search/presentation/component/CherrydanSearchTextField.kt @@ -0,0 +1,143 @@ +package com.hyunjung.search.presentation.component + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.semantics.error +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.hyunjung.core.presentation.designsystem.CherrydanTypography +import com.hyunjung.search.presentation.component.CherrydanTextFieldDefaults.cursorColor +import com.hyunjung.search.presentation.component.CherrydanTextFieldDefaults.defaultErrorSemantics +import com.hyunjung.search.presentation.component.CherrydanTextFieldDefaults.textColor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CherrydanSearchTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = CherrydanTypography.Main5_R, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource? = null, + shape: Shape = TextFieldDefaults.shape, + colors: TextFieldColors = TextFieldDefaults.colors() +) { + @Suppress("NAME_SHADOWING") + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + val textColor = + textStyle.color.takeOrElse { + val focused = interactionSource.collectIsFocusedAsState().value + colors.textColor(enabled, isError, focused) + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + + CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { + BasicTextField( + value = value, + modifier = + modifier + .defaultErrorSemantics(isError) + .defaultMinSize( + minWidth = CherrydanTextFieldDefaults.MinWidth, + minHeight = CherrydanTextFieldDefaults.MinHeight + ), + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + textStyle = mergedTextStyle, + cursorBrush = SolidColor(colors.cursorColor(isError)), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + decorationBox = + @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = visualTransformation, + innerTextField = innerTextField, + placeholder = placeholder, + label = label, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + shape = shape, + singleLine = singleLine, + enabled = enabled, + isError = isError, + interactionSource = interactionSource, + colors = colors, + contentPadding = PaddingValues(8.dp) + ) + } + ) + } +} + +object CherrydanTextFieldDefaults { + + const val DEFAULT_ERROR_MESSAGE = "입력값이 올바르지 않습니다." + val MinWidth: Dp = 280.dp + val MinHeight: Dp = 40.dp + + fun Modifier.defaultErrorSemantics(isError: Boolean): Modifier = + if (isError) semantics { error(DEFAULT_ERROR_MESSAGE) } else this + + @Stable + internal fun TextFieldColors.textColor( + enabled: Boolean, + isError: Boolean, + focused: Boolean, + ): Color = + when { + !enabled -> disabledTextColor + isError -> errorTextColor + focused -> focusedTextColor + else -> unfocusedTextColor + } + + @Stable + internal fun TextFieldColors.cursorColor(isError: Boolean): Color = + if (isError) errorCursorColor else cursorColor +} \ No newline at end of file diff --git a/search/presentation/src/main/java/com/hyunjung/search/presentation/component/SearchToolBar.kt b/search/presentation/src/main/java/com/hyunjung/search/presentation/component/SearchToolBar.kt new file mode 100644 index 0000000..82b1efe --- /dev/null +++ b/search/presentation/src/main/java/com/hyunjung/search/presentation/component/SearchToolBar.kt @@ -0,0 +1,166 @@ +package com.hyunjung.search.presentation.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hyunjung.core.presentation.designsystem.BackIcon +import com.hyunjung.core.presentation.designsystem.CherrydanColors +import com.hyunjung.core.presentation.designsystem.CherrydanTheme +import com.hyunjung.core.presentation.designsystem.CherrydanTypography +import com.hyunjung.core.presentation.designsystem.SearchCancelIcon + +@Composable +fun SearchToolbar( + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + onSearchTriggered: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth(), + ) { + IconButton(onClick = { onBackClick() }) { + Icon( + imageVector = BackIcon, + contentDescription = null, + ) + } + SearchTextField( + onSearchQueryChanged = onSearchQueryChanged, + onSearchTriggered = onSearchTriggered, + searchQuery = searchQuery, + ) + Text( + text = "닫기", + style = CherrydanTypography.Main5_R, + color = CherrydanColors.Black, + modifier = Modifier + .padding(start = 12.dp, end = 16.dp) + .clickable(onClick = onBackClick) + ) + } +} + +@Composable +private fun RowScope.SearchTextField( + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + onSearchTriggered: (String) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val onSearchExplicitlyTriggered = { + keyboardController?.hide() + onSearchTriggered(searchQuery) + } + + CherrydanSearchTextField( + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + focusedContainerColor = CherrydanColors.Gray1, + unfocusedContainerColor = CherrydanColors.Gray1, + ), + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton( + onClick = { + onSearchQueryChanged("") + }, + ) { + Icon( + imageVector = SearchCancelIcon, + contentDescription = null, + tint = Color.Unspecified + ) + } + } + }, + placeholder = { + Text( + text = "검색어를 입력해 주세요.", + style = CherrydanTypography.Main5_R, + color = CherrydanColors.Gray4, + ) + }, + onValueChange = { + if ("\n" !in it) onSearchQueryChanged(it) + }, + modifier = Modifier + .weight(1f) + .height(40.dp) + .focusRequester(focusRequester) + .onKeyEvent { + if (it.key == Key.Enter) { + if (searchQuery.isBlank()) return@onKeyEvent false + onSearchExplicitlyTriggered() + true + } else { + false + } + } + .testTag("searchTextField"), + shape = RoundedCornerShape(4.dp), + value = searchQuery, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search, + ), + keyboardActions = KeyboardActions( + onSearch = { + if (searchQuery.isBlank()) return@KeyboardActions + onSearchExplicitlyTriggered() + }, + ), + maxLines = 1, + singleLine = true, + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +@Preview(showBackground = true) +fun SearchToolbarPreview() { + CherrydanTheme { + val searchQuery = remember { mutableStateOf("") } + + SearchToolbar( + searchQuery = searchQuery.value, + onSearchQueryChanged = { searchQuery.value = it }, + onSearchTriggered = {}, + onBackClick = {} + ) + } +} \ No newline at end of file From a972a96c72bfcbaf58f29ac4ffdcc25ddc31ff34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=98=84=EC=A0=95?= Date: Sun, 29 Jun 2025 06:41:32 +0900 Subject: [PATCH 4/4] fix: Modify package --- .../notification/presentation/NotificationScreen.kt | 6 +++--- .../presentation/component/NotificationToggleItem.kt | 2 +- .../java/com/hyunjung/search/presentation/SearchScreen.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/presentation/src/main/java/com/hyunjung/notification/presentation/NotificationScreen.kt b/notification/presentation/src/main/java/com/hyunjung/notification/presentation/NotificationScreen.kt index e549e22..3c95b13 100644 --- a/notification/presentation/src/main/java/com/hyunjung/notification/presentation/NotificationScreen.kt +++ b/notification/presentation/src/main/java/com/hyunjung/notification/presentation/NotificationScreen.kt @@ -36,10 +36,10 @@ 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.hyunjung.auth.presentation.AlertType +import com.hyunjung.notification.presentation.component.AlertType import com.hyunjung.auth.presentation.LocalCherrydanContentColor -import com.hyunjung.auth.presentation.NotificationActiveToggleItem -import com.hyunjung.auth.presentation.NotificationToggleItem +import com.hyunjung.notification.presentation.component.NotificationActiveToggleItem +import com.hyunjung.notification.presentation.component.NotificationToggleItem import com.hyunjung.core.presentation.designsystem.BackIcon import com.hyunjung.core.presentation.designsystem.CherrydanColors import com.hyunjung.core.presentation.designsystem.CherrydanTheme diff --git a/notification/presentation/src/main/java/com/hyunjung/notification/presentation/component/NotificationToggleItem.kt b/notification/presentation/src/main/java/com/hyunjung/notification/presentation/component/NotificationToggleItem.kt index 17818a7..95f1bf9 100644 --- a/notification/presentation/src/main/java/com/hyunjung/notification/presentation/component/NotificationToggleItem.kt +++ b/notification/presentation/src/main/java/com/hyunjung/notification/presentation/component/NotificationToggleItem.kt @@ -1,4 +1,4 @@ -package com.hyunjung.auth.presentation +package com.hyunjung.notification.presentation.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt b/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt index 1096189..7ad88a8 100644 --- a/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt +++ b/search/presentation/src/main/java/com/hyunjung/search/presentation/SearchScreen.kt @@ -1,4 +1,4 @@ -package com.hyunjung.auth.presentation +package com.hyunjung.search.presentation import androidx.compose.foundation.background import androidx.compose.foundation.clickable