320 lines
17 KiB
Kotlin
320 lines
17 KiB
Kotlin
|
|
// 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("전체 내용 클립보드에 복사")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|