1 Commits

Author SHA1 Message Date
ttipo 65c2016ab5 chore(flutter): 출시 준비 — 앱 이름·패키지 ID·아이콘·시작 화면·서명·서버 주소 확인
템플릿 기본값(tma_app · com.example)으로 남아 있던 앱 정체성과 출시 빌드 설정을 갖춘다.
- 앱 이름 rup, 패키지(번들) ID com.ttipo.rup (Android·iOS — 스토어에 올린 뒤에는 바꿀 수 없다). 웹 제목도 rup
- 앱 아이콘: 브랜드 주황 + 흰 워드마크 (원본 branding/, flutter_launcher_icons 로 크기별 생성 · Android 적응형 아이콘)
- 앱이 뜨는 동안의 화면을 첫 화면(스플래시)과 같은 노란색으로 — 흰 화면이 번쩍이지 않게 (Android 12+ 포함, iOS)
- Android 출시 서명: android/key.properties(커밋 안 함, 예시 key.properties.example). 없으면 경고와 함께 디버그 키
- 서버 주소는 --dart-define=API_URL 이 .env 보다 먼저. 출시 빌드가 http·개발용 주소를 가리키면 시작하지 않는다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:42:45 +09:00
60 changed files with 231 additions and 170 deletions
+3 -2
View File
@@ -7,8 +7,9 @@
# 빌드 타임 분리가 필요하면 --dart-define 또는 .env.{flavor} 도입 고려.
# ==========================================
# 백엔드 API 베이스 URL (백엔드 글로벌 prefix /api 포함)
# 안드로이드 에뮬레이터에서 호스트 접근 시: http://10.0.2.2:3000/api
# 백엔드 API 베이스 URL (백엔드 글로벌 prefix /api 포함) — 개발용
# 안드로이드 에뮬레이터에서 호스트 접근 시: http://10.0.2.2:3000/api (또는 adb reverse tcp:3000 tcp:3000 후 localhost)
# 출시 빌드는 이 값 대신 --dart-define=API_URL=https://<도메인>/api 로 넘긴다 (https·공개 주소가 아니면 앱이 시작하지 않는다)
API_URL=http://localhost:3000/api
# 기록을 어디에 둘지 — api: 서버(기본, 로그인한 회원의 기록) / local: 이 기기 (서버 없이 화면만 볼 때)
+32 -8
View File
@@ -1,3 +1,6 @@
import java.io.FileInputStream
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,8 +8,16 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
// 출시 서명 키 — android/key.properties (커밋하지 않는다, 예시는 key.properties.example).
// 파일이 없으면 디버그 키로 서명한다 — 스토어에는 올릴 수 없고, 기기에서 릴리스 빌드를 확인할 때만 쓴다.
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) FileInputStream(keystorePropertiesFile).use { load(it) }
}
val hasReleaseKey = keystorePropertiesFile.exists()
android {
namespace = "com.example.tma_app"
namespace = "com.ttipo.rup"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -20,21 +31,34 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.tma_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
// 스토어에 올린 뒤에는 바꿀 수 없다 (iOS 번들 ID 도 같은 값)
applicationId = "com.ttipo.rup"
// 버전은 pubspec.yaml 의 version(이름+빌드 번호)을 따른다
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
if (hasReleaseKey) {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (hasReleaseKey) {
signingConfigs.getByName("release")
} else {
logger.warn("android/key.properties 가 없어 릴리스 빌드를 디버그 키로 서명합니다 — 스토어에 올릴 수 없습니다.")
signingConfigs.getByName("debug")
}
}
}
}
@@ -2,9 +2,9 @@
<!-- 명함 이미지를 앨범에 저장 (Android 10 이하만 필요 — gal) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<application
android:label="tma_app"
android:label="rup"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/launcher_icon"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
@@ -1,4 +1,4 @@
package com.example.tma_app
package com.ttipo.rup
import io.flutter.embedding.android.FlutterActivity
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<!-- 앱이 뜨는 동안의 화면 — 첫 화면(스플래시)과 같은 노란 바탕만 둔다 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item android:drawable="@color/launch_background" />
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<!-- 앱이 뜨는 동안의 화면 — 첫 화면(스플래시)과 같은 노란 바탕만 둔다 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item android:drawable="@color/launch_background" />
</layer-list>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Android 12+ 시스템 스플래시 — 바탕을 첫 화면(스플래시)과 같은 노란색으로, 가운데는 앱 아이콘 -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Android 12+ 시스템 스플래시 — 바탕을 첫 화면(스플래시)과 같은 노란색으로, 가운데는 앱 아이콘 -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 적응형 아이콘 바탕 — 브랜드 주황 (flutter_launcher_icons 가 만든 값) -->
<color name="ic_launcher_background">#FF7500</color>
<!-- 앱이 뜨는 동안의 바탕 — 첫 화면(스플래시, AppColors.splashYellow)과 같은 색이라 깜빡임이 없다 -->
<color name="launch_background">#FFDE84</color>
</resources>
+10
View File
@@ -0,0 +1,10 @@
# 출시 서명 키 설정 — 이 파일을 key.properties 로 복사해 값을 채운다 (key.properties 와 키 파일은 커밋하지 않는다).
#
# 키 만들기 (한 번만 · 잃어버리면 같은 앱으로 업데이트를 올릴 수 없으니 안전한 곳에 백업):
# keytool -genkey -v -keystore ~/rup-upload.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
#
# storeFile 은 android/app 기준 경로 또는 절대 경로
storeFile=/path/to/rup-upload.jks
storePassword=change-me-store-password
keyAlias=upload
keyPassword=change-me-key-password
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+8 -8
View File
@@ -372,7 +372,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -388,7 +388,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -405,7 +405,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -420,7 +420,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -431,7 +431,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -488,7 +488,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -551,7 +551,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -573,7 +573,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_BUNDLE_IDENTIFIER = com.ttipo.rup;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -19,7 +19,7 @@
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="1" green="0.87058823529411766" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+2 -2
View File
@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Tma App</string>
<string>rup</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -34,7 +34,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>tma_app</string>
<string>rup</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
+22 -2
View File
@@ -15,8 +15,28 @@ DataMode readDataMode() {
return dotenv.maybeGet('DATA_MODE') == DataMode.local.name ? DataMode.local : DataMode.api;
}
/// `.env` 의 API_URL — 안드로이드 에뮬레이터에서 호스트에 붙을 때는 http://10.0.2.2:3000/api
String readApiBaseUrl() => (dotenv.isInitialized ? dotenv.maybeGet('API_URL') : null) ?? kDefaultApiUrl;
/// 빌드할 때 넘긴 API 주소 — `flutter build … --dart-define=API_URL=https://…/api` (출시 빌드는 이것을 쓴다)
const String _kDefinedApiUrl = String.fromEnvironment('API_URL');
/// 서버 주소 — 빌드 때 넘긴 값 → `.env` 의 API_URL → 개발 서버 순.
/// 안드로이드 에뮬레이터에서 호스트에 붙을 때는 http://10.0.2.2:3000/api (또는 adb reverse 로 localhost)
String readApiBaseUrl() {
if (_kDefinedApiUrl.isNotEmpty) return _kDefinedApiUrl;
return (dotenv.isInitialized ? dotenv.maybeGet('API_URL') : null) ?? kDefaultApiUrl;
}
/// 내 기기에서만 닿는 주소 — 출시 빌드가 여기를 가리키면 사용자 기기에서 서버에 닿지 않는다
const Set<String> _kLocalHosts = {'localhost', '127.0.0.1', '10.0.2.2', '0.0.0.0'};
/// 출시 빌드의 서버 주소에 문제가 있으면 그 이유, 괜찮으면 null.
/// 출시 빌드는 https 만 쓸 수 있고(Android·iOS 모두 평문 http 를 막는다) 개발용 주소를 가리키면 안 된다
String? releaseApiUrlProblem(String url) {
final uri = Uri.tryParse(url);
if (uri == null || uri.host.isEmpty) return '서버 주소(API_URL)를 읽을 수 없습니다: $url';
if (uri.scheme != 'https') return '출시 빌드의 서버 주소는 https 여야 합니다: $url';
if (_kLocalHosts.contains(uri.host)) return '출시 빌드가 개발용 서버 주소를 가리킵니다: $url';
return null;
}
final dataModeProvider = Provider<DataMode>((ref) => readDataMode());
+7
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@@ -20,6 +21,12 @@ Future<void> main() async {
// .env 파일이 없는 경우 무시 (기본값 사용)
}
// 출시 빌드가 개발 서버·평문 http 를 가리키면 바로 멈춘다 — 사용자 기기에서 조용히 모든 요청이 실패하는 것보다 낫다
if (kReleaseMode && readDataMode() == DataMode.api) {
final problem = releaseApiUrlProblem(readApiBaseUrl());
if (problem != null) throw StateError(problem);
}
// 시안은 세로 화면 전용이다
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
+56
View File
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.0.1"
archive:
dependency: transitive
description:
name: archive
sha256: "58c8d17d8d589c4259f773522815115de873c397b977a29993588487d3f9acb4"
url: "https://pub.dev"
source: hosted
version: "4.5.0"
args:
dependency: transitive
description:
@@ -49,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_config:
dependency: transitive
description:
@@ -57,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -222,6 +246,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.2.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -376,6 +408,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
image_picker:
dependency: "direct main"
description:
@@ -464,6 +504,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
@@ -672,6 +720,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.2"
pub_semver:
dependency: transitive
description:
+13
View File
@@ -58,6 +58,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -117,3 +118,15 @@ flutter:
- family: UhBeeKeongKeong
fonts:
- asset: assets/fonts/UhBeeKeongKeong.ttf
# 앱 아이콘 — 원본은 branding/ (브랜드 주황 + 흰 워드마크, 1024px).
# 원본을 바꾸면 `dart run flutter_launcher_icons` 로 Android·iOS 크기별 아이콘을 다시 만든다.
flutter_launcher_icons:
android: launcher_icon
ios: true
image_path: branding/app_icon.png
remove_alpha_ios: true
min_sdk_android: 23
# Android 8+ 적응형 아이콘 — 바탕은 브랜드 주황, 앞은 안전 영역 안의 워드마크
adaptive_icon_background: "#FF7500"
adaptive_icon_foreground: branding/app_icon_foreground.png
+27
View File
@@ -0,0 +1,27 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tma_app/core/config/app_config.dart';
void main() {
group('releaseApiUrlProblem', () {
test('공개 https 주소는 출시 빌드에 쓸 수 있다', () {
expect(releaseApiUrlProblem('https://api.rup.app/api'), isNull);
});
test('평문 http 는 출시 빌드에 쓸 수 없다 (기기가 막는다)', () {
expect(releaseApiUrlProblem('http://api.rup.app/api'), contains('https'));
});
test('개발용 주소는 출시 빌드에 쓸 수 없다', () {
expect(releaseApiUrlProblem('https://localhost:3000/api'), contains('개발용'));
expect(releaseApiUrlProblem('https://10.0.2.2/api'), contains('개발용'));
});
test('읽을 수 없는 주소', () {
expect(releaseApiUrlProblem('api'), isNotNull);
});
});
test('빌드 때 넘긴 값이 없고 .env 도 읽지 않았으면 개발 서버 주소', () {
expect(readApiBaseUrl(), kDefaultApiUrl);
});
}
+2 -2
View File
@@ -23,13 +23,13 @@
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="tma_app">
<meta name="apple-mobile-web-app-title" content="rup">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>tma_app</title>
<title>rup</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "tma_app",
"short_name": "tma_app",
"name": "rup",
"short_name": "rup",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",