diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 00000000..91f95584
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 1e536c6c..62464c5c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt
new file mode 100644
index 00000000..0c745866
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt
@@ -0,0 +1,141 @@
+package com.texthip.thip.ui.common.bottomsheet
+
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import kotlinx.coroutines.launch
+
+@Composable
+fun CustomBottomSheet(
+ onDismiss: () -> Unit,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val animatableOffset = remember { Animatable(300f) }
+ var offsetY by remember { mutableFloatStateOf(0f) }
+ var isDismissing by remember { mutableStateOf(false) }
+
+ // 등장 애니메이션
+ LaunchedEffect(Unit) {
+ animatableOffset.animateTo(
+ targetValue = 0f,
+ animationSpec = tween(durationMillis = 300)
+ )
+ }
+
+ // 바깥 클릭 감지
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ if (!isDismissing) {
+ isDismissing = true
+ scope.launch {
+ animatableOffset.animateTo(300f, tween(300))
+ onDismiss()
+ }
+ }
+ }
+ .zIndex(1f)
+ )
+
+ // BottomSheet 본체
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .zIndex(2f),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .offset(y = (offsetY + animatableOffset.value).dp)
+ .background(
+ color = colors.DarkGrey,
+ shape = RoundedCornerShape(topEnd = 12.dp, topStart = 12.dp)
+ )
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onVerticalDrag = { _, dragAmount ->
+ if (dragAmount > 0) {
+ offsetY += dragAmount / 2
+ }
+ },
+ onDragEnd = {
+ if (offsetY > 100f && !isDismissing) {
+ isDismissing = true
+ scope.launch {
+ animatableOffset.animateTo(300f, tween(300))
+ onDismiss()
+ }
+ } else {
+ offsetY = 0f
+ }
+ }
+ )
+ }
+ .clickable(enabled = true) {}
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ content()
+ }
+ }
+ }
+}
+
+
+@Preview()
+@Composable
+fun PreviewCustomBottomSheet() {
+ var showSheet by remember { mutableStateOf(true) }
+
+ ThipTheme {
+ Box(Modifier.fillMaxSize()) {
+ Text(
+ text = "Main Content Area",
+ color = Color.White,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+
+ if (showSheet) {
+ CustomBottomSheet(
+ onDismiss = { showSheet = false }
+ ) {
+ Text(
+ "바텀 시트 예시",
+ color = Color.White,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ Button(
+ onClick = { showSheet = false },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("닫기", color = Color.Black)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt
similarity index 68%
rename from app/src/main/java/com/texthip/thip/ui/group/myroom/component/GenreChipRow.kt
rename to app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt
index ae3fa69a..1a69c9f5 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GenreChipRow.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt
@@ -1,22 +1,21 @@
-package com.texthip.thip.ui.group.myroom.component
+package com.texthip.thip.ui.common.buttons
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
-import com.texthip.thip.ui.common.buttons.OptionChipButton
+import androidx.compose.ui.unit.dp
@Composable
fun GenreChipRow(
+ modifier: Modifier = Modifier.width(4.dp),
genres: List,
selectedIndex: Int,
onSelect: (Int) -> Unit
) {
Row(
- Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
) {
genres.forEachIndexed { idx, genre ->
OptionChipButton(
@@ -25,6 +24,9 @@ fun GenreChipRow(
isSelected = selectedIndex == idx,
onClick = { onSelect(idx) }
)
+ if (idx < genres.size - 1) {
+ Spacer(modifier = modifier)
+ }
}
}
}
@@ -37,4 +39,4 @@ fun PreviewGenreChipRow() {
selectedIndex = 0,
onSelect = {}
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt
index 1bc7fa03..e9cdb408 100644
--- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt
@@ -28,7 +28,6 @@ import com.texthip.thip.ui.theme.ThipTheme.typography
@Composable
fun CardBookSearch(
modifier: Modifier = Modifier,
- number: Int,
title: String,
imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스
onClick: () -> Unit = {}
@@ -36,18 +35,9 @@ fun CardBookSearch(
Row(
modifier = modifier
.fillMaxWidth()
- .clickable { onClick() }
- .padding(vertical = 8.dp),
+ .clickable { onClick() },
verticalAlignment = Alignment.CenterVertically
) {
- // 넘버
- Text(
- text = "$number.",
- style = typography.menu_m500_s16_h24,
- color = colors.White,
- modifier = Modifier.padding(end = 12.dp)
- )
-
// 이미지
Box(
modifier = Modifier
@@ -84,7 +74,6 @@ fun CardBookSearchPreview() {
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CardBookSearch(
- number = 1,
title = "단 한번의 삶"
)
}
diff --git a/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt
new file mode 100644
index 00000000..f841086e
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt
@@ -0,0 +1,105 @@
+package com.texthip.thip.ui.common.forms
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.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.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.texthip.thip.R
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+@Composable
+fun SearchBookTextField(
+ modifier: Modifier = Modifier,
+ hint: String,
+ onSearch: (String) -> Unit = {}
+) {
+ var text by rememberSaveable { mutableStateOf("") }
+ val myStyle = typography.menu_r400_s14_h24.copy(lineHeight = 14.sp)
+
+ Box(
+ modifier = modifier.height(48.dp)
+ ) {
+ OutlinedTextField(
+ value = text,
+ onValueChange = {
+ text = it
+ },
+ placeholder = {
+ Text(
+ text = hint,
+ color = colors.Grey02,
+ style = myStyle
+ )
+ },
+ textStyle = myStyle,
+ modifier = Modifier.fillMaxSize(),
+ shape = RoundedCornerShape(12.dp),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = colors.White,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedContainerColor = colors.DarkGrey02,
+ unfocusedContainerColor = colors.DarkGrey02,
+ cursorColor = colors.NeonGreen
+ ),
+ trailingIcon = {
+ Row(
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_x_circle_grey),
+ contentDescription = "Clear text",
+ modifier = Modifier
+ .clickable { text = "" },
+ tint = Color.Unspecified
+ )
+
+ Spacer(Modifier.width(20.dp))
+ Icon(
+ painter = painterResource(id = R.drawable.ic_search),
+ contentDescription = "Search",
+ modifier = Modifier
+ .clickable { onSearch(text) },
+ tint = colors.White
+ )
+ Spacer(Modifier.width(8.dp))
+ }
+ },
+ singleLine = true
+ )
+ }
+}
+
+@Preview()
+@Composable
+private fun SearchBookTextFieldPreview() {
+ SearchBookTextField(
+ hint = "책 제목, 저자검색",
+ onSearch = { /* 검색 실행 */ }
+ )
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/common/forms/WarningTextField.kt b/app/src/main/java/com/texthip/thip/ui/common/forms/WarningTextField.kt
index 533cfcea..2074ddac 100644
--- a/app/src/main/java/com/texthip/thip/ui/common/forms/WarningTextField.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/forms/WarningTextField.kt
@@ -2,11 +2,12 @@ package com.texthip.thip.ui.common.forms
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
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.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@@ -14,12 +15,13 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -29,76 +31,92 @@ import com.texthip.thip.ui.theme.ThipTheme.typography
@Composable
fun WarningTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
hint: String,
warningMessage: String = "경고 메시지를 입력해주세요.",
- showWarning: Boolean
+ showWarning: Boolean = false,
+ maxLength: Int = Int.MAX_VALUE,
+ isNumberOnly: Boolean = false,
+ keyboardType: KeyboardType = KeyboardType.Text
) {
- var text by rememberSaveable { mutableStateOf("") }
val myStyle = typography.menu_r400_s14_h24.copy(lineHeight = 14.sp)
-
- Column {
- OutlinedTextField(
- value = text,
- onValueChange = { text = it },
- placeholder = {
- Text(
- text = hint,
- color = colors.Grey02,
- style = myStyle
- )
- },
- textStyle = myStyle,
- modifier = modifier.size(width = 320.dp, height = 48.dp),
- shape = RoundedCornerShape(12.dp),
- colors = TextFieldDefaults.colors(
- focusedTextColor = colors.White,
- focusedIndicatorColor = if (showWarning) colors.Red else Color.Transparent,
- unfocusedIndicatorColor = if (showWarning) colors.Red else Color.Transparent,
- focusedContainerColor = colors.Black,
- unfocusedContainerColor = colors.Black,
- cursorColor = colors.NeonGreen
- ),
- trailingIcon = {
- if (text.isNotEmpty()) {
- Icon(
- painter = painterResource(id = R.drawable.ic_x_circle_white),
- contentDescription = "Clear text",
- modifier = Modifier.clickable { text = "" },
- tint = Color.Unspecified
- )
- } else {
- Icon(
- painter = painterResource(id = R.drawable.ic_x_circle),
- contentDescription = "Clear text"
- )
- }
- },
- singleLine = true
- )
-
- if (showWarning) {
- Spacer(modifier = Modifier.height(4.dp))
+ OutlinedTextField(
+ value = value,
+ onValueChange = { input ->
+ var filtered = input
+ if (isNumberOnly) filtered = filtered.filter { it.isDigit() }
+ if (filtered.length > maxLength) filtered = filtered.take(maxLength)
+ onValueChange(filtered)
+ },
+ placeholder = {
Text(
- text = warningMessage,
- color = colors.Red,
- style = typography.info_r400_s12.copy(lineHeight = 12.sp)
+ text = hint,
+ color = colors.Grey02,
+ style = myStyle
)
- }
+ },
+ textStyle = myStyle,
+ modifier = modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = colors.White,
+ focusedIndicatorColor = if (showWarning) colors.Red else Color.Transparent,
+ unfocusedIndicatorColor = if (showWarning) colors.Red else Color.Transparent,
+ focusedContainerColor = colors.DarkGrey50,
+ unfocusedContainerColor = colors.DarkGrey50,
+ cursorColor = colors.NeonGreen
+ ),
+ trailingIcon = {
+ if (value.isNotEmpty()) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_x_circle_white),
+ contentDescription = "Clear text",
+ modifier = Modifier.clickable { onValueChange("") },
+ tint = Color.Unspecified
+ )
+ } else {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_x_circle),
+ contentDescription = "Clear text"
+ )
+ }
+ },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = keyboardType)
+ )
+ if (showWarning) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = warningMessage,
+ color = colors.Red,
+ style = typography.info_r400_s12.copy(lineHeight = 12.sp)
+ )
}
}
+
@Composable
@Preview(showBackground = true, backgroundColor = 0xFF000000, widthDp = 360, heightDp = 200)
fun WarningTextFieldPreviewEmpty() {
+ var password by remember { mutableStateOf("") }
+
Box(
modifier = Modifier.size(width = 360.dp, height = 200.dp),
contentAlignment = Alignment.Center
) {
WarningTextField(
- hint = "인풋 텍스트",
- showWarning = true,
- warningMessage = "경고 메시지를 입력해주세요."
+ value = password,
+ onValueChange = { password = it },
+ hint = "4자리 숫자로 입장 비밀번호를 설정",
+ showWarning = password.isNotEmpty() && password.length < 4,
+ warningMessage = "4자리 숫자를 입력해주세요.",
+ maxLength = 4,
+ isNumberOnly = true,
+ keyboardType = KeyboardType.NumberPassword
)
}
}
@@ -106,13 +124,21 @@ fun WarningTextFieldPreviewEmpty() {
@Composable
@Preview(showBackground = true, backgroundColor = 0xFF000000, widthDp = 360, heightDp = 200)
fun WarningTextFieldPreviewNormal() {
+ var password by remember { mutableStateOf("") }
+
Box(
modifier = Modifier.size(width = 360.dp, height = 200.dp),
contentAlignment = Alignment.Center
) {
WarningTextField(
- hint = "인풋 텍스트",
- showWarning = false
+ value = password,
+ onValueChange = { password = it },
+ hint = "4자리 숫자로 입장 비밀번호를 설정",
+ showWarning = false,
+ warningMessage = "4자리 숫자를 입력해주세요.",
+ maxLength = 4,
+ isNumberOnly = true,
+ keyboardType = KeyboardType.NumberPassword
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt
new file mode 100644
index 00000000..9d8965e6
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt
@@ -0,0 +1,74 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.cards.CardBookSearch
+import com.texthip.thip.ui.common.modal.drawVerticalScrollbar
+import com.texthip.thip.ui.group.makeroom.mock.BookData
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+
+
+@Composable
+fun GroupBookListWithScrollbar(
+ books: List,
+ onBookClick: (BookData) -> Unit
+) {
+ val scrollState = rememberScrollState()
+
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(320.dp)
+ ) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState)
+ .drawVerticalScrollbar(scrollState)
+ ) {
+ books.forEach { book ->
+ CardBookSearch(
+ title = book.title,
+ imageRes = book.imageRes,
+ onClick = { onBookClick(book) }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+ Spacer(modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 6.dp)
+ .height(1.dp)
+ .background(color = colors.Grey02)
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+ }
+ }
+}
+
+@Preview()
+@Composable
+fun PreviewBookListWithScrollbar() {
+ ThipTheme {
+ Column {
+ GroupBookListWithScrollbar(
+ books = List(20) { BookData("Book $it", R.drawable.bookcover_sample) },
+ onBookClick = {}
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt
new file mode 100644
index 00000000..3c31caf8
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt
@@ -0,0 +1,131 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.layout.Column
+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.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.forms.SearchBookTextField
+import com.texthip.thip.ui.group.makeroom.mock.BookData
+import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks
+import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet
+import com.texthip.thip.ui.common.header.HeaderMenuBarTab
+
+@Composable
+fun GroupBookSearchBottomSheet(
+ onDismiss: () -> Unit,
+ onBookSelect: (BookData) -> Unit,
+ onRequestBook: () -> Unit,
+ savedBooks: List = emptyList(),
+ groupBooks: List = emptyList()
+) {
+ // 책이 있는지 여부 체크
+ val hasBooks = savedBooks.isNotEmpty() || groupBooks.isNotEmpty()
+ var selectedTab by rememberSaveable { mutableIntStateOf(0) }
+ val tabs = listOf(
+ stringResource(R.string.group_saved_book), stringResource(R.string.group_book)
+ )
+ val books = if (selectedTab == 0) savedBooks else groupBooks
+
+ CustomBottomSheet(
+ onDismiss = onDismiss
+ ) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ ) {
+ // 검색창
+ SearchBookTextField(
+ hint = stringResource(R.string.group_book_search_hint),
+ onSearch = { /* 검색 구현 */ }
+ )
+ Spacer(Modifier.height(20.dp))
+ }
+
+ if (hasBooks) {
+ HeaderMenuBarTab(
+ titles = tabs,
+ selectedTabIndex = selectedTab,
+ onTabSelected = { selectedTab = it },
+ indicatorColor = ThipTheme.colors.White,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, bottom = 20.dp)
+ ) {
+ Spacer(Modifier.height(20.dp))
+ if (books.isEmpty()) {
+ EmptyBookSheetContent(onRequestBook = onRequestBook)
+ } else {
+ GroupBookListWithScrollbar(
+ books = books,
+ onBookClick = onBookSelect
+ )
+ }
+ }
+ } else {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, bottom = 20.dp)
+ ) {
+ Spacer(Modifier.height(20.dp))
+ EmptyBookSheetContent(onRequestBook = onRequestBook)
+ }
+ }
+ }
+}
+
+
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewBookSearchBottomSheet_HasBooks() {
+ ThipTheme {
+ var showSheet by remember { mutableStateOf(true) }
+ if (showSheet) {
+ GroupBookSearchBottomSheet(
+ onDismiss = { showSheet = false },
+ onBookSelect = {},
+ onRequestBook = {},
+ savedBooks = dummySavedBooks, // 데이터 있음
+ groupBooks = dummyGroupBooks
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewBookSearchBottomSheet_Empty() {
+ ThipTheme {
+ var showSheet by remember { mutableStateOf(true) }
+ if (showSheet) {
+ GroupBookSearchBottomSheet(
+ onDismiss = { showSheet = false },
+ onBookSelect = {},
+ onRequestBook = {},
+ savedBooks = emptyList(), // 데이터 없음
+ groupBooks = emptyList()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt
new file mode 100644
index 00000000..9b5655e0
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt
@@ -0,0 +1,168 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+import java.time.LocalDate
+
+@Composable
+fun GroupDatePicker(
+ selectedDate: LocalDate,
+ minDate: LocalDate,
+ maxDate: LocalDate,
+ onDateSelected: (LocalDate) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ // 선택된 날짜에서 년/월/일 추출
+ val year = selectedDate.year
+ val month = selectedDate.monthValue
+ val day = selectedDate.dayOfMonth
+
+ // 유효한 범위 계산
+ val years = (minDate.year..maxDate.year).toList()
+ val months = (1..12).toList()
+ val days = (1..selectedDate.lengthOfMonth()).toList()
+
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ GroupWheelPicker(
+ modifier = Modifier.width(48.dp),
+ items = years,
+ selectedItem = year,
+ onItemSelected = { newYear ->
+ val newDate = try {
+ LocalDate.of(newYear, month, day)
+ } catch (e: Exception) {
+ LocalDate.of(newYear, month, 1)
+ }
+ onDateSelected(newDate)
+ },
+ displayText = { it.toString() }
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = stringResource(R.string.group_year),
+ style = typography.info_r400_s12,
+ color = colors.White
+ )
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ GroupWheelPicker(
+ modifier = Modifier.width(32.dp),
+ items = months,
+ selectedItem = month,
+ onItemSelected = { newMonth ->
+ val newDate = try {
+ LocalDate.of(year, newMonth, day)
+ } catch (e: Exception) {
+ LocalDate.of(year, newMonth, 1)
+ }
+ onDateSelected(newDate)
+ },
+ displayText = { it.toString() }
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = stringResource(R.string.group_month),
+ style = typography.info_r400_s12,
+ color = colors.White
+ )
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ GroupWheelPicker(
+ modifier = Modifier.width(32.dp),
+ items = days,
+ selectedItem = day,
+ onItemSelected = { newDay ->
+ val newDate = LocalDate.of(year, month, newDay)
+ onDateSelected(newDate)
+ },
+ displayText = { it.toString() }
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = stringResource(R.string.group_day),
+ style = typography.info_r400_s12,
+ color = colors.White
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DatePickerGroupPreview() {
+ ThipTheme {
+ val today = LocalDate.now()
+ val tomorrow = today.plusDays(1)
+ val maxDate = today.plusMonths(12)
+
+ var startDate by remember { mutableStateOf(today) }
+ var endDate by remember { mutableStateOf(tomorrow) }
+
+ Box(
+ modifier = Modifier
+ .background(colors.Black)
+ .padding(16.dp)
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ // 시작 날짜
+ Text("시작 날짜", color = colors.White)
+ GroupDatePicker(
+ selectedDate = startDate,
+ minDate = today,
+ maxDate = maxDate,
+ onDateSelected = { newDate ->
+ startDate = newDate
+ }
+ )
+
+ // 끝 날짜
+ Text("끝 날짜", color = colors.White)
+ GroupDatePicker(
+ selectedDate = endDate,
+ minDate = today,
+ maxDate = maxDate,
+ onDateSelected = { newDate ->
+ endDate = newDate
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt
new file mode 100644
index 00000000..d0d1028b
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt
@@ -0,0 +1,66 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.buttons.ActionMediumButton
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+
+@Composable
+fun EmptyBookSheetContent(
+ onRequestBook: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 30.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_notification),
+ contentDescription = null,
+ tint = colors.Grey02
+ )
+ Spacer(Modifier.height(12.dp))
+ Text(
+ text = stringResource(R.string.group_register_book_comment),
+ color = ThipTheme.colors.Grey02,
+ style = ThipTheme.typography.copy_m500_s14_h20
+ )
+ Spacer(Modifier.height(24.dp))
+
+ ActionMediumButton(
+ text = stringResource(R.string.group_register_book),
+ contentColor = colors.White,
+ backgroundColor = colors.Purple,
+ modifier = Modifier
+ .width(97.dp)
+ .height(44.dp),
+ onClick = { onRequestBook() },
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun EmptyBookSheetContentPreview() {
+ ThipTheme {
+ EmptyBookSheetContent(
+ onRequestBook = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupInputField.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupInputField.kt
new file mode 100644
index 00000000..2e4a589e
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupInputField.kt
@@ -0,0 +1,104 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
+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.graphics.SolidColor
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+@Composable
+fun GroupInputField(
+ title: String,
+ hint: String,
+ value: String,
+ onValueChange: (String) -> Unit,
+ maxLength: Int = 75
+) {
+ val isOverflow = value.length >= maxLength
+
+ Column(
+ Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = title,
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
+
+ Box(modifier = Modifier.fillMaxWidth()) {
+ BasicTextField(
+ value = value,
+ onValueChange = { new ->
+ if (new.length <= maxLength) onValueChange(new)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp, bottom = 12.dp),
+ textStyle = typography.menu_r400_s14_h24.copy(color = colors.White),
+ cursorBrush = SolidColor(colors.NeonGreen),
+ decorationBox = { innerTextField ->
+ Box(
+ Modifier.fillMaxWidth()
+ ) {
+ if (value.isEmpty()) {
+ Text(
+ hint,
+ style = typography.menu_r400_s14_h24,
+ color = colors.Grey02
+ )
+ }
+ innerTextField()
+ }
+ }
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ Text(
+ text = stringResource(R.string.group_input_count, value.length, maxLength),
+ style = typography.info_r400_s12,
+ color = if (isOverflow) colors.Red else colors.NeonGreen
+ )
+ }
+ }
+}
+
+
+@Preview()
+@Composable
+fun PreviewRoomTitleInputField() {
+ var text by remember { mutableStateOf("") }
+
+ ThipTheme {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ GroupInputField(
+ title = "방 제목",
+ hint = "방 제목을 입력해주세요",
+ value = text,
+ onValueChange = { text = it },
+ maxLength = 15
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupMemberLimitPicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupMemberLimitPicker.kt
new file mode 100644
index 00000000..09d9d1cc
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupMemberLimitPicker.kt
@@ -0,0 +1,84 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+@Composable
+fun MemberLimitPicker(
+ modifier: Modifier = Modifier,
+ selectedCount: Int = 30,
+ onCountSelected: (Int) -> Unit = { }
+) {
+ val memberCounts = remember { (1..30).toList() }
+
+ Column(
+ modifier = modifier.fillMaxWidth()
+ ) {
+ // 제목
+ Text(
+ text = stringResource(R.string.group_room_member_limit_title),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
+
+ // 인원 선택기
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 숫자 선택기
+ GroupWheelPicker(
+ modifier = Modifier.width(32.dp),
+ items = memberCounts,
+ selectedItem = selectedCount,
+ onItemSelected = onCountSelected,
+ displayText = { it.toString() }
+ )
+
+ // 단위 텍스트
+ Text(
+ text = stringResource(R.string.group_room_limit),
+ style = typography.info_r400_s12,
+ color = colors.White,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MemberLimitPickerPreview() {
+ ThipTheme {
+ var selectedCount by remember { mutableIntStateOf(30) }
+
+ MemberLimitPicker(
+ selectedCount = selectedCount,
+ onCountSelected = { selectedCount = it }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt
new file mode 100644
index 00000000..3b8acac4
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt
@@ -0,0 +1,194 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
+
+@Composable
+fun GroupRoomDurationPicker(
+ modifier: Modifier = Modifier,
+ onDateRangeSelected: (LocalDate, LocalDate) -> Unit = { _, _ -> }
+) {
+ val today = LocalDate.now()
+ val maxDate = today.plusMonths(12)
+ var isInitialized by rememberSaveable { mutableStateOf(false) }
+
+ var startDate by rememberSaveable { mutableStateOf(today) }
+ var endDate by rememberSaveable { mutableStateOf(today.plusDays(1)) }
+ var isPickerTouched by rememberSaveable { mutableStateOf(false) }
+
+ // 첫 시작 시에만 모든 날짜를 오늘 기준으로 초기화
+ LaunchedEffect(Unit) {
+ if (!isInitialized) {
+ startDate = today
+ endDate = today.plusDays(1)
+ isInitialized = true
+ }
+ }
+
+ // 날짜 범위 계산
+ val daysBetween = ChronoUnit.DAYS.between(startDate, endDate)
+ val isOverLimit = daysBetween > 91
+
+ // 날짜 선택 콜백
+ LaunchedEffect(startDate, endDate) {
+ if (endDate.isAfter(startDate)) {
+ onDateRangeSelected(startDate, endDate)
+ }
+ }
+
+ // 날짜 유효성 검사 및 자동 조정
+ LaunchedEffect(startDate) {
+ val adjustedStartDate = when {
+ startDate.isBefore(today) -> today
+ startDate.isAfter(maxDate) -> maxDate
+ else -> startDate
+ }
+
+ if (adjustedStartDate != startDate) {
+ startDate = adjustedStartDate
+ }
+
+ // 끝 날짜가 시작 날짜보다 빠르면 조정
+ if (endDate.isBefore(startDate.plusDays(1))) {
+ endDate = startDate.plusDays(1)
+ }
+ }
+
+ LaunchedEffect(endDate) {
+ val adjustedEndDate = when {
+ endDate.isAfter(maxDate) -> maxDate
+ endDate.isBefore(startDate.plusDays(1)) -> startDate.plusDays(1)
+ else -> endDate
+ }
+
+ if (adjustedEndDate != endDate) {
+ endDate = adjustedEndDate
+ }
+ }
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ text = stringResource(R.string.group_room_duration_title),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 시작 날짜 Picker
+ GroupDatePicker(
+ selectedDate = startDate,
+ minDate = today,
+ maxDate = maxDate,
+ onDateSelected = { newDate ->
+ startDate = newDate
+ },
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = { isPickerTouched = true }
+ )
+ }
+ )
+
+ // 구분자
+ Text(
+ text = "~",
+ style = typography.info_r400_s12,
+ color = colors.White,
+ modifier = Modifier.padding(horizontal = 10.dp)
+ )
+
+ // 끝 날짜 Picker
+ GroupDatePicker(
+ selectedDate = endDate,
+ minDate = today,
+ maxDate = maxDate,
+ onDateSelected = { newDate ->
+ endDate = newDate
+ },
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = { isPickerTouched = true }
+ )
+ }
+ )
+ }
+
+ // 안내/에러 메시지
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ when {
+ isOverLimit -> {
+ Text(
+ text = stringResource(R.string.group_room_duration_comment),
+ style = typography.info_r400_s12,
+ color = colors.Red,
+ textAlign = TextAlign.End,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ }
+ !isPickerTouched -> {
+ Text(
+ text = stringResource(R.string.group_room_duration_initial_comment),
+ style = typography.info_r400_s12,
+ color = colors.NeonGreen,
+ textAlign = TextAlign.End,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ }
+ else -> {
+ Text(
+ text = stringResource(
+ R.string.group_room_duration_active_comment,
+ startDate.monthValue,
+ startDate.dayOfMonth
+ ),
+ style = typography.info_r400_s12,
+ color = colors.NeonGreen,
+ textAlign = TextAlign.End,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MeetingDurationPickerPreview() {
+ ThipTheme {
+ GroupRoomDurationPicker { startDate, endDate ->
+ println("Selected date range: $startDate to $endDate")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt
new file mode 100644
index 00000000..0b31c50b
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt
@@ -0,0 +1,152 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.buttons.OptionChipButton
+import com.texthip.thip.ui.group.makeroom.mock.BookData
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+@Composable
+fun GroupSelectBook(
+ selectedBook: BookData?,
+ onChangeBookClick: () -> Unit,
+ onSelectBookClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.group_select_book_title),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White,
+ modifier = Modifier.align(Alignment.Start)
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ if (selectedBook == null) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .clickable { onSelectBookClick() }
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(
+ BorderStroke(1.dp, colors.Grey02),
+ shape = RoundedCornerShape(12.dp)
+ )
+ .padding(vertical = 10.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_search),
+ contentDescription = "검색 아이콘",
+ tint = colors.Grey01
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.group_book_search),
+ style = typography.menu_m500_s16_h24,
+ color = colors.Grey
+ )
+ }
+ } else {
+ // 선택된 상태: 커버, 제목, 저자, 변경 버튼
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Image(
+ painter = painterResource(selectedBook.imageRes),
+ contentDescription = selectedBook.title,
+ modifier = Modifier
+ .height(80.dp)
+ .width(60.dp)
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f),
+ verticalArrangement = Arrangement.Top
+ ) {
+ Text(
+ text = selectedBook.title,
+ color = colors.White,
+ style = typography.menu_sb600_s14_h24
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ selectedBook.author?.let {
+ Text(
+ text = stringResource(
+ R.string.group_selected_book_author,
+ selectedBook.author
+ ),
+ color = colors.Grey01,
+ style = typography.info_r400_s12,
+ maxLines = 1
+ )
+ }
+ }
+ OptionChipButton(
+ text = stringResource(R.string.change),
+ onClick = onChangeBookClick,
+ isSelected = true
+ )
+ }
+ }
+ }
+}
+
+
+private val dummyBook = BookData(
+ title = "호르몬 체인지",
+ imageRes = R.drawable.bookcover_sample,
+ author = "최정화"
+)
+
+@Preview(showBackground = true)
+@Composable
+fun GroupSelectBookPreview_Unselected() {
+ ThipTheme {
+ GroupSelectBook(
+ selectedBook = null,
+ onChangeBookClick = {},
+ onSelectBookClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun GroupSelectBookPreview_Selected() {
+ ThipTheme {
+ GroupSelectBook(
+ selectedBook = dummyBook,
+ onChangeBookClick = {},
+ onSelectBookClick = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupWheelPicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupWheelPicker.kt
new file mode 100644
index 00000000..b7d353c0
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupWheelPicker.kt
@@ -0,0 +1,271 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.exponentialDecay
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.ui.group.makeroom.util.WheelPickerUtils
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+import kotlinx.coroutines.launch
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+@Composable
+fun GroupWheelPicker(
+ modifier: Modifier = Modifier,
+ items: List,
+ selectedItem: T,
+ onItemSelected: (T) -> Unit,
+ displayText: (T) -> String = { it.toString() },
+ selectedBackgroundColor: Color = colors.DarkGrey,
+ itemHeight: Int = 20,
+ isCircular: Boolean = true
+) {
+ if (items.isEmpty()) return
+
+ val isScrollEnabled = items.size > 1
+ val circular = isCircular && items.size > 2
+
+ val selectedIndex = items.indexOf(selectedItem)
+ val animatableOffset = remember { Animatable(0f) }
+ val coroutineScope = rememberCoroutineScope()
+ var isDragging by remember { mutableStateOf(false) }
+ var velocity by remember { mutableFloatStateOf(0f) }
+
+ val density = LocalDensity.current
+ val itemHeightPx = with(density) { itemHeight.dp.toPx() }
+ val spacingPx = with(density) { 9.dp.toPx() }
+ val itemSpacing = itemHeightPx + spacingPx
+
+ fun getCircularIndex(index: Int): Int {
+ return WheelPickerUtils.getCircularIndex(index, items.size)
+ }
+
+ fun normalizeOffset(offset: Float): Float {
+ return WheelPickerUtils.normalizeOffset(offset, itemSpacing, items.size, circular)
+ }
+
+ fun offsetToIndex(offset: Float): Int {
+ return WheelPickerUtils.offsetToIndex(offset, itemSpacing, items.size, circular)
+ }
+
+ // 선택 아이템이 바뀌면 중앙에 오도록 offset 이동
+ LaunchedEffect(selectedItem) {
+ if (!isDragging && isScrollEnabled) {
+ val targetOffset = -selectedIndex * itemSpacing
+ animatableOffset.animateTo(
+ if (circular) normalizeOffset(targetOffset) else targetOffset,
+ animationSpec = spring()
+ )
+ }
+ }
+
+ // 오프셋이 바뀔 때 마다 선택 아이템을 갱신
+ LaunchedEffect(animatableOffset.value) {
+ if (!isDragging && isScrollEnabled) {
+ val newSelectedIndex = offsetToIndex(animatableOffset.value)
+ if (items[newSelectedIndex] != selectedItem) {
+ onItemSelected(items[newSelectedIndex])
+ }
+ }
+ }
+
+ Box(
+ modifier = modifier
+ .height((itemHeight * 3 + 36).dp)
+ ) {
+ // 중앙 고정 박스
+ Box(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .background(
+ selectedBackgroundColor,
+ RoundedCornerShape(4.dp)
+ )
+ .padding(horizontal = 8.dp, vertical = 9.dp)
+ ) {
+ Text(
+ text = displayText(selectedItem),
+ style = typography.info_r400_s12,
+ color = Color.Transparent,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ // 아이템들
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(
+ if (isScrollEnabled) {
+ Modifier.pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = {
+ isDragging = true
+ velocity = 0f
+ },
+ onDragEnd = {
+ isDragging = false
+ coroutineScope.launch {
+ // 관성 스크롤
+ if (abs(velocity) > 100f) {
+ animatableOffset.animateDecay(
+ initialVelocity = velocity,
+ animationSpec = exponentialDecay(
+ frictionMultiplier = 0.9f,
+ absVelocityThreshold = 0.1f
+ )
+ )
+ }
+
+ // offset 스냅
+ val currOffset = animatableOffset.value
+ val normalized =
+ if (circular) normalizeOffset(currOffset) else currOffset
+ val snapIndex = (-normalized / itemSpacing).roundToInt()
+ val snapOffset = -snapIndex * itemSpacing
+ animatableOffset.animateTo(
+ if (circular) normalizeOffset(snapOffset) else snapOffset,
+ animationSpec = spring(
+ dampingRatio = 0.8f,
+ stiffness = 400f
+ )
+ )
+ }
+ }
+ ) { _, dragAmount ->
+ velocity = dragAmount.y
+ coroutineScope.launch {
+ val newOffset = animatableOffset.value + dragAmount.y
+ if (circular) {
+ animatableOffset.snapTo(normalizeOffset(newOffset))
+ } else {
+ val maxOffset = itemSpacing + spacingPx
+ val minOffset =
+ -(items.size - 1) * itemSpacing - itemSpacing - spacingPx
+ animatableOffset.snapTo(
+ newOffset.coerceIn(
+ minOffset,
+ maxOffset
+ )
+ )
+ }
+ }
+ }
+ }
+ } else {
+ Modifier
+ }
+ )
+ ) {
+ val currOffset =
+ if (circular && isScrollEnabled) normalizeOffset(animatableOffset.value) else animatableOffset.value
+ val centerIndex = if (isScrollEnabled) (-currOffset / itemSpacing).roundToInt() else 0
+
+ // 중앙 + 위 아래 한 개만 보이도록!
+ val visibleRange = if (isScrollEnabled) -1..1 else 0..0
+
+ visibleRange.forEach { relIdx ->
+ val displayIndex = centerIndex + relIdx
+ val actualIndex =
+ if (circular && isScrollEnabled) getCircularIndex(displayIndex) else {
+ if (displayIndex in 0 until items.size) displayIndex else return@forEach
+ }
+ val item = items[actualIndex]
+ val itemOffset =
+ if (isScrollEnabled) currOffset + (displayIndex * itemSpacing) else 0f
+ val itemY = itemOffset + itemSpacing + spacingPx
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(itemHeight.dp)
+ .offset { IntOffset(0, itemY.roundToInt()) },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = displayText(item),
+ style = typography.info_r400_s12,
+ color = colors.White,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+
+ // 그라데이션 오버레이 (아이템이 여러 개일 때만 표시)
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ colors.Black.copy(alpha = 0.8f),
+ Color.Transparent,
+ Color.Transparent,
+ colors.Black.copy(alpha = 0.8f)
+ ),
+ startY = 0f,
+ endY = Float.POSITIVE_INFINITY
+ )
+ )
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun WheelPickerPreview() {
+ var selectedYear by remember { mutableStateOf(2025) }
+ val years = (2020..2030).toList() // 11개
+
+ var selectedSingleItem by remember { mutableStateOf("Only Item") }
+ val singleItemList = listOf("Only Item") // 1개
+
+ Box(
+ modifier = Modifier
+ .background(Color.Black),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 순환식 년 선택기 (4개 이상이므로 순환)
+ GroupWheelPicker(
+ modifier = Modifier.width(60.dp),
+ items = years,
+ selectedItem = selectedYear,
+ onItemSelected = { selectedYear = it },
+ displayText = { it.toString() },
+ isCircular = true
+ )
+
+ // 단일 아이템 선택기 (스크롤 비활성화)
+ GroupWheelPicker(
+ modifier = Modifier.width(60.dp),
+ items = singleItemList,
+ selectedItem = selectedSingleItem,
+ onItemSelected = { selectedSingleItem = it },
+ displayText = { it },
+ isCircular = false
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/SectionDivider.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/SectionDivider.kt
new file mode 100644
index 00000000..8643e413
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/SectionDivider.kt
@@ -0,0 +1,23 @@
+package com.texthip.thip.ui.group.makeroom.component
+
+import androidx.compose.foundation.background
+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.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.ui.theme.ThipTheme.colors
+
+@Composable
+fun SectionDivider() {
+ Spacer(modifier = Modifier.padding(top = 32.dp))
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(colors.DarkGrey02)
+ )
+ Spacer(modifier = Modifier.padding(top = 32.dp))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt
new file mode 100644
index 00000000..7d3366ff
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt
@@ -0,0 +1,30 @@
+package com.texthip.thip.ui.group.makeroom.mock
+
+import com.texthip.thip.R
+
+data class BookData(
+ val title: String,
+ val imageRes: Int, // drawable 리소스 or 이미지 URL
+ val author: String? = null
+)
+
+val dummySavedBooks = listOf(
+ BookData("토마토 컬러면", R.drawable.bookcover_sample),
+ BookData("사슴", R.drawable.bookcover_sample),
+ BookData("토마토 컬러면", R.drawable.bookcover_sample),
+ BookData("사슴", R.drawable.bookcover_sample),
+ BookData("토마토 컬러면", R.drawable.bookcover_sample),
+ BookData("사슴", R.drawable.bookcover_sample),
+ BookData("토마토 컬러면", R.drawable.bookcover_sample),
+ BookData("사슴", R.drawable.bookcover_sample)
+)
+val dummyGroupBooks = listOf(
+ BookData("명작 읽기방", R.drawable.bookcover_sample),
+ BookData("또 다른 방", R.drawable.bookcover_sample),
+ BookData("명작 읽기방", R.drawable.bookcover_sample),
+ BookData("또 다른 방", R.drawable.bookcover_sample),
+ BookData("명작 읽기방", R.drawable.bookcover_sample),
+ BookData("또 다른 방", R.drawable.bookcover_sample),
+ BookData("명작 읽기방", R.drawable.bookcover_sample),
+ BookData("또 다른 방", R.drawable.bookcover_sample)
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt
new file mode 100644
index 00000000..b41d0b8b
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt
@@ -0,0 +1,15 @@
+package com.texthip.thip.ui.group.makeroom.mock
+
+import java.time.LocalDate
+
+data class GroupMakeRoomRequest(
+ val selectedBook: BookData?,
+ val genreIndex: Int,
+ val roomTitle: String,
+ val roomDescription: String,
+ val meetingStartDate: LocalDate,
+ val meetingEndDate: LocalDate,
+ val memberLimit: Int,
+ val isPrivate: Boolean,
+ val password: String = ""
+)
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt
new file mode 100644
index 00000000..7143a1d2
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt
@@ -0,0 +1,56 @@
+package com.texthip.thip.ui.group.makeroom.mock
+
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
+
+data class GroupMakeRoomUiState(
+ val selectedBook: BookData? = null,
+ val showBookSearchSheet: Boolean = false,
+ val selectedGenreIndex: Int = -1,
+ val roomTitle: String = "",
+ val roomDescription: String = "",
+ val meetingStartDate: LocalDate = LocalDate.now(),
+ val meetingEndDate: LocalDate = LocalDate.now().plusDays(1),
+ val memberLimit: Int = 30,
+ val isPrivate: Boolean = false,
+ val password: String = "",
+ val isLoading: Boolean = false,
+ val errorMessage: String? = null
+) {
+ // 유효성 검사 로직
+ val isDurationValid: Boolean
+ get() {
+ val daysBetween = ChronoUnit.DAYS.between(meetingStartDate, meetingEndDate)
+ return daysBetween in 1..90
+ }
+
+ val isCountValid: Boolean
+ get() = memberLimit in 2..30
+
+ val isPasswordValid: Boolean
+ get() = !isPrivate || password.length == 4
+
+ val isFormValid: Boolean
+ get() = selectedBook != null &&
+ selectedGenreIndex >= 0 &&
+ roomTitle.isNotBlank() &&
+ roomDescription.isNotBlank() &&
+ isDurationValid &&
+ isCountValid &&
+ isPasswordValid
+
+ // 서버 전송용 데이터로 변환
+ fun toRequest(): GroupMakeRoomRequest {
+ return GroupMakeRoomRequest(
+ selectedBook = selectedBook,
+ genreIndex = selectedGenreIndex,
+ roomTitle = roomTitle.trim(),
+ roomDescription = roomDescription.trim(),
+ meetingStartDate = meetingStartDate,
+ meetingEndDate = meetingEndDate,
+ memberLimit = memberLimit,
+ isPrivate = isPrivate,
+ password = if (isPrivate) password else ""
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt
new file mode 100644
index 00000000..8211d6c4
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt
@@ -0,0 +1,256 @@
+package com.texthip.thip.ui.group.makeroom.screen
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.buttons.GenreChipRow
+import com.texthip.thip.ui.common.buttons.ToggleSwitchButton
+import com.texthip.thip.ui.common.forms.WarningTextField
+import com.texthip.thip.ui.common.topappbar.InputTopAppBar
+import com.texthip.thip.ui.group.makeroom.component.GroupBookSearchBottomSheet
+import com.texthip.thip.ui.group.makeroom.component.GroupInputField
+import com.texthip.thip.ui.group.makeroom.component.GroupRoomDurationPicker
+import com.texthip.thip.ui.group.makeroom.component.GroupSelectBook
+import com.texthip.thip.ui.group.makeroom.component.MemberLimitPicker
+import com.texthip.thip.ui.group.makeroom.component.SectionDivider
+import com.texthip.thip.ui.group.makeroom.mock.BookData
+import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest
+import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks
+import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks
+import com.texthip.thip.ui.group.makeroom.viewmodel.ApiResult
+import com.texthip.thip.ui.group.makeroom.viewmodel.GroupCreateResponse
+import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel
+import com.texthip.thip.ui.group.makeroom.viewmodel.GroupRepository
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+
+@Composable
+fun GroupMakeRoomScreen(
+ viewModel: GroupMakeRoomViewModel,
+ onNavigateBack: () -> Unit,
+ onGroupCreated: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val uiState by viewModel.uiState.collectAsState()
+ val scrollState = rememberScrollState()
+ val genres = viewModel.genres
+
+ // 에러 메시지 표시
+ LaunchedEffect(uiState.errorMessage) {
+ uiState.errorMessage?.let { message ->
+ viewModel.clearError()
+ }
+ }
+
+ Box {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .then(if (uiState.showBookSearchSheet) Modifier.blur(5.dp) else Modifier),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ InputTopAppBar(
+ title = stringResource(R.string.group_making_group),
+ isRightButtonEnabled = uiState.isFormValid && !uiState.isLoading,
+ onLeftClick = onNavigateBack,
+ onRightClick = {
+ viewModel.createGroup(
+ onSuccess = onGroupCreated,
+ onError = { /* 에러는 uiState.errorMessage로 처리 */ }
+ )
+ }
+ )
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ .fillMaxSize()
+ .padding(horizontal = 20.dp),
+ verticalArrangement = Arrangement.Top,
+ ) {
+ Spacer(modifier = Modifier.padding(top = 20.dp))
+
+ GroupSelectBook(
+ selectedBook = uiState.selectedBook,
+ onChangeBookClick = { viewModel.toggleBookSearchSheet(true) },
+ onSelectBookClick = { viewModel.toggleBookSearchSheet(true) }
+ )
+
+ SectionDivider()
+
+ Text(
+ text = stringResource(R.string.group_book_genre),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White,
+ )
+ Spacer(modifier = Modifier.padding(top = 12.dp))
+ GenreChipRow(
+ modifier = Modifier.width(18.dp),
+ genres = genres,
+ selectedIndex = uiState.selectedGenreIndex,
+ onSelect = viewModel::selectGenre
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ Text(
+ text = stringResource(R.string.group_genre_select_comment),
+ style = typography.info_r400_s12,
+ color = colors.NeonGreen
+ )
+ }
+
+ SectionDivider()
+
+ GroupInputField(
+ title = stringResource(R.string.group_room_title),
+ hint = stringResource(R.string.group_room_title_hint),
+ value = uiState.roomTitle,
+ maxLength = 15,
+ onValueChange = viewModel::updateRoomTitle
+ )
+
+ SectionDivider()
+
+ GroupInputField(
+ title = stringResource(R.string.group_room_explain),
+ hint = stringResource(R.string.group_room_explain_hint),
+ value = uiState.roomDescription,
+ onValueChange = viewModel::updateRoomDescription
+ )
+
+ SectionDivider()
+
+ GroupRoomDurationPicker(
+ onDateRangeSelected = viewModel::setDateRange
+ )
+
+ SectionDivider()
+
+ MemberLimitPicker(
+ selectedCount = uiState.memberLimit,
+ onCountSelected = viewModel::setMemberLimit
+ )
+
+ SectionDivider()
+
+ Text(
+ text = stringResource(R.string.group_private_option),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
+ Spacer(modifier = Modifier.padding(top = 12.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.group_private_comment),
+ style = typography.menu_r400_s14_h24,
+ color = colors.White
+ )
+ ToggleSwitchButton(
+ isChecked = uiState.isPrivate,
+ onToggleChange = viewModel::togglePrivate
+ )
+ }
+
+ if (uiState.isPrivate) {
+ Spacer(modifier = Modifier.height(12.dp))
+ WarningTextField(
+ value = uiState.password,
+ onValueChange = viewModel::updatePassword,
+ hint = stringResource(R.string.group_password_hint),
+ showWarning = uiState.password.isNotEmpty() && uiState.password.length < 4,
+ warningMessage = stringResource(R.string.group_private_warning_message),
+ maxLength = 4,
+ isNumberOnly = true,
+ keyboardType = KeyboardType.NumberPassword
+ )
+ }
+
+ Spacer(modifier = Modifier.padding(top = 134.dp))
+ }
+ }
+
+ if (uiState.showBookSearchSheet) {
+ GroupBookSearchBottomSheet(
+ onDismiss = { viewModel.toggleBookSearchSheet(false) },
+ onBookSelect = { book: BookData ->
+ viewModel.selectBook(book)
+ viewModel.toggleBookSearchSheet(false)
+ },
+ onRequestBook = {
+ viewModel.toggleBookSearchSheet(false)
+ },
+ savedBooks = dummySavedBooks,
+ groupBooks = dummyGroupBooks
+ )
+ }
+
+ // 로딩 인디케이터
+ /*if (uiState.isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(colors.Black.copy(alpha = 0.5f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = colors.NeonGreen)
+ }
+ }*/
+ }
+}
+
+
+@Preview
+@Composable
+private fun GroupMakeRoomScreenPreview() {
+ // Preview용 MockViewModel 생성
+ val mockViewModel = object : GroupMakeRoomViewModel(MockGroupRepository()) {
+ // 필요한 경우 Preview용 초기 상태 설정
+ init {
+ // 예시: 미리 선택된 책이 있는 상태로 Preview
+ // selectBook(BookData(id = "1", title = "예시 책", author = "작가"))
+ // selectGenre(0)
+ }
+ }
+
+ ThipTheme {
+ GroupMakeRoomScreen(
+ viewModel = mockViewModel,
+ onNavigateBack = { },
+ onGroupCreated = { }
+ )
+ }
+}
+
+// Preview용 Mock Repository
+class MockGroupRepository : GroupRepository {
+ override suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult {
+ return ApiResult(
+ isSuccess = true,
+ data = GroupCreateResponse(
+ groupId = "mock_group_id",
+ groupName = "Mock Group"
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupRegisterBookScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupRegisterBookScreen.kt
new file mode 100644
index 00000000..f9ec8805
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupRegisterBookScreen.kt
@@ -0,0 +1,66 @@
+package com.texthip.thip.ui.group.makeroom.screen
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.texthip.thip.R
+import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar
+import com.texthip.thip.ui.theme.ThipTheme
+import com.texthip.thip.ui.theme.ThipTheme.colors
+import com.texthip.thip.ui.theme.ThipTheme.typography
+
+@Composable
+fun GroupRegisterBookScreen(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ DefaultTopAppBar(
+ title = stringResource(R.string.group_request_book),
+ onLeftClick = {},
+ )
+ Column (
+ modifier = Modifier
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.customer_center_email),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
+ Spacer(modifier = Modifier.padding(top = 8.dp))
+
+ Text(
+ text = stringResource(R.string.group_request_book_comment_1),
+ style = typography.copy_r400_s14,
+ color = colors.White
+ )
+ Text(
+ text = stringResource(R.string.group_request_book_comment_2),
+ style = typography.copy_r400_s14,
+ color = colors.White
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun GroupRegisterBookPreview() {
+ ThipTheme {
+ GroupRegisterBookScreen()
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/util/WheelPickerDisplayUtils.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/util/WheelPickerDisplayUtils.kt
new file mode 100644
index 00000000..9a355d52
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/util/WheelPickerDisplayUtils.kt
@@ -0,0 +1,30 @@
+package com.texthip.thip.ui.group.makeroom.util
+
+import kotlin.math.roundToInt
+
+object WheelPickerUtils {
+ @JvmStatic
+ fun getCircularIndex(index: Int, size: Int): Int {
+ return ((index % size) + size) % size
+ }
+
+ @JvmStatic
+ fun normalizeOffset(offset: Float, itemSpacing: Float, size: Int, circular: Boolean): Float {
+ if (!circular) return offset
+ val total = size * itemSpacing
+ return ((offset % total) + total) % total
+ }
+
+ @JvmStatic
+ fun offsetToIndex(
+ offset: Float,
+ itemSpacing: Float,
+ size: Int,
+ circular: Boolean
+ ): Int {
+ val normalized = if (circular) normalizeOffset(offset, itemSpacing, size, circular) else offset
+ val centerIndex = (-normalized / itemSpacing).roundToInt()
+ return if (circular) getCircularIndex(centerIndex, size)
+ else centerIndex.coerceIn(0, size - 1)
+ }
+}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
new file mode 100644
index 00000000..20e28599
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt
@@ -0,0 +1,125 @@
+package com.texthip.thip.ui.group.makeroom.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.texthip.thip.ui.group.makeroom.mock.BookData
+import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest
+import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.time.LocalDate
+
+// 나중에 서버와 연동할 때 사용할 뷰모델 예시
+open class GroupMakeRoomViewModel(
+ private val groupRepository: GroupRepository // 의존성 주입
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(GroupMakeRoomUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술")
+
+ // 책 선택
+ fun selectBook(book: BookData) {
+ _uiState.value = _uiState.value.copy(selectedBook = book)
+ }
+
+ // 책 검색 시트 표시 상태 변경
+ fun toggleBookSearchSheet(show: Boolean) {
+ _uiState.value = _uiState.value.copy(showBookSearchSheet = show)
+ }
+
+ // 장르 선택
+ fun selectGenre(index: Int) {
+ _uiState.value = _uiState.value.copy(selectedGenreIndex = index)
+ }
+
+ // 방 제목 변경
+ fun updateRoomTitle(title: String) {
+ _uiState.value = _uiState.value.copy(roomTitle = title)
+ }
+
+ // 방 설명 변경
+ fun updateRoomDescription(description: String) {
+ _uiState.value = _uiState.value.copy(roomDescription = description)
+ }
+
+ // 모임 날짜 범위 설정
+ fun setDateRange(startDate: LocalDate, endDate: LocalDate) {
+ _uiState.value = _uiState.value.copy(
+ meetingStartDate = startDate,
+ meetingEndDate = endDate
+ )
+ }
+
+ // 인원 수 설정
+ fun setMemberLimit(count: Int) {
+ _uiState.value = _uiState.value.copy(memberLimit = count)
+ }
+
+ // 비밀방 설정
+ fun togglePrivate(isPrivate: Boolean) {
+ _uiState.value = _uiState.value.copy(
+ isPrivate = isPrivate,
+ password = if (!isPrivate) "" else _uiState.value.password
+ )
+ }
+
+ // 비밀번호 설정
+ fun updatePassword(password: String) {
+ _uiState.value = _uiState.value.copy(password = password)
+ }
+
+ // 그룹 생성 요청
+ fun createGroup(onSuccess: () -> Unit, onError: (String) -> Unit) {
+ val currentState = _uiState.value
+
+ if (!currentState.isFormValid) {
+ //onError("입력 정보를 확인해주세요")
+ return
+ }
+
+ viewModelScope.launch {
+ try {
+ _uiState.value = currentState.copy(isLoading = true, errorMessage = null)
+
+ val request = currentState.toRequest()
+ val result = groupRepository.createGroup(request)
+
+ if (result.isSuccess) {
+ onSuccess()
+ } else {
+ //onError(result.message ?: "그룹 생성에 실패했습니다")
+ }
+ } catch (e: Exception) {
+ //onError("네트워크 오류가 발생했습니다: ${e.message}")
+ } finally {
+ _uiState.value = _uiState.value.copy(isLoading = false)
+ }
+ }
+ }
+
+ // 에러 메시지 클리어
+ fun clearError() {
+ _uiState.value = _uiState.value.copy(errorMessage = null)
+ }
+}
+
+// Repository 예시
+interface GroupRepository {
+ suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult
+}
+
+// API 응답 클래스 예시
+data class ApiResult(
+ val isSuccess: Boolean,
+ val data: T? = null,
+ val message: String? = null
+)
+
+data class GroupCreateResponse(
+ val groupId: String,
+ val groupName: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt
index c7e3f7a8..a6fb52fd 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt
@@ -16,6 +16,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.texthip.thip.R
+import com.texthip.thip.ui.common.buttons.GenreChipRow
import com.texthip.thip.ui.common.cards.CardItemRoom
import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData
import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData
@@ -29,8 +30,7 @@ fun GroupRoomDeadlineSection(
roomSections: List,
onRoomClick: (GroupCardItemRoomData) -> Unit
) {
- val cardWidth = 320.dp
- val pageSpacing = 12.dp
+ val sideMargin = 30.dp
val pagerState = rememberPagerState(
initialPage = 0,
@@ -44,20 +44,24 @@ fun GroupRoomDeadlineSection(
) {
BoxWithConstraints(
modifier = Modifier
- .fillMaxWidth()
- .height(588.dp),
+ .fillMaxWidth(),
contentAlignment = Alignment.Center
) {
- val horizontalPadding = ((maxWidth - cardWidth) / 2).coerceAtLeast(0.dp)
+ val horizontalPadding = sideMargin
+ val cardWidth = maxWidth - (horizontalPadding * 2)
+ val scale = 0.9f
+ val desiredGap = 12.dp // TODO: 이 부분을 10dp로 하면 양 옆의 카드에 살짝 다음 내용이 보여서 12정도가 어떤지
+
+ val pageSpacing = (-(cardWidth - (cardWidth * scale)) / 2) + desiredGap
HorizontalPager(
state = pagerState,
- contentPadding = PaddingValues(horizontal = horizontalPadding),
+ contentPadding = PaddingValues(horizontal = 30.dp),
pageSpacing = pageSpacing,
modifier = Modifier.fillMaxWidth()
) { page ->
val section = roomSections[page]
- var selectedGenre by remember { mutableStateOf(0) }
+ var selectedGenre by remember { mutableIntStateOf(0) }
val isCurrent = pagerState.currentPage == page
val scale = if (isCurrent) 1f else 0.9f
@@ -79,7 +83,7 @@ fun GroupRoomDeadlineSection(
),
shape = RoundedCornerShape(14.dp)
)
- .padding(vertical = 20.dp, horizontal = 12.dp)
+ .padding(vertical = 20.dp, horizontal = 20.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
@@ -127,14 +131,6 @@ fun GroupRoomDeadlineSection(
}
}
}
-
- SimplePagerIndicator(
- pageCount = roomSections.size,
- currentPage = pagerState.currentPage,
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(top = 8.dp)
- )
}
}
@@ -259,4 +255,4 @@ fun PreviewGroupRoomPagerSection() {
onRoomClick = {}
)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt
index 98ac640b..9b5a2d6b 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt
@@ -57,7 +57,7 @@ fun GroupMainCard(
Card(
modifier = Modifier
- .width(320.dp)
+ .fillMaxWidth()
.height(176.dp)
.clickable { onClick() },
shape = RoundedCornerShape(18.dp),
diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt
index 69147a66..3b2ab2a2 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt
@@ -21,16 +21,20 @@ fun GroupPager(
groupCards: List,
onCardClick: (GroupCardData) -> Unit
) {
- val cardWidth = 320.dp
- val pageSpacing = 6.dp
+ val scale = 0.86f
+ val desiredGap = 10.dp
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.height(192.dp)
) {
- val screenWidth = maxWidth
- val horizontalPadding = ((screenWidth - cardWidth) / 2).coerceAtLeast(0.dp)
+ val horizontalPadding = 30.dp
+ val cardWidth = maxWidth - (horizontalPadding * 2)
+
+ val pageSpacing = with(this) {
+ (-(cardWidth - (cardWidth * scale)) / 2f) + desiredGap
+ }
val pagerState = rememberPagerState(
initialPage = 0,
diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt
index 1938fbe2..7f9b4889 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt
@@ -70,11 +70,11 @@ fun GroupMyScreen(
}
)
- Spacer(modifier = Modifier.height(20.dp))
+ Spacer(modifier = Modifier.height(10.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(20.dp),
- contentPadding = PaddingValues(bottom = 20.dp),
+ contentPadding = PaddingValues(top = 10.dp, bottom = 20.dp),
modifier = Modifier
.fillMaxSize()
) {
diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
index cfd918e8..e9746096 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
@@ -90,7 +90,7 @@ fun GroupScreen(
roomSections = roomSections,
onRoomClick = { viewModel.onRoomCardClick(it) }
)
- Spacer(Modifier.height(32.dp))
+ Spacer(Modifier.height(102.dp))
}
// 오른쪽 하단 FAB
FloatingButton(
diff --git a/app/src/main/java/com/texthip/thip/ui/myPage/screen/MyPageScreen.kt b/app/src/main/java/com/texthip/thip/ui/myPage/screen/MyPageScreen.kt
index e746dfca..e5f3d666 100644
--- a/app/src/main/java/com/texthip/thip/ui/myPage/screen/MyPageScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/myPage/screen/MyPageScreen.kt
@@ -109,7 +109,7 @@ fun MyPageScreen(
)
MenuItemButton(
text = stringResource(R.string.notification_settings),
- icon = painterResource(R.drawable.ic_notice),
+ icon = painterResource(R.drawable.ic_notification),
contentColor = colors.White,
backgroundColor = colors.DarkGrey02,
hasRightIcon = true,
diff --git a/app/src/main/res/drawable/ic_notice.xml b/app/src/main/res/drawable/ic_notice.xml
index 4cec80dd..5ccd9f64 100644
--- a/app/src/main/res/drawable/ic_notice.xml
+++ b/app/src/main/res/drawable/ic_notice.xml
@@ -4,14 +4,14 @@
android:viewportWidth="25"
android:viewportHeight="24">
+
+
diff --git a/app/src/main/res/drawable/ic_x_circle_grey.xml b/app/src/main/res/drawable/ic_x_circle_grey.xml
new file mode 100644
index 00000000..300f0c30
--- /dev/null
+++ b/app/src/main/res/drawable/ic_x_circle_grey.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d3b28e70..1880926f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -166,4 +166,42 @@
내 기록
페이지별 보기
총평 보기
+
+
+ 모임 만들기
+ 저장한 책
+ 모임 책
+ 책 제목, 저자 검색
+ 책 등록하기
+ 현재 등록된 책이 아닙니다.\n원하시는 책을 신청해주세요.
+ 책 신청
+ 이메일로 책 제목, 출판사를 보내주시면
+ 빠른 시일내로 책을 추가해드릴게요!
+ 책 선택
+ 책 장르
+ 책을 가장 잘 설명하는 장르를 하나 골라주세요
+ 방 제목
+ 방 제목을 입력해주세요
+ 한 줄 소개
+ 방에 대한 짧은 소개글을 작성해주세요
+ 모임 활동기간
+ 모임 활동 기간은 최대 3개월까지 설정 가능합니다.
+ (선택된 기간: %1$d일)
+ 인원 제한
+ 모집 인원은 최대 30명까지 입력할 수 있습니다.
+ 명의 독서 메이트를 모집합니다
+ 1개만 선택 가능합니다.
+ %1$s 저
+ 4자리 숫자로 입장 비밀번호를 설정
+ 년
+ 월
+ 일
+ 공개 설정
+ 비공개로 설정하기
+ 4자리 숫자를 입력해주세요.
+ %1$d월 %2$d일 자정에 자동으로 모집 마감되고 활동이 가능합니다.
+ 검색해서 찾기
+ 모임방 활동이 시작되면, 독서메이트 모집이 자동으로 종료돼요.
+ %1$d / %2$d
+
\ No newline at end of file