Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 99 additions & 21 deletions app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package com.texthip.thip.ui.common.buttons

import android.annotation.SuppressLint
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.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.texthip.thip.ui.theme.ThipTheme

@SuppressLint("ConfigurationScreenWidthHeight")
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using @SuppressLint for ConfigurationScreenWidthHeight should be avoided. Consider using LocalDensity or other Compose-native approaches to handle screen size detection instead of suppressing the lint warning.

Copilot uses AI. Check for mistakes.
@Composable
fun GenreChipRow(
modifier: Modifier = Modifier.width(4.dp),
Expand All @@ -21,35 +27,95 @@ fun GenreChipRow(
onSelect: (Int) -> Unit,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = horizontalArrangement
) {
genres.forEachIndexed { idx, genre ->
OptionChipButton(
modifier = Modifier
.clip(RoundedCornerShape(20.dp)), // 버튼 모양에 맞게 클리핑
text = genre,
isFilled = true,
isSelected = selectedIndex == idx,
onClick = {
if (selectedIndex == idx) {
onSelect(-1)
} else {
onSelect(idx)
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp

if (screenWidthDp < 360) {
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded value 360 should be extracted as a named constant to improve code readability and maintainability.

Copilot uses AI. Check for mistakes.
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.Center
) {
genres.take(3).forEachIndexed { idx, genre ->
OptionChipButton(
modifier = Modifier
.clip(RoundedCornerShape(20.dp)),
text = genre,
isFilled = true,
isSelected = selectedIndex == idx,
onClick = {
if (selectedIndex == idx) {
onSelect(-1)
} else {
onSelect(idx)
}
}
)
if (idx < 2) {
Spacer(modifier = modifier)
}
}
}
Spacer(modifier = Modifier.height(8.dp))

Row(
horizontalArrangement = Arrangement.Center
) {
genres.drop(3).forEachIndexed { relativeIdx, genre ->
val idx = relativeIdx + 3
OptionChipButton(
modifier = Modifier
.clip(RoundedCornerShape(20.dp)),
text = genre,
isFilled = true,
isSelected = selectedIndex == idx,
onClick = {
if (selectedIndex == idx) {
onSelect(-1)
} else {
onSelect(idx)
}
}
)
if (relativeIdx < genres.drop(3).size - 1) {
Spacer(modifier = modifier)
}
}
)
if (idx < genres.size - 1) {
Spacer(modifier = modifier)
}
}
} else {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = horizontalArrangement
) {
genres.forEachIndexed { idx, genre ->
OptionChipButton(
modifier = Modifier
.clip(RoundedCornerShape(20.dp)),
text = genre,
isFilled = true,
isSelected = selectedIndex == idx,
onClick = {
if (selectedIndex == idx) {
onSelect(-1)
} else {
onSelect(idx)
}
}
)
if (idx < genres.size - 1) {
Spacer(modifier = modifier)
}
}
}
}
}

@Preview()
@Preview(name = "Normal Screen (>=360dp)", widthDp = 400)
@Composable
fun PreviewGenreChipRow() {
fun PreviewGenreChipRowNormal() {
ThipTheme {
GenreChipRow(
genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술"),
Expand All @@ -58,3 +124,15 @@ fun PreviewGenreChipRow() {
)
}
}

@Preview(name = "Small Screen (<360dp)", widthDp = 320)
@Composable
fun PreviewGenreChipRowSmall() {
ThipTheme {
GenreChipRow(
genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술"),
selectedIndex = 2,
onSelect = {}
)
}
}
54 changes: 37 additions & 17 deletions app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.texthip.thip.R
import com.texthip.thip.ui.common.buttons.OutlinedButton
import com.texthip.thip.ui.theme.ThipTheme.colors
Expand All @@ -34,7 +35,9 @@ fun CardInputBook(
modifier: Modifier = Modifier,
title: String,
author: String,
imageRes: Int? = R.drawable.img_book_cover_sample, // 기본 이미지 리소스
imageUrl: String? = null, // 이미지 URL (AsyncImage 사용)
imageRes: Int? = R.drawable.img_book_cover_sample, // 기본 이미지 리소스 (fallback)
showChangeButton: Boolean = true, // 변경 버튼 표시 여부
onChangeClick: () -> Unit = {}
) {
Row(
Expand All @@ -47,14 +50,26 @@ fun CardInputBook(
modifier = Modifier
.size(width = 60.dp, height = 80.dp)
) {

imageRes?.let {
Image(
painter = painterResource(id = it),
if (!imageUrl.isNullOrBlank()) {
// URL 이미지가 있는 경우 AsyncImage 사용
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
contentScale = ContentScale.Crop,
fallback = imageRes?.let { painterResource(id = it) },
error = imageRes?.let { painterResource(id = it) }
)
} else {
// URL이 없는 경우 기본 이미지 리소스 사용
imageRes?.let {
Image(
painter = painterResource(id = it),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}

Expand All @@ -76,20 +91,24 @@ fun CardInputBook(
Text(
text = stringResource(R.string.card_input_author, author),
style = typography.view_m500_s12_h20,
color = colors.Grey01
color = colors.Grey01,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// 텍스트 정보와 버튼 사이 19dp 고정 간격
Spacer(modifier = Modifier.width(19.dp))

OutlinedButton(
modifier = Modifier
.size(width = 49.dp, height = 33.dp)
.align(Alignment.Bottom),
text = stringResource(R.string.change),
textStyle = typography.view_m500_s14,
onClick = onChangeClick
)
if (showChangeButton) {
Spacer(modifier = Modifier.width(19.dp))

OutlinedButton(
modifier = Modifier
.size(width = 49.dp, height = 33.dp)
.align(Alignment.Bottom),
text = stringResource(R.string.change),
textStyle = typography.view_m500_s14,
onClick = onChangeClick
)
}
}
}

Expand All @@ -105,6 +124,7 @@ fun CardInputBookPreview() {
CardInputBook(
title = "책제목입니다.책제목입니다.책제목입니다.책제목입니다.책제목입니다.",
author = "리처드 도킨스",
imageUrl = null, // 기본 이미지 사용
onChangeClick = {}
)
}
Expand Down
36 changes: 27 additions & 9 deletions app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,31 @@ fun MyFeedCard(
onBookClick: () -> Unit = {}
) {
val hasImages = feedItem.imageUrls.isNotEmpty()
val maxLines = if (hasImages) 3 else 8
var isTextTruncated by remember { mutableStateOf(false) }
val maxTextLines = if (hasImages) 3 else 8

// 실제 텍스트 줄 수를 기준으로 표시할 텍스트 계산
val processedText = remember(feedItem.content, hasImages) {
val lines = feedItem.content.split("\n")
val nonEmptyLines = mutableListOf<Int>() // 실제 텍스트가 있는 줄의 인덱스

lines.forEachIndexed { index, line ->
if (line.trim().isNotEmpty()) {
nonEmptyLines.add(index)
}
}

if (nonEmptyLines.size <= maxTextLines) {
// 실제 텍스트 줄이 제한보다 적으면 전체 표시
feedItem.content
} else {
// 실제 텍스트 줄이 제한을 초과하면, maxTextLines 번째 텍스트 줄까지만 표시
val lastAllowedLineIndex = nonEmptyLines[maxTextLines - 1]
lines.take(lastAllowedLineIndex + 1).joinToString("\n")
}
Comment on lines +60 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

개행(‘\n’) 기준 자르기는 시각적 줄 수를 반영하지 않아 긴 단락이 통째로 노출됩니다

프리뷰의 feed1(라인 218)의 긴 단일 문단처럼 개행이 없으면 현재 로직은 전체 본문을 그대로 출력해 “…더보기”도 표시되지 않습니다. 실제 UI 줄바꿈(폭 기준)을 반영하려면 TextmaxLinesonTextLayout.hasVisualOverflow를 함께 사용해 시각적 줄수 기반 클램핑을 해야 합니다. 아래처럼 최소 수정으로 보강해 주세요.

적용 diff 1 (잘림 상태 계산 보강):

-    // 잘림 여부는 파생 값으로 계산
-    val isTextTruncated = processedText != feedItem.content
+    // 시각적 줄바꿈까지 고려한 잘림 여부 상태
+    var isTextTruncated by remember(processedText, maxTextLines) {
+        mutableStateOf(processedText != feedItem.content)
+    }

적용 diff 2 (Text에 시각적 클램핑 추가):

             Text(
                 text = processedText,
                 style = typography.feedcopy_r400_s14_h20,
                 color = colors.White,
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(top = 16.dp),
+                maxLines = maxTextLines,
+                overflow = TextOverflow.Clip,
+                onTextLayout = { result ->
+                    isTextTruncated = result.hasVisualOverflow || (processedText != feedItem.content)
+                }
             )

추가 import:

import androidx.compose.ui.text.style.TextOverflow
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt around
lines 60-67, the current logic slices content by '\n' which doesn't reflect
visual line-wrapping; replace this with a visual-line-aware clamp: keep the
existing newline-based fallback but render the preview Text with maxLines =
maxTextLines and provide onTextLayout = { if (it.hasVisualOverflow) set a flag
to indicate overflow } so you can decide whether to show the full text or the
clamped preview; add TextOverflow import and set overflow =
TextOverflow.Ellipsis (or Clip) on the Text; update the boolean/previewText
selection to prefer the visual-clamped output when onTextLayout reports
overflow, falling back to the newline-sliced string only when needed.

}

// 잘림 여부는 파생 값으로 계산
val isTextTruncated = processedText != feedItem.content

Column(
modifier = modifier
Expand Down Expand Up @@ -99,20 +122,15 @@ fun MyFeedCard(
.clickable { onContentClick() }
) {
Text(
text = feedItem.content,
text = processedText,
style = typography.feedcopy_r400_s14_h20,
color = colors.White,
maxLines = maxLines,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
// 3. onTextLayout 콜백을 사용하여 텍스트가 잘렸는지 확인
onTextLayout = { textLayoutResult ->
isTextTruncated = textLayoutResult.hasVisualOverflow
}
)

// 4. 텍스트가 잘렸을 경우에만 "...더보기" 이미지를 우측 하단에 표시
// 텍스트가 잘린 경우에만 "...더보기" 표시
if (isTextTruncated) {
Image(
painter = painterResource(id = R.drawable.ic_text_more),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
Expand Down Expand Up @@ -145,6 +147,7 @@ fun FeedCommentScreen(
)
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun FeedCommentContent(
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -330,11 +333,13 @@ private fun FeedCommentContent(
}
}
if (feedDetail.tagList.isNotEmpty()) {
Row(
Modifier
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 20.dp, end = 20.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxLines = 2 // 최대 2줄로 제한
) {
feedDetail.tagList.forEach { tag ->
OptionChipButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,21 @@ class FeedWriteViewModel @Inject constructor(
updateState { it.copy(selectedBook = book) }
}

fun setPreselectedBookForFeed(isbn: String, bookTitle: String, bookAuthor: String, bookImageUrl: String) {
val preselectedBook = BookData(
title = bookTitle,
imageUrl = bookImageUrl,
author = bookAuthor,
isbn = isbn
)
updateState {
it.copy(
selectedBook = preselectedBook,
isBookPreselected = true
)
}
}

fun toggleBookSearchSheet(show: Boolean) {
updateState { it.copy(showBookSearchSheet = show) }
if (show) {
Expand Down
Loading