[Sample] 카드 인식 모듈화 작업(1)

수호·2025년 11월 19일
post-thumbnail

차세대를 시작하기 전 Clean Architecture 구조로 payon 인식 모듈화 작업 샘플 프로젝트 개발

  1. 추가된 구조
추가된 구조
core/common
	- util/Util.kt (byte→String / String→byte 등 공통으로 쓰는 함수 정리)
	- NFCEventBus (NFC Tag 값 전달 → 다른 모듈에서 알 수 있는 방법 flow로 처리)
core/network
	→ 전체적인 network 관리를 여기서 할 예정
core/payon
	→ payon sector 접근 및 network통신 처리 (NFC를 읽는 기능은 MainActivity 공통으로 처리)
feature/reader
	→ nfc 읽는 화면
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val nfcAdapter by lazy { NfcAdapter.getDefaultAdapter(this) }
    val currentRouteFlow = MutableStateFlow<String?>(null)
    val currentReadTypeFlow = MutableStateFlow<String?>(null)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val appState = rememberBeaverAppState()
            val navController = appState.navController
            BeaverPaySampleTheme {
                NavGraph(appState)
            }

            // NavRoute 변경 감지
            val navBackStack by navController.currentBackStackEntryAsState()

            LaunchedEffect(navBackStack) {
                val destination = navBackStack?.destination
                val args = navBackStack?.arguments

                val route = destination?.route
                val readType = args?.getString("readType")

                Timber.e("🔥 route changed = $route, readType = $readType")

                currentRouteFlow.value = route
                currentReadTypeFlow.value = readType
            }

        }

        // 📌 여기서 routeFlow 변화 감지하고 NFC on/off 처리
        lifecycleScope.launch {
            currentReadTypeFlow.collect { type ->
                Timber.e("🔄 routeFlow collect → $type")

                if (type == "payon") {
                    enableReaderMode()
                } else {
                    disableReaderMode()
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
    }

    override fun onPause() {
        super.onPause()
    }

    fun enableReaderMode() {
        Timber.e("🔵 enableReaderMode() 실행")
        nfcAdapter?.enableReaderMode(
            this,
            { tag ->
                Timber.e("ReaderMode Tag detected: ${tag.id.joinToString { "%02X".format(it) }}")
                NFCEventBus.onTagDetected(tag)
            },
            NfcAdapter.FLAG_READER_NFC_A or
                    NfcAdapter.FLAG_READER_NFC_B or
                    NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
            null
        )
    }

    fun disableReaderMode() {
        Timber.e("🟥 disableReaderMode() 실행")
        nfcAdapter?.disableReaderMode(this)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)

        Timber.e("🔥 onNewIntent 호출, ACTION = ${intent.action}")

        if (intent.action == NfcAdapter.ACTION_TECH_DISCOVERED ||
            intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED ||
            intent.action == NfcAdapter.ACTION_TAG_DISCOVERED
        ) {
            val tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
            } else {
                @Suppress("DEPRECATION")
                intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
            }

            if (tag == null) {
                Timber.d("Tag is NULL")
                return
            }

            Timber.e("✅ Tag detected! techList = ${tag.techList.joinToString()}")

            NFCEventBus.onTagDetected(tag)
        }
    }
}
object NFCEventBus {
    private val _nfcFlow = MutableSharedFlow<Tag>()
    val nfcFlow = _nfcFlow

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    fun onTagDetected(tag: Tag) {
        scope.launch {
            _nfcFlow.emit(tag)
        }
    }
}
@HiltViewModel
class ReaderViewModel @Inject constructor(
    val cardReaderUseCase: CardReaderUseCase
) : ViewModel() {

    private val _tagInfo = MutableStateFlow("")
    val tagInfo = _tagInfo

    private val _uiEvent = MutableSharedFlow<UiEvent>()
    val uiEvent: MutableSharedFlow<UiEvent> = _uiEvent

    init {
        viewModelScope.launch {
            NFCEventBus.nfcFlow.collect { tag ->
                val id = tag.id.joinToString("") { "%02X".format(it) }
                _tagInfo.value = "Tag ID: $id"

                cardReaderUseCase(ReaderType.PAYON, tag).collect { result ->
                    when(result){
                        is CardResult.Loading -> {
                            _uiEvent.emit(UiEvent.Loading(result.isLoading))
                        }
                        is CardResult.Success -> {
                            _uiEvent.emit(
                                UiEvent.ShowDialog(
                                    "카드인식 성공",
                                    "Card : ${Util().bytesToHex(result.data.track2Data)}"
                                )
                            )
                        }
                        is CardResult.Error -> {
                            _uiEvent.emit(UiEvent.ShowDialog(result.code, result.message))
                        }
                    }
                }

            }
        }
    }
}
  1. Compose에서 RouteFlow 변화를 감지하여 NFC ON/OFF 처리 담당

    → 현재는 reader/payon 일 경우 enable, 나머지일 경우 disable

  2. NFC 인식 시 NewIntent에서 Tag 값을 *MutableSharedFlow*로 가지고 있음

  3. ViewModel에서 collect해서 Tag값을 feature/reader 모듈로 가져올 수 있음

🔥 emit vs collect

emitcollect
역할Flow에 값 넣기Flow로부터 값 받기
위치보통 Activity, Repository, EventBus보통 ViewModel, UI
종류ProducerConsumer
실행 시기이벤트가 발생할 때Flow를 collect하는 동안 계속
suspend 여부suspend (SharedFlow에서는 tryEmit 사용 가능)suspend
profile
처음부터 다시 시작!!

0개의 댓글