diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index dfe7816..0c69b78 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -2,6 +2,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape @@ -37,6 +38,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.contentType import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.* +import io.ktor.util.Attributes import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -123,15 +125,6 @@ data class AppAnalyticsRequestListResponse(val data: List Unit) { var logText by remember { mutableStateOf("여기에 통신 기록이 표시됩니다.") } var statusMessage by remember { mutableStateOf("리포트 조회를 위한 정보를 입력하세요.") } - // State for Sales Report var salesAnalysisResult by remember { mutableStateOf>(emptyList()) } var rawTsvReport by remember { mutableStateOf("") } - // State for Analytics Report var analyticsReportSet by remember { mutableStateOf(null) } var analyticsRequestId by remember { mutableStateOf(null) } @@ -364,7 +355,8 @@ fun App(onSaveTsvRequest: (name: String, tsvData: String) -> Unit) { try { val token = createJWT(key, issuerId, keyId) statusMessage = "분석 리포트 생성 요청 중..." - val requestBody = AnalyticsReportRequest(data = AnalyticsReportRequest.Data(attributes = AnalyticsReportRequest.Attributes(), relationships = AnalyticsReportRequest.Relationships(app = AnalyticsReportRequest.App(data = AnalyticsReportRequest.AppData(id = adamId))))) + val requestBody = AnalyticsReportRequest(data = AnalyticsReportRequest.Data(attributes = AnalyticsReportRequest.Attributes() + , relationships = AnalyticsReportRequest.Relationships(app = AnalyticsReportRequest.App(data = AnalyticsReportRequest.AppData(id = adamId))))) val response: HttpResponse = client.post("https://api.appstoreconnect.apple.com/v1/analyticsReportRequests") { header(HttpHeaders.Authorization, "Bearer $token") contentType(ContentType.Application.Json) @@ -410,12 +402,19 @@ fun App(onSaveTsvRequest: (name: String, tsvData: String) -> Unit) { try { val token = createJWT(key, issuerId, keyId) statusMessage = "리포트 요청 상태 확인 중... (ID: $reqId)" - val response: HttpResponse = client.get("https://api.appstoreconnect.apple.com/v1/analyticsReportRequests/$reqId") { + + val response: HttpResponse = client.get("https://api.appstoreconnect.apple.com/v1/analyticsReportRequests/$reqId/reports") { header(HttpHeaders.Authorization, "Bearer $token") + parameter("filter[category]", "APP_USAGE") } + if (response.status.isSuccess()) { - val status = response.body().data.attributes.requestState - statusMessage = "현재 상태: $status" + val reports = response.body().data + if (reports.isNotEmpty()) { + statusMessage = "현재 상태: COMPLETE (완료)" + } else { + statusMessage = "현재 상태: GENERATING (생성 중)" + } } else { throw Exception("상태 확인 실패 (${response.status}): ${response.bodyAsText()}") } @@ -439,7 +438,7 @@ fun App(onSaveTsvRequest: (name: String, tsvData: String) -> Unit) { val reportId = reportsResponse.body().data.firstOrNull()?.id if (reportId == null) { - statusMessage = "다운로드 리포트가 아직 준비되지 않았습니다. 잠시 후 다시 시도하세요." + statusMessage = "다운로드 리포트가 아직 준비되지 않았습니다. 잠시 후 '요청 상태 확인'으로 다시 시도하세요." return@launch } @@ -667,7 +666,10 @@ fun SalesResultView(statusMessage: String, result: List, onSave Spacer(Modifier.height(12.dp)); Divider() LazyColumn { - result.forEach { summary -> item { AppSummaryCard(summary); Spacer(Modifier.height(8.dp)) } } + items(result) { summary -> + AppSummaryCard(summary) + Spacer(Modifier.height(8.dp)) + } } Spacer(Modifier.height(16.dp)) Button(onClick = onSaveRequest, modifier = Modifier.fillMaxWidth(), enabled = result.isNotEmpty()) { @@ -738,12 +740,8 @@ private fun ReportSection(title: String, data: List?) { Spacer(Modifier.height(8.dp)) when { - data == null -> { - Text("오류가 발생하여 데이터를 가져오지 못했습니다.", color = MaterialTheme.colors.error) - } - data.isEmpty() -> { - Text("해당 기간에 집계된 데이터가 없습니다.") - } + data == null -> Text("오류가 발생하여 데이터를 가져오지 못했습니다.", color = MaterialTheme.colors.error) + data.isEmpty() -> Text("해당 기간에 집계된 데이터가 없습니다.") else -> { val totalDownloads = data.sumOf { it.downloads } val totalRedownloads = data.sumOf { it.redownloads } @@ -760,9 +758,7 @@ private fun ReportSection(title: String, data: List?) { } Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.onSurface.copy(alpha = 0.1f)).padding(horizontal = 8.dp, vertical = 8.dp), - ) { + Row(modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.onSurface.copy(alpha = 0.1f)).padding(horizontal = 8.dp, vertical = 8.dp)) { Text("날짜", modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold) Text("신규", modifier = Modifier.weight(1f), textAlign = TextAlign.End, fontWeight = FontWeight.Bold) Text("재설치", modifier = Modifier.weight(1f), textAlign = TextAlign.End, fontWeight = FontWeight.Bold) @@ -799,12 +795,7 @@ fun LogPanel(logText: String, modifier: Modifier = Modifier) { Divider() Box(modifier = Modifier.weight(1f).background(Color.White, MaterialTheme.shapes.small).padding(8.dp)) { SelectionContainer { - Text( - text = logText, - modifier = Modifier.fillMaxSize().verticalScroll(scrollState), - fontFamily = FontFamily.Monospace, - fontSize = 12.sp - ) + Text(text = logText, modifier = Modifier.fillMaxSize().verticalScroll(scrollState), fontFamily = FontFamily.Monospace, fontSize = 12.sp) } } } @@ -816,11 +807,7 @@ fun SimpleDatePicker(initialDate: LocalDate, onDateSelected: (LocalDate) -> Unit Dialog(onDismissRequest = onDismissRequest) { Surface(shape = MaterialTheme.shapes.medium, modifier = Modifier.padding(16.dp).widthIn(max = 320.dp), elevation = 8.dp) { Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Button(onClick = { displayedYearMonth = displayedYearMonth.minusMonths(1) }) { Text("<") } Text(text = displayedYearMonth.format(DateTimeFormatter.ofPattern("yyyy년 MM월")), style = MaterialTheme.typography.h6) Button(onClick = { displayedYearMonth = displayedYearMonth.plusMonths(1) }) { Text(">") } @@ -832,34 +819,31 @@ fun SimpleDatePicker(initialDate: LocalDate, onDateSelected: (LocalDate) -> Unit } } Spacer(Modifier.height(8.dp)) - val daysInMonth = displayedYearMonth.lengthOfMonth() - val firstDayOfMonth = displayedYearMonth.atDay(1).dayOfWeek.value % 7 - // Using a simple grid layout with Rows and Columns as LazyVerticalGrid is not stable in all environments. + val firstDayOfMonth = displayedYearMonth.atDay(1).dayOfWeek.value % 7 + val daysInMonth = displayedYearMonth.lengthOfMonth() + val totalCells = firstDayOfMonth + daysInMonth + val rows = (totalCells + 6) / 7 + Column { - val totalCells = firstDayOfMonth + daysInMonth - val rows = (totalCells + 6) / 7 for (row in 0 until rows) { Row { for (col in 0 until 7) { val dayIndex = row * 7 + col - if (dayIndex < firstDayOfMonth || dayIndex >= totalCells) { - Box(Modifier.size(40.dp)) - } else { - val day = dayIndex - firstDayOfMonth + 1 - val date = displayedYearMonth.atDay(day) - val isSelected = date == initialDate - Box( - modifier = Modifier.size(40.dp) - .clip(CircleShape) - .background(if (isSelected) MaterialTheme.colors.primary else Color.Transparent) - .clickable { onDateSelected(date) }, - contentAlignment = Alignment.Center - ) { - Text( - text = "$day", - color = if (isSelected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface - ) + Box(modifier = Modifier.size(40.dp)) { + if (dayIndex >= firstDayOfMonth && dayIndex < totalCells) { + val day = dayIndex - firstDayOfMonth + 1 + val date = displayedYearMonth.atDay(day) + val isSelected = date == initialDate + Box( + modifier = Modifier.fillMaxSize() + .clip(CircleShape) + .background(if (isSelected) MaterialTheme.colors.primary else Color.Transparent) + .clickable { onDateSelected(date) }, + contentAlignment = Alignment.Center + ) { + Text(text = "$day", color = if (isSelected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface) + } } } } @@ -880,8 +864,7 @@ fun main() = application { Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { App( onSaveTsvRequest = { name, tsvData -> val dialog = FileDialog(Frame(), "TSV 파일로 저장", FileDialog.SAVE).apply { - file = "$name.tsv" - isVisible = true + file = "$name.tsv"; isVisible = true } if (dialog.directory != null && dialog.file != null) { try {