- 개발 빌드에서는 영수증 업로드 → 커스텀 바텀싯(카메라/앨범 선택) → 권한
다이얼로그 → 촬영/선택 흐름이 정상. - 빌드 심사에서는 “카메라/앨범 선택 시 바텀싯이 잠깐 보였다가
- 바로 사라져서 진행이 어렵다”는 피드백을 반복 수신.
혹시 토스 미니앱에서 권한 요청 전에 커스텀 안내 바텀싯을 한 번 더 띄우는
사례나, openPermissionDialog 호출 타이밍을 조정해서 심사 통과한 경험이
있나요 ㅠ 계속 반려되네여
혹시 토스 미니앱에서 권한 요청 전에 커스텀 안내 바텀싯을 한 번 더 띄우는
사례나, openPermissionDialog 호출 타이밍을 조정해서 심사 통과한 경험이
있나요 ㅠ 계속 반려되네여
@choi_j 님 안녕하세요
미니앱 이름이 어떻게 되실까요?
안녕하세요 ![]()
커스텀 바텀싯이 닫히면서 권한 다이얼로그가 같이 닫히는게 아닐지요 ..?
바로 더치 baro-dutch 입니다 ㅜㅜ
음 … 로컬에서는 몇번 테스트해봐도 서로 영향을 주는게 없는데 심사만 가면 올라왔다 바로 내려간다 그래서 좀 혼란스럽네요 ㅠ
혹시 관련 코드를 메세지로 공유주실 수 있을까요 ?! 저도 테스트 해보겠습니다.
혹시 어떤 코드 말씀하시는 걸까요??
아 제 로컬에서 테스트할 수 있도록 작성하신 코드 전문을 주실 수 있을지요
..
넵 ! 잠시만요 메세지로 보내드릴게요 !
코드를 확인해보니, 권한 확인이 되기 전에 모달을 setModalVisible(false) 로 종료시키고 계신 것 같습니다.
테스트한 코드를 공유드려요.
intoss-private://my-granite-app?_deploymentId=019aa0f6-99de-796a-91c8-2a7470640c4e
import { createRoute } from '@granite-js/react-native';
import React, { useState } from 'react';
import {
StyleSheet,
View,
Text,
Modal,
TouchableWithoutFeedback,
} from 'react-native';
import { Button } from '@toss/tds-react-native';
import {
fetchAlbumPhotos,
FetchAlbumPhotosPermissionError,
openCamera,
OpenCameraPermissionError,
} from '@apps-in-toss/framework';
export const Route = createRoute('/', {
component: Page,
});
function Page() {
const [isModalVisible, setModalVisible] = useState(false);
const isPermissionGranted = (status: string | null | undefined) => status === 'allowed' || status === 'limited';
const isPermissionRequiredMessage = (error: unknown, keyword: 'camera' | 'album') => {
if (!(error instanceof Error)) {
return false;
}
const message = error.message.toLowerCase();
if (keyword === 'camera') {
return message.includes('camera permission is required') || message.includes('camera permission required');
}
return (
message.includes('album permission is required') ||
message.includes('album permission required') ||
message.includes('photo permission is required') ||
message.includes('photos permission is required') ||
message.includes('photo library permission is required')
);
};
const ensurePermission = async (type: 'camera' | 'album') => {
const permissionAPI = type === 'camera' ? openCamera : fetchAlbumPhotos;
if (
typeof permissionAPI.getPermission !== 'function' ||
typeof permissionAPI.openPermissionDialog !== 'function'
) {
return true;
}
try {
const currentPermission = await permissionAPI.getPermission();
if (isPermissionGranted(currentPermission)) {
console.log(`${type === 'camera' ? '카메라' : '앨범'} 권한이 이미 허용되어 있습니다.`);
return true;
}
const requestedPermission = await permissionAPI.openPermissionDialog();
const granted = isPermissionGranted(requestedPermission);
console.log(`${type === 'camera' ? '카메라' : '앨범'} 권한 요청 결과:`, granted ? '허용됨' : '거부됨');
return granted;
} catch (error) {
console.error(
`${type === 'camera' ? '카메라' : '앨범'} 권한 상태를 확인하는 중 문제가 발생했습니다:`,
error,
);
return false;
}
};
const handleCamera = async () => {
try {
const hasPermission = await ensurePermission('camera');
// 권한 확인이 완료된 후에 모달 닫기
setModalVisible(false);
if (hasPermission) {
console.log('카메라 권한 확인 완료');
}
} catch (error) {
// 에러가 발생해도 모달 닫기
setModalVisible(false);
if (error instanceof OpenCameraPermissionError || isPermissionRequiredMessage(error, 'camera')) {
console.log('카메라 권한이 필요합니다.');
} else {
console.error('카메라 실행에 실패했어요:', error);
}
}
};
const handleLibrary = async () => {
try {
const hasPermission = await ensurePermission('album');
// 권한 확인이 완료된 후에 모달 닫기
setModalVisible(false);
if (hasPermission) {
console.log('앨범 권한 확인 완료');
}
} catch (error) {
// 에러가 발생해도 모달 닫기
setModalVisible(false);
if (error instanceof FetchAlbumPhotosPermissionError || isPermissionRequiredMessage(error, 'album')) {
console.log('앨범 권한이 필요합니다.');
} else {
console.error('앨범을 가져오는 데 실패했어요:', error);
}
}
};
return (
<View style={styles.screen}>
<View style={styles.container}>
<Text style={styles.title}>권한 테스트</Text>
<Button onPress={() => setModalVisible(true)} size="large" style="fill" display="block">
영수증 업로드
</Button>
</View>
<Modal
animationType="slide"
transparent
visible={isModalVisible}
onRequestClose={() => {
setModalVisible(false);
}}
>
<TouchableWithoutFeedback
onPress={() => {
setModalVisible(false);
}}
>
<View style={styles.bottomSheetOverlay}>
<TouchableWithoutFeedback onPress={() => {}}>
<View style={styles.bottomSheetContainer}>
<Text style={styles.bottomSheetTitle}>영수증 업로드</Text>
<View style={{ gap: 8 }}>
<Button
size="large"
style="weak"
display="block"
onPress={() => {
void handleCamera();
}}
>
카메라로 촬영
</Button>
<Button
size="large"
style="weak"
display="block"
onPress={() => {
void handleLibrary();
}}
>
앨범에서 선택
</Button>
<Button size="large" style="weak" display="block" onPress={() => setModalVisible(false)}>
취소
</Button>
</View>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: 'white',
},
container: {
flex: 1,
paddingHorizontal: 24,
paddingVertical: 16,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#1A202C',
marginBottom: 24,
},
bottomSheetOverlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
bottomSheetContainer: {
backgroundColor: 'white',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingHorizontal: 24,
paddingTop: 24,
paddingBottom: 32,
},
bottomSheetTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1A202C',
textAlign: 'center',
marginBottom: 24,
},
});
export default Page;
테스트 정말 감사합니다 ! 근데 제가 의도한건 그 업수증 업로드 버튼 선택하면 권한 동의 하고 바로 쭉 앨범선택 까지 이어지는 flow 인데 그 영수증 모달이 내려가는게 문제가 된걸까요? ㅠㅠㅠ
심사측에서는
**
영수증 업로드 > 앨범에서 선택 / 카메라 촬영 터치 > 권한 동의 바텀싯이 정상적으로 노출되지 않아 플로우를 진행할 수 없습니다.**
권한 동의 바텀싯에서 문제가 생기는 것 같아서요 !
그 영수증 업로드 모달은 닫히도록 설계를 한건데 그게 잘못된걸까요? ㅠ 어렵네요
제가 확인했을때는 모달이 닫히면서 권한동의 창이 같이 닫혔었습니다!
setModalVisible(false) 이 함수 호출 시점을 권한 확인 완료 후로 바꿔보시면 어떨까요 ?
말씀 감사합니다 ! 무사히 통과 되었습니다 !!!