2025-10-13 16:18:55 +09:00
|
|
|
// ui/tabs/tabs.kt
|
2025-10-13 13:26:39 +09:00
|
|
|
package ui.tabs
|
|
|
|
|
|
2025-10-13 16:54:39 +09:00
|
|
|
import androidx.compose.foundation.HorizontalScrollbar
|
2025-10-13 13:26:39 +09:00
|
|
|
import androidx.compose.foundation.Image
|
|
|
|
|
import androidx.compose.foundation.border
|
|
|
|
|
import androidx.compose.foundation.clickable
|
|
|
|
|
import androidx.compose.foundation.layout.*
|
|
|
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
|
|
|
import androidx.compose.foundation.lazy.LazyRow
|
|
|
|
|
import androidx.compose.foundation.lazy.items
|
2025-10-13 16:18:55 +09:00
|
|
|
import androidx.compose.foundation.lazy.itemsIndexed
|
2025-10-13 16:54:39 +09:00
|
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
|
|
import androidx.compose.foundation.rememberScrollbarAdapter
|
2025-10-13 13:26:39 +09:00
|
|
|
import androidx.compose.foundation.rememberScrollState
|
|
|
|
|
import androidx.compose.foundation.verticalScroll
|
|
|
|
|
import androidx.compose.material.*
|
2025-10-13 16:18:55 +09:00
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
|
|
import androidx.compose.material.icons.filled.Delete
|
2025-10-13 13:26:39 +09:00
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
|
import androidx.compose.ui.layout.ContentScale
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
import coil3.ImageLoader
|
|
|
|
|
import coil3.compose.rememberAsyncImagePainter
|
|
|
|
|
import models.SearchResult
|
2025-10-13 16:18:55 +09:00
|
|
|
import utils.Strings
|
2025-10-13 13:26:39 +09:00
|
|
|
import java.io.File
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun ScrapBasedPostTab(
|
|
|
|
|
isLoading: Boolean, keywords: List<String>, searchResults: List<SearchResult>, scrapedFiles: List<File>, selectedFiles: Set<File>,
|
2025-10-13 16:18:55 +09:00
|
|
|
viewedFileContent: String,
|
|
|
|
|
combinedImagesFromSelectedFiles: List<String>,
|
|
|
|
|
currentSelectedImages: Set<String>,
|
2025-10-13 13:26:39 +09:00
|
|
|
userPrompt: String, imageLoader: ImageLoader, manualKeyword: String, userScrapComment: String, userMainTopic: String,
|
|
|
|
|
onManualKeywordChange: (String) -> Unit, onUserPromptChange: (String) -> Unit, onUserScrapCommentChange: (String) -> Unit, onUserMainTopicChange: (String) -> Unit,
|
|
|
|
|
onFetchTrends: () -> Unit, onKeywordSelect: (String) -> Unit, onSearchResultSelect: (SearchResult) -> Unit,
|
|
|
|
|
onRefreshFiles: () -> Unit, onFileSelectToggle: (File, Boolean) -> Unit, onFileView: (File) -> Unit,
|
|
|
|
|
onImageSelect: (String) -> Unit, onSaveChanges: () -> Unit, onGeneratePost: () -> Unit
|
|
|
|
|
) {
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
Row(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
// 1. 키워드 및 검색
|
|
|
|
|
Column(modifier = Modifier.weight(1.5f).border(1.dp, Color.LightGray).padding(4.dp)) {
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth()) {
|
2025-10-13 16:18:55 +09:00
|
|
|
OutlinedTextField(value = manualKeyword, onValueChange = onManualKeywordChange, label = { Text(Strings.LABEL_KEYWORD_INPUT) }, modifier = Modifier.weight(1f), singleLine = true, enabled = !isLoading)
|
2025-10-13 13:26:39 +09:00
|
|
|
Spacer(Modifier.width(4.dp))
|
2025-10-13 16:18:55 +09:00
|
|
|
Button(onClick = { onKeywordSelect(manualKeyword) }, enabled = !isLoading && manualKeyword.isNotBlank()) { Text(Strings.BUTTON_SEARCH) }
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
2025-10-13 16:18:55 +09:00
|
|
|
Button(onClick = onFetchTrends, modifier = Modifier.fillMaxWidth(), enabled = !isLoading) { Text(Strings.BUTTON_FETCH_TRENDS) }
|
2025-10-13 13:26:39 +09:00
|
|
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
|
|
|
|
LazyColumn(modifier = Modifier.weight(1f)) {
|
|
|
|
|
items(keywords) { keyword -> Text(keyword, modifier = Modifier.fillMaxWidth().clickable { if (!isLoading) onKeywordSelect(keyword) }.padding(8.dp)) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 2. 검색 결과 및 스크랩
|
|
|
|
|
Column(modifier = Modifier.weight(2f).border(1.dp, Color.LightGray).padding(4.dp)) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_SEARCH_RESULTS, style = MaterialTheme.typography.h6, modifier = Modifier.padding(4.dp))
|
2025-10-13 13:26:39 +09:00
|
|
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
items(searchResults) { result ->
|
|
|
|
|
Column(modifier = Modifier.fillMaxWidth().clickable { if (!isLoading) onSearchResultSelect(result) }.padding(8.dp)) {
|
|
|
|
|
Text(result.title, style = MaterialTheme.typography.subtitle1, color = MaterialTheme.colors.primary)
|
|
|
|
|
Text(result.url, style = MaterialTheme.typography.caption, maxLines = 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 3. 파일 관리 및 생성
|
|
|
|
|
Column(modifier = Modifier.weight(3f).padding(horizontal = 4.dp).verticalScroll(rememberScrollState())) {
|
|
|
|
|
// 저장된 파일
|
|
|
|
|
Column(modifier = Modifier.fillMaxWidth().border(1.dp, Color.LightGray).padding(4.dp)) {
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_SAVED_FILES, style = MaterialTheme.typography.h6, modifier = Modifier.weight(1f).padding(4.dp))
|
|
|
|
|
Button(onClick = onRefreshFiles, enabled = !isLoading) { Text(Strings.BUTTON_REFRESH) }
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
Box(modifier = Modifier.heightIn(max = 200.dp)) {
|
|
|
|
|
LazyColumn {
|
|
|
|
|
items(scrapedFiles) { file ->
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth().clickable { if (!isLoading) onFileView(file) }.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
|
Checkbox(checked = file in selectedFiles, onCheckedChange = { isChecked -> onFileSelectToggle(file, isChecked) }, enabled = !isLoading)
|
|
|
|
|
Text(file.name, modifier = Modifier.padding(start = 4.dp), maxLines = 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_FILE_CONTENT, style = MaterialTheme.typography.subtitle1)
|
2025-10-13 13:26:39 +09:00
|
|
|
Text(text = viewedFileContent, modifier = Modifier.height(100.dp).fillMaxWidth().border(1.dp, Color.LightGray).padding(4.dp).verticalScroll(rememberScrollState()), style = MaterialTheme.typography.body2)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_SELECT_IMAGES, style = MaterialTheme.typography.subtitle1, modifier = Modifier.weight(1f))
|
|
|
|
|
Button(onClick = onSaveChanges, enabled = !isLoading && combinedImagesFromSelectedFiles.isNotEmpty()) { Text(Strings.BUTTON_SAVE_IMAGE_SELECTION) }
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
2025-10-13 16:54:39 +09:00
|
|
|
// ⭐️ [수정] 스크롤바를 표시하기 위해 Box로 감싸고 HorizontalScrollbar 추가
|
|
|
|
|
val imageListState = rememberLazyListState()
|
|
|
|
|
Box(modifier = Modifier.fillMaxWidth().height(120.dp).border(1.dp, Color.LightGray)) {
|
|
|
|
|
LazyRow(
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), // 스크롤바 공간 확보
|
|
|
|
|
state = imageListState
|
|
|
|
|
) {
|
|
|
|
|
items(combinedImagesFromSelectedFiles) { imageUrl ->
|
|
|
|
|
val isSelected = imageUrl in currentSelectedImages
|
|
|
|
|
Box(modifier = Modifier.padding(4.dp)) {
|
|
|
|
|
Image(painter = rememberAsyncImagePainter(model = imageUrl, imageLoader = imageLoader), contentDescription = "Scraped Image", modifier = Modifier.size(100.dp).clickable { onImageSelect(imageUrl) }.border(if (isSelected) 4.dp else 0.dp, MaterialTheme.colors.primary), contentScale = ContentScale.Crop)
|
|
|
|
|
}
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-13 16:54:39 +09:00
|
|
|
HorizontalScrollbar(
|
|
|
|
|
modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(),
|
|
|
|
|
adapter = rememberScrollbarAdapter(imageListState)
|
|
|
|
|
)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
|
|
|
|
|
// 글 생성 제어
|
|
|
|
|
Column(modifier = Modifier.fillMaxWidth().border(1.dp, Color.LightGray).padding(8.dp)) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.LABEL_LLM_REQUEST, style = MaterialTheme.typography.h6)
|
|
|
|
|
OutlinedTextField(value = userPrompt, onValueChange = onUserPromptChange, modifier = Modifier.fillMaxWidth().height(100.dp), label = { Text(Strings.LABEL_USER_PROMPT) })
|
2025-10-13 13:26:39 +09:00
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = userMainTopic,
|
|
|
|
|
onValueChange = onUserMainTopicChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_MAIN_TOPIC) },
|
2025-10-13 13:26:39 +09:00
|
|
|
singleLine = true
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
|
2025-10-13 16:18:55 +09:00
|
|
|
OutlinedTextField(value = userScrapComment, onValueChange = onUserScrapCommentChange, modifier = Modifier.fillMaxWidth().height(100.dp), label = { Text(Strings.LABEL_USER_COMMENT) })
|
2025-10-13 13:26:39 +09:00
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
Button(onClick = onGeneratePost, enabled = !isLoading && selectedFiles.isNotEmpty(), modifier = Modifier.fillMaxWidth()) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.buttonGeneratePost(selectedFiles.size))
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun DirectPostTab(
|
|
|
|
|
isLoading: Boolean,
|
|
|
|
|
userOwnContent: String,
|
|
|
|
|
onUserOwnContentChange: (String) -> Unit,
|
|
|
|
|
uploadedImageFiles: List<File>,
|
|
|
|
|
imageLoader: ImageLoader,
|
|
|
|
|
userPrompt: String,
|
|
|
|
|
onUserPromptChange: (String) -> Unit,
|
|
|
|
|
onUploadImage: () -> Unit,
|
|
|
|
|
onRemoveUploadedImage: (File) -> Unit,
|
|
|
|
|
onGeneratePost: () -> Unit
|
|
|
|
|
) {
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
Row(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
|
|
|
|
// 1. 내용 작성
|
|
|
|
|
Column(modifier = Modifier.weight(2f).padding(end = 8.dp)) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_DIRECT_POST, style = MaterialTheme.typography.h6, modifier = Modifier.padding(4.dp))
|
2025-10-13 13:26:39 +09:00
|
|
|
OutlinedTextField(
|
|
|
|
|
value = userOwnContent,
|
|
|
|
|
onValueChange = onUserOwnContentChange,
|
|
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_DIRECT_POST_CONTENT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 이미지 및 생성 제어
|
|
|
|
|
Column(modifier = Modifier.weight(1f).border(1.dp, Color.LightGray).padding(8.dp)) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_IMAGE_UPLOAD, style = MaterialTheme.typography.h6)
|
|
|
|
|
Button(onClick = onUploadImage, enabled = !isLoading, modifier = Modifier.fillMaxWidth()) { Text(Strings.BUTTON_UPLOAD_IMAGE) }
|
2025-10-13 13:26:39 +09:00
|
|
|
Spacer(Modifier.height(8.dp))
|
2025-10-13 16:54:39 +09:00
|
|
|
|
|
|
|
|
// ⭐️ [수정] 스크롤바를 표시하기 위해 Box로 감싸고 HorizontalScrollbar 추가
|
|
|
|
|
val imageListState = rememberLazyListState()
|
|
|
|
|
Box(modifier = Modifier.fillMaxWidth().height(120.dp)) {
|
|
|
|
|
LazyRow(
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
|
|
|
|
|
state = imageListState
|
|
|
|
|
) {
|
|
|
|
|
items(uploadedImageFiles) { file ->
|
|
|
|
|
Box(modifier = Modifier.padding(4.dp)) {
|
|
|
|
|
Image(
|
|
|
|
|
painter = rememberAsyncImagePainter(model = file, imageLoader = imageLoader),
|
|
|
|
|
contentDescription = "Uploaded Image",
|
|
|
|
|
modifier = Modifier.size(100.dp).clickable { onRemoveUploadedImage(file) },
|
|
|
|
|
contentScale = ContentScale.Crop
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-13 16:54:39 +09:00
|
|
|
HorizontalScrollbar(
|
|
|
|
|
modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(),
|
|
|
|
|
adapter = rememberScrollbarAdapter(imageListState)
|
|
|
|
|
)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.LABEL_LLM_REQUEST, style = MaterialTheme.typography.h6)
|
2025-10-13 13:26:39 +09:00
|
|
|
OutlinedTextField(
|
|
|
|
|
value = userPrompt,
|
|
|
|
|
onValueChange = onUserPromptChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth().height(150.dp),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_USER_PROMPT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
Button(
|
|
|
|
|
onClick = onGeneratePost,
|
|
|
|
|
enabled = !isLoading && userOwnContent.isNotBlank() && uploadedImageFiles.isNotEmpty(),
|
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
2025-10-13 16:18:55 +09:00
|
|
|
) { Text(Strings.BUTTON_GENERATE_POST_DIRECT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun ReceiptAnalyzerTab(
|
|
|
|
|
isLoading: Boolean,
|
|
|
|
|
receiptFiles: List<File>,
|
|
|
|
|
receiptAnalysisResult: String,
|
|
|
|
|
isAnalyzing: Boolean,
|
|
|
|
|
receiptContextPrompt: String,
|
|
|
|
|
imageLoader: ImageLoader,
|
|
|
|
|
onUploadReceipt: () -> Unit,
|
|
|
|
|
onRemoveReceipt: (File) -> Unit,
|
|
|
|
|
onReceiptContextPromptChange: (String) -> Unit,
|
|
|
|
|
onAnalyzeReceipts: () -> Unit,
|
|
|
|
|
onCancelAnalysis: () -> Unit
|
|
|
|
|
) {
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.TITLE_RECEIPT_ANALYZER, style = MaterialTheme.typography.h5, modifier = Modifier.padding(bottom = 8.dp))
|
|
|
|
|
Button(onClick = onUploadReceipt, enabled = !isLoading, modifier = Modifier.fillMaxWidth()) { Text(Strings.BUTTON_UPLOAD_RECEIPT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
Spacer(Modifier.height(8.dp))
|
2025-10-13 16:54:39 +09:00
|
|
|
|
|
|
|
|
// ⭐️ [수정] 스크롤바를 표시하기 위해 Box로 감싸고 HorizontalScrollbar 추가
|
|
|
|
|
val imageListState = rememberLazyListState()
|
|
|
|
|
Box(modifier = Modifier.fillMaxWidth().height(120.dp)) {
|
|
|
|
|
LazyRow(
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
|
|
|
|
|
state = imageListState
|
|
|
|
|
) {
|
|
|
|
|
items(receiptFiles) { file ->
|
|
|
|
|
Box(modifier = Modifier.padding(4.dp)) {
|
|
|
|
|
Image(
|
|
|
|
|
painter = rememberAsyncImagePainter(model = file, imageLoader = imageLoader),
|
|
|
|
|
contentDescription = "Receipt Image",
|
|
|
|
|
modifier = Modifier.size(100.dp).clickable { onRemoveReceipt(file) },
|
|
|
|
|
contentScale = ContentScale.Crop
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-13 16:54:39 +09:00
|
|
|
HorizontalScrollbar(
|
|
|
|
|
modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(),
|
|
|
|
|
adapter = rememberScrollbarAdapter(imageListState)
|
|
|
|
|
)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = receiptContextPrompt,
|
|
|
|
|
onValueChange = onReceiptContextPromptChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
2025-10-13 16:18:55 +09:00
|
|
|
placeholder = { Text(Strings.PLACEHOLDER_RECEIPT_CONTEXT) },
|
|
|
|
|
label = { Text(Strings.LABEL_RECEIPT_CONTEXT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
if (isAnalyzing) {
|
|
|
|
|
Button(onClick = onCancelAnalysis, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error)) {
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
|
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp)
|
|
|
|
|
Spacer(Modifier.width(8.dp))
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.BUTTON_CANCEL_ANALYSIS)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Button(onClick = onAnalyzeReceipts, enabled = !isLoading && receiptFiles.isNotEmpty(), modifier = Modifier.fillMaxWidth()) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.buttonStartAnalysis(receiptFiles.size))
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = receiptAnalysisResult,
|
|
|
|
|
onValueChange = {},
|
|
|
|
|
readOnly = true,
|
|
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_ANALYSIS_RESULT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (isLoading && !isAnalyzing) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun LogTab(logs: List<String>) {
|
|
|
|
|
TextField(value = logs.joinToString("\n"), onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxSize().padding(8.dp))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun ResultTab(
|
|
|
|
|
result: String,
|
|
|
|
|
onRequestResultChange: (String) -> Unit,
|
|
|
|
|
revisionRequest: String,
|
|
|
|
|
onRevisionRequestChange: (String) -> Unit,
|
|
|
|
|
isLoading: Boolean,
|
|
|
|
|
onRevise: () -> Unit,
|
|
|
|
|
onCopyToClipboard: () -> Unit
|
|
|
|
|
) {
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = result,
|
|
|
|
|
onValueChange = onRequestResultChange,
|
|
|
|
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_BLOG_RESULT) }
|
2025-10-13 13:26:39 +09:00
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = revisionRequest,
|
|
|
|
|
onValueChange = onRevisionRequestChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth().height(100.dp),
|
2025-10-13 16:18:55 +09:00
|
|
|
label = { Text(Strings.LABEL_REVISION_REQUEST) },
|
|
|
|
|
placeholder = { Text(Strings.PLACEHOLDER_REVISION_REQUEST) },
|
2025-10-13 13:26:39 +09:00
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
Button(
|
|
|
|
|
onClick = onRevise,
|
|
|
|
|
enabled = !isLoading && revisionRequest.isNotBlank(),
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
) {
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = MaterialTheme.colors.onPrimary, strokeWidth = 2.dp)
|
|
|
|
|
} else {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.BUTTON_REVISE_POST)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.width(8.dp))
|
|
|
|
|
Button(
|
|
|
|
|
onClick = onCopyToClipboard,
|
|
|
|
|
enabled = !isLoading && result.isNotBlank(),
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
) {
|
2025-10-13 16:18:55 +09:00
|
|
|
Text(Strings.BUTTON_COPY_TO_CLIPBOARD)
|
2025-10-13 13:26:39 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-13 16:18:55 +09:00
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun SettingsTab(
|
|
|
|
|
generatePromptPrefix: String,
|
|
|
|
|
onGeneratePromptPrefixChange: (String) -> Unit,
|
|
|
|
|
generatePromptInstructions: List<String>,
|
|
|
|
|
onGeneratePromptInstructionsChange: (List<String>) -> Unit,
|
|
|
|
|
revisePrompt: String,
|
|
|
|
|
onRevisePromptChange: (String) -> Unit,
|
|
|
|
|
receiptPrompt: String,
|
|
|
|
|
onReceiptPromptChange: (String) -> Unit,
|
|
|
|
|
articleSelectors: List<String>,
|
|
|
|
|
onArticleSelectorsChange: (List<String>) -> Unit,
|
|
|
|
|
onSave: () -> Unit,
|
|
|
|
|
onReset: () -> Unit,
|
|
|
|
|
isLoading: Boolean
|
|
|
|
|
) {
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
Column(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
.padding(16.dp)
|
|
|
|
|
.verticalScroll(rememberScrollState())
|
|
|
|
|
) {
|
|
|
|
|
Text("프롬프트 및 설정", style = MaterialTheme.typography.h5, modifier = Modifier.padding(bottom = 16.dp))
|
|
|
|
|
|
|
|
|
|
// --- 블로그 글 생성 프롬프트 섹션 ---
|
|
|
|
|
Text("블로그 글 생성 프롬프트", style = MaterialTheme.typography.h6)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = generatePromptPrefix,
|
|
|
|
|
onValueChange = onGeneratePromptPrefixChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth().height(150.dp),
|
|
|
|
|
label = { Text("기본 역할 (Prefix)") },
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
|
Text("요청사항 목록", style = MaterialTheme.typography.subtitle1)
|
|
|
|
|
|
|
|
|
|
// LazyColumn은 Column 내에서 높이가 지정되어야 하므로 Box로 감싸서 제한
|
|
|
|
|
Box(modifier = Modifier.heightIn(max = 250.dp)) {
|
|
|
|
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
itemsIndexed(generatePromptInstructions) { index, instruction ->
|
|
|
|
|
Row(
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
) {
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = instruction,
|
|
|
|
|
onValueChange = { newText ->
|
|
|
|
|
val newList = generatePromptInstructions.toMutableList()
|
|
|
|
|
newList[index] = newText
|
|
|
|
|
onGeneratePromptInstructionsChange(newList)
|
|
|
|
|
},
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
singleLine = true,
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.width(8.dp))
|
|
|
|
|
IconButton(onClick = {
|
|
|
|
|
val newList = generatePromptInstructions.toMutableList()
|
|
|
|
|
newList.removeAt(index)
|
|
|
|
|
onGeneratePromptInstructionsChange(newList)
|
|
|
|
|
}, enabled = !isLoading) {
|
|
|
|
|
Icon(Icons.Default.Delete, contentDescription = "삭제")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Button(
|
|
|
|
|
onClick = { onGeneratePromptInstructionsChange(generatePromptInstructions + "") },
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
) {
|
|
|
|
|
Text("요청사항 항목 추가")
|
|
|
|
|
}
|
|
|
|
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
|
|
|
|
|
|
|
|
|
// ⭐️ [추가] 스크래핑 CSS 셀렉터 설정 UI
|
|
|
|
|
Text("아티클 스크래핑 CSS 셀렉터", style = MaterialTheme.typography.h6)
|
|
|
|
|
Text(
|
|
|
|
|
"스크랩 시 본문을 찾기 위해 사용되는 CSS 셀렉터 목록입니다. 우선순위가 높은 것을 위로 배치하세요.",
|
|
|
|
|
style = MaterialTheme.typography.caption,
|
|
|
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
|
|
|
)
|
|
|
|
|
Box(modifier = Modifier.heightIn(max = 250.dp)) {
|
|
|
|
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
itemsIndexed(articleSelectors) { index, selector ->
|
|
|
|
|
Row(
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
) {
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = selector,
|
|
|
|
|
onValueChange = { newText ->
|
|
|
|
|
val newList = articleSelectors.toMutableList()
|
|
|
|
|
newList[index] = newText
|
|
|
|
|
onArticleSelectorsChange(newList)
|
|
|
|
|
},
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
singleLine = true,
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.width(8.dp))
|
|
|
|
|
IconButton(onClick = {
|
|
|
|
|
val newList = articleSelectors.toMutableList()
|
|
|
|
|
newList.removeAt(index)
|
|
|
|
|
onArticleSelectorsChange(newList)
|
|
|
|
|
}, enabled = !isLoading) {
|
|
|
|
|
Icon(Icons.Default.Delete, contentDescription = "셀렉터 삭제")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Button(
|
|
|
|
|
onClick = { onArticleSelectorsChange(articleSelectors + "") },
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
) {
|
|
|
|
|
Text("셀렉터 항목 추가")
|
|
|
|
|
}
|
|
|
|
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- 글 수정 및 영수증 분석 프롬프트 ---
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = revisePrompt,
|
|
|
|
|
onValueChange = onRevisePromptChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth().height(150.dp),
|
|
|
|
|
label = { Text("블로그 글 수정 프롬프트") },
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
|
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = receiptPrompt,
|
|
|
|
|
onValueChange = onReceiptPromptChange,
|
|
|
|
|
modifier = Modifier.fillMaxWidth().height(150.dp),
|
|
|
|
|
label = { Text("영수증 분석 프롬프트") },
|
|
|
|
|
enabled = !isLoading
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.height(24.dp))
|
|
|
|
|
|
|
|
|
|
// --- 저장 및 초기화 버튼 ---
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
Button(onClick = onSave, enabled = !isLoading, modifier = Modifier.weight(1f)) {
|
|
|
|
|
Text("설정 저장하기")
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.width(16.dp))
|
|
|
|
|
Button(
|
|
|
|
|
onClick = onReset,
|
|
|
|
|
enabled = !isLoading,
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary)
|
|
|
|
|
) {
|
|
|
|
|
Text("기본값으로 초기화")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) }
|
|
|
|
|
}
|
|
|
|
|
}
|