From b86eadce045d09cc5a395296d742cdcd707c2f81 Mon Sep 17 00:00:00 2001
From: Sehwan Yun <39579912+l5x5l@users.noreply.github.com>
Date: Sat, 27 Jul 2024 16:39:13 +0900
Subject: [PATCH] =?UTF-8?q?[Feature]=20#21=20=EA=B2=80=EC=83=89=20?=
=?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20(#23)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [BASE] #21 검색 화면 모듈 생성
* [FEATURE] #21 필터 영역 제외한 검색 화면 UI 구현 및 PokitInput에서 아이콘 클릭 이벤트를 수신할 수 있도록 변경
* [FEATURE] #21 필터 영역 및 달력 제외한 filter bottomsheet ui 구현
* [FEATURE] #21 달력 ui 구현
* [FEATURE] #21 달력 ui 수정
* [FEATURE] #21 검색 화면에 달력 UI 연결 및 viewModel 구현
* [UI] #21 PokitInput의 singleline 속성 추가 및 KeyboardAction와 focusRequester를 인자 추가 (기본값 존재)
* [CHORE] #21 세부 사항 수정
- 아래 문제점 수정
: "모아보기", "안읽음" 필터 클릭시 포킷 선택으로 bottomSheet가 호출되던 문제
: bottomSheet에서 저장하기 클릭시 필터가 전부 취소되어 있을 때 필터 버튼만 표시되던 문제
- 아래 기능 구현
: 키보드 엔터 클릭시 검색 결과 화면으로 이동
: 검색어 제거 클릭시 검색창 활성화
: 정렬 기준 변경 로직 추가
* [CHORE] #21 ktlint 적용
---
.../ui/components/atom/input/PokitInput.kt | 24 +-
.../subcomponents/icon/PokitInputIcon.kt | 14 +-
.../pokit/core/ui/utils/ModifierUtils.kt | 11 +
feature/search/.gitignore | 1 +
feature/search/build.gradle.kts | 64 ++++
feature/search/consumer-rules.pro | 0
feature/search/proguard-rules.pro | 21 ++
.../pokit/search/ExampleInstrumentedTest.kt | 22 ++
feature/search/src/main/AndroidManifest.xml | 4 +
.../java/pokitmons/pokit/search/Preview.kt | 20 ++
.../pokitmons/pokit/search/SearchScreen.kt | 127 ++++++++
.../pokitmons/pokit/search/SearchViewModel.kt | 129 ++++++++
.../search/components/atom/CalendarCell.kt | 91 ++++++
.../search/components/calendar/Calendar.kt | 149 +++++++++
.../search/components/calendar/Preview.kt | 52 +++
.../pokit/search/components/filter/Filter.kt | 169 ++++++++++
.../pokit/search/components/filter/Preview.kt | 45 +++
.../filterbottomsheet/FilterBottomSheet.kt | 104 ++++++
.../FilterBottomSheetContent.kt | 298 ++++++++++++++++++
.../components/filterbottomsheet/Preview.kt | 23 ++
.../components/recentsearchword/Preview.kt | 27 ++
.../recentsearchword/RecentSearchWord.kt | 118 +++++++
.../searchitemlist/SearchItemList.kt | 79 +++++
.../search/components/toolbar/Toolbar.kt | 86 +++++
.../pokit/search/model/CalendarCell.kt | 8 +
.../pokit/search/model/CalendarPage.kt | 41 +++
.../java/pokitmons/pokit/search/model/Date.kt | 15 +
.../pokitmons/pokit/search/model/Filter.kt | 39 +++
.../java/pokitmons/pokit/search/model/Link.kt | 83 +++++
.../pokitmons/pokit/search/model/Pokit.kt | 15 +
.../pokit/search/model/SearchScreenState.kt | 15 +
.../pokit/search/utils/CalendarUtils.kt | 115 +++++++
feature/search/src/main/res/values/string.xml | 35 ++
.../pokitmons/pokit/search/ExampleUnitTest.kt | 16 +
34 files changed, 2053 insertions(+), 7 deletions(-)
create mode 100644 core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt
create mode 100644 feature/search/.gitignore
create mode 100644 feature/search/build.gradle.kts
create mode 100644 feature/search/consumer-rules.pro
create mode 100644 feature/search/proguard-rules.pro
create mode 100644 feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt
create mode 100644 feature/search/src/main/AndroidManifest.xml
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/Preview.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt
create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt
create mode 100644 feature/search/src/main/res/values/string.xml
create mode 100644 feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt
diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt
index cfe8f5e9..79e24e41 100644
--- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt
+++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -11,6 +12,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@@ -21,6 +24,7 @@ import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputState
import pokitmons.pokit.core.ui.components.atom.input.subcomponents.container.PokitInputContainer
import pokitmons.pokit.core.ui.components.atom.input.subcomponents.icon.PokitInputIcon
import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.core.ui.utils.conditional
@Composable
fun PokitInput(
@@ -29,10 +33,13 @@ fun PokitInput(
onChangeText: (String) -> Unit,
icon: PokitInputIcon?,
modifier: Modifier = Modifier,
+ onClickIcon: (() -> Unit)? = null,
shape: PokitInputShape = PokitInputShape.RECTANGLE,
readOnly: Boolean = false,
enable: Boolean = true,
isError: Boolean = false,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ focusRequester: FocusRequester? = null,
) {
var focused by remember { mutableStateOf(false) }
val state = remember(focused, isError, readOnly, enable) {
@@ -52,10 +59,15 @@ fun PokitInput(
onValueChange = onChangeText,
textStyle = textStyle,
enabled = (enable && !readOnly),
- maxLines = 1,
- modifier = Modifier.onFocusChanged { focusState ->
- focused = focusState.isFocused
- },
+ singleLine = true,
+ modifier = Modifier
+ .onFocusChanged { focusState ->
+ focused = focusState.isFocused
+ }
+ .conditional(focusRequester != null) {
+ focusRequester(focusRequester!!)
+ },
+ keyboardActions = keyboardActions,
decorationBox = { innerTextField ->
PokitInputContainer(
iconPosition = icon?.position,
@@ -68,7 +80,7 @@ fun PokitInput(
}
if (icon?.position == PokitInputIconPosition.LEFT) {
- PokitInputIcon(state = state, resourceId = icon.resourceId)
+ PokitInputIcon(state = state, resourceId = icon.resourceId, onClick = onClickIcon)
Box(modifier = Modifier.width(8.dp))
}
@@ -81,7 +93,7 @@ fun PokitInput(
}
if (icon?.position == PokitInputIconPosition.RIGHT) {
- PokitInputIcon(state = state, resourceId = icon.resourceId)
+ PokitInputIcon(state = state, resourceId = icon.resourceId, onClick = onClickIcon)
}
}
}
diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt
index b3b89e04..0dec8891 100644
--- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt
+++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt
@@ -1,8 +1,11 @@
package pokitmons.pokit.core.ui.components.atom.input.subcomponents.icon
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
@@ -14,6 +17,7 @@ import pokitmons.pokit.core.ui.theme.PokitTheme
internal fun PokitInputIcon(
state: PokitInputState,
resourceId: Int,
+ onClick: (() -> Unit)? = null,
) {
val iconColor = getColor(state = state)
@@ -21,7 +25,15 @@ internal fun PokitInputIcon(
painter = painterResource(id = resourceId),
contentDescription = null,
tint = iconColor,
- modifier = Modifier.size(24.dp)
+ modifier = Modifier.size(24.dp).then(
+ other = onClick?.let { method ->
+ Modifier.clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = method
+ )
+ } ?: Modifier
+ )
)
}
diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt
new file mode 100644
index 00000000..6220a9eb
--- /dev/null
+++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt
@@ -0,0 +1,11 @@
+package pokitmons.pokit.core.ui.utils
+
+import androidx.compose.ui.Modifier
+
+internal fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): Modifier {
+ return if (condition) {
+ then(modifier(Modifier))
+ } else {
+ this
+ }
+}
diff --git a/feature/search/.gitignore b/feature/search/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/search/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts
new file mode 100644
index 00000000..ef9a8247
--- /dev/null
+++ b/feature/search/build.gradle.kts
@@ -0,0 +1,64 @@
+plugins {
+ alias(libs.plugins.com.android.library)
+ alias(libs.plugins.org.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "pokitmons.pokit.search"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.orbit.compose)
+ implementation(libs.orbit.core)
+ implementation(libs.orbit.viewmodel)
+
+ implementation(project(":core:ui"))
+}
diff --git a/feature/search/consumer-rules.pro b/feature/search/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/search/proguard-rules.pro b/feature/search/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/search/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/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt b/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..b07df8db
--- /dev/null
+++ b/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package pokitmons.pokit.search
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+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
+ assertEquals("pokitmons.pokit.search.test", appContext.packageName)
+ }
+}
diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/search/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt
new file mode 100644
index 00000000..d549c85f
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt
@@ -0,0 +1,20 @@
+package pokitmons.pokit.search
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import pokitmons.pokit.core.ui.theme.PokitTheme
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview() {
+ PokitTheme {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ SearchScreen()
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt
new file mode 100644
index 00000000..545e0dec
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt
@@ -0,0 +1,127 @@
+package pokitmons.pokit.search
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.components.filter.FilterArea
+import pokitmons.pokit.search.components.filterbottomsheet.FilterBottomSheet
+import pokitmons.pokit.search.components.recentsearchword.RecentSearchWord
+import pokitmons.pokit.search.components.searchitemlist.SearchItemList
+import pokitmons.pokit.search.components.toolbar.Toolbar
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.search.model.FilterType
+import pokitmons.pokit.search.model.Link
+import pokitmons.pokit.search.model.SearchScreenState
+import pokitmons.pokit.search.model.SearchScreenStep
+
+@Composable
+fun SearchScreenContainer(
+ viewModel: SearchViewModel,
+ onBackPressed: () -> Unit,
+) {
+ val state by viewModel.state.collectAsState()
+ val searchWord by viewModel.searchWord.collectAsState()
+ val linkList by viewModel.linkList.collectAsState()
+
+ SearchScreen(
+ state = state,
+ currentSearchWord = searchWord,
+ linkList = linkList,
+ onClickBack = onBackPressed,
+ inputSearchWord = viewModel::inputSearchWord,
+ onClickSearch = viewModel::applyCurrentSearchWord,
+ onClickRecentSearchWord = viewModel::applySearchWord,
+ onClickUseRecentSearchWord = viewModel::toggleUseRecentSearchWord,
+ onClickRemoveAllRecentSearchWord = viewModel::removeAllRecentSearchWord,
+ onClickRemoveRecentSearchWord = viewModel::removeRecentSearchWord,
+ onClickFilterSelect = viewModel::showFilterBottomSheet,
+ onClickFilterItem = viewModel::showFilterBottomSheetWithType,
+ hideBottomSheet = viewModel::hideFilterBottomSheet,
+ onClickFilterSave = viewModel::setFilter,
+ toggleSortOrder = viewModel::toggleSortOrder
+ )
+}
+
+@Composable
+fun SearchScreen(
+ state: SearchScreenState = SearchScreenState(),
+ currentSearchWord: String = "",
+ linkList: List = emptyList(),
+ onClickBack: () -> Unit = {},
+ inputSearchWord: (String) -> Unit = {},
+ onClickSearch: () -> Unit = {},
+ onClickRecentSearchWord: (String) -> Unit = {},
+ onClickUseRecentSearchWord: () -> Unit = {},
+ onClickRemoveAllRecentSearchWord: () -> Unit = {},
+ onClickRemoveRecentSearchWord: (String) -> Unit = {},
+ onClickFilterSelect: () -> Unit = {},
+ onClickFilterItem: (FilterType) -> Unit = {},
+ hideBottomSheet: () -> Unit = {},
+ onClickFilterSave: (Filter) -> Unit = {},
+ toggleSortOrder: () -> Unit = {},
+) {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Toolbar(
+ onClickBack = onClickBack,
+ inputSearchWord = inputSearchWord,
+ currentSearchWord = currentSearchWord,
+ onClickSearch = onClickSearch,
+ onClickRemove = remember { { inputSearchWord("") } }
+ )
+
+ if (state.step == SearchScreenStep.INPUT) {
+ RecentSearchWord(
+ onClickRemoveAll = onClickRemoveAllRecentSearchWord,
+ onToggleAutoSave = onClickUseRecentSearchWord,
+ useAutoSave = state.useRecentSearchWord,
+ recentSearchWords = state.recentSearchWords,
+ onClickRemoveSearchWord = onClickRemoveRecentSearchWord,
+ onClickSearchWord = onClickRecentSearchWord
+ )
+ }
+
+ if (state.step == SearchScreenStep.RESULT) {
+ FilterArea(
+ filter = state.filter,
+ onClickFilter = onClickFilterSelect,
+ onClickBookmark = remember { { onClickFilterItem(FilterType.Collect) } },
+ onClickPokitName = remember { { onClickFilterItem(FilterType.Pokit) } },
+ onClickPeriod = remember { { onClickFilterItem(FilterType.Period) } }
+ )
+ }
+
+ HorizontalDivider(
+ thickness = 6.dp,
+ color = PokitTheme.colors.backgroundPrimary
+ )
+
+ if (state.step == SearchScreenStep.RESULT) {
+ SearchItemList(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onToggleSort = toggleSortOrder,
+ useRecentOrder = state.sortRecent,
+ links = linkList
+ )
+ }
+
+ FilterBottomSheet(
+ filter = state.filter ?: Filter(),
+ firstShowType = state.firstBottomSheetFilterType,
+ show = state.showFilterBottomSheet,
+ onDismissRequest = hideBottomSheet,
+ onSaveClilck = onClickFilterSave
+ )
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt
new file mode 100644
index 00000000..b8c0055a
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt
@@ -0,0 +1,129 @@
+package pokitmons.pokit.search
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.search.model.FilterType
+import pokitmons.pokit.search.model.Link
+import pokitmons.pokit.search.model.SearchScreenState
+import pokitmons.pokit.search.model.SearchScreenStep
+import pokitmons.pokit.search.model.sampleLinks
+
+class SearchViewModel : ViewModel() {
+ private val _searchWord = MutableStateFlow("")
+ val searchWord = _searchWord.asStateFlow()
+
+ private val _state = MutableStateFlow(SearchScreenState())
+ val state = _state.asStateFlow()
+
+ private var appliedSearchWord = ""
+
+ private val _linkList = MutableStateFlow>(emptyList())
+ val linkList = _linkList.asStateFlow()
+
+ init {
+ _linkList.update { sampleLinks }
+ }
+
+ fun inputSearchWord(newSearchWord: String) {
+ _searchWord.update { newSearchWord }
+ val currentState = state.value
+
+ if (newSearchWord.isEmpty() && currentState.step == SearchScreenStep.RESULT) {
+ _state.update { state ->
+ state.copy(step = SearchScreenStep.INPUT)
+ }
+ }
+ }
+
+ fun applyCurrentSearchWord() {
+ appliedSearchWord = searchWord.value
+
+ if (appliedSearchWord.isNotEmpty()) {
+ _state.update { state ->
+ state.copy(step = SearchScreenStep.RESULT)
+ }
+ }
+ }
+
+ fun applySearchWord(word: String) {
+ appliedSearchWord = word
+ _searchWord.update { word }
+
+ if (appliedSearchWord.isNotEmpty()) {
+ _state.update { state ->
+ state.copy(step = SearchScreenStep.RESULT)
+ }
+ }
+ }
+
+ fun toggleUseRecentSearchWord() {
+ _state.update { state ->
+ state.copy(useRecentSearchWord = !state.useRecentSearchWord)
+ }
+ }
+
+ fun removeRecentSearchWord(word: String) {
+ _state.update { state ->
+ state.copy(recentSearchWords = state.recentSearchWords.filter { name -> name != word })
+ }
+ }
+
+ fun removeAllRecentSearchWord() {
+ _state.update { state ->
+ state.copy(
+ recentSearchWords = emptyList()
+ )
+ }
+ }
+
+ fun showFilterBottomSheet() {
+ _state.update { state ->
+ state.copy(
+ showFilterBottomSheet = true
+ )
+ }
+ }
+
+ fun showFilterBottomSheetWithType(type: FilterType) {
+ _state.update { state ->
+ state.copy(
+ showFilterBottomSheet = true,
+ firstBottomSheetFilterType = type
+ )
+ }
+ }
+
+ fun hideFilterBottomSheet() {
+ _state.update { state ->
+ state.copy(
+ showFilterBottomSheet = false
+ )
+ }
+ }
+
+ fun setFilter(filter: Filter) {
+ _state.update { state ->
+ state.copy(
+ showFilterBottomSheet = false,
+ filter = if (filter == Filter.DefaultFilter) {
+ null
+ } else {
+ filter
+ }
+ )
+ }
+
+ // todo refresh 기능 구현
+ }
+
+ fun toggleSortOrder() {
+ _state.update { state ->
+ state.copy(sortRecent = !state.sortRecent)
+ }
+
+ // todo refresh 기능 구현
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt
new file mode 100644
index 00000000..2cb7dc3d
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt
@@ -0,0 +1,91 @@
+package pokitmons.pokit.search.components.atom
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+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.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.core.ui.theme.color.Orange100
+import pokitmons.pokit.search.model.Date
+
+@Composable
+internal fun CalendarCellView(
+ date: Date,
+ onClick: ((Date) -> Unit)? = null,
+ state: CalendarCellState = CalendarCellState.ACTIVE,
+) {
+ val backgroundColor = getBackgroundColor(state = state)
+ val textColor = getTextColor(state = state)
+
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .then(
+ other = onClick?.let { method ->
+ Modifier.clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = { method(date) }
+ )
+ } ?: Modifier
+ )
+ .background(
+ color = backgroundColor,
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ textAlign = TextAlign.Center,
+ text = date.day.toString(),
+ style = PokitTheme.typography.body1Medium.copy(color = textColor)
+ )
+ }
+}
+
+enum class CalendarCellState {
+ INACTIVE, ACTIVE, SELECTED, IN_RANGE
+}
+
+@Composable
+private fun getTextColor(state: CalendarCellState): Color {
+ return when (state) {
+ CalendarCellState.INACTIVE -> {
+ PokitTheme.colors.textTertiary
+ }
+ CalendarCellState.ACTIVE -> {
+ PokitTheme.colors.textSecondary
+ }
+ CalendarCellState.IN_RANGE -> {
+ PokitTheme.colors.inverseWh
+ }
+ CalendarCellState.SELECTED -> {
+ PokitTheme.colors.inverseWh
+ }
+ }
+}
+
+@Composable
+private fun getBackgroundColor(state: CalendarCellState): Color {
+ return when (state) {
+ CalendarCellState.IN_RANGE -> {
+ Orange100
+ }
+ CalendarCellState.SELECTED -> {
+ PokitTheme.colors.brand
+ }
+ else -> {
+ Color.Unspecified
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt
new file mode 100644
index 00000000..d2c92eb2
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt
@@ -0,0 +1,149 @@
+package pokitmons.pokit.search.components.calendar
+
+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.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.R
+import pokitmons.pokit.search.components.atom.CalendarCellState
+import pokitmons.pokit.search.components.atom.CalendarCellView
+import pokitmons.pokit.search.model.CalendarPage
+import pokitmons.pokit.search.model.Date
+import pokitmons.pokit.search.utils.getCells
+import pokitmons.pokit.search.utils.weekDayStringResources
+
+@Composable
+internal fun CalendarView(
+ calendarPage: CalendarPage,
+ modifier: Modifier = Modifier,
+ startDate: Date? = null,
+ endDate: Date? = null,
+ onClickCell: (Date) -> Unit = {},
+) {
+ var currentPage by remember { mutableStateOf(calendarPage) }
+ val calendarCells = remember(
+ currentPage,
+ startDate,
+ endDate
+ ) {
+ getCells(year = currentPage.year, month = currentPage.month, startDate = startDate, endDate = endDate)
+ }
+
+ Column(
+ modifier = modifier
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(id = R.string.format_calendar, currentPage.year, currentPage.month),
+ style = PokitTheme.typography.body1Bold.copy(color = PokitTheme.colors.textPrimary)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ IconButton(
+ modifier = Modifier.size(40.dp),
+ onClick = remember {
+ {
+ currentPage = currentPage.prevPage()
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_left),
+ contentDescription = "prev month"
+ )
+ }
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ IconButton(
+ modifier = Modifier.size(40.dp),
+ onClick = remember {
+ {
+ currentPage = currentPage.nextPage()
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_right),
+ contentDescription = "next month"
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(count = 7),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(weekDayStringResources) { stringRes ->
+ Box(
+ modifier = Modifier.size(40.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ textAlign = TextAlign.Center,
+ text = stringResource(id = stringRes),
+ style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary)
+ )
+ }
+ }
+
+ items(calendarCells) { cell ->
+ if (cell.date.year != currentPage.year || cell.date.month != currentPage.month) {
+ CalendarCellView(
+ date = cell.date,
+ onClick = null,
+ state = CalendarCellState.INACTIVE
+ )
+ } else if (cell.selected) {
+ CalendarCellView(
+ date = cell.date,
+ onClick = onClickCell,
+ state = CalendarCellState.SELECTED
+ )
+ } else if (cell.inSelectRange) {
+ CalendarCellView(
+ date = cell.date,
+ onClick = onClickCell,
+ state = CalendarCellState.IN_RANGE
+ )
+ } else {
+ CalendarCellView(
+ date = cell.date,
+ onClick = onClickCell,
+ state = CalendarCellState.ACTIVE
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt
new file mode 100644
index 00000000..89781404
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt
@@ -0,0 +1,52 @@
+package pokitmons.pokit.search.components.calendar
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.width
+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.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.model.CalendarPage
+import pokitmons.pokit.search.model.Date
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview() {
+ var startDate by remember { mutableStateOf(null) }
+ var endDate by remember { mutableStateOf(null) }
+
+ PokitTheme {
+ Column(
+ modifier = Modifier.width(300.dp).fillMaxHeight(),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ CalendarView(
+ calendarPage = CalendarPage(endDate),
+ startDate = startDate,
+ endDate = endDate,
+ onClickCell = remember {
+ { date ->
+ val currentStartDate = startDate
+ if (currentStartDate != null && endDate == null) {
+ if (date <= currentStartDate) {
+ startDate = date
+ } else {
+ endDate = date
+ }
+ } else {
+ startDate = date
+ endDate = null
+ }
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt
new file mode 100644
index 00000000..4f18074a
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt
@@ -0,0 +1,169 @@
+package pokitmons.pokit.search.components.filter
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.components.atom.button.PokitButton
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonShape
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonStyle
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonType
+import pokitmons.pokit.search.R
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+
+@Composable
+fun FilterArea(
+ filter: Filter? = null,
+ onClickFilter: () -> Unit = {},
+ onClickPeriod: () -> Unit = {},
+ onClickPokitName: () -> Unit = {},
+ onClickBookmark: () -> Unit = {},
+) {
+ val scrollState = rememberScrollState()
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 20.dp, bottom = 24.dp)
+ .horizontalScroll(scrollState),
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Spacer(modifier = Modifier.width(20.dp))
+
+ if (filter == null) {
+ PokitButton(
+ text = stringResource(id = R.string.filter),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_filter,
+ position = PokitButtonIconPosition.LEFT
+ ),
+ onClick = onClickFilter,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE,
+ type = PokitButtonType.SECONDARY
+ )
+
+ PokitButton(
+ text = stringResource(id = R.string.pokit_name),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickPokitName,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE,
+ type = PokitButtonType.SECONDARY
+ )
+
+ PokitButton(
+ text = stringResource(id = R.string.collect_show),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickBookmark,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE,
+ type = PokitButtonType.SECONDARY
+ )
+
+ PokitButton(
+ text = stringResource(id = R.string.period),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickPeriod,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE,
+ type = PokitButtonType.SECONDARY
+ )
+ } else {
+ PokitButton(
+ text = stringResource(id = R.string.filter),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_filter,
+ position = PokitButtonIconPosition.LEFT
+ ),
+ onClick = onClickFilter,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.FILLED
+ )
+
+ filter.selectedPokits.map { pokit ->
+ PokitButton(
+ text = pokit.title,
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickPokitName,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE
+ )
+ }
+
+ if (filter.bookmark) {
+ PokitButton(
+ text = stringResource(id = R.string.bookmark),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickBookmark,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE
+ )
+ }
+
+ if (filter.notRead) {
+ PokitButton(
+ text = stringResource(id = R.string.not_read),
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickBookmark,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE
+ )
+ }
+
+ filter.getDateString()?.let { dateString ->
+ PokitButton(
+ text = dateString,
+ icon = PokitButtonIcon(
+ resourceId = coreDrawable.icon_24_arrow_down,
+ position = PokitButtonIconPosition.RIGHT
+ ),
+ onClick = onClickPeriod,
+ size = PokitButtonSize.SMALL,
+ shape = PokitButtonShape.ROUND,
+ style = PokitButtonStyle.STROKE
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(20.dp))
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt
new file mode 100644
index 00000000..176015e0
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt
@@ -0,0 +1,45 @@
+package pokitmons.pokit.search.components.filter
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.model.Date
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.search.model.samplePokits
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview() {
+ PokitTheme {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ FilterArea()
+
+ FilterArea(
+ filter = Filter(
+ selectedPokits = samplePokits
+ )
+ )
+
+ FilterArea(
+ filter = Filter(
+ startDate = Date(year = 2024, month = 7, day = 20),
+ endDate = Date(year = 2024, month = 7, day = 20)
+ )
+ )
+
+ FilterArea(
+ filter = Filter(
+ bookmark = true
+ )
+ )
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt
new file mode 100644
index 00000000..648c08ae
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt
@@ -0,0 +1,104 @@
+package pokitmons.pokit.search.components.filterbottomsheet
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.rememberModalBottomSheetState
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.search.model.FilterType
+import pokitmons.pokit.search.model.Pokit
+import pokitmons.pokit.search.model.samplePokits
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+@Composable
+fun FilterBottomSheet(
+ filter: Filter = Filter(),
+ firstShowType: FilterType = FilterType.Pokit,
+ onSaveClilck: (Filter) -> Unit = {},
+ pokits: List = samplePokits,
+ show: Boolean = false,
+ onDismissRequest: () -> Unit = {},
+) {
+ val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ var visibility by remember { mutableStateOf(show) }
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(show) {
+ if (visibility && !show) {
+ scope.launch {
+ bottomSheetState.hide()
+ }.invokeOnCompletion {
+ onDismissRequest()
+ visibility = false
+ }
+ } else {
+ visibility = show
+ }
+ }
+
+ if (visibility) {
+ ModalBottomSheet(
+ onDismissRequest = remember {
+ {
+ onDismissRequest()
+ visibility = false
+ }
+ },
+ sheetState = bottomSheetState,
+ scrimColor = Color.Transparent,
+ shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
+ containerColor = PokitTheme.colors.backgroundBase,
+ dragHandle = {
+ Column {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box(
+ Modifier
+ .clip(RoundedCornerShape(4.dp))
+ .width(36.dp)
+ .height(4.dp)
+ .background(color = PokitTheme.colors.iconTertiary)
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+ }
+ ) {
+ FilterBottomSheetContent(
+ filter = filter,
+ firstShowType = firstShowType,
+ onSaveClilck = onSaveClilck,
+ pokits = pokits
+ )
+
+ Spacer(
+ Modifier.windowInsetsBottomHeight(
+ WindowInsets.navigationBarsIgnoringVisibility
+ )
+ )
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt
new file mode 100644
index 00000000..63584cd0
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt
@@ -0,0 +1,298 @@
+package pokitmons.pokit.search.components.filterbottomsheet
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.HorizontalDivider
+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.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.components.atom.button.PokitButton
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize
+import pokitmons.pokit.core.ui.components.atom.checkbox.PokitCheckbox
+import pokitmons.pokit.core.ui.components.atom.chip.PokitChip
+import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipIconPosiion
+import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipState
+import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList
+import pokitmons.pokit.core.ui.components.block.tap.PokitTap
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.R
+import pokitmons.pokit.search.components.calendar.CalendarView
+import pokitmons.pokit.search.model.CalendarPage
+import pokitmons.pokit.search.model.Filter
+import pokitmons.pokit.search.model.FilterType
+import pokitmons.pokit.search.model.Pokit
+import pokitmons.pokit.search.model.samplePokits
+import pokitmons.pokit.core.ui.R.string as coreString
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun FilterBottomSheetContent(
+ filter: Filter = Filter(),
+ firstShowType: FilterType = FilterType.Pokit,
+ onSaveClilck: (Filter) -> Unit = {},
+ pokits: List = samplePokits,
+) {
+ var currentFilter by remember { mutableStateOf(filter) }
+ var currentShowType by remember { mutableStateOf(firstShowType) }
+ val scrollState = rememberScrollState()
+ val pagerState = rememberPagerState(
+ initialPage = firstShowType.index
+ ) {
+ FilterType.entries.size
+ }
+
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ FilterType.entries.map { entry ->
+ PokitTap(
+ modifier = Modifier.weight(1f),
+ text = stringResource(id = entry.stringResourceId),
+ data = entry,
+ onClick = remember {
+ { type ->
+ currentShowType = type
+ }
+ },
+ selectedItem = currentShowType
+ )
+ }
+ }
+
+ HorizontalPager(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(390.dp),
+ state = pagerState,
+ userScrollEnabled = false
+ ) {
+ when (currentShowType) {
+ FilterType.Pokit -> {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(pokits) { pokit ->
+ PokitList(
+ item = pokit,
+ title = pokit.title,
+ sub = stringResource(id = coreString.pokit_count_format, pokit.count),
+ onClickItem = { item ->
+ currentFilter = currentFilter.addPokit(item)
+ }
+ )
+ }
+ }
+ }
+
+ FilterType.Collect -> {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ currentFilter = currentFilter.copy(bookmark = !currentFilter.bookmark)
+ }
+ .padding(12.dp)
+ ) {
+ PokitCheckbox(
+ checked = currentFilter.bookmark,
+ onClick = { currentFilter = currentFilter.copy(bookmark = !currentFilter.bookmark) }
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = stringResource(id = R.string.bookmark),
+ style = PokitTheme.typography.body1Medium.copy(color = PokitTheme.colors.textPrimary)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ currentFilter = currentFilter.copy(notRead = !currentFilter.notRead)
+ }
+ .padding(12.dp)
+ ) {
+ PokitCheckbox(
+ checked = currentFilter.notRead,
+ onClick = {
+ currentFilter = currentFilter.copy(notRead = !currentFilter.notRead)
+ }
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = stringResource(id = R.string.not_read),
+ style = PokitTheme.typography.body1Medium.copy(color = PokitTheme.colors.textPrimary)
+ )
+ }
+ }
+ }
+
+ FilterType.Period -> {
+ CalendarView(
+ calendarPage = CalendarPage(filter.endDate),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp),
+ startDate = currentFilter.startDate,
+ endDate = currentFilter.endDate,
+ onClickCell = remember {
+ { date ->
+ val startDate = currentFilter.startDate
+ val endDate = currentFilter.endDate
+ currentFilter = if (startDate != null && endDate == null) {
+ if (date <= startDate) {
+ currentFilter.copy(startDate = date)
+ } else {
+ currentFilter.copy(endDate = date)
+ }
+ } else {
+ currentFilter.copy(
+ startDate = date,
+ endDate = null
+ )
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = PokitTheme.colors.borderTertiary
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
+ .horizontalScroll(scrollState),
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Spacer(modifier = Modifier.width(20.dp).height(32.dp))
+
+ currentFilter.selectedPokits.map { pokit ->
+ PokitChip(
+ data = pokit,
+ text = pokit.title,
+ removeIconPosition = PokitChipIconPosiion.RIGHT,
+ state = PokitChipState.STROKE,
+ onClickRemove = remember {
+ { item ->
+ currentFilter = currentFilter.copy(
+ selectedPokits = currentFilter.selectedPokits.filter { it.id != item.id }
+ )
+ }
+ },
+ onClickItem = null
+ )
+ }
+
+ if (currentFilter.bookmark) {
+ PokitChip(
+ data = stringResource(id = R.string.bookmark),
+ text = stringResource(id = R.string.bookmark),
+ removeIconPosition = PokitChipIconPosiion.RIGHT,
+ state = PokitChipState.STROKE,
+ onClickRemove = remember {
+ {
+ currentFilter = currentFilter.copy(bookmark = false)
+ }
+ },
+ onClickItem = null
+ )
+ }
+
+ if (currentFilter.notRead) {
+ PokitChip(
+ data = stringResource(id = R.string.not_read),
+ text = stringResource(id = R.string.not_read),
+ removeIconPosition = PokitChipIconPosiion.RIGHT,
+ state = PokitChipState.STROKE,
+ onClickRemove = remember {
+ {
+ currentFilter = currentFilter.copy(notRead = false)
+ }
+ },
+ onClickItem = null
+ )
+ }
+
+ currentFilter.getDateString()?.let { dateString ->
+ PokitChip(
+ data = dateString,
+ text = dateString,
+ removeIconPosition = PokitChipIconPosiion.RIGHT,
+ state = PokitChipState.STROKE,
+ onClickRemove = remember {
+ {
+ currentFilter = currentFilter.copy(startDate = null, endDate = null)
+ }
+ },
+ onClickItem = null
+ )
+ }
+
+ Spacer(modifier = Modifier.width(20.dp))
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ PokitButton(
+ modifier = Modifier.fillMaxWidth(),
+ size = PokitButtonSize.LARGE,
+ text = stringResource(id = R.string.search),
+ icon = null,
+ onClick = remember {
+ {
+ onSaveClilck(currentFilter)
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt
new file mode 100644
index 00000000..16a5bcef
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt
@@ -0,0 +1,23 @@
+package pokitmons.pokit.search.components.filterbottomsheet
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview() {
+ PokitTheme {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ FilterBottomSheetContent()
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt
new file mode 100644
index 00000000..002c3898
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt
@@ -0,0 +1,27 @@
+package pokitmons.pokit.search.components.recentsearchword
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview() {
+ PokitTheme {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ RecentSearchWord(useAutoSave = false)
+
+ RecentSearchWord(useAutoSave = true)
+
+ RecentSearchWord(useAutoSave = true, recentSearchWords = listOf("Android", "IOS", "Server", "Design", "PM"))
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt
new file mode 100644
index 00000000..b15b4939
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt
@@ -0,0 +1,118 @@
+package pokitmons.pokit.search.components.recentsearchword
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.components.atom.chip.PokitChip
+import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipIconPosiion
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.R.string as SearchString
+
+@Composable
+internal fun RecentSearchWord(
+ onClickRemoveAll: () -> Unit = {},
+ onToggleAutoSave: () -> Unit = {},
+ useAutoSave: Boolean = true,
+ recentSearchWords: List = emptyList(),
+ onClickRemoveSearchWord: (String) -> Unit = {},
+ onClickSearchWord: (String) -> Unit = {},
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 28.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(id = SearchString.recent_search_word),
+ style = PokitTheme.typography.body2Bold.copy(color = PokitTheme.colors.textPrimary)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ modifier = Modifier
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onClickRemoveAll
+ )
+ .padding(4.dp),
+ text = stringResource(id = SearchString.remove_all),
+ style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary)
+ )
+
+ Text(
+ text = "|",
+ style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary)
+ )
+
+ Text(
+ modifier = Modifier
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onToggleAutoSave
+ )
+ .padding(4.dp),
+ text = stringResource(
+ id = if (useAutoSave) SearchString.off_use_auto_save else SearchString.on_auto_save
+ ),
+ style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ if (!useAutoSave) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 7.dp),
+ textAlign = TextAlign.Center,
+ text = stringResource(id = SearchString.describe_auto_save_is_off),
+ style = PokitTheme.typography.body3Regular.copy(color = PokitTheme.colors.textTertiary)
+ )
+ } else if (recentSearchWords.isEmpty()) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 7.dp),
+ textAlign = TextAlign.Center,
+ text = stringResource(id = SearchString.describe_recent_search_word_is_empty),
+ style = PokitTheme.typography.body3Regular.copy(color = PokitTheme.colors.textTertiary)
+ )
+ } else {
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ items(recentSearchWords) { searchWord ->
+ PokitChip(
+ data = searchWord,
+ text = searchWord,
+ removeIconPosition = PokitChipIconPosiion.RIGHT,
+ onClickRemove = onClickRemoveSearchWord,
+ onClickItem = onClickSearchWord
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt
new file mode 100644
index 00000000..c462b3f3
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt
@@ -0,0 +1,79 @@
+package pokitmons.pokit.search.components.searchitemlist
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.search.model.Link
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+import pokitmons.pokit.search.R.string as SearchString
+
+@Composable
+internal fun SearchItemList(
+ modifier: Modifier = Modifier,
+ onToggleSort: () -> Unit = {},
+ useRecentOrder: Boolean = true,
+ links: List = emptyList(),
+ onClickLinkKebab: (Link) -> Unit = {},
+ onClickLink: (Link) -> Unit = {},
+) {
+ Column(
+ modifier = modifier
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(start = 12.dp, top = 16.dp)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onToggleSort
+ )
+ .padding(all = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ modifier = Modifier.size(18.dp),
+ painter = painterResource(id = coreDrawable.icon_24_align),
+ contentDescription = "change sort order",
+ colorFilter = ColorFilter.tint(color = PokitTheme.colors.iconPrimary)
+ )
+
+ Text(
+ text = stringResource(id = if (useRecentOrder) SearchString.sort_order_recent else SearchString.sort_order_older),
+ style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textSecondary)
+ )
+ }
+
+ LazyColumn {
+ items(links) { link ->
+ LinkCard(
+ item = link,
+ title = link.title,
+ sub = "${link.dateString} · ${link.domainUrl}",
+ painter = painterResource(id = coreDrawable.icon_24_google),
+ notRead = link.isRead,
+ badgeText = stringResource(id = link.linkType.textResourceId),
+ onClickKebab = onClickLinkKebab,
+ onClickItem = onClickLink,
+ modifier = Modifier.padding(20.dp)
+ )
+ }
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt
new file mode 100644
index 00000000..40dc69b0
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt
@@ -0,0 +1,86 @@
+package pokitmons.pokit.search.components.toolbar
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+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.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.components.atom.input.PokitInput
+import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputIcon
+import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputIconPosition
+import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputShape
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+import pokitmons.pokit.search.R.string as SearchString
+
+@Composable
+internal fun Toolbar(
+ onClickBack: () -> Unit = {},
+ inputSearchWord: (String) -> Unit = {},
+ currentSearchWord: String = "",
+ onClickSearch: () -> Unit = {},
+ onClickRemove: () -> Unit = {},
+) {
+ val focusManager = LocalFocusManager.current
+ val focusRequester = remember { FocusRequester() }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ modifier = Modifier.size(40.dp),
+ onClick = onClickBack
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(id = coreDrawable.icon_24_arrow_left),
+ contentDescription = "back button"
+ )
+ }
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ PokitInput(
+ text = currentSearchWord,
+ hintText = stringResource(id = SearchString.placeholder_input_search_word),
+ onChangeText = inputSearchWord,
+ shape = PokitInputShape.ROUND,
+ icon = if (currentSearchWord.isNotEmpty()) {
+ PokitInputIcon(
+ position = PokitInputIconPosition.RIGHT,
+ resourceId = coreDrawable.icon_24_x
+ )
+ } else {
+ null
+ },
+ onClickIcon = remember {
+ {
+ onClickRemove()
+ focusRequester.requestFocus()
+ }
+ },
+ keyboardActions = remember {
+ KeyboardActions {
+ onClickSearch()
+ focusManager.clearFocus()
+ }
+ },
+ focusRequester = focusRequester
+ )
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt
new file mode 100644
index 00000000..a19b2db3
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt
@@ -0,0 +1,8 @@
+package pokitmons.pokit.search.model
+
+data class CalendarCell(
+ val date: Date = Date(),
+ val selected: Boolean = false,
+ val inSelectRange: Boolean = false,
+ val currentMonth: Boolean = true,
+)
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt
new file mode 100644
index 00000000..92a20ebb
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt
@@ -0,0 +1,41 @@
+package pokitmons.pokit.search.model
+
+import java.util.Calendar
+
+data class CalendarPage(
+ val year: Int,
+ val month: Int,
+) {
+ constructor(date: Date?) : this(
+ year = date?.year ?: Calendar.getInstance().get(Calendar.YEAR),
+ month = date?.month ?: (Calendar.getInstance().get(Calendar.MONTH) + 1)
+ )
+
+ fun prevPage(): CalendarPage {
+ return if (month == 1) {
+ CalendarPage(
+ year = year - 1,
+ month = 12
+ )
+ } else {
+ CalendarPage(
+ year = year,
+ month = month - 1
+ )
+ }
+ }
+
+ fun nextPage(): CalendarPage {
+ return if (month == 12) {
+ CalendarPage(
+ year = year + 1,
+ month = 1
+ )
+ } else {
+ CalendarPage(
+ year = year,
+ month = month + 1
+ )
+ }
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt
new file mode 100644
index 00000000..aa65a803
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt
@@ -0,0 +1,15 @@
+package pokitmons.pokit.search.model
+
+data class Date(
+ val year: Int = 2024,
+ val month: Int = 1,
+ val day: Int = 1,
+) : Comparable {
+ override fun compareTo(other: Date): Int {
+ return compareValuesBy(this, other, { it.year }, { it.month }, { it.day })
+ }
+
+ override fun toString(): String {
+ return "${year % 1000}.$month.$day"
+ }
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt
new file mode 100644
index 00000000..aad44146
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt
@@ -0,0 +1,39 @@
+package pokitmons.pokit.search.model
+
+import androidx.compose.runtime.Immutable
+import pokitmons.pokit.search.R
+
+@Immutable
+data class Filter(
+ val selectedPokits: List = emptyList(),
+ val bookmark: Boolean = false,
+ val notRead: Boolean = false,
+ val startDate: Date? = null,
+ val endDate: Date? = null,
+) {
+ fun addPokit(pokit: Pokit): Filter {
+ return if (selectedPokits.contains(pokit)) {
+ this
+ } else {
+ this.copy(selectedPokits = listOf(pokit) + selectedPokits)
+ }
+ }
+
+ fun getDateString(): String? {
+ return if (startDate != null && endDate != null) {
+ "$startDate~$endDate"
+ } else if (startDate != null) {
+ "$startDate"
+ } else {
+ null
+ }
+ }
+
+ companion object {
+ val DefaultFilter = Filter()
+ }
+}
+
+enum class FilterType(val stringResourceId: Int, val index: Int) {
+ Pokit(R.string.pokit, 0), Collect(R.string.collect_show, 1), Period(R.string.period, 2)
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt
new file mode 100644
index 00000000..15121d73
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt
@@ -0,0 +1,83 @@
+package pokitmons.pokit.search.model
+
+import pokitmons.pokit.search.R
+
+data class Link(
+ val id: String,
+ val title: String,
+ val dateString: String,
+ val domainUrl: String,
+ val isRead: Boolean,
+ val linkType: LinkType,
+ val url: String,
+ val memo: String,
+ val bookmark: Boolean,
+ val imageUrl: String? = null,
+)
+
+enum class LinkType(val textResourceId: Int) {
+ TEXT(R.string.text),
+}
+
+internal val sampleLinks = listOf(
+ Link(
+ id = "1",
+ title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법",
+ imageUrl = null,
+ dateString = "2024.04.12",
+ domainUrl = "youtu.be",
+ isRead = true,
+ linkType = LinkType.TEXT,
+ url = "",
+ memo = "",
+ bookmark = true
+ ),
+ Link(
+ id = "2",
+ title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법",
+ imageUrl = null,
+ dateString = "2024.05.12",
+ domainUrl = "youtu.be",
+ isRead = false,
+ linkType = LinkType.TEXT,
+ url = "",
+ memo = "",
+ bookmark = true
+ ),
+ Link(
+ id = "3",
+ title = "포킷포킷",
+ imageUrl = null,
+ dateString = "2024.04.12",
+ domainUrl = "pokitmons.pokit",
+ isRead = true,
+ linkType = LinkType.TEXT,
+ url = "",
+ memo = "",
+ bookmark = true
+ ),
+ Link(
+ id = "4",
+ title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법",
+ imageUrl = null,
+ dateString = "2024.06.12",
+ domainUrl = "youtu.be",
+ isRead = true,
+ linkType = LinkType.TEXT,
+ url = "",
+ memo = "",
+ bookmark = true
+ ),
+ Link(
+ id = "5",
+ title = "마지막 링크입니다.",
+ imageUrl = null,
+ dateString = "2024.07.14",
+ domainUrl = "youtu.be",
+ isRead = false,
+ linkType = LinkType.TEXT,
+ url = "",
+ memo = "",
+ bookmark = true
+ )
+)
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt
new file mode 100644
index 00000000..bcc29728
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt
@@ -0,0 +1,15 @@
+package pokitmons.pokit.search.model
+
+data class Pokit(
+ val title: String = "",
+ val id: String = "",
+ val count: Int = 0,
+)
+
+internal val samplePokits = listOf(
+ Pokit(title = "안드로이드", id = "1", count = 2),
+ Pokit(title = "IOS", id = "2", count = 2),
+ Pokit(title = "디자인", id = "3", count = 2),
+ Pokit(title = "PM", id = "4", count = 1),
+ Pokit(title = "서버", id = "5", count = 2)
+)
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt
new file mode 100644
index 00000000..034bf214
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt
@@ -0,0 +1,15 @@
+package pokitmons.pokit.search.model
+
+data class SearchScreenState(
+ val step: SearchScreenStep = SearchScreenStep.INPUT,
+ val filter: Filter? = null,
+ val recentSearchWords: List = emptyList(),
+ val useRecentSearchWord: Boolean = false,
+ val showFilterBottomSheet: Boolean = false,
+ val firstBottomSheetFilterType: FilterType = FilterType.Pokit,
+ val sortRecent: Boolean = true,
+)
+
+enum class SearchScreenStep {
+ INPUT, RESULT
+}
diff --git a/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt b/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt
new file mode 100644
index 00000000..0e0c3297
--- /dev/null
+++ b/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt
@@ -0,0 +1,115 @@
+package pokitmons.pokit.search.utils
+
+import pokitmons.pokit.search.R
+import pokitmons.pokit.search.model.CalendarCell
+import pokitmons.pokit.search.model.Date
+import java.util.Calendar
+
+fun getCells(
+ year: Int,
+ month: Int,
+ startDate: Date?,
+ endDate: Date?,
+): List {
+ val cells = getDefaultCalendarCells(year = year, month = month)
+ if (startDate == null && endDate == null) {
+ return cells
+ }
+
+ val changedCells = cells.map { cell ->
+ if (cell.date == startDate || cell.date == endDate) {
+ cell.copy(selected = true)
+ } else if (startDate != null && endDate != null && cell.date > startDate && cell.date < endDate) {
+ cell.copy(inSelectRange = true)
+ } else {
+ cell
+ }
+ }
+
+ return changedCells
+}
+
+private fun getDayAmountOfMonth(year: Int, month: Int): Int {
+ return when (month) {
+ 2 -> {
+ if (year % 4 != 0 || (year % 100 == 0 && year % 400 != 0)) {
+ 28
+ } else {
+ 29
+ }
+ }
+
+ in listOf(1, 3, 5, 7, 8, 10, 12) -> {
+ 31
+ }
+
+ else -> {
+ 30
+ }
+ }
+}
+
+/**
+ * 달력 상에서 이전달에 해당하는 요일의 리스트를 리턴합니다.
+ */
+private fun lastDaysOfPrevMonth(year: Int, month: Int): List {
+ val calendar = Calendar.getInstance()
+ val prevMonthIndex = month - 1
+
+ calendar.set(Calendar.YEAR, year)
+ calendar.set(Calendar.MONTH, prevMonthIndex)
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+
+ val prevMonth = if (prevMonthIndex == 0) {
+ 12
+ } else {
+ prevMonthIndex
+ }
+
+ val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
+ val amountOfDayOfPrevMonth = getDayAmountOfMonth(year, prevMonth)
+
+ return List(startDayOfWeek - 1) { i -> amountOfDayOfPrevMonth - (startDayOfWeek - 2 - i) }
+}
+
+/**
+ * 달력 상에서 다음달에 해당하는 요일의 리스트를 리턴합니다.
+ */
+private fun firstDaysOfNextMonth(year: Int, month: Int): List {
+ val calendar = Calendar.getInstance()
+
+ calendar.set(Calendar.YEAR, year)
+ calendar.set(Calendar.MONTH, month)
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+
+ val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
+
+ return List(8 - startDayOfWeek) { it + 1 }
+}
+
+private fun getDefaultCalendarCells(year: Int, month: Int): List {
+ val lastMonthCells = lastDaysOfPrevMonth(year, month).map { day ->
+ if (month == 1) {
+ CalendarCell(date = Date(year = year - 1, month = 12, day = day), currentMonth = false)
+ } else {
+ CalendarCell(date = Date(year = year, month = month - 1, day = day), currentMonth = false)
+ }
+ }
+
+ val datOfMonth = getDayAmountOfMonth(year, month)
+ val currentMonthCells = (1..datOfMonth).map { day ->
+ CalendarCell(date = Date(year = year, month = month, day = day), currentMonth = true)
+ }
+
+ val nextMonthCells = firstDaysOfNextMonth(year, month).map { day ->
+ if (month == 12) {
+ CalendarCell(date = Date(year = year + 1, month = 1, day = day), currentMonth = false)
+ } else {
+ CalendarCell(date = Date(year = year, month = month + 1, day = day), currentMonth = false)
+ }
+ }
+
+ return lastMonthCells + currentMonthCells + nextMonthCells
+}
+
+val weekDayStringResources = listOf(R.string.SUN, R.string.MON, R.string.TUE, R.string.WED, R.string.THU, R.string.FRI, R.string.SAT)
diff --git a/feature/search/src/main/res/values/string.xml b/feature/search/src/main/res/values/string.xml
new file mode 100644
index 00000000..baceab83
--- /dev/null
+++ b/feature/search/src/main/res/values/string.xml
@@ -0,0 +1,35 @@
+
+
+ 내용을 입력해주세요.
+
+ 최근 검색어
+ 전체 삭제
+ 자동 저장 켜기
+ 자동 저장 끄기
+ 최근 검색 저장 기능이 꺼져있습니다.
+ 검색 내역이 없습니다.
+
+ 최신순
+ 오래된순
+
+ 텍스트
+
+ 필터
+ 포킷명
+ 포킷
+ 기간
+ 모아보기
+ 즐겨찾기
+ 안읽음
+ 검색하기
+
+ 월
+ 화
+ 수
+ 목
+ 금
+ 토
+ 일
+
+ %d년 %d월
+
\ No newline at end of file
diff --git a/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt b/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt
new file mode 100644
index 00000000..44a19e7b
--- /dev/null
+++ b/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package pokitmons.pokit.search
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * 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)
+ }
+}