320 lines
17 KiB
Kotlin
Raw Normal View History

2025-10-13 13:26:39 +09:00
// ui/tabs/ScrapBasedPostTab.kt
package ui.tabs
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
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
// (필요한 import 추가)
import androidx.compose.material.*
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 io.ktor.client.request.url
import models.SearchResult
import java.io.File
@Composable
fun ScrapBasedPostTab(
isLoading: Boolean, keywords: List<String>, searchResults: List<SearchResult>, scrapedFiles: List<File>, selectedFiles: Set<File>,
viewedFileContent: String, imagesForSelection: List<String>, currentSelectedImages: Set<String>,
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()) {
OutlinedTextField(value = manualKeyword, onValueChange = onManualKeywordChange, label = { Text("키워드 직접 입력") }, modifier = Modifier.weight(1f), singleLine = true, enabled = !isLoading)
Spacer(Modifier.width(4.dp))
Button(onClick = { onKeywordSelect(manualKeyword) }, enabled = !isLoading && manualKeyword.isNotBlank()) { Text("검색") }
}
Spacer(Modifier.height(8.dp))
Button(onClick = onFetchTrends, modifier = Modifier.fillMaxWidth(), enabled = !isLoading) { Text("트렌드 가져오기") }
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)) {
Text("검색 결과 (클릭하여 스크랩)", style = MaterialTheme.typography.h6, modifier = Modifier.padding(4.dp))
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) {
Text("저장된 파일", style = MaterialTheme.typography.h6, modifier = Modifier.weight(1f).padding(4.dp))
Button(onClick = onRefreshFiles, enabled = !isLoading) { Text("새로고침") }
}
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))
Text("파일 내용", style = MaterialTheme.typography.subtitle1)
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) {
Text("대표 이미지 선택 (다중 가능)", style = MaterialTheme.typography.subtitle1, modifier = Modifier.weight(1f))
Button(onClick = onSaveChanges, enabled = !isLoading && imagesForSelection.isNotEmpty()) { Text("선택 이미지 저장") }
}
LazyRow(modifier = Modifier.fillMaxWidth().height(120.dp).border(1.dp, Color.LightGray)) {
items(imagesForSelection) { 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)
}
}
}
}
Spacer(Modifier.height(16.dp))
// 글 생성 제어
Column(modifier = Modifier.fillMaxWidth().border(1.dp, Color.LightGray).padding(8.dp)) {
Text("LLM 요청사항", style = MaterialTheme.typography.h6)
OutlinedTextField(value = userPrompt, onValueChange = onUserPromptChange, modifier = Modifier.fillMaxWidth().height(100.dp), label = { Text("글 스타일, 톤앤매너 등을 지시하세요.") })
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = userMainTopic,
onValueChange = onUserMainTopicChange,
modifier = Modifier.fillMaxWidth(),
label = { Text("글의 핵심 주제 (예: 2025년 최신 IT 트렌드)") },
singleLine = true
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(value = userScrapComment, onValueChange = onUserScrapCommentChange, modifier = Modifier.fillMaxWidth().height(100.dp), label = { Text("작성자 코멘트 (스크랩한 내용에 대한 당신의 생각)") })
Spacer(Modifier.height(8.dp))
Button(onClick = onGeneratePost, enabled = !isLoading && selectedFiles.isNotEmpty(), modifier = Modifier.fillMaxWidth()) {
Text("선택한 파일(${selectedFiles.size}개)로 글 생성")
}
}
}
}
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)) {
Text("직접 작성 (여행 기록, 정보 공유 등)", style = MaterialTheme.typography.h6, modifier = Modifier.padding(4.dp))
OutlinedTextField(
value = userOwnContent,
onValueChange = onUserOwnContentChange,
modifier = Modifier.fillMaxSize(),
label = { Text("블로그에 올릴 내용을 직접 작성하세요.") }
)
}
// 2. 이미지 및 생성 제어
Column(modifier = Modifier.weight(1f).border(1.dp, Color.LightGray).padding(8.dp)) {
Text("이미지 업로드", style = MaterialTheme.typography.h6)
Button(onClick = onUploadImage, enabled = !isLoading, modifier = Modifier.fillMaxWidth()) { Text("내 PC에서 이미지 업로드") }
Spacer(Modifier.height(8.dp))
LazyRow(modifier = Modifier.fillMaxWidth().height(120.dp)) {
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
)
}
}
}
Spacer(Modifier.height(16.dp))
Text("LLM 요청사항", style = MaterialTheme.typography.h6)
OutlinedTextField(
value = userPrompt,
onValueChange = onUserPromptChange,
modifier = Modifier.fillMaxWidth().height(150.dp),
label = { Text("글 스타일, 톤앤매너 등을 지시하세요.") }
)
Spacer(Modifier.height(8.dp))
Button(
onClick = onGeneratePost,
enabled = !isLoading && userOwnContent.isNotBlank() && uploadedImageFiles.isNotEmpty(),
modifier = Modifier.fillMaxWidth()
) { Text("작성한 내용으로 글 생성") }
}
}
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)) {
Text("영수증 분석기", style = MaterialTheme.typography.h5, modifier = Modifier.padding(bottom = 8.dp))
Button(onClick = onUploadReceipt, enabled = !isLoading, modifier = Modifier.fillMaxWidth()) { Text("영수증 이미지 업로드") }
Spacer(Modifier.height(8.dp))
LazyRow(modifier = Modifier.fillMaxWidth().height(120.dp)) {
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
)
}
}
}
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = receiptContextPrompt,
onValueChange = onReceiptContextPromptChange,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("추가 정보를 입력하면 더 정확하게 분석할 수 있습니다.") },
label = { Text("추가 정보 입력 (예: 부산 출장 경비)") }
)
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))
Text("분석 중단하기")
}
}
} else {
Button(onClick = onAnalyzeReceipts, enabled = !isLoading && receiptFiles.isNotEmpty(), modifier = Modifier.fillMaxWidth()) {
Text("선택한 영수증 분석 시작 (${receiptFiles.size}개)")
}
}
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = receiptAnalysisResult,
onValueChange = {},
readOnly = true,
modifier = Modifier.fillMaxSize(),
label = { Text("분석 결과 (내용 복사하여 사용)") }
)
}
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(),
label = { Text("블로그 글 결과 (LLM 생성 본문)") }
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = revisionRequest,
onValueChange = onRevisionRequestChange,
modifier = Modifier.fillMaxWidth().height(100.dp),
label = { Text("추가 요청사항") },
placeholder = { Text("예: 문체를 좀 더 전문적으로 바꿔줘. 1번 항목을 더 자세히 설명해줘.") },
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 {
Text("LLM으로 글 보완하기")
}
}
Spacer(Modifier.width(8.dp))
Button(
onClick = onCopyToClipboard,
enabled = !isLoading && result.isNotBlank(),
modifier = Modifier.weight(1f)
) {
Text("전체 내용 클립보드에 복사")
}
}
}
}
}