This commit is contained in:
lunaticbum 2025-09-12 16:32:01 +09:00
parent f76d3c652c
commit b3c6f02551

View File

@ -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<AppAnalyticsRequestDat
@Serializable
data class AppAnalyticsRequestData(val id: String)
@Serializable
data class SingleAnalyticsReportRequestResponse(val data: SingleAnalyticsReportRequestData)
@Serializable
data class SingleAnalyticsReportRequestData(val attributes: SingleAnalyticsReportRequestAttributes)
@Serializable
data class SingleAnalyticsReportRequestAttributes(val requestState: String)
data class DailyDownloadRecord(
val date: LocalDate,
val downloads: Int,
@ -240,11 +233,9 @@ fun App(onSaveTsvRequest: (name: String, tsvData: String) -> Unit) {
var logText by remember { mutableStateOf("여기에 통신 기록이 표시됩니다.") }
var statusMessage by remember { mutableStateOf("리포트 조회를 위한 정보를 입력하세요.") }
// State for Sales Report
var salesAnalysisResult by remember { mutableStateOf<List<AppSalesSummary>>(emptyList()) }
var rawTsvReport by remember { mutableStateOf("") }
// State for Analytics Report
var analyticsReportSet by remember { mutableStateOf<AnalyticsReportSet?>(null) }
var analyticsRequestId by remember { mutableStateOf<String?>(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<SingleAnalyticsReportRequestResponse>().data.attributes.requestState
statusMessage = "현재 상태: $status"
val reports = response.body<ReportListResponse>().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<ReportListResponse>().data.firstOrNull()?.id
if (reportId == null) {
statusMessage = "다운로드 리포트가 아직 준비되지 않았습니다. 잠시 후 다시 시도하세요."
statusMessage = "다운로드 리포트가 아직 준비되지 않았습니다. 잠시 후 '요청 상태 확인'으로 다시 시도하세요."
return@launch
}
@ -667,7 +666,10 @@ fun SalesResultView(statusMessage: String, result: List<AppSalesSummary>, 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<DailyDownloadRecord>?) {
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<DailyDownloadRecord>?) {
}
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 {