Kotlin Multiplatform 네이티브 앱으로 전환 (Android + iOS 공용 코드)

TWA 래퍼를 걷어내고 Compose Multiplatform 으로 다시 만들었다. 엔진·UI·모드
전부 commonMain 한 벌이라 Android 와 iOS 가 같은 코드를 쓴다.

- engine/: 웹 JS 엔진을 Kotlin 으로 이식 (SRS+180 킥, 7-bag, T-spin, B2B,
  콤보, All Clear, 점수·레벨). commonTest 28개 통과
- game/: 모드 규칙 + 중력/락딜레이/DAS/ARR 타이밍
- ui/: 네오브루탈 컴포넌트, 메인/솔로시트/인게임/결과/설정
- ui/Responsive.kt: 고정 해상도를 버리고 가용 크기에서 셀 크기·배율 계산.
  폰 세로, 플립 초세로, 플립 커버(소형), 폴드 펼침, 태블릿/아이패드 가로 대응
- androidMain: resizeableActivity + configChanges 로 접기/펴기·회전 시 재시작 없음
- iosMain + iosApp/: MainViewController + SwiftUI 껍데기 + XcodeGen 설정
  (실제 iOS 빌드는 macOS + Xcode 필요)
- scripts/release.sh: 버전 상승 → 테스트 → 서명 빌드 → 태그 → Gitea 릴리스 자동화
This commit is contained in:
2026-09-23 10:37:17 +09:00
parent 0839705b97
commit cbd0f62319
87 changed files with 2706 additions and 783 deletions

View File

@@ -0,0 +1,109 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
// ── 버전 관리 ────────────────────────────────────────────────────
// 기본값은 gradle.properties. CI/릴리스 스크립트는
// ./gradlew assembleRelease -PappVersionName=1.2.0 -PappVersionCode=7
// 처럼 덮어쓴다.
val appVersionName: String = (findProperty("appVersionName") as String?) ?: "0.0.1"
val appVersionCode: Int = ((findProperty("appVersionCode") as String?) ?: "1").toInt()
kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}
// iOS 타깃 — 실제 컴파일은 macOS + Xcode 에서만 가능하다.
// 리눅스에서는 설정만 되고 빌드 태스크를 실행하지 않는다.
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(libs.androidx.lifecycle.viewmodel)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
}
}
}
android {
namespace = "kr.tkrmagid.tetrig"
compileSdk = 36
defaultConfig {
applicationId = "kr.tkrmagid.tetrig"
minSdk = 24
targetSdk = 36
versionCode = appVersionCode
versionName = appVersionName
}
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
// keystore.properties 가 있으면 릴리스 서명을 붙인다. 없으면 unsigned.
val keystorePropsFile = rootProject.file("keystore.properties")
val keystoreProps = Properties().apply {
if (keystorePropsFile.exists()) keystorePropsFile.inputStream().use { load(it) }
}
if (keystoreProps.isNotEmpty()) {
signingConfigs {
create("release") {
storeFile = file(keystoreProps.getProperty("storeFile"))
storePassword = keystoreProps.getProperty("storePassword")
keyAlias = keystoreProps.getProperty("keyAlias")
keyPassword = keystoreProps.getProperty("keyPassword")
}
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
if (keystoreProps.isNotEmpty()) {
signingConfig = signingConfigs.getByName("release")
}
}
getByName("debug") {
applicationIdSuffix = ".debug"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
dependencies {
debugImplementation(compose.uiTooling)
}