UI 도 고치고... 이것저것 손댔다. AI 시켜서.
This commit is contained in:
+672
-671
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.wordpuzzle.hangul"
|
package="com.wordpuzzle.hangul"
|
||||||
android:versionCode="1"
|
android:versionCode="2"
|
||||||
android:versionName="1.0.1" >
|
android:versionName="1.0.1" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:testOnly="true"
|
||||||
android:theme="@style/Theme.WordPuzzle" >
|
android:theme="@style/Theme.WordPuzzle" >
|
||||||
|
|
||||||
<!-- AdMob Sample App ID for testing -->
|
<!-- AdMob Sample App ID for testing -->
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.wordpuzzle.hangul"
|
package="com.wordpuzzle.hangul"
|
||||||
android:versionCode="1"
|
android:versionCode="2"
|
||||||
android:versionName="1.0.1" >
|
android:versionName="1.0.1" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:testOnly="true"
|
||||||
android:theme="@style/Theme.WordPuzzle" >
|
android:theme="@style/Theme.WordPuzzle" >
|
||||||
|
|
||||||
<!-- AdMob Sample App ID for testing -->
|
<!-- AdMob Sample App ID for testing -->
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.wordpuzzle.hangul"
|
package="com.wordpuzzle.hangul"
|
||||||
android:versionCode="1"
|
android:versionCode="2"
|
||||||
android:versionName="1.0.1" >
|
android:versionName="1.0.1" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:testOnly="true"
|
||||||
android:theme="@style/Theme.WordPuzzle" >
|
android:theme="@style/Theme.WordPuzzle" >
|
||||||
|
|
||||||
<!-- AdMob Sample App ID for testing -->
|
<!-- AdMob Sample App ID for testing -->
|
||||||
|
|||||||
@@ -1,35 +1,54 @@
|
|||||||
package com.wordpuzzle.hangul.ui.game
|
package com.wordpuzzle.hangul.ui.game
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.SwapHoriz
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.wordpuzzle.hangul.data.model.CrosswordCell
|
||||||
import com.wordpuzzle.hangul.data.model.CrosswordClue
|
import com.wordpuzzle.hangul.data.model.CrosswordClue
|
||||||
import com.wordpuzzle.hangul.data.model.Direction
|
import com.wordpuzzle.hangul.data.model.Direction
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClueBar(
|
fun ClueBar(
|
||||||
|
grid: List<List<CrosswordCell>>,
|
||||||
activeClue: CrosswordClue?,
|
activeClue: CrosswordClue?,
|
||||||
clues: List<CrosswordClue>,
|
clues: List<CrosswordClue>,
|
||||||
onSwitchDirection: () -> Unit,
|
focusedRow: Int = -1,
|
||||||
|
focusedCol: Int = -1,
|
||||||
isExpandedMode: Boolean = false,
|
isExpandedMode: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val activePalette = getCluePalette(clues, activeClue)
|
val clueFontSize = if (isExpandedMode) 13.5.sp else 12.sp
|
||||||
val barHeight = if (isExpandedMode) 68.dp else 50.dp
|
val barHeight = if (isExpandedMode) 78.dp else 70.dp
|
||||||
val clueFontSize = if (isExpandedMode) 14.sp else 13.sp
|
|
||||||
|
val matchingClues = remember(focusedRow, focusedCol, clues, activeClue) {
|
||||||
|
if (focusedRow >= 0 && focusedCol >= 0) {
|
||||||
|
val found = clues.filter { isCellInClue(focusedRow, focusedCol, it) }
|
||||||
|
if (found.isNotEmpty()) {
|
||||||
|
found.sortedBy { it.direction }
|
||||||
|
} else if (activeClue != null) {
|
||||||
|
listOf(activeClue)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} else if (activeClue != null) {
|
||||||
|
listOf(activeClue)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstClue = matchingClues.getOrNull(0)
|
||||||
|
val secondClue = matchingClues.getOrNull(1)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -37,23 +56,79 @@ fun ClueBar(
|
|||||||
.height(barHeight)
|
.height(barHeight)
|
||||||
.padding(horizontal = 10.dp, vertical = 2.dp),
|
.padding(horizontal = 10.dp, vertical = 2.dp),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = Color(0xFFF1F5F9),
|
color = Color.Transparent,
|
||||||
shadowElevation = 2.dp
|
shadowElevation = 0.dp
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 10.dp, vertical = 2.dp)
|
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxSize(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
// 첫 번째 힌트 줄
|
||||||
|
if (firstClue != null) {
|
||||||
|
SingleClueItem(
|
||||||
|
clue = firstClue,
|
||||||
|
grid = grid,
|
||||||
|
clues = clues,
|
||||||
|
clueFontSize = clueFontSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "퍼즐의 빈 칸을 터치해 단어를 시작하세요.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 두 번째 힌트 줄 (위치 고정을 위해 항상 영역 확보)
|
||||||
|
if (secondClue != null) {
|
||||||
|
SingleClueItem(
|
||||||
|
clue = secondClue,
|
||||||
|
grid = grid,
|
||||||
|
clues = clues,
|
||||||
|
clueFontSize = clueFontSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.fillMaxWidth().height(22.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SingleClueItem(
|
||||||
|
clue: CrosswordClue,
|
||||||
|
grid: List<List<CrosswordCell>>,
|
||||||
|
clues: List<CrosswordClue>,
|
||||||
|
clueFontSize: TextUnit
|
||||||
|
) {
|
||||||
|
val isCompleted = isClueCompleted(grid, clue)
|
||||||
|
val palette = getCluePalette(clues, clue)
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState(0)
|
||||||
|
|
||||||
|
// 새로운 단어가 선택될 때마다 힌트 텍스트 스크롤 위치를 맨 앞으로 리셋
|
||||||
|
LaunchedEffect(clue.id) {
|
||||||
|
scrollState.scrollTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (activeClue != null) {
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(4.dp),
|
shape = RoundedCornerShape(4.dp),
|
||||||
color = activePalette.accent,
|
color = if (isCompleted) Color(0xFF388E3C) else palette.accent,
|
||||||
modifier = Modifier.padding(end = 6.dp)
|
modifier = Modifier.padding(end = 6.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (activeClue.direction == Direction.ACROSS) "가로 ${activeClue.number}" else "세로 ${activeClue.number}",
|
text = if (clue.direction == Direction.ACROSS) {
|
||||||
|
if (isCompleted) "가로 ${clue.number} ✓" else "가로 ${clue.number}"
|
||||||
|
} else {
|
||||||
|
if (isCompleted) "세로 ${clue.number} ✓" else "세로 ${clue.number}"
|
||||||
|
},
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -61,76 +136,29 @@ fun ClueBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizedText(
|
// 힌트 텍스트가 길 경우 2줄로 넘어가지 않고 가로로 부드럽게 스크롤되도록 처리
|
||||||
text = activeClue.clue,
|
Box(
|
||||||
targetFontSize = clueFontSize,
|
modifier = Modifier
|
||||||
minFontSize = 8.5.sp,
|
.weight(1f)
|
||||||
fontWeight = FontWeight.Medium,
|
.horizontalScroll(scrollState)
|
||||||
color = Color(0xFF263238),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
maxLines = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = onSwitchDirection,
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.SwapHoriz,
|
|
||||||
contentDescription = "가로/세로 전환",
|
|
||||||
tint = activePalette.accent,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
Text(
|
||||||
text = "퍼즐의 빈 칸을 터치해 단어를 시작하세요.",
|
text = clue.clue,
|
||||||
fontSize = 12.sp,
|
fontSize = clueFontSize,
|
||||||
color = Color.Gray,
|
fontWeight = FontWeight.Medium,
|
||||||
modifier = Modifier.weight(1f)
|
color = if (isCompleted) Color(0xFF1B5E20) else Color(0xFF263238),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun isCellInClue(r: Int, c: Int, clue: CrosswordClue?): Boolean {
|
||||||
fun AutoResizedText(
|
if (clue == null) return false
|
||||||
text: String,
|
return if (clue.direction == Direction.ACROSS) {
|
||||||
modifier: Modifier = Modifier,
|
r == clue.startRow && c in clue.startCol until (clue.startCol + clue.length)
|
||||||
color: Color = Color.Unspecified,
|
|
||||||
fontWeight: FontWeight? = null,
|
|
||||||
minFontSize: TextUnit = 8.5.sp,
|
|
||||||
targetFontSize: TextUnit = 13.sp,
|
|
||||||
maxLines: Int = 2,
|
|
||||||
style: TextStyle = LocalTextStyle.current
|
|
||||||
) {
|
|
||||||
var resizedFontSize by remember(text) { mutableStateOf(targetFontSize) }
|
|
||||||
var shouldDraw by remember(text) { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 폰트 크기에 비례하여 줄 간격을 촘촘하게 축소하여 2번째 줄 잘림 완벽 방지
|
|
||||||
val computedLineHeight = (resizedFontSize.value * 1.18f).sp
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = color,
|
|
||||||
fontWeight = fontWeight,
|
|
||||||
fontSize = resizedFontSize,
|
|
||||||
lineHeight = computedLineHeight,
|
|
||||||
maxLines = maxLines,
|
|
||||||
overflow = TextOverflow.Clip,
|
|
||||||
onTextLayout = { result ->
|
|
||||||
if (result.hasVisualOverflow && resizedFontSize > minFontSize) {
|
|
||||||
resizedFontSize = (resizedFontSize.value - 0.5f).coerceAtLeast(minFontSize.value).sp
|
|
||||||
} else {
|
} else {
|
||||||
shouldDraw = true
|
c == clue.startCol && r in clue.startRow until (clue.startRow + clue.length)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = modifier.drawWithContent {
|
|
||||||
if (shouldDraw) {
|
|
||||||
drawContent()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = style
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ fun getCluePalette(clues: List<CrosswordClue>, clue: CrosswordClue?): PastelPale
|
|||||||
return PASTEL_PALETTES[index % PASTEL_PALETTES.size]
|
return PASTEL_PALETTES[index % PASTEL_PALETTES.size]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 단어가 완성(해당 단어의 모든 셀에 정확한 글자가 입력)되었는지 검증합니다.
|
||||||
|
*/
|
||||||
|
fun isClueCompleted(grid: List<List<CrosswordCell>>, clue: CrosswordClue): Boolean {
|
||||||
|
for (i in 0 until clue.length) {
|
||||||
|
val r = if (clue.direction == Direction.ACROSS) clue.startRow else clue.startRow + i
|
||||||
|
val c = if (clue.direction == Direction.ACROSS) clue.startCol + i else clue.startCol
|
||||||
|
if (r !in grid.indices || c !in grid[r].indices) return false
|
||||||
|
val cell = grid[r][c]
|
||||||
|
if (cell.isBlank || cell.userChar == null || cell.userChar != cell.correctChar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CrosswordGrid(
|
fun CrosswordGrid(
|
||||||
grid: List<List<CrosswordCell>>,
|
grid: List<List<CrosswordCell>>,
|
||||||
@@ -61,15 +77,15 @@ fun CrosswordGrid(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
// 절대 원칙: 스크린 가로 폭 전체에 100% 무조건 맞춰서 정사각형 퍼즐 보드 생성
|
// 스크린 가로 폭 전체에 무조건 맞춰서 정사각형 퍼즐 보드 생성
|
||||||
val fullWidth = maxWidth - 6.dp
|
val fullWidth = maxWidth - 6.dp
|
||||||
val boardSize = fullWidth.coerceAtLeast(120.dp)
|
val boardSize = fullWidth.coerceAtLeast(120.dp)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(boardSize),
|
modifier = Modifier.size(boardSize),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = Color(0xFFECEFF1),
|
color = Color.Transparent,
|
||||||
shadowElevation = 3.dp
|
shadowElevation = 0.dp
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -91,6 +107,7 @@ fun CrosswordGrid(
|
|||||||
val isFocused = (r == focusedRow && c == focusedCol)
|
val isFocused = (r == focusedRow && c == focusedCol)
|
||||||
val matchingClues = clues.filter { isCellInClue(r, c, it) }
|
val matchingClues = clues.filter { isCellInClue(r, c, it) }
|
||||||
val isInActiveClue = isCellInClue(r, c, activeClue)
|
val isInActiveClue = isCellInClue(r, c, activeClue)
|
||||||
|
val isCompletedWord = matchingClues.isNotEmpty() && matchingClues.any { isClueCompleted(grid, it) }
|
||||||
|
|
||||||
GridCellView(
|
GridCellView(
|
||||||
cell = cell,
|
cell = cell,
|
||||||
@@ -99,6 +116,7 @@ fun CrosswordGrid(
|
|||||||
activeClue = activeClue,
|
activeClue = activeClue,
|
||||||
isFocused = isFocused,
|
isFocused = isFocused,
|
||||||
isInActiveClue = isInActiveClue,
|
isInActiveClue = isInActiveClue,
|
||||||
|
isCompletedWord = isCompletedWord,
|
||||||
gridSize = gridSize,
|
gridSize = gridSize,
|
||||||
onClick = { onCellClick(r, c) },
|
onClick = { onCellClick(r, c) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -122,6 +140,7 @@ fun GridCellView(
|
|||||||
activeClue: CrosswordClue?,
|
activeClue: CrosswordClue?,
|
||||||
isFocused: Boolean,
|
isFocused: Boolean,
|
||||||
isInActiveClue: Boolean,
|
isInActiveClue: Boolean,
|
||||||
|
isCompletedWord: Boolean,
|
||||||
gridSize: Int,
|
gridSize: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -149,6 +168,7 @@ fun GridCellView(
|
|||||||
|
|
||||||
val backgroundBrush = when {
|
val backgroundBrush = when {
|
||||||
isFocused -> Brush.linearGradient(listOf(Color(0xFFFFE082), Color(0xFFFFD54F)))
|
isFocused -> Brush.linearGradient(listOf(Color(0xFFFFE082), Color(0xFFFFD54F)))
|
||||||
|
isCompletedWord -> Brush.linearGradient(listOf(Color(0xFFC8E6C9), Color(0xFFA5D6A7))) // 완성된 단어: 초록색 배경
|
||||||
isIntersection -> Brush.linearGradient(listOf(primaryPalette.bg, secondaryPalette!!.bg))
|
isIntersection -> Brush.linearGradient(listOf(primaryPalette.bg, secondaryPalette!!.bg))
|
||||||
else -> Brush.linearGradient(listOf(primaryPalette.bg, primaryPalette.bg))
|
else -> Brush.linearGradient(listOf(primaryPalette.bg, primaryPalette.bg))
|
||||||
}
|
}
|
||||||
@@ -161,11 +181,12 @@ fun GridCellView(
|
|||||||
|
|
||||||
val borderColor = when {
|
val borderColor = when {
|
||||||
isFocused -> PrimaryIndigo
|
isFocused -> PrimaryIndigo
|
||||||
|
isCompletedWord -> Color(0xFF388E3C) // 완성된 단어: 진한 녹색 테두리
|
||||||
isInActiveClue -> activeBorderColor
|
isInActiveClue -> activeBorderColor
|
||||||
else -> primaryPalette.border
|
else -> primaryPalette.border
|
||||||
}
|
}
|
||||||
|
|
||||||
val borderWidth = if (isFocused) 2.5.dp else if (isInActiveClue) 2.dp else 1.2.dp
|
val borderWidth = if (isFocused) 2.5.dp else if (isCompletedWord || isInActiveClue) 2.dp else 1.2.dp
|
||||||
|
|
||||||
val charFontSize = when {
|
val charFontSize = when {
|
||||||
gridSize <= 5 -> 22.sp
|
gridSize <= 5 -> 22.sp
|
||||||
@@ -188,7 +209,11 @@ fun GridCellView(
|
|||||||
text = clueNum.toString(),
|
text = clueNum.toString(),
|
||||||
fontSize = badgeFontSize,
|
fontSize = badgeFontSize,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
color = if (isFocused) PrimaryIndigoDark else Color(0xFF546E7A),
|
color = when {
|
||||||
|
isFocused -> PrimaryIndigoDark
|
||||||
|
isCompletedWord -> Color(0xFF1B5E20)
|
||||||
|
else -> Color(0xFF546E7A)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.padding(start = 2.dp, top = 1.dp)
|
.padding(start = 2.dp, top = 1.dp)
|
||||||
@@ -201,6 +226,7 @@ fun GridCellView(
|
|||||||
fontSize = charFontSize,
|
fontSize = charFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = when {
|
color = when {
|
||||||
|
isCompletedWord -> Color(0xFF1B5E20) // 완성된 단어 글자: 짙은 선명 녹색
|
||||||
cell.isRevealedByHint -> CorrectGreen
|
cell.isRevealedByHint -> CorrectGreen
|
||||||
else -> TextPrimary
|
else -> TextPrimary
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,21 +99,12 @@ fun GameScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Column {
|
|
||||||
Text(
|
Text(
|
||||||
text = "스테이지 $stageNumber",
|
text = "스테이지 $stageNumber",
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color(0xFF212121)
|
color = Color(0xFF212121)
|
||||||
)
|
)
|
||||||
uiState.stage?.let {
|
|
||||||
Text(
|
|
||||||
text = it.title,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
color = Color(0xFF757575)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
@@ -128,16 +119,16 @@ fun GameScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
// 1. 새로고침 (셔플) 버튼 - 힌트 버튼 왼편 배치
|
// 1. 새로고침 (셔플) 버튼
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.onShuffleTiles() },
|
onClick = { viewModel.onShuffleTiles() },
|
||||||
modifier = Modifier.size(34.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Refresh,
|
imageVector = Icons.Default.Refresh,
|
||||||
contentDescription = "문자 새로고침 및 재배치",
|
contentDescription = "문자 새로고침 및 재배치",
|
||||||
tint = PrimaryIndigo,
|
tint = PrimaryIndigo,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,15 +139,15 @@ fun GameScreen(
|
|||||||
containerColor = Color(0xFFFFF3E0),
|
containerColor = Color(0xFFFFF3E0),
|
||||||
contentColor = SecondaryOrange
|
contentColor = SecondaryOrange
|
||||||
),
|
),
|
||||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
|
||||||
modifier = Modifier.height(34.dp)
|
modifier = Modifier.height(32.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Lightbulb,
|
imageVector = Icons.Default.Lightbulb,
|
||||||
contentDescription = "힌트",
|
contentDescription = "힌트",
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(15.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(3.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${uiState.hintCount}",
|
text = "${uiState.hintCount}",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -165,6 +156,7 @@ fun GameScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
windowInsets = TopAppBarDefaults.windowInsets,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = BackgroundLight
|
containerColor = BackgroundLight
|
||||||
)
|
)
|
||||||
@@ -223,9 +215,11 @@ fun GameScreen(
|
|||||||
Spacer(modifier = Modifier.height(if (isUltraLongRatio) 8.dp else 4.dp))
|
Spacer(modifier = Modifier.height(if (isUltraLongRatio) 8.dp else 4.dp))
|
||||||
|
|
||||||
ClueBar(
|
ClueBar(
|
||||||
|
grid = stage.grid,
|
||||||
activeClue = uiState.activeClue,
|
activeClue = uiState.activeClue,
|
||||||
clues = stage.clues,
|
clues = stage.clues,
|
||||||
onSwitchDirection = { viewModel.onSwitchDirection() },
|
focusedRow = uiState.focusedRow,
|
||||||
|
focusedCol = uiState.focusedCol,
|
||||||
isExpandedMode = isUltraLongRatio
|
isExpandedMode = isUltraLongRatio
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -22,7 +19,6 @@ import com.wordpuzzle.hangul.data.model.TileItem
|
|||||||
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigo
|
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigo
|
||||||
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigoLight
|
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigoLight
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TilePoolView(
|
fun TilePoolView(
|
||||||
tiles: List<TileItem>,
|
tiles: List<TileItem>,
|
||||||
@@ -38,8 +34,8 @@ fun TilePoolView(
|
|||||||
vertical = if (isExpandedMode) 4.dp else 2.dp
|
vertical = if (isExpandedMode) 4.dp else 2.dp
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = Color.White,
|
color = Color.Transparent,
|
||||||
shadowElevation = 4.dp
|
shadowElevation = 0.dp
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -52,54 +48,42 @@ fun TilePoolView(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
// 하단 문자 패널 - 사용 가능한 타일만 노출
|
val columns = 6
|
||||||
val availableTiles = tiles.filter { !it.isUsed }
|
val chunkedTiles = tiles.chunked(columns)
|
||||||
|
|
||||||
val tileSize = if (isExpandedMode) {
|
val fontSize = if (isExpandedMode) 28.sp else 24.sp
|
||||||
when {
|
val spacingDp = if (isExpandedMode) 6.dp else 5.dp
|
||||||
availableTiles.size > 16 -> 50.dp
|
|
||||||
availableTiles.size > 10 -> 58.dp
|
|
||||||
else -> 66.dp
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when {
|
|
||||||
availableTiles.size > 16 -> 36.dp
|
|
||||||
availableTiles.size > 10 -> 40.dp
|
|
||||||
else -> 44.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val fontSize = if (isExpandedMode) {
|
Column(
|
||||||
when {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
availableTiles.size > 16 -> 24.sp
|
|
||||||
availableTiles.size > 10 -> 28.sp
|
|
||||||
else -> 32.sp
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when {
|
|
||||||
availableTiles.size > 16 -> 16.sp
|
|
||||||
availableTiles.size > 10 -> 18.sp
|
|
||||||
else -> 20.sp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val spacingDp = if (isExpandedMode) 8.dp else 5.dp
|
|
||||||
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(vertical = if (isExpandedMode) 4.dp else 2.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacingDp, Alignment.CenterHorizontally),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(spacingDp)
|
verticalArrangement = Arrangement.spacedBy(spacingDp)
|
||||||
) {
|
) {
|
||||||
availableTiles.forEach { tile ->
|
chunkedTiles.forEach { rowTiles ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacingDp)
|
||||||
|
) {
|
||||||
|
for (i in 0 until columns) {
|
||||||
|
val tile = rowTiles.getOrNull(i)
|
||||||
|
if (tile != null && !tile.isUsed) {
|
||||||
LetterTileButton(
|
LetterTileButton(
|
||||||
tile = tile,
|
tile = tile,
|
||||||
size = tileSize,
|
|
||||||
fontSize = fontSize,
|
fontSize = fontSize,
|
||||||
onClick = { onTileClick(tile) }
|
onClick = { onTileClick(tile) },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.aspectRatio(1f)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// 사용된 타일 및 빈 셀은 동일한 1:1 정사각형 고정 영역 유지
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,13 +93,12 @@ fun TilePoolView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun LetterTileButton(
|
fun LetterTileButton(
|
||||||
tile: TileItem,
|
tile: TileItem,
|
||||||
size: Dp = 40.dp,
|
onClick: () -> Unit,
|
||||||
fontSize: TextUnit = 18.sp,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit
|
fontSize: TextUnit = 24.sp
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.size(size)
|
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.background(PrimaryIndigoLight)
|
.background(PrimaryIndigoLight)
|
||||||
.border(
|
.border(
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ fun MainLobbyScreen(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (previewWordList.isNotEmpty()) {
|
if (previewWordList.isNotEmpty()) {
|
||||||
@@ -241,11 +241,11 @@ fun MainLobbyScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -254,37 +254,43 @@ fun MainLobbyScreen(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = "💡 퍼즐 단어 미리보기",
|
text = "💡 퍼즐 단어 미리보기",
|
||||||
fontSize = 12.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = PrimaryIndigo
|
color = PrimaryIndigo
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Refresh,
|
imageVector = Icons.Default.Refresh,
|
||||||
contentDescription = "다른 단어 보기",
|
contentDescription = "다른 단어 보기",
|
||||||
tint = PrimaryIndigo,
|
tint = PrimaryIndigo,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = "터치하여 단어 변경 🔄",
|
||||||
|
fontSize = 11.5.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF757575)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = previewWord!!.word,
|
text = previewWord!!.word,
|
||||||
fontSize = 18.sp,
|
fontSize = 22.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
color = Color(0xFF212121)
|
color = Color(0xFF1A237E)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
AutoVerticalScrollingText(
|
AutoVerticalScrollingText(
|
||||||
text = previewWord!!.meaning,
|
text = previewWord!!.meaning,
|
||||||
fontSize = 12.sp,
|
fontSize = 14.5.sp,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 20.sp,
|
||||||
color = Color(0xFF616161),
|
color = Color(0xFF424242),
|
||||||
height = 36.dp
|
height = 48.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.4.0"
|
agp = "9.4.1"
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.2.10"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|||||||
+2
-2
@@ -4,5 +4,5 @@
|
|||||||
# Location of the SDK. This is only used by Gradle.
|
# Location of the SDK. This is only used by Gradle.
|
||||||
# For customization when using a Version Control System, please read the
|
# For customization when using a Version Control System, please read the
|
||||||
# header note.
|
# header note.
|
||||||
#Wed Sep 16 13:58:22 KST 2026
|
#Mon Sep 21 23:15:50 KST 2026
|
||||||
sdk.dir=/home/gerd/Workspace/Android/SDK
|
sdk.dir=D\:\\Android\\SDK
|
||||||
|
|||||||
Reference in New Issue
Block a user