..
This commit is contained in:
parent
f76d3c652c
commit
b3c6f02551
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user