Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7fe12e2
feat: 카드 상세 팝업에 인스타그램 스토리 공유 추가
soyeonLee126 Aug 18, 2026
b7b1a67
Merge remote-tracking branch 'origin/develop' into feature/share
soyeonLee126 Aug 20, 2026
ef00ce8
feat: 공유 이미지 하단을 시안의 GAMSS 워드마크로 맞춘다
soyeonLee126 Aug 20, 2026
c36a239
feat: 공유하기에 카카오톡/인스타그램 선택 추가
soyeonLee126 Aug 20, 2026
b66299f
fix: 카드 공유 안정성과 테스트를 보강한다
seunghee17 Aug 21, 2026
01c04cc
fix: 인스타 스토리 공유 시 비트맵 변환 실패로 인한 크래시를 막는다
seunghee17 Sep 3, 2026
098959a
Merge remote-tracking branch 'origin/develop' into feature/share
seunghee17 Sep 7, 2026
bb88dea
fix: develop 병합 중 accept theirs로 유실된 카드 공유 기능을 복원한다
seunghee17 Sep 7, 2026
2ea9e36
chore: 공유 실패 로그 레벨을 warn에서 info로 낮춘다
seunghee17 Sep 7, 2026
23984a8
refactor: 공유 문구 주석을 실제 값에 맞게 고치고 불필요한 nullable 클릭 처리를 정리한다
seunghee17 Sep 7, 2026
2e68cfd
fix: 카카오톡 공유 중복 탭을 막고 인스타그램 스토리 공유 실패 테스트를 보강한다
seunghee17 Sep 7, 2026
1cafcb6
fix: 인스타그램 스토리 공유 이미지 생성을 IO로 분리
seunghee17 Sep 9, 2026
3a5db89
fix: 인스타그램 공유 실패 처리 정리
seunghee17 Sep 9, 2026
f57f789
test: 스토리 공유 캐시 정리 테스트 파일명 보정
seunghee17 Sep 9, 2026
670ab96
fix: 인스타그램 스토리 공유 이미지 배경색 처리를 상수 참조로 통일하고 실패 시 캐시 파일을 정리한다
seunghee17 Sep 9, 2026
8c8f4a0
fix: 인스타그램 스토리 캡처 상태 전환 순서를 캡처 함수 내부로 통합한다
seunghee17 Sep 9, 2026
b94b304
refactor: 아카이브 카드 공유 실패 처리를 뷰모델로 옮기고 UI 상태를 State로 승격
seunghee17 Sep 9, 2026
f895aec
refactor: import 순서 정리
seunghee17 Sep 9, 2026
7675e91
fix: 앱 시작 시 만료된 인스타그램 스토리 공유 캐시를 정리한다
seunghee17 Sep 15, 2026
ba5ae64
fix: 아카이브 카드 캡처 중 워드마크 전환이 노출되지 않도록 스크림 처리
seunghee17 Sep 15, 2026
08db7bd
fix: 인스타그램 공유 캐시 정리 시점 변경
seunghee17 Sep 15, 2026
1e222d6
fix: 카드 공유 중복 실행 방지
seunghee17 Sep 16, 2026
8db6471
Merge branch 'develop' of https://github.com/Nexters/GAMSS-Android in…
seunghee17 Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.gamss.android.app.share

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.gamss.android.core.ui.share.StoryShareResult
import com.gamss.android.core.ui.share.shareBitmapToInstagramStory
import com.gamss.android.core.ui.share.shareTextToKakaoTalk
import java.io.File
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class CardShareInstrumentedTest {

private val applicationContext = ApplicationProvider.getApplicationContext<Context>()

@Test
fun 카카오톡_공유_인텐트에_패키지와_문구를_담는다() {
val context = RecordingContext(applicationContext)

val shared = context.shareTextToKakaoTalk("카드 문구\nhttps://gamss.kr/cards/42")

val intent = requireNotNull(context.startedIntent)
assertTrue(shared)
assertEquals(Intent.ACTION_SEND, intent.action)
assertEquals("text/plain", intent.type)
assertEquals("com.kakao.talk", intent.`package`)
assertEquals("카드 문구\nhttps://gamss.kr/cards/42", intent.getStringExtra(Intent.EXTRA_TEXT))
}

@Test
fun 카카오톡_공유가_보안_예외로_막히면_false를_반환한다() {
val context = ThrowingContext(applicationContext, SecurityException("test"))

assertFalse(context.shareTextToKakaoTalk("공유 문구"))
}

@Test
fun 카카오톡이_없으면_false를_반환한다() {
val context = ThrowingContext(applicationContext, ActivityNotFoundException("test"))

assertFalse(context.shareTextToKakaoTalk("공유 문구"))
}

@Test
fun 인스타그램_공유_인텐트에_FileProvider_Uri와_읽기_권한을_담는다() = runBlocking {
val context = RecordingContext(applicationContext)
val bitmap = createBitmap(width = 20, height = 30)

val result = try {
context.shareBitmapToInstagramStory(bitmap)
} finally {
bitmap.recycle()
}

val intent = requireNotNull(context.startedIntent)
assertEquals(StoryShareResult.Shared, result)
assertEquals("com.instagram.share.ADD_TO_STORY", intent.action)
assertEquals("com.instagram.android", intent.`package`)
assertEquals("image/png", intent.type)
assertEquals("content", intent.data?.scheme)
assertTrue(intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
assertNotNull(applicationContext.contentResolver.openInputStream(requireNotNull(intent.data)).use { it?.read() })
}

@Test
fun 새_스토리_이미지를_만들기_전에_한시간_지난_캐시를_지운다() = runBlocking {
val shareDir = File(applicationContext.cacheDir, "shared_images").apply { mkdirs() }
val expired = File(shareDir, "story_share_expired.png").apply {
writeBytes(byteArrayOf(1))
setLastModified(System.currentTimeMillis() - 2 * 60 * 60 * 1_000L)
}
val context = RecordingContext(applicationContext)
val bitmap: Bitmap = createBitmap(width = 20, height = 30)

try {
assertEquals(StoryShareResult.Shared, context.shareBitmapToInstagramStory(bitmap))
} finally {
bitmap.recycle()
}

assertFalse(expired.exists())
}

@Test
fun 인스타그램이_없으면_InstagramUnavailable을_반환한다() = runBlocking {
val context = ThrowingContext(applicationContext, ActivityNotFoundException("test"))
val bitmap = createBitmap(width = 20, height = 30)

val result = try {
context.shareBitmapToInstagramStory(bitmap)
} finally {
bitmap.recycle()
}

assertEquals(StoryShareResult.InstagramUnavailable, result)
}

@Test
fun 공유_캐시_디렉터리_자리에_파일이_있으면_ImageUnavailable을_반환한다() = runBlocking {
// shared_images 자리를 디렉터리가 아닌 일반 파일로 선점해, 캐시 쓰기가 실제로
// 실패하는 경로(IOException)를 재현한다.
val shareDirPath = File(applicationContext.cacheDir, "shared_images")
shareDirPath.deleteRecursively()
check(shareDirPath.createNewFile()) { "테스트 전제 조건: $shareDirPath 를 파일로 선점하지 못했다" }
val context = RecordingContext(applicationContext)
val bitmap = createBitmap(width = 20, height = 30)

val result = try {
context.shareBitmapToInstagramStory(bitmap)
} finally {
bitmap.recycle()
shareDirPath.delete()
}

assertEquals(StoryShareResult.ImageUnavailable, result)
assertNull(context.startedIntent)
}

private class RecordingContext(base: Context) : ContextWrapper(base) {
var startedIntent: Intent? = null

override fun startActivity(intent: Intent) {
startedIntent = intent
}
}

/** [startActivity] 를 호출하면 [exception] 을 던진다. 공유 대상 앱이 없거나 권한이 없는 상황을 흉내낸다. */
private class ThrowingContext(base: Context, private val exception: RuntimeException) : ContextWrapper(base) {
override fun startActivity(intent: Intent) {
throw exception
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
<package android:name="com.instagram.android" />
<package android:name="com.kakao.talk" />
</queries>

<application
Expand Down Expand Up @@ -67,6 +69,16 @@
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
11 changes: 11 additions & 0 deletions app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.gamss.android.app.navigation.GamssRootNavHost
import com.gamss.android.core.designsystem.theme.GamssTheme
import com.gamss.android.core.ui.share.clearStaleInstagramStoryShareCache
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -43,4 +47,11 @@ class MainActivity : ComponentActivity() {
}
}
}

// 인스타그램에서 돌아와 화면이 다시 보일 때마다 지운다. 최초 실행 때도 onCreate 직후
// onResume 이 불려서 앱 시작 시점 정리도 그대로 진행됨.
override fun onResume() {
super.onResume()
lifecycleScope.launch(Dispatchers.IO) { clearStaleInstagramStoryShareCache() }
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="shared_images" path="shared_images/" />
</paths>
Loading
Loading