diff --git a/README.md b/README.md index 24ecf31..fbed6a1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,13 @@ TETR.IO 스타일의 **모바일 실시간 대전 블록 스태커** 웹 게임. 이 문서는 앱 제작을 위한 **최종 빌드 진입점(entry point)** 이다. 모든 설계 결정·수치·에셋의 **정본(source of truth)은 [`TETRIG-최종명세서.md`](./TETRIG-최종명세서.md)** 이며, 구현 시 반드시 명세서와 UX 목업을 함께 열어 작업한다. -> **비용 0 · 아이폰/안드로이드 모두 지원 (2026-08-22 확정)**: 명세서대로 **웹 게임 + PWA(홈화면 설치)** 로 간다. Cloudflare Pages(무료)에 배포하면 iPhone Safari·Android Chrome에서 그대로 돌아가고, "홈 화면에 추가"로 전체화면 앱처럼 실행된다(`manifest.webmanifest` + `sw.js` 오프라인 캐시 + 앱 아이콘). **맥·Xcode·앱스토어/구글플레이 등록비·Apple Developer 멤버십이 전혀 필요 없다.** (네이티브 iOS 앱만 유료·맥 필수라 웹/PWA 경로가 무료로 크로스플랫폼을 달성하는 유일한 방법이다.) 클라이언트 진입점은 [`index.html`](./index.html)(빌드 산출물), 소스는 [`src/`](./src), 서버 계획(Cloudflare Workers + D1 + WebSocket)은 명세서 그대로. +> **배포 경로 (2026-09-24 확정)**: 출시 대상은 **Google Play 안드로이드 네이티브 앱** +> (`app/`, Kotlin Multiplatform + Compose). 아이폰은 App Store 대신 **웹 PWA** +> (`https://tetrig.pages.dev`, "홈 화면에 추가")로 커버한다 — iOS 네이티브는 +> Apple Developer Program 연 $99 + 맥이 필요해 접었다. 필요한 비용은 Play 개발자 +> 등록비 $25(1회)뿐이고, 웹/서버는 Cloudflare 무료 티어로 간다. 웹 클라이언트 +> 진입점은 [`index.html`](./index.html)(빌드 산출물), 소스는 [`src/`](./src), +> 서버 계획(Cloudflare Workers + D1 + WebSocket)은 명세서 그대로. --- @@ -99,19 +105,20 @@ wrangler deploy # 배포 ### 네이티브 앱 (Kotlin Multiplatform + Compose Multiplatform) -`app/` 이 실제 출시 대상이다. 엔진과 UI 전부 `commonMain` 한 벌이고 -Android / iOS 가 그걸 공유한다. +`app/` 이 실제 출시 대상이고, **출시 플랫폼은 Google Play(안드로이드) 하나다.** +iOS 타깃은 2026-09-24 에 제거했다 — Apple Developer Program 연 $99 와 맥이 +필요해 비용 대비 실익이 없다고 판단했다. 아이폰 사용자는 웹 PWA +(`https://tetrig.pages.dev`)로 커버한다. 엔진과 UI 전부 `commonMain` 한 벌이라, +나중에 iOS 가 필요해지면 `build.gradle.kts` 에 `iosTarget` 만 다시 넣으면 된다. ``` app/composeApp/src/ commonMain/ 엔진(engine/) · 모드·타이밍(game/) · UI(ui/) · 저장소(data/) - commonTest/ 엔진 테스트 (Android/iOS 양쪽에서 동일하게 돈다) + commonTest/ 엔진 테스트 28개 androidMain/ MainActivity, 매니페스트, 아이콘, SharedPreferences 구현 - iosMain/ MainViewController, NSUserDefaults 구현 -app/iosApp/ SwiftUI 껍데기 + XcodeGen 설정 ``` -안드로이드 빌드 (이 저장소에서 바로 가능): +안드로이드 빌드: ```bash export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 @@ -123,18 +130,22 @@ cp keystore.properties.sample keystore.properties # 서명 키 경로/비번 # → composeApp/build/outputs/bundle/release/composeApp-release.aab ``` -iOS 빌드 (**macOS + Xcode 필요**. 리눅스에서는 코드만 컴파일 검증되고 실제 -프레임워크 빌드는 되지 않는다): +### Play Console 제출 자산 -```bash -brew install xcodegen -cd app/iosApp && xcodegen generate && open iosApp.xcodeproj -# Xcode 에서 실행하면 prebuild 스크립트가 -# ./gradlew :composeApp:embedAndSignAppleFrameworkForXcode 를 돌려 -# ComposeApp.framework 를 만들어 링크한다. +`store/` 에 스토어 등록에 필요한 것이 다 들어 있다. + +``` +store/listing.md 앱 이름·설명·데이터보안 설문 답안·콘텐츠 등급 가이드 +store/privacy.html 개인정보처리방침 (https://tetrig.pages.dev/privacy 로 배포됨) +store/feature_graphic.png 1024x500 피처 그래픽 +store/icon-512-play.png 512x512 스토어 아이콘 (알파 없는 정사각형) +store/shots/0*.png 휴대전화 스크린샷 1080x2400 4장 +store/shots/tablet_*.png 태블릿 세로 1600x2560 2장 + 가로 2560x1600 1장 +store/make_feature_graphic.py, store/make_play_icon.py 자산 재생성 스크립트 ``` -App Store 출시에는 Apple Developer Program 등록(연 $99)과 맥이 필요하다. +앱은 `INTERNET` 권한조차 요청하지 않는다(`aapt2 dump badging` 으로 확인). +그래서 Play 데이터 보안 설문은 전 항목 "수집 안 함"으로 끝난다. ### 기기 호환 @@ -147,8 +158,7 @@ App Store 출시에는 Apple Developer Program 등록(연 $99)과 맥이 필요 | 갤럭시 Z 플립 본화면(초세로) | 보드를 세로로 더 키움 | | 갤럭시 Z 플립 커버(소형) | 축소 배율 + 넥스트 3개로 감축 | | 갤럭시 Z 폴드 펼침(정사각형에 가까움) | 넓은 폭 레이아웃, 카드 가로 배치 | -| 태블릿 / 아이패드 가로 | 보드 가운데, 조작을 좌우 엄지 영역으로 분리 | -| 아이폰 | 동일 코드. 세이프에어리어 처리 | +| 태블릿 가로 | 보드 가운데, 조작을 좌우 엄지 영역으로 분리 | 접었다 펴기·회전은 액티비티 재시작 없이 처리한다 (`configChanges` + `resizeableActivity=true`). @@ -168,7 +178,8 @@ scripts/release.sh patch # 또는 minor / major ### 웹 (Cloudflare Pages) -`index.html` 기반 PWA 는 그대로 유지한다 (아이폰 미설치 사용자용 / 데모). +`index.html` 기반 PWA 는 그대로 유지한다 (아이폰 사용자용 / 데모). +`dist/privacy.html` 은 개인정보처리방침이고 `/privacy` 로 서빙된다. ```bash node build.js diff --git a/app/composeApp/build.gradle.kts b/app/composeApp/build.gradle.kts index a02d085..cc05985 100644 --- a/app/composeApp/build.gradle.kts +++ b/app/composeApp/build.gradle.kts @@ -1,4 +1,3 @@ -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 @@ -23,14 +22,9 @@ kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } - // iOS 타깃 — 실제 컴파일은 macOS + Xcode 에서만 가능하다. - // 리눅스에서는 설정만 되고 빌드 태스크를 실행하지 않는다. - listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "ComposeApp" - isStatic = true - } - } + // iOS 타깃은 뺐다 (2026-09-24 결정). Apple Developer Program 연 $99 + 맥이 + // 필요해 비용 대비 실익이 없다고 판단. 아이폰은 웹 PWA(tetrig.pages.dev)로 커버한다. + // 구조는 commonMain 중심 그대로라, 나중에 iosTarget 만 다시 추가하면 된다. sourceSets { commonMain.dependencies { diff --git a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/data/Store.kt b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/data/Store.kt index 64e0c56..1b7dff5 100644 --- a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/data/Store.kt +++ b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/data/Store.kt @@ -2,7 +2,7 @@ package kr.tkrmagid.tetrig.data import androidx.compose.runtime.mutableStateMapOf -/** 플랫폼 키-값 저장소 (Android SharedPreferences / iOS NSUserDefaults). */ +/** 플랫폼 키-값 저장소 (Android 는 SharedPreferences). 기기 밖으로 나가지 않는다. */ interface Store { fun getString(key: String, default: String): String fun putString(key: String, value: String) diff --git a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/MainScreen.kt b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/MainScreen.kt index b7cc2b6..e54265f 100644 --- a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/MainScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/MainScreen.kt @@ -64,18 +64,18 @@ fun MainScreen( ModeCard("솔로", "SPRINT · TIME ATTACK · ENDLESS", Palette.Cobalt, s) { showModes = true } } Box(Modifier.weight(1f)) { - ModeCard("멀티 · 리그전", "실시간 1:1 랭크 매치 · 배치 10경기", Palette.Safety, s) { onSoon("멀티 · 리그전") } + ModeCard("멀티 · 리그전", "실시간 1:1 랭크 매치 · 배치 10경기", Palette.Safety, s, soon = true) { onSoon("멀티 · 리그전") } } Box(Modifier.weight(1f)) { - ModeCard("커스텀", "코드로 만들고 참가 · 인원 제한 없음", null, s) { onSoon("커스텀") } + ModeCard("커스텀", "코드로 만들고 참가 · 인원 제한 없음", null, s, soon = true) { onSoon("커스텀") } } } } else { Box(Modifier.widthIn(max = 720.dp).align(Alignment.CenterHorizontally)) { Column { ModeCard("솔로", "SPRINT · TIME ATTACK · ENDLESS", Palette.Cobalt, s) { showModes = true } - ModeCard("멀티 · 리그전", "실시간 1:1 랭크 매치 · 배치 10경기", Palette.Safety, s) { onSoon("멀티 · 리그전") } - ModeCard("커스텀", "코드로 만들고 참가 · 인원 제한 없음", null, s) { onSoon("커스텀") } + ModeCard("멀티 · 리그전", "실시간 1:1 랭크 매치 · 배치 10경기", Palette.Safety, s, soon = true) { onSoon("멀티 · 리그전") } + ModeCard("커스텀", "코드로 만들고 참가 · 인원 제한 없음", null, s, soon = true) { onSoon("커스텀") } } } } @@ -144,6 +144,9 @@ private fun ModeCard( sub: String, barColor: Color?, scale: Float, + // 아직 구현되지 않은 모드는 카드에 '준비 중' 배지를 달아 둔다. 스토어 스크린샷에 + // 없는 기능이 있는 것처럼 보이면 Play 정책(허위 표시) 위반이 된다. + soon: Boolean = false, onClick: () -> Unit, ) { Box(Modifier.fillMaxWidth().padding(bottom = (14 * scale).dp)) { @@ -159,7 +162,19 @@ private fun ModeCard( .weight(1f) .padding(start = (14 * scale).dp, top = (18 * scale).dp, end = 8.dp), ) { - Disp(title, size = (26 * scale).sp) + Row(verticalAlignment = Alignment.CenterVertically) { + Disp(title, size = (26 * scale).sp) + if (soon) { + Spacer(Modifier.width((8 * scale).dp)) + Box( + Modifier + .background(Palette.Sub) + .padding(horizontal = (7 * scale).dp, vertical = (2 * scale).dp), + ) { + Mono("준비 중", color = Palette.Bone, size = (11 * scale).sp, weight = FontWeight.Bold) + } + } + } Spacer(Modifier.height(6.dp)) Mono(sub, color = Palette.Sub, size = (12 * scale).sp) } diff --git a/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/MainViewController.kt b/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/MainViewController.kt deleted file mode 100644 index ecb18b9..0000000 --- a/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/MainViewController.kt +++ /dev/null @@ -1,7 +0,0 @@ -package kr.tkrmagid.tetrig - -import androidx.compose.ui.window.ComposeUIViewController -import kr.tkrmagid.tetrig.ui.App - -@Suppress("unused", "FunctionName") -fun MainViewController() = ComposeUIViewController { App() } diff --git a/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/data/Store.ios.kt b/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/data/Store.ios.kt deleted file mode 100644 index 6c36489..0000000 --- a/app/composeApp/src/iosMain/kotlin/kr/tkrmagid/tetrig/data/Store.ios.kt +++ /dev/null @@ -1,16 +0,0 @@ -package kr.tkrmagid.tetrig.data - -import platform.Foundation.NSUserDefaults - -private class IosStore : Store { - private val defaults = NSUserDefaults.standardUserDefaults - - override fun getString(key: String, default: String): String = - defaults.stringForKey(key) ?: default - - override fun putString(key: String, value: String) { - defaults.setObject(value, forKey = key) - } -} - -actual fun platformStore(): Store = IosStore() diff --git a/app/iosApp/iosApp/ContentView.swift b/app/iosApp/iosApp/ContentView.swift deleted file mode 100644 index 89799d5..0000000 --- a/app/iosApp/iosApp/ContentView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI -import UIKit -import ComposeApp - -struct ComposeView: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> UIViewController { - // Kotlin 쪽 MainViewController() — commonMain 의 App() 을 그대로 띄운다. - MainViewControllerKt.MainViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} -} - -struct ContentView: View { - var body: some View { - ComposeView() - .ignoresSafeArea(.keyboard) - } -} diff --git a/app/iosApp/iosApp/iOSApp.swift b/app/iosApp/iosApp/iOSApp.swift deleted file mode 100644 index 215dc02..0000000 --- a/app/iosApp/iosApp/iOSApp.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI - -@main -struct iOSApp: App { - var body: some Scene { - WindowGroup { - ContentView() - .ignoresSafeArea(.all) - } - } -} diff --git a/app/iosApp/project.yml b/app/iosApp/project.yml deleted file mode 100644 index dd3458c..0000000 --- a/app/iosApp/project.yml +++ /dev/null @@ -1,53 +0,0 @@ -# XcodeGen 설정 — macOS 에서 `xcodegen generate` 로 iosApp.xcodeproj 를 만든다. -# brew install xcodegen -# cd app/iosApp && xcodegen generate && open iosApp.xcodeproj -# Gradle 이 만든 ComposeApp.framework 를 링크한다. -name: iosApp -options: - bundleIdPrefix: kr.tkrmagid - deploymentTarget: - iOS: "15.0" - -settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: kr.tkrmagid.tetrig - MARKETING_VERSION: "2.0.0" - CURRENT_PROJECT_VERSION: "2" - SWIFT_VERSION: "5.0" - TARGETED_DEVICE_FAMILY: "1,2" # iPhone + iPad - ENABLE_USER_SCRIPT_SANDBOXING: NO - -targets: - iosApp: - type: application - platform: iOS - sources: - - path: iosApp - info: - path: iosApp/Info.plist - properties: - CFBundleDisplayName: TETRIG - UILaunchScreen: {} - UISupportedInterfaceOrientations: - - UIInterfaceOrientationPortrait - - UIInterfaceOrientationLandscapeLeft - - UIInterfaceOrientationLandscapeRight - UISupportedInterfaceOrientations~ipad: - - UIInterfaceOrientationPortrait - - UIInterfaceOrientationPortraitUpsideDown - - UIInterfaceOrientationLandscapeLeft - - UIInterfaceOrientationLandscapeRight - UIRequiresFullScreen: false - settings: - base: - FRAMEWORK_SEARCH_PATHS: - - "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)" - OTHER_LDFLAGS: - - "-framework" - - "ComposeApp" - preBuildScripts: - - name: Build Compose framework - script: | - cd "$SRCROOT/../" - ./gradlew :composeApp:embedAndSignAppleFrameworkForXcode - basedOnDependencyAnalysis: false diff --git a/store/feature_graphic.png b/store/feature_graphic.png new file mode 100644 index 0000000..973e95f Binary files /dev/null and b/store/feature_graphic.png differ diff --git a/store/icon-512-play.png b/store/icon-512-play.png new file mode 100644 index 0000000..bb47474 Binary files /dev/null and b/store/icon-512-play.png differ diff --git a/store/listing.md b/store/listing.md new file mode 100644 index 0000000..0baa5bc --- /dev/null +++ b/store/listing.md @@ -0,0 +1,102 @@ +# TETRIG — Play Console 스토어 등록 정보 + +Play Console → 스토어 설정 / 기본 스토어 등록정보에 그대로 붙여 넣는 값이다. +길이 제한은 Play 기준(앱 이름 30자, 간단한 설명 80자, 자세한 설명 4000자)을 지켰다. + +--- + +## 앱 이름 (30자 이내) + +``` +TETRIG +``` + +## 간단한 설명 (80자 이내) — 현재 31자 + +``` +스프린트·타임어택·엔들리스. 광고 없는 순수 블록 퍼즐. +``` + +## 자세한 설명 (4000자 이내) + +``` +TETRIG은 군더더기를 걷어낸 블록 스태킹 퍼즐입니다. 로그인도, 광고도, 결제도 없습니다. 앱을 열면 바로 게임이 시작됩니다. + +■ 세 가지 솔로 모드 +· SPRINT — 40줄을 얼마나 빨리 지우는지 겨루는 기록 경기입니다. +· TIME ATTACK — 2분 동안 최대 점수를 뽑아내는 모드입니다. 시간이 갈수록 레벨이 올라갑니다. +· ENDLESS — 제한이 없습니다. 탑아웃해도 이어서 계속 쌓을 수 있어 연습에 좋습니다. + +■ 제대로 된 조작감 +· SRS+180 회전과 킥 테이블을 구현해, 익숙한 회전 보정이 그대로 통합니다. +· 7-bag 랜덤이라 같은 블록이 몰려 나오지 않습니다. +· 홀드, 고스트(착지 위치 미리보기), 하드 드롭, 소프트 드롭, 180도 회전을 모두 지원합니다. +· T-스핀, 백투백, 콤보, 퍼펙트 클리어를 판정해 점수에 반영합니다. +· DAS/ARR, 락 딜레이를 조정해 반응이 끊기지 않게 맞췄습니다. + +■ 어떤 화면에서든 맞춰집니다 +· 일반 스마트폰은 물론 갤럭시 폴드(접었을 때·펼쳤을 때), 플립, 태블릿까지 화면 크기에 따라 배치가 바뀝니다. +· 가로 화면에서는 조작 버튼이 좌우로 갈라져 양손 엄지에 닿습니다. +· 폴드를 펼치거나 화면을 돌려도 게임이 재시작되지 않고 그대로 이어집니다. +· 태블릿·폴드용 '큰 버튼' 옵션과 왼손잡이용 좌우 반전 옵션이 있습니다. + +■ 개인정보를 수집하지 않습니다 +· 인터넷 권한조차 요청하지 않습니다. 앱은 네트워크에 전혀 접속하지 않습니다. +· 기록과 설정은 기기 안에만 저장되고 어디로도 전송되지 않습니다. +· 회원가입, 광고, 추적, 분석 도구가 없습니다. +· 비행기 모드에서도 모든 기능이 동일하게 동작합니다. + +■ 광고도 결제도 없습니다 +전체 기능이 처음부터 열려 있습니다. 결제 유도, 에너지·하트 같은 플레이 제한, 보상형 광고가 없습니다. + +개인정보처리방침: https://tetrig.pages.dev/privacy + +TETRIG은 개인이 만든 앱입니다. Tetris Holding LLC 및 관련 회사와 아무 관련이 없습니다. +``` + +--- + +## 그래픽 자산 + +| 항목 | 파일 | 규격 | 상태 | +|---|---|---|---| +| 앱 아이콘 | `store/icon-512-play.png` | 512×512 PNG, 알파 없는 풀블리드 | 준비됨 | +| 그래픽 이미지(피처) | `store/feature_graphic.png` | 1024×500 PNG | 준비됨 | +| 휴대전화 스크린샷 | `store/shots/01_main.png` ~ `04_settings.png` | 1080×2400 | 4장 (Play 최소 2장) | +| 7·10인치 태블릿 | `store/shots/tablet_01_main.png`, `tablet_02_game.png` | 1600×2560 | 2장 | +| 태블릿 가로 | `store/shots/tablet_03_landscape.png` | 2560×1600 | 1장 | + +피처 그래픽은 `python3 store/make_feature_graphic.py`, 앱 아이콘은 +`python3 store/make_play_icon.py` 로 다시 생성할 수 있다. 앱 내부 런처 아이콘은 +모서리가 둥근 `icon-512.png` 를 그대로 쓰고, 스토어 등록용만 정사각형 풀블리드다. + +--- + +## 데이터 보안(Data safety) 설문 답안 + +릴리스 APK의 권한을 확인한 결과 `INTERNET` 권한이 없다(`kr.tkrmagid.tetrig.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` 하나뿐). 그래서 모든 문항이 "아니요"로 끝난다. + +- 앱이 필요한 사용자 데이터 유형을 수집하거나 공유하나요? → **아니요** +- 모든 사용자 데이터가 전송 중에 암호화되나요? → (수집 없음이므로 해당 없음) +- 사용자가 데이터 삭제를 요청할 수 있나요? → (수집 없음이므로 해당 없음) +- 앱에 광고가 포함되어 있나요? → **아니요** +- 인앱 구매 → **없음** + +근거: 기록·설정은 안드로이드 SharedPreferences 로 기기 내부에만 저장되고 외부로 나가지 않는다. + +## 콘텐츠 등급 설문 + +퍼즐 게임 카테고리로, 폭력·성적 내용·욕설·도박·약물 요소가 전부 없고 사용자 간 소통 기능도 없다. 전 문항 "아니요"로 답하면 전체이용가(ESRB Everyone / PEGI 3 / GRAC 전체이용가)가 나온다. + +## 기타 Play Console 입력값 + +- 앱 또는 게임: **게임** +- 카테고리: **퍼즐** +- 태그: 퍼즐 / 캐주얼 / 아케이드 +- 무료 또는 유료: **무료** +- 대상 연령: 전체 +- 광고 포함: **아니요** +- 개인정보처리방침 URL: `https://tetrig.pages.dev/privacy` +- 이메일: `tkrmagid@gmail.com` +- 패키지명: `kr.tkrmagid.tetrig` +- 업로드 파일: `app/composeApp/build/outputs/bundle/release/composeApp-release.aab` diff --git a/store/make_feature_graphic.py b/store/make_feature_graphic.py new file mode 100644 index 0000000..c3c8896 --- /dev/null +++ b/store/make_feature_graphic.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Play Console 피처 그래픽(1024x500) 생성. + +앱 팔레트(NEOBRUTAL)를 그대로 써서 로고 + 테트로미노 악센트를 배치한다. +가장자리는 스토어에서 잘릴 수 있으므로 핵심 요소는 가운데로 모았다. + + python3 store/make_feature_graphic.py +""" +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parent.parent +OUT = ROOT / "store" / "feature_graphic.png" + +W, H = 1024, 500 +BONE = (0xED, 0xE7, 0xDA) +INK = (0x17, 0x16, 0x1B) +SAFETY = (0xFF, 0x4E, 0x11) +SUB = (0x6E, 0x6A, 0x5E) +PIECES = { + "I": (0x3E, 0xC1, 0xB6), + "O": (0xF5, 0xC5, 0x31), + "T": (0xA7, 0x6B, 0xF2), + "S": (0x7F, 0xCC, 0x4C), + "Z": (0xF2, 0x5C, 0x4C), + "J": (0x4C, 0x7D, 0xF2), + "L": (0xF2, 0x91, 0x3D), +} + +img = Image.new("RGB", (W, H), BONE) +d = ImageDraw.Draw(img) + +# 은은한 격자 +for x in range(0, W, 32): + d.line([(x, 0), (x, H)], fill=(0xE3, 0xDC, 0xCD), width=1) +for y in range(0, H, 32): + d.line([(0, y), (W, y)], fill=(0xE3, 0xDC, 0xCD), width=1) + + +def block(x, y, s, color): + """네오브루탈 블록: 오렌지 그림자 + 잉크 테두리.""" + d.rounded_rectangle([x + 5, y + 5, x + s + 5, y + s + 5], radius=6, fill=SAFETY) + d.rounded_rectangle([x, y, x + s, y + s], radius=6, fill=color, outline=INK, width=3) + + +# 오른쪽: 쌓인 스택 실루엣 +S = 44 +stack = [ + (0, 0, "I"), (1, 0, "I"), (2, 0, "I"), + (1, 1, "T"), (2, 1, "L"), + (0, 2, "J"), (1, 2, "O"), (2, 2, "O"), + (0, 3, "S"), (1, 3, "Z"), (2, 3, "Z"), +] +bx, by = 778, 145 +for cx, cy, p in stack: + block(bx + cx * (S + 6), by + cy * (S + 6), S, PIECES[p]) + +# 로고 +logo = Image.open(ROOT / "tetrig-logo.png").convert("RGBA") +lw = 640 +logo = logo.resize((lw, int(logo.height * lw / logo.width)), Image.LANCZOS) +img.paste(logo, (62, 92), logo) + + +def font(size, bold=True): + name = "DejaVuSansMono-Bold.ttf" if bold else "DejaVuSansMono.ttf" + return ImageFont.truetype(f"/usr/share/fonts/truetype/dejavu/{name}", size) + + +# 태그라인 바 +tag = "SPRINT · TIME ATTACK · ENDLESS" +f = font(24) +tw = d.textlength(tag, font=f) +tx, ty = 80, 352 +d.rectangle([tx + 6, ty + 6, tx + tw + 36, ty + 48], fill=SAFETY) +d.rectangle([tx, ty, tx + tw + 30, ty + 42], fill=INK) +d.text((tx + 15, ty + 9), tag, font=f, fill=BONE) + +img.save(OUT) +print(OUT, img.size) diff --git a/store/make_play_icon.py b/store/make_play_icon.py new file mode 100644 index 0000000..c9d878e --- /dev/null +++ b/store/make_play_icon.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Play Console 앱 아이콘(512x512, 알파 없는 정사각형) 생성. + +기존 icon-512.png 는 모서리가 투명한 라운드 아이콘이라 Play 스토어처럼 +자체 마스킹을 적용하는 곳에서는 모서리가 비어 보인다. 여기서는 같은 디자인을 +정사각형 풀블리드로 다시 그려 알파 없이 저장한다. + + python3 store/make_play_icon.py +""" +from pathlib import Path +from PIL import Image, ImageDraw + +ROOT = Path(__file__).resolve().parent.parent +OUT = ROOT / "store" / "icon-512-play.png" + +S = 512 +BONE = (0xED, 0xE7, 0xDA) +INK = (0x17, 0x16, 0x1B) +SAFETY = (0xFF, 0x4E, 0x11) + +img = Image.new("RGB", (S, S), BONE) +d = ImageDraw.Draw(img) + +# T 미노: 위 3칸 + 아래 가운데 1칸 (원본 아이콘과 동일한 배치) +CELL = 118 +GAP = 10 +OFF = 14 # 오렌지 그림자 오프셋 +cells = [(0, 0), (1, 0), (2, 0), (1, 1)] +total_w = 3 * CELL + 2 * GAP +ox = (S - total_w) // 2 +oy = 150 + +for cx, cy in cells: + x = ox + cx * (CELL + GAP) + y = oy + cy * (CELL + GAP) + d.rounded_rectangle([x + OFF, y + OFF, x + CELL + OFF, y + CELL + OFF], + radius=16, fill=SAFETY) + d.rounded_rectangle([x, y, x + CELL, y + CELL], radius=16, fill=INK) + +# 하단 해저드 스트라이프 +bar_top = S - 78 +d.rectangle([0, bar_top, S, S], fill=BONE) +step = 34 +for i in range(-S, S * 2, step): + d.polygon([(i, S), (i + step // 2, S), (i + step // 2 + 78, bar_top), (i + 78, bar_top)], + fill=INK) +d.line([(0, bar_top), (S, bar_top)], fill=INK, width=6) + +img.save(OUT) +print(OUT, img.size, img.mode) diff --git a/store/privacy.html b/store/privacy.html new file mode 100644 index 0000000..9489481 --- /dev/null +++ b/store/privacy.html @@ -0,0 +1,102 @@ + + + + + +TETRIG 개인정보처리방침 / Privacy Policy + + + +
+

TETRIG 개인정보처리방침

+

최종 수정일: 2026-09-24 · 앱 패키지: kr.tkrmagid.tetrig

+ +
+

요약: TETRIG은 개인정보를 일절 수집하지 않습니다. 앱에는 인터넷 접근 권한조차 없으며, + 모든 데이터는 기기 안에만 저장됩니다.

+
+ +

1. 수집하는 개인정보

+

없습니다. TETRIG은 이름, 이메일, 전화번호, 위치, 연락처, 사진, 기기 식별자 등 + 어떤 개인정보도 수집·요청·전송하지 않습니다. 회원가입 절차가 없습니다.

+ +

2. 기기에 저장되는 정보

+

아래 항목만 기기 내부 저장소(Android SharedPreferences)에 저장되며 외부로 전송되지 않습니다.

+ +

앱을 삭제하면 이 데이터도 함께 삭제됩니다.

+ +

3. 권한

+

TETRIG은 인터넷, 저장소, 위치, 카메라, 마이크, 연락처 등 어떤 런타임 권한도 요청하지 않습니다.

+ +

4. 제3자 제공 · 광고 · 분석

+

광고 SDK, 분석 SDK, 크래시 리포팅 도구를 일절 포함하지 않습니다. + 따라서 제3자에게 제공되는 데이터도 없습니다.

+ +

5. 아동 개인정보

+

어떤 이용자로부터도 개인정보를 수집하지 않으므로, 만 14세 미만 아동의 개인정보 역시 수집하지 않습니다.

+ +

6. 변경 고지

+

향후 온라인 대전 기능이 추가되어 수집 항목이 생기면, 이 문서를 갱신하고 + 앱 업데이트 시점에 안내합니다.

+ +

7. 문의

+

개인정보 관련 문의: tkrmagid@gmail.com

+ +
+ +

TETRIG Privacy Policy

+

Last updated: 2026-09-24 · Package: kr.tkrmagid.tetrig

+ +
+

Summary: TETRIG collects no personal data. The app does not even hold the + INTERNET permission. All data stays on your device.

+
+ +

1. Data we collect

+

None. TETRIG does not collect, request, or transmit any personal information such as name, + email, phone number, location, contacts, photos, or device identifiers. There is no account system.

+ +

2. Data stored on your device

+ +

Uninstalling the app removes this data.

+ +

3. Permissions

+

TETRIG requests no runtime permissions and has no internet access.

+ +

4. Third parties, ads, analytics

+

No advertising SDK, no analytics SDK, no crash reporting. No data is shared with anyone.

+ +

5. Children

+

Since no personal data is collected from any user, none is collected from children either.

+ +

6. Changes

+

If online multiplayer is added later and data collection begins, this document will be updated + and the change announced with the app update.

+ +

7. Contact

+

tkrmagid@gmail.com

+
+ + diff --git a/store/shots/01_main.png b/store/shots/01_main.png new file mode 100644 index 0000000..339b33e Binary files /dev/null and b/store/shots/01_main.png differ diff --git a/store/shots/02_modes.png b/store/shots/02_modes.png new file mode 100644 index 0000000..9e7eefe Binary files /dev/null and b/store/shots/02_modes.png differ diff --git a/store/shots/03_game.png b/store/shots/03_game.png new file mode 100644 index 0000000..b0b5a3b Binary files /dev/null and b/store/shots/03_game.png differ diff --git a/store/shots/04_settings.png b/store/shots/04_settings.png new file mode 100644 index 0000000..f95f208 Binary files /dev/null and b/store/shots/04_settings.png differ diff --git a/store/shots/tablet_01_main.png b/store/shots/tablet_01_main.png new file mode 100644 index 0000000..92a6199 Binary files /dev/null and b/store/shots/tablet_01_main.png differ diff --git a/store/shots/tablet_02_game.png b/store/shots/tablet_02_game.png new file mode 100644 index 0000000..5227ecb Binary files /dev/null and b/store/shots/tablet_02_game.png differ diff --git a/store/shots/tablet_03_landscape.png b/store/shots/tablet_03_landscape.png new file mode 100644 index 0000000..7d60194 Binary files /dev/null and b/store/shots/tablet_03_landscape.png differ