Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26fb793ab9 | |||
| 597880e807 | |||
| 6fb83848f5 | |||
| fef630d266 | |||
| fdcbb615ab | |||
| 161cc8060d | |||
| 6c2dec6cd3 | |||
| fa4c50a054 | |||
| 707d81d850 | |||
| 4d8861d79c |
206
GITEA_RELEASE_GUIDE.md
Normal file
206
GITEA_RELEASE_GUIDE.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Gitea 릴리즈 작업 가이드
|
||||
|
||||
> ShiftRing 프로젝트 Gitea 릴리즈 자동화 문서
|
||||
> 저장소: https://git.webpluss.net/sanjeok77/ShiftRing
|
||||
|
||||
---
|
||||
|
||||
## 🔐 인증 정보
|
||||
|
||||
**Personal Access Token (PAT)**
|
||||
- 위치: `.env.local` 파일
|
||||
- 형식: `e3b515eaa0a6683c921ca3bf718e281ed30a6075`
|
||||
- 사용자: `sanjeok77`
|
||||
|
||||
**인증 헤더**
|
||||
```bash
|
||||
-u "sanjeok77:TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 릴리즈 생성 절차
|
||||
|
||||
### 1. 버전 업데이트 (3곳)
|
||||
|
||||
#### 1.1 `app/build.gradle.kts` - 앱 날부 버전
|
||||
```kotlin
|
||||
defaultConfig {
|
||||
versionCode = 1125 // ← 이전: 1124
|
||||
versionName = "1.2.5" // ← 이전: "1.2.4"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 `version.json` - 서버 버전 정보
|
||||
```json
|
||||
{
|
||||
"versionCode": 1125,
|
||||
"versionName": "1.2.5",
|
||||
"apkUrl": "https://git.webpluss.net/attachments/{UUID}",
|
||||
"changelog": "v1.2.5: 변경사항 요약",
|
||||
"forceUpdate": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 Git 태그 및 릴리즈
|
||||
- 태그: `v1.2.5`
|
||||
- 브랜치: `dev` (개발) → `main` (배포)
|
||||
|
||||
---
|
||||
|
||||
### 2. 릴리즈 생성 (API)
|
||||
|
||||
**엔드포인트**
|
||||
```bash
|
||||
POST https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases
|
||||
```
|
||||
|
||||
**요청 예시**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-u "sanjeok77:TOKEN" \
|
||||
"https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases" \
|
||||
-d '{
|
||||
"tag_name": "v1.2.5",
|
||||
"name": "v1.2.5 - 릴리즈 제목",
|
||||
"body": "## 변경사항\n\n- 기능1\n- 기능2",
|
||||
"prerelease": false,
|
||||
"target_commitish": "dev"
|
||||
}'
|
||||
```
|
||||
|
||||
**응답 예시**
|
||||
```json
|
||||
{
|
||||
"id": 30,
|
||||
"tag_name": "v1.2.5",
|
||||
"upload_url": "https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases/30/assets"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. APK 빌드
|
||||
|
||||
**릴리즈 빌드**
|
||||
```bash
|
||||
./gradlew :app:assembleRelease
|
||||
```
|
||||
|
||||
**출력 경로**
|
||||
```
|
||||
app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
**서명 설정** (`keystore.properties`)
|
||||
```properties
|
||||
storePassword=비밀번호
|
||||
keyAlias=별칭
|
||||
keyPassword=비밀번호
|
||||
storeFile=../release.jks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. APK 업로드 (API)
|
||||
|
||||
**엔드포인트**
|
||||
```bash
|
||||
POST https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases/{release_id}/assets
|
||||
```
|
||||
|
||||
**요청 예시**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-u "sanjeok77:TOKEN" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@app/build/outputs/apk/release/app-release.apk" \
|
||||
"https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases/30/assets?name=app.apk"
|
||||
```
|
||||
|
||||
**응답 예시**
|
||||
```json
|
||||
{
|
||||
"id": 37,
|
||||
"name": "app.apk",
|
||||
"size": 5236988,
|
||||
"browser_download_url": "https://git.webpluss.net/attachments/b8f53c11-743f-416c-87ae-bd478c781abf"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. version.json 업데이트
|
||||
|
||||
**APK URL 업데이트**
|
||||
```json
|
||||
{
|
||||
"apkUrl": "https://git.webpluss.net/attachments/{UUID}"
|
||||
}
|
||||
```
|
||||
|
||||
**주의**: `releases/download/v1.2.5/app.apk` 형식이 아닌 `attachments/{UUID}` 형식 사용
|
||||
|
||||
---
|
||||
|
||||
## 🔧 유틸리티 명령어
|
||||
|
||||
### 릴리즈 조회
|
||||
```bash
|
||||
curl -s -u "sanjeok77:TOKEN" \
|
||||
"https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases" | jq '.[].tag_name'
|
||||
```
|
||||
|
||||
### 특정 릴리즈 조회
|
||||
```bash
|
||||
curl -s -u "sanjeok77:TOKEN" \
|
||||
"https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases/30"
|
||||
```
|
||||
|
||||
### 첨부파일 삭제
|
||||
```bash
|
||||
curl -s -u "sanjeok77:TOKEN" \
|
||||
-X DELETE \
|
||||
"https://git.webpluss.net/api/v1/repos/sanjeok77/ShiftRing/releases/30/assets/{asset_id}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **버전 일치**: `build.gradle.kts`, `version.json`, Git 태그 3곳 모두 동일 버전 사용
|
||||
2. **APK 파일명**: 반드시 `app.apk`로 업로드 (클리어 이름 지정)
|
||||
3. **UUID**: 업로드 후 반환된 UUID를 `version.json`에 반영
|
||||
4. **브랜치**:
|
||||
- 개발: `dev` 브랜치에 커밋
|
||||
- 배포: `main` 브랜치에 cherry-pick
|
||||
|
||||
---
|
||||
|
||||
## 📁 관련 파일
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `app/build.gradle.kts` | 앱 날부 버전 설정 |
|
||||
| `version.json` | 서버 버전 정보 |
|
||||
| `keystore.properties` | 서명 키 설정 |
|
||||
| `release.jks` | 서명 키스토어 |
|
||||
| `.env.local` | API 토큰 저장 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 변경 이력
|
||||
|
||||
| 날짜 | 버전 | 작업 |
|
||||
|------|------|------|
|
||||
| 2026-02-28 | v1.2.5 | 알람 시스템 단순화 릴리즈 |
|
||||
| 2026-02-28 | v1.2.4 | 버그 수정 릴리즈 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 참고 링크
|
||||
|
||||
- 릴리즈 페이지: https://git.webpluss.net/sanjeok77/ShiftRing/releases
|
||||
- API 문서: https://git.webpluss.net/api/swagger
|
||||
- Swagger UI: https://git.webpluss.net/api/swagger#/repository/repoCreateRelease
|
||||
@@ -20,8 +20,10 @@ android {
|
||||
applicationId = "com.example.shiftalarm"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1119
|
||||
versionName = "1.1.9"
|
||||
versionCode = 1130
|
||||
versionName = "1.3.0"
|
||||
versionName = "1.2.5"
|
||||
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -78,6 +78,14 @@ class AlarmActivity : AppCompatActivity() {
|
||||
|
||||
// 알람 시작 (화면 상태와 무관하게 항상 실행)
|
||||
startAlarm()
|
||||
|
||||
// 마스터 알람이 꺼져있으면 알람 화면을 즉시 종료
|
||||
val prefs = getSharedPreferences("ShiftAlarmPrefs", Context.MODE_PRIVATE)
|
||||
if (!ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)) {
|
||||
Toast.makeText(this, "전체 알람이 꺼져있습니다.", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
setupControls()
|
||||
|
||||
// 5분 후 자동 스누즈
|
||||
@@ -209,6 +217,7 @@ class AlarmActivity : AppCompatActivity() {
|
||||
val dx = event.rawX - startX
|
||||
if (abs(dx) > maxSwipe * 0.8f) {
|
||||
// Trigger Dismiss
|
||||
@Suppress("DEPRECATION")
|
||||
(getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50)
|
||||
Toast.makeText(this, "알람 해제 완료", Toast.LENGTH_SHORT).show()
|
||||
stopAlarm(); finish()
|
||||
@@ -247,6 +256,7 @@ class AlarmActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun handleSnooze() {
|
||||
@Suppress("DEPRECATION")
|
||||
(getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50)
|
||||
val snoozeRepeat = intent.getIntExtra("EXTRA_SNOOZE_REPEAT", 3)
|
||||
val text = if (snoozeRepeat == 99) "다시 울림 설정됨" else "다시 울림 (${snoozeRepeat}회 남음)"
|
||||
@@ -274,21 +284,25 @@ class AlarmActivity : AppCompatActivity() {
|
||||
else android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI
|
||||
}
|
||||
|
||||
// AudioAttributes 강화: 화면 켜진 상태에서도 알람음이 울리도록
|
||||
// AudioAttributes 강화: 무음/진동 모드에서도 알람음이 울리도록
|
||||
val audioAttrs = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) // 볼륨 강제 적용
|
||||
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
|
||||
.build()
|
||||
|
||||
// AudioManager를 통해 알람 볼륨 설정
|
||||
// AudioManager를 통해 알람 볼륨 설정 및 무음 모드 우회
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM)
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM)
|
||||
|
||||
// 무음 모드에서도 알람음이 울리도록 STREAM_ALARM 사용 (벨소리와 독립)
|
||||
// 알람 스트림은 다른 스트림과 달리 무음 모드에서도 울림
|
||||
val originalRingerMode = audioManager.ringerMode
|
||||
|
||||
// 알람 볼륨을 최대로 설정 (사용자가 나중에 조정 가능)
|
||||
try {
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_ALARM, maxVolume, 0)
|
||||
Log.d("AlarmActivity", "알람 볼륨 설정: $maxVolume (RingerMode: $originalRingerMode)")
|
||||
} catch (e: Exception) {
|
||||
Log.w("AlarmActivity", "알람 볼륨 설정 실패", e)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ class AlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
Log.d(TAG, "===== 알람 수신 (Receiver) =====")
|
||||
|
||||
// 마스터 알람이 꺼져있으면 알람 무시
|
||||
val prefs = context.getSharedPreferences("ShiftAlarmPrefs", Context.MODE_PRIVATE)
|
||||
if (!ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)) {
|
||||
Log.w(TAG, "마스터 알람이 꺼져있어 알람을 무시합니다.")
|
||||
return
|
||||
}
|
||||
|
||||
val alarmId = intent?.getIntExtra("EXTRA_ALARM_ID", -1) ?: -1
|
||||
val isCustom = intent?.getBooleanExtra("EXTRA_IS_CUSTOM", false) ?: false
|
||||
|
||||
@@ -50,6 +57,7 @@ class AlarmReceiver : BroadcastReceiver() {
|
||||
private fun startAlarm(context: Context, intent: Intent?) {
|
||||
// WakeLock 획득 (화면 켜기 및 Activity 실행 보장)
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
@Suppress("DEPRECATION")
|
||||
val wakeLock = pm.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
"ShiftAlarm::AlarmWakeLock"
|
||||
@@ -71,6 +79,8 @@ class AlarmReceiver : BroadcastReceiver() {
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
|
||||
Log.d(TAG, "ForegroundService 시작 완료")
|
||||
|
||||
// 2. AlarmActivity 직접 실행 (알람 화면 표시)
|
||||
val activityIntent = Intent(context, AlarmActivity::class.java).apply {
|
||||
putExtra("EXTRA_SHIFT", intent?.getStringExtra("EXTRA_SHIFT") ?: "근무")
|
||||
@@ -88,8 +98,15 @@ class AlarmReceiver : BroadcastReceiver() {
|
||||
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
}
|
||||
|
||||
context.startActivity(activityIntent)
|
||||
Log.d(TAG, "AlarmActivity 실행 완료")
|
||||
// 지연 후 Activity 시작 (ForegroundService가 알림을 먼저 표시하도록)
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
try {
|
||||
context.startActivity(activityIntent)
|
||||
Log.d(TAG, "AlarmActivity 실행 완료")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "AlarmActivity 실행 실패", e)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "알람 실행 실패", e)
|
||||
|
||||
@@ -137,7 +137,9 @@ private fun cancelAllPendingIntentsForUniqueId(context: Context, uniqueId: Int)
|
||||
for (day in 1..31) {
|
||||
try {
|
||||
val alarmId = 200000000 + year * 1000000 + month * 10000 + day * 100 + baseId
|
||||
val intent = Intent(context, AlarmReceiver::class.java)
|
||||
val intent = Intent(context, AlarmReceiver::class.java).apply {
|
||||
action = "com.example.shiftalarm.ALARM_TRIGGER"
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, alarmId, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
@@ -200,7 +202,9 @@ private fun cancelTestAlarm(context: Context) {
|
||||
|
||||
private fun cancelAlarmInternal(context: Context, alarmId: Int) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(context, AlarmReceiver::class.java)
|
||||
val intent = Intent(context, AlarmReceiver::class.java).apply {
|
||||
action = "com.example.shiftalarm.ALARM_TRIGGER"
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, alarmId, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
|
||||
@@ -39,14 +39,21 @@ object AppUpdateManager {
|
||||
reader.close()
|
||||
|
||||
val json = JSONObject(result)
|
||||
val serverVersionCode = json.getInt("versionCode")
|
||||
val serverVersionName = json.getString("versionName")
|
||||
val apkUrl = json.getString("apkUrl")
|
||||
val changelog = json.optString("changelog", "버그 수정 및 성능 향상")
|
||||
|
||||
val pInfo = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
||||
val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
pInfo.longVersionCode.toInt()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pInfo.versionCode
|
||||
}
|
||||
val currentVersionName = pInfo.versionName ?: "0.0.0"
|
||||
|
||||
if (isNewerVersion(serverVersionName, currentVersionName)) {
|
||||
if (serverVersionCode > currentVersionCode) {
|
||||
activity.runOnUiThread {
|
||||
showUpdateDialog(activity, serverVersionName, changelog, apkUrl)
|
||||
}
|
||||
@@ -71,29 +78,6 @@ object AppUpdateManager {
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun isNewerVersion(server: String, current: String): Boolean {
|
||||
try {
|
||||
// Clean version strings (remove non-numeric suffixes if any)
|
||||
val sClean = server.split("-")[0].split(" ")[0]
|
||||
val cClean = current.split("-")[0].split(" ")[0]
|
||||
|
||||
val sParts = sClean.split(".").map { it.filter { char -> char.isDigit() }.let { p -> if (p.isEmpty()) 0 else p.toInt() } }
|
||||
val cParts = cClean.split(".").map { it.filter { char -> char.isDigit() }.let { p -> if (p.isEmpty()) 0 else p.toInt() } }
|
||||
|
||||
val length = Math.max(sParts.size, cParts.size)
|
||||
for (i in 0 until length) {
|
||||
val s = if (i < sParts.size) sParts[i] else 0
|
||||
val c = if (i < cParts.size) cParts[i] else 0
|
||||
if (s > c) return true
|
||||
if (s < c) return false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AppUpdateManager", "Version comparison failed: ${e.message}")
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(activity: Activity, version: String, changelog: String, apkUrl: String) {
|
||||
com.google.android.material.dialog.MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("새로운 업데이트 발견 (v$version)")
|
||||
|
||||
@@ -451,13 +451,18 @@ class FragmentSettingsAlarm : Fragment(), SharedPreferences.OnSharedPreferenceCh
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
timePicker.hour = parts[0].toInt(); timePicker.minute = parts[1].toInt()
|
||||
} else {
|
||||
timePicker.currentHour = parts[0].toInt(); timePicker.currentMinute = parts[1].toInt()
|
||||
@Suppress("DEPRECATION")
|
||||
timePicker.currentHour = parts[0].toInt()
|
||||
@Suppress("DEPRECATION")
|
||||
timePicker.currentMinute = parts[1].toInt()
|
||||
}
|
||||
|
||||
btnSelectSound.setOnClickListener {
|
||||
val intent = Intent(android.media.RingtoneManager.ACTION_RINGTONE_PICKER).apply {
|
||||
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_TYPE, android.media.RingtoneManager.TYPE_ALARM)
|
||||
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, if (currentDialogSoundUri != null) android.net.Uri.parse(currentDialogSoundUri) else null as android.net.Uri?)
|
||||
// 무음 선택 방지: 시스템 알람음만 선택 가능
|
||||
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
|
||||
}
|
||||
startActivityForResult(intent, 100)
|
||||
}
|
||||
@@ -485,7 +490,9 @@ class FragmentSettingsAlarm : Fragment(), SharedPreferences.OnSharedPreferenceCh
|
||||
|
||||
btnCancel.setOnClickListener { dialog.dismiss() }
|
||||
btnSave.setOnClickListener {
|
||||
@Suppress("DEPRECATION")
|
||||
val h = if (android.os.Build.VERSION.SDK_INT >= 23) timePicker.hour else timePicker.currentHour
|
||||
@Suppress("DEPRECATION")
|
||||
val m = if (android.os.Build.VERSION.SDK_INT >= 23) timePicker.minute else timePicker.currentMinute
|
||||
val time = String.format("%02d:%02d", h, m)
|
||||
|
||||
|
||||
14
version.json
14
version.json
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"versionCode": 1119,
|
||||
"versionName": "1.1.9",
|
||||
"apkUrl": "https://git.webpluss.net/sanjeok77/ShiftRing/releases/download/v1.1.9/app.apk",
|
||||
"changelog": "v1.1.9: version.json URL 수정 (서버 연결 실패 해결)",
|
||||
"versionCode": 1130,
|
||||
"versionName": "1.3.0",
|
||||
"apkUrl": "https://git.webpluss.net/sanjeok77/ShiftRing/releases/download/v1.3.0/app.apk",
|
||||
"changelog": "v1.3.0: versionCode 기반 업데이트 체크 개선",
|
||||
"forceUpdate": false
|
||||
}
|
||||
"versionCode": 1125,
|
||||
"versionName": "1.2.5",
|
||||
"apkUrl": "https://git.webpluss.net/sanjeok77/ShiftRing/releases/download/v1.2.5/app.apk",
|
||||
"changelog": "v1.2.5: 알람 시스템 단순화 - 삭제된 알람 버그 수정",
|
||||
"forceUpdate": false
|
||||
}
|
||||
Reference in New Issue
Block a user