feat: 퍼즐 화면 UI 가변 높이 고정, 스타터 배치 로직 개선 및 메인 화면 단어 미리보기 기능 추가
This commit is contained in:
Generated
+3
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+2349
File diff suppressed because it is too large
Load Diff
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -31,6 +31,9 @@ interface WordDao {
|
|||||||
@Query("SELECT * FROM words WHERE isLearned = 0 ORDER BY difficulty ASC LIMIT :limit")
|
@Query("SELECT * FROM words WHERE isLearned = 0 ORDER BY difficulty ASC LIMIT :limit")
|
||||||
suspend fun getWordsForPreLearning(limit: Int): List<WordEntity>
|
suspend fun getWordsForPreLearning(limit: Int): List<WordEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM words ORDER BY RANDOM() LIMIT 1")
|
||||||
|
suspend fun getRandomWord(): WordEntity?
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertAll(words: List<WordEntity>)
|
suspend fun insertAll(words: List<WordEntity>)
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,23 @@ class WordRepository(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getRandomWord(): KoreanWord? = withContext(Dispatchers.IO) {
|
||||||
|
val entity = wordDao.getRandomWord()
|
||||||
|
if (entity != null) {
|
||||||
|
KoreanWord(
|
||||||
|
id = entity.id,
|
||||||
|
word = entity.word,
|
||||||
|
meaning = entity.meaning,
|
||||||
|
category = entity.category,
|
||||||
|
difficulty = entity.difficulty,
|
||||||
|
example = entity.example,
|
||||||
|
isLearned = entity.isLearned
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
getWordForWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun markWordLearned(wordText: String) = withContext(Dispatchers.IO) {
|
suspend fun markWordLearned(wordText: String) = withContext(Dispatchers.IO) {
|
||||||
wordDao.markWordLearned(wordText, System.currentTimeMillis())
|
wordDao.markWordLearned(wordText, System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.wordpuzzle.hangul.domain.engine
|
|||||||
|
|
||||||
import com.wordpuzzle.hangul.data.model.CrosswordCell
|
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.TileItem
|
import com.wordpuzzle.hangul.data.model.TileItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ object TilePoolGenerator {
|
|||||||
/**
|
/**
|
||||||
* 스테이지 그리드와 단어 목록을 바탕으로,
|
* 스테이지 그리드와 단어 목록을 바탕으로,
|
||||||
* 1) 모든 글자 타일을 무작위로 섞고,
|
* 1) 모든 글자 타일을 무작위로 섞고,
|
||||||
* 2) 섞인 문자 중 단어 수(clues.size)만큼을 임의로 선택하여 그리드의 맞는 빈 칸에 사전 배치합니다.
|
* 2) 특정 단어에 힌트 문자가 몰리지 않도록 각 단어(clues)에 골고루 1개씩 문자를 사전 배치합니다.
|
||||||
* 3) 새로고침 시에도 이 함수를 호출하여 다른 문자가 새롭게 임의 선택되어 배치됩니다.
|
* 3) 새로고침 시에도 이 함수를 호출하여 다른 문자가 새롭게 임의 선택되어 배치됩니다.
|
||||||
*/
|
*/
|
||||||
fun generateLayoutWithStarterLetters(
|
fun generateLayoutWithStarterLetters(
|
||||||
@@ -30,15 +31,50 @@ object TilePoolGenerator {
|
|||||||
return Pair(cleanGrid, emptyList())
|
return Pair(cleanGrid, emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 단어 수(clues.size)만큼 그리드의 빈 칸 위치(row, col)를 임의로 직접 선택
|
val targetStarterCount = clues.size.coerceAtMost(nonBlankCells.size - 1).coerceAtLeast(1)
|
||||||
val starterCount = clues.size.coerceAtMost(nonBlankCells.size - 1).coerceAtLeast(1)
|
val selectedStarterPositions = mutableSetOf<Pair<Int, Int>>()
|
||||||
val selectedStarterPositions = nonBlankCells
|
|
||||||
.shuffled()
|
|
||||||
.take(starterCount)
|
|
||||||
.map { it.row to it.col }
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
// 2. 선택된 위치의 셀에 정확한 정답 글자(correctChar)를 사전 배치 (오배치 원천 차단)
|
// 각 clue별 셀 좌표 리스트 매핑
|
||||||
|
val clueCellsMap = clues.associateWith { clue ->
|
||||||
|
val cellsInClue = mutableListOf<Pair<Int, Int>>()
|
||||||
|
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 cleanGrid.indices && c in cleanGrid[r].indices && !cleanGrid[r][c].isBlank) {
|
||||||
|
cellsInClue.add(r to c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cellsInClue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 단어(clues)들을 무작위 순서로 순회하며 각 단어마다 아직 선택되지 않은 셀 중 1개를 선택하여 배치 (특정 단어 몰림 방지)
|
||||||
|
val shuffledClues = clues.shuffled()
|
||||||
|
for (clue in shuffledClues) {
|
||||||
|
if (selectedStarterPositions.size >= targetStarterCount) break
|
||||||
|
|
||||||
|
val cells = clueCellsMap[clue] ?: continue
|
||||||
|
val unselectedCells = cells.filter { it !in selectedStarterPositions }
|
||||||
|
|
||||||
|
if (unselectedCells.isNotEmpty()) {
|
||||||
|
val chosenPos = unselectedCells.shuffled().first()
|
||||||
|
selectedStarterPositions.add(chosenPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 단어 교차점 중복 선택 등으로 targetStarterCount에 미달한 경우, 남은 셀 중 무작위 추가 선택
|
||||||
|
if (selectedStarterPositions.size < targetStarterCount) {
|
||||||
|
val remainingCells = nonBlankCells
|
||||||
|
.map { it.row to it.col }
|
||||||
|
.filter { it !in selectedStarterPositions }
|
||||||
|
.shuffled()
|
||||||
|
|
||||||
|
for (pos in remainingCells) {
|
||||||
|
if (selectedStarterPositions.size >= targetStarterCount) break
|
||||||
|
selectedStarterPositions.add(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 선택된 위치의 셀에 정확한 정답 글자(correctChar)를 사전 배치
|
||||||
val matrix = cleanGrid.map { row ->
|
val matrix = cleanGrid.map { row ->
|
||||||
row.map { cell ->
|
row.map { cell ->
|
||||||
if (!cell.isBlank && (cell.row to cell.col) in selectedStarterPositions) {
|
if (!cell.isBlank && (cell.row to cell.col) in selectedStarterPositions) {
|
||||||
@@ -49,7 +85,7 @@ object TilePoolGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 각 셀과 1:1 대응되는 타일 목록 생성 (사전 배치된 타일은 isUsed = true)
|
// 4. 각 셀과 1:1 대응되는 타일 목록 생성 (사전 배치된 타일은 isUsed = true)
|
||||||
val tiles = nonBlankCells.map { cell ->
|
val tiles = nonBlankCells.map { cell ->
|
||||||
val isUsed = (cell.row to cell.col) in selectedStarterPositions
|
val isUsed = (cell.row to cell.col) in selectedStarterPositions
|
||||||
TileItem(
|
TileItem(
|
||||||
@@ -59,7 +95,7 @@ object TilePoolGenerator {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 타일 목록을 무작위로 섞어서 반환
|
// 5. 타일 목록을 무작위로 섞어서 반환
|
||||||
val shuffledTiles = tiles.shuffled()
|
val shuffledTiles = tiles.shuffled()
|
||||||
|
|
||||||
return Pair(matrix, shuffledTiles)
|
return Pair(matrix, shuffledTiles)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.wordpuzzle.hangul.service
|
package com.wordpuzzle.hangul.service
|
||||||
|
|
||||||
|
import android.R
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -82,13 +83,13 @@ object WordNotificationManager {
|
|||||||
|
|
||||||
// 잠금화면용 확장 스타일 알림 구성
|
// 잠금화면용 확장 스타일 알림 구성
|
||||||
val bigTextStyle = NotificationCompat.BigTextStyle()
|
val bigTextStyle = NotificationCompat.BigTextStyle()
|
||||||
.setBigContentTitle("💡 예습 단어: ${word.word} [${word.category}]")
|
.setBigContentTitle("${word.word} [${word.category}]")
|
||||||
.bigText("${word.meaning}\n\n예) “${word.example}”")
|
.bigText("${word.meaning}\n\n예) “${word.example}”")
|
||||||
.setSummaryText("한글 십자말풀이 예습")
|
.setSummaryText("한글 십자말풀이")
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
.setSmallIcon(R.drawable.ic_dialog_info)
|
||||||
.setContentTitle("💡 예습 단어: ${word.word} [${word.category}]")
|
.setContentTitle("${word.word} [${word.category}]")
|
||||||
.setContentText(word.meaning)
|
.setContentText(word.meaning)
|
||||||
.setStyle(bigTextStyle)
|
.setStyle(bigTextStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
package com.wordpuzzle.hangul.ui.game
|
package com.wordpuzzle.hangul.ui.game
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.SwapHoriz
|
import androidx.compose.material.icons.filled.SwapHoriz
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
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.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.CrosswordClue
|
import com.wordpuzzle.hangul.data.model.CrosswordClue
|
||||||
import com.wordpuzzle.hangul.data.model.Direction
|
import com.wordpuzzle.hangul.data.model.Direction
|
||||||
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigo
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClueBar(
|
fun ClueBar(
|
||||||
@@ -29,6 +31,7 @@ fun ClueBar(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
.padding(horizontal = 10.dp, vertical = 2.dp),
|
.padding(horizontal = 10.dp, vertical = 2.dp),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
color = Color(0xFFF1F5F9),
|
color = Color(0xFFF1F5F9),
|
||||||
@@ -36,7 +39,7 @@ fun ClueBar(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -55,9 +58,10 @@ fun ClueBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
AutoResizedText(
|
||||||
text = activeClue.clue,
|
text = activeClue.clue,
|
||||||
fontSize = 13.sp,
|
targetFontSize = 13.sp,
|
||||||
|
minFontSize = 8.5.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = Color(0xFF263238),
|
color = Color(0xFF263238),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -86,3 +90,40 @@ fun ClueBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoResizedText(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
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) }
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = color,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
fontSize = resizedFontSize,
|
||||||
|
maxLines = maxLines,
|
||||||
|
overflow = TextOverflow.Clip,
|
||||||
|
onTextLayout = { result ->
|
||||||
|
if (result.hasVisualOverflow && resizedFontSize > minFontSize) {
|
||||||
|
resizedFontSize = (resizedFontSize.value - 0.5f).coerceAtLeast(minFontSize.value).sp
|
||||||
|
} else {
|
||||||
|
shouldDraw = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.drawWithContent {
|
||||||
|
if (shouldDraw) {
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = style
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.wordpuzzle.hangul.ui.main
|
package com.wordpuzzle.hangul.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -19,6 +21,8 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.wordpuzzle.hangul.data.datastore.UserPreferencesManager
|
import com.wordpuzzle.hangul.data.datastore.UserPreferencesManager
|
||||||
@@ -27,6 +31,9 @@ import com.wordpuzzle.hangul.data.repository.WordRepository
|
|||||||
import com.wordpuzzle.hangul.ui.common.AdBannerView
|
import com.wordpuzzle.hangul.ui.common.AdBannerView
|
||||||
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigo
|
import com.wordpuzzle.hangul.ui.theme.PrimaryIndigo
|
||||||
import com.wordpuzzle.hangul.ui.theme.SecondaryOrange
|
import com.wordpuzzle.hangul.ui.theme.SecondaryOrange
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainLobbyScreen(
|
fun MainLobbyScreen(
|
||||||
@@ -40,6 +47,7 @@ fun MainLobbyScreen(
|
|||||||
) {
|
) {
|
||||||
var previewWord by remember { mutableStateOf<KoreanWord?>(null) }
|
var previewWord by remember { mutableStateOf<KoreanWord?>(null) }
|
||||||
var isLockScreenEnabled by remember { mutableStateOf(false) }
|
var isLockScreenEnabled by remember { mutableStateOf(false) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
previewWord = wordRepo.getWordForWidget()
|
previewWord = wordRepo.getWordForWidget()
|
||||||
@@ -169,12 +177,20 @@ fun MainLobbyScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사전 학습 (Pre-Learning) 단어 카드
|
// 퍼즐 단어 미리보기 카드 (단어 설명 영역 2줄 고정 높이 적용하여 하단 레이아웃 위치 완전 고정)
|
||||||
if (previewWord != null) {
|
if (previewWord != null) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||||
|
.clickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val nextWord = wordRepo.getRandomWord()
|
||||||
|
if (nextWord != null) {
|
||||||
|
previewWord = nextWord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
@@ -185,12 +201,21 @@ fun MainLobbyScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
text = "💡 다음 퍼즐 예습 단어",
|
Text(
|
||||||
fontSize = 12.sp,
|
text = "💡 퍼즐 단어 미리보기",
|
||||||
fontWeight = FontWeight.Bold,
|
fontSize = 12.sp,
|
||||||
color = PrimaryIndigo
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
color = PrimaryIndigo
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Refresh,
|
||||||
|
contentDescription = "다른 단어 보기",
|
||||||
|
tint = PrimaryIndigo,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "[${previewWord!!.category}]",
|
text = "[${previewWord!!.category}]",
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
@@ -198,7 +223,7 @@ fun MainLobbyScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = previewWord!!.word,
|
text = previewWord!!.word,
|
||||||
@@ -207,18 +232,20 @@ fun MainLobbyScreen(
|
|||||||
color = Color(0xFF212121)
|
color = Color(0xFF212121)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
Text(
|
AutoVerticalScrollingText(
|
||||||
text = previewWord!!.meaning,
|
text = previewWord!!.meaning,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color(0xFF616161)
|
lineHeight = 17.sp,
|
||||||
|
color = Color(0xFF616161),
|
||||||
|
height = 38.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스테이지 선택 타이틀
|
// 스테이지 선택 타이틀 (항상 고정된 위치 유지)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -265,6 +292,53 @@ fun MainLobbyScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoVerticalScrollingText(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
|
fontSize: TextUnit = 12.sp,
|
||||||
|
lineHeight: TextUnit = 17.sp,
|
||||||
|
height: Dp = 38.dp
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
LaunchedEffect(text) {
|
||||||
|
scrollState.scrollTo(0)
|
||||||
|
delay(1500)
|
||||||
|
while (isActive) {
|
||||||
|
if (scrollState.maxValue > 0) {
|
||||||
|
scrollState.animateScrollTo(
|
||||||
|
value = scrollState.maxValue,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = (scrollState.maxValue * 35).coerceAtLeast(1500),
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
delay(2000)
|
||||||
|
scrollState.scrollTo(0)
|
||||||
|
delay(1200)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(height)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = fontSize,
|
||||||
|
lineHeight = lineHeight,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StageCard(
|
fun StageCard(
|
||||||
stageNumber: Int,
|
stageNumber: Int,
|
||||||
|
|||||||
Reference in New Issue
Block a user