diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt index 29708fe86..7ed2383f3 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt @@ -1,5 +1,7 @@ package com.nexters.boolti.data.network.response +import com.nexters.boolti.data.util.toLocalDate +import com.nexters.boolti.data.util.toLocalDateTime import com.nexters.boolti.domain.model.Show import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -10,13 +12,16 @@ import java.time.LocalDateTime internal data class HostedShowDto( @SerialName("showId") val showId: String, @SerialName("showName") val showName: String, + @SerialName("date") val date: String, + @SerialName("salesStartTime") val salesStartDate: String, + @SerialName("salesEndTime") val salesEndDate: String, ) { fun toDomain(): Show = Show( id = showId, name = showName, - date = LocalDateTime.now(), - salesStartDate = LocalDate.now(), - salesEndDate = LocalDate.now(), + date = date.toLocalDateTime(), + salesStartDate = salesStartDate.toLocalDate(), + salesEndDate = salesEndDate.toLocalDate(), thumbnailImage = "", ) } diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index ad4f4ba0d..a7cb6c9ab 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt index 404bd2ed6..74e0fe15d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt @@ -2,7 +2,9 @@ package com.nexters.boolti.presentation import android.Manifest import android.graphics.Color +import android.hardware.Camera import android.os.Bundle +import android.os.VibrationEffect import android.view.KeyEvent import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle @@ -19,6 +21,7 @@ import com.journeyapps.barcodescanner.BarcodeResult import com.journeyapps.barcodescanner.DecoratedBarcodeView import com.journeyapps.barcodescanner.DefaultDecoderFactory import com.nexters.boolti.presentation.extension.requestPermission +import com.nexters.boolti.presentation.extension.vibrator import com.nexters.boolti.presentation.screen.qr.QrScanScreen import com.nexters.boolti.presentation.theme.BooltiTheme import dagger.hilt.android.AndroidEntryPoint @@ -27,6 +30,7 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class QrScanActivity : ComponentActivity() { + private var isBackCamera = true private val barcodeView: DecoratedBarcodeView by lazy { DecoratedBarcodeView(this).apply { @@ -43,6 +47,14 @@ class QrScanActivity : ComponentActivity() { private val callback = BarcodeCallback { result: BarcodeResult -> result.text ?: return@BarcodeCallback viewModel.scan(result.text) + + vibrator.vibrate( + VibrationEffect.createOneShot( + 100, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { barcodeView.pause() @@ -63,7 +75,8 @@ class QrScanActivity : ComponentActivity() { BooltiTheme { QrScanScreen( barcodeView = barcodeView, - onClickClose = { finish() } + onClickClose = { finish() }, + onClickSwitchCamera = ::switchCamera ) } } @@ -82,4 +95,15 @@ class QrScanActivity : ComponentActivity() { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { return barcodeView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event) } + + private fun switchCamera() { + barcodeView.pause() + isBackCamera = !isBackCamera + barcodeView.cameraSettings.requestedCameraId = if (isBackCamera) { + Camera.CameraInfo.CAMERA_FACING_BACK + } else { + Camera.CameraInfo.CAMERA_FACING_FRONT + } + barcodeView.resume() + } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt index 3c68a133f..5e5959e0f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt @@ -9,11 +9,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector @Composable fun CircleBgIcon( modifier: Modifier = Modifier, - painter: Painter, + imageVector: ImageVector, bgColor: Color, ) { Box( @@ -21,6 +22,6 @@ fun CircleBgIcon( .clip(CircleShape) .background(bgColor) ) { - Icon(painter = painter, contentDescription = null) + Icon(imageVector = imageVector, contentDescription = null) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt index f5b0e0138..b6cb025a6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt @@ -38,7 +38,7 @@ fun ToastSnackbarHost( ) { if (leadingIcon != null) { leadingIcon() - Spacer(modifier = Modifier.padding(end = 12.dp)) + Spacer(modifier = Modifier.padding(end = 8.dp)) } Text( modifier = Modifier.padding(vertical = 12.dp), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt index 6a71a2ead..ccab08655 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt @@ -4,7 +4,12 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.content.pm.PackageManager +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getSystemService fun Context.requireActivity(): Activity { var ctx = this @@ -24,3 +29,10 @@ fun Context.requireActivity(): Activity { fun Context.checkGrantedPermission(permission: String): Boolean { return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED } + +val Context.vibrator: Vibrator + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + (getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).defaultVibrator + } else { + getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt index 7000af1fe..37665aa0c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -36,7 +37,7 @@ import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey30 -import com.nexters.boolti.presentation.theme.Grey60 +import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.point1 import java.time.LocalDate import java.time.LocalDateTime @@ -60,7 +61,9 @@ fun HostedShowScreen( } ) { innerPadding -> if (uiState.shows.isEmpty()) { - EmptyHostedShow(modifier = modifier.padding(innerPadding)) + EmptyHostedShow(modifier = modifier + .padding(innerPadding) + .fillMaxSize()) } else { HostedShows( modifier = modifier @@ -96,7 +99,7 @@ private fun HostedShowItem( onClick: (showId: String, showName: String) -> Unit, ) { val enable = LocalDate.now().toEpochDay() <= show.date.toLocalDate().toEpochDay() - val tint = if (enable) White else Grey60 + val tint = if (enable) White else Grey50 Row( modifier = Modifier @@ -119,18 +122,6 @@ private fun HostedShowItem( } } -@Preview -@Composable -fun HostedShowItemPreview() { - BooltiTheme { - Surface { - HostedShowItem( - Show("", "hello world", LocalDateTime.now(), LocalDate.now(), LocalDate.now(), "") - ) { _, _ -> } - } - } -} - @Composable fun EmptyHostedShow( modifier: Modifier, @@ -163,3 +154,46 @@ fun EmptyHostedShow( } } } + +@Preview +@Composable +fun HostedShowItemPreview() { + BooltiTheme { + Surface { + HostedShowItem( + Show("", "hello world", LocalDateTime.now(), LocalDate.now(), LocalDate.now(), "") + ) { _, _ -> } + } + } +} + +@Preview +@Composable +fun OutDatedHostedShowItemPreview() { + BooltiTheme { + Surface { + HostedShowItem( + Show( + "", + "hello world", + LocalDateTime.now().minusDays(1), + LocalDate.now(), + LocalDate.now(), + "" + ) + ) { _, _ -> } + } + } +} + +@Preview(widthDp = 360, heightDp = 760) +@Composable +fun EmptyShowItemPreview() { + BooltiTheme { + Surface { + EmptyHostedShow( + Modifier + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt index 966433862..d1faec25f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt @@ -1,13 +1,19 @@ package com.nexters.boolti.presentation.screen.qr +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -21,14 +27,19 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel @@ -39,13 +50,17 @@ import com.nexters.boolti.presentation.QrScanEvent import com.nexters.boolti.presentation.QrScanViewModel import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog -import com.nexters.boolti.presentation.component.BtCloseableAppBar +import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtAppBarDefaults import com.nexters.boolti.presentation.component.CircleBgIcon import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.theme.Error +import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Success import com.nexters.boolti.presentation.theme.Warning +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable @@ -53,11 +68,11 @@ fun QrScanScreen( barcodeView: DecoratedBarcodeView, viewModel: QrScanViewModel = hiltViewModel(), onClickClose: () -> Unit, + onClickSwitchCamera: () -> Unit, ) { var showEntryCodeDialog by remember { mutableStateOf(false) } val snackbarHostState = remember { SnackbarHostState() } var snackbarIconId by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() val successMessage = stringResource(R.string.message_ticket_validated) val notTodayErrMessage = stringResource(R.string.error_show_not_today) @@ -65,76 +80,130 @@ fun QrScanScreen( val notMatchedErrMessage = stringResource(R.string.error_ticket_not_matched) val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var bottomPadding by remember { mutableStateOf(0.dp) } + var borderColor: Color by remember { mutableStateOf(Color.Transparent) } + var showingBorderJob: Job? = null + + val context = LocalContext.current + val cameraSwitchable = rememberSaveable { hasBothSidesCameras(context) } LaunchedEffect(barcodeView) { barcodeView.resume() } LaunchedEffect(viewModel.event) { - scope.launch { - viewModel.event.collect { event -> - val (iconId, errMessage) = when (event) { - is QrScanEvent.ScanError -> { - when (event.errorType) { - QrErrorType.ShowNotToday -> Pair( - R.drawable.ic_warning, - notTodayErrMessage - ) + viewModel.event.collect { event -> + val (iconId, errMessage, color) = when (event) { + is QrScanEvent.ScanError -> { + when (event.errorType) { + QrErrorType.ShowNotToday -> Triple( + R.drawable.ic_warning, + notTodayErrMessage, + Warning + ) - QrErrorType.UsedTicket -> Pair( - R.drawable.ic_error, - usedTicketErrMessage - ) + QrErrorType.UsedTicket -> Triple( + R.drawable.ic_error, + usedTicketErrMessage, + Error + ) - QrErrorType.TicketNotFound -> Pair( - R.drawable.ic_error, - notMatchedErrMessage - ) - } + QrErrorType.TicketNotFound -> Triple( + R.drawable.ic_error, + notMatchedErrMessage, + Error + ) } - - is QrScanEvent.ScanSuccess -> Pair(R.drawable.ic_error, successMessage) } - snackbarIconId = iconId + + is QrScanEvent.ScanSuccess -> Triple( + R.drawable.ic_error, + successMessage, + Success + ) + } + snackbarIconId = iconId + + launch { + snackbarHostState.currentSnackbarData?.dismiss() snackbarHostState.showSnackbar(errMessage) } + + showingBorderJob?.cancel() + showingBorderJob = launch { + borderColor = color + delay(4000L) + borderColor = Color.Transparent + } } } Scaffold( modifier = Modifier.navigationBarsPadding(), topBar = { - BtCloseableAppBar( + BtAppBar( title = uiState.showName, - onClickClose = onClickClose, + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + onClick = onClickClose, + iconRes = R.drawable.ic_arrow_back, + ) + }, + actionButtons = { + if (cameraSwitchable) { + BtAppBarDefaults.AppBarIconButton( + onClick = onClickSwitchCamera, + iconRes = R.drawable.ic_camera_flip, + ) + } + } ) }, bottomBar = { QrScanBottombar { showEntryCodeDialog = true } }, snackbarHost = { - ToastSnackbarHost( - hostState = snackbarHostState, - modifier = Modifier.padding(bottom = 100.dp), - leadingIcon = { - snackbarIconId?.let { - CircleBgIcon( - painter = painterResource(it), - bgColor = when (it) { - R.drawable.ic_check -> Success - R.drawable.ic_error -> Error - else -> Warning - } - ) - } - }, - ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + ToastSnackbarHost( + modifier = Modifier + .offset { // Scaffold의 inner padding 만큼 상단을 뚫고 나가는 문제가 있음. 해당 값 보정. + IntOffset( + 0, + bottomPadding + .toPx() + .toInt() + ) + } + .padding(top = 18.dp + 44.dp), // 44.dp 는 top bar 높이 값 수동 계산 + hostState = snackbarHostState, + leadingIcon = { + snackbarIconId?.let { + CircleBgIcon( + imageVector = ImageVector.vectorResource(it), + bgColor = when (it) { + R.drawable.ic_check -> Success + R.drawable.ic_error -> Error + else -> Warning + } + ) + } + }, + ) + } }, ) { innerPadding -> + LaunchedEffect(innerPadding) { + bottomPadding = innerPadding.calculateBottomPadding() + } + AndroidView( modifier = Modifier .padding(innerPadding) - .fillMaxSize(), + .fillMaxSize() + .border(width = 2.dp, color = borderColor), factory = { barcodeView }, ) @@ -149,12 +218,20 @@ fun QrScanScreen( @Composable private fun QrScanBottombar(onClick: () -> Unit) { - Box( + Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { + Text( + modifier = Modifier.padding(top = 20.dp), + text = stringResource(R.string.entry_code_notice), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall.copy( + color = Grey50, + ), + ) Row( modifier = Modifier .clickable(onClick = onClick) @@ -167,13 +244,13 @@ private fun QrScanBottombar(onClick: () -> Unit) { .size(20.dp) .padding(end = 4.dp), painter = painterResource(id = R.drawable.ic_book), - tint = Grey50, + tint = Grey30, contentDescription = stringResource(R.string.show_entry_code), ) Text( text = stringResource(id = R.string.show_entry_code), - style = MaterialTheme.typography.bodySmall, - color = Grey50, + style = MaterialTheme.typography.titleSmall, + color = Grey30, ) } } @@ -202,3 +279,26 @@ private fun EntryCodeDialog( ) } } + +private fun hasBothSidesCameras(context: Context): Boolean { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraIds = cameraManager.cameraIdList + + var hasFrontCamera = false + var hasBackCamera = false + + for (cameraId in cameraIds) { + val characteristics = cameraManager.getCameraCharacteristics(cameraId) + val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING) + + if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { + hasFrontCamera = true + } + + if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + hasBackCamera = true + } + } + + return hasFrontCamera && hasBackCamera +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Theme.kt b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Theme.kt index 66b58cffe..699b26ac3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Theme.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Theme.kt @@ -1,9 +1,15 @@ package com.nexters.boolti.presentation.theme +import android.app.Activity import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( primary = Orange01, @@ -19,39 +25,22 @@ private val DarkColorScheme = darkColorScheme( onSurfaceVariant = Grey15, outline = Grey80, outlineVariant = Grey10, - -// secondary = PurpleGrey80, -// tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( -// primary = Purple40, -// secondary = PurpleGrey40, -// tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ ) @Composable fun BooltiTheme( - darkTheme: Boolean = true, // FIXME light theme이 추가된다면 이 부분을 isSystemInDarkTheme()로 수정하세요. content: @Composable () -> Unit ) { - val colorScheme = when { - darkTheme -> DarkColorScheme - else -> LightColorScheme + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = false + } } MaterialTheme( - colorScheme = colorScheme, + colorScheme = DarkColorScheme, typography = Typography, content = content ) diff --git a/presentation/src/main/res/drawable/ic_camera_flip.xml b/presentation/src/main/res/drawable/ic_camera_flip.xml new file mode 100644 index 000000000..63fbd7efc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_camera_flip.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 17bdc172a..ca164b633 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ 모두 지우기 아직 공연일이 아니에요 - 이미 사용된 티켓이에요 + 이미 입장에 사용한 티켓이에요 이 공연의 티켓이 아니에요 존재하지 않는 티켓이에요 @@ -241,7 +241,8 @@ 입장 코드는 주최자 계정의 마이 > QR 스캔 > 해당 공연 스캐너에서 확인 가능해요. 입장 코드를 입력해 주세요 올바른 입장 코드를 입력해 주세요 - 입장코드 보기 + 입장 코드 보기 + QR 코드가 인식되지 않을 경우 티켓 화면 하단의\n\'입장 코드 입력하기\'에 아래 입장 코드를 입력해 주세요. 코드 복사 식별 코드를 복사했어요 @@ -322,11 +323,11 @@ 링크를 삭제했어요 유효한 링크가 아니에요 - - QR 스캔 - 주최한 공연이 없어요 - 공연을 주최하고 QR 스캐너로\n관객 입장을 관리해 보세요 - 사용되었어요 + + 입장 확인 + 입장을 확인할 공연이 없어요 + 공연을 주최하고 QR 코드로\n관객 입장을 확인해 보세요 + 입장을 확인했어요 결제 내역이 없어요