Jetpack Compose로 Google Map과 ARCore 연동하기: 카메라 방향 화살표 UI 만들기 🗺️ AR
미해결
최근 Jetpack Compose를 사용하여 안드로이드 앱을 개발하던 중 흥미로운 아이디어가 떠올랐습니다. 바로 Google Map 위에 AR(증강현실)을 오버레이하여, 내가 바라보는 방향을 화살표로 알려주는 기능이었죠. 이 포스트는 그 아이디어를 현실로 만들어가는 과정을 기록한 것입니다.
1단계: 목표 설정 및 현재 진행 상황
나의 목표: ARCore 예제를 참고해서 Google Map을 Compose로 보여주고, 그 위에 ARCore가 제시하는 정보를 활용해서 내가 바라보는 방향을 향해 화살표를 렌더링하는 UI를 구성하고 싶어. 현재는 Google Map에 나의 마지막 위치를 얻어서 표시하는 기능까지는 구현했어. 이번에 하고 싶은 것은 ARCore의 Session 정보를 활용해서 View를 하나 띄우고 카메라가 바라보는 방향으로 화살표가 가도록 만들어 보고 싶어.
목표는 명확했습니다. 이미 구현된 지도 위에 AR 뷰를 띄우고, ARCore 세션에서 얻은 카메라의 방향 값으로 3D 화살표를 실시간으로 움직이는 것이죠.
2단계: ARCore와 Jetpack Compose 통합하기
가장 먼저 부딪힌 문제는 '어떻게 Jetpack Compose 환경에서 AR 뷰를 자연스럽게 통합할 것인가?' 였습니다. 검색 결과, sceneview-android 라이브러리가 사실상의 표준이라는 것을 알게 되었습니다. 이 라이브러리는 ARCore와 3D 렌더링 엔진인 Filament를 Compose에서 쉽게 사용할 수 있도록 도와줍니다.
@Composable fun ArMapScreen() { val userLocation = LatLng(36.3504, 127.3845) // 대전시청 예시 val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(userLocation, 15f) }
Box(modifier = Modifier.fillMaxSize()) { // 1. Google Map (배경) GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Marker(state = rememberMarkerState(position = userLocation), title = "My Location") }
// 2. AR Scene (오버레이) ArDirectionOverlay() } }
@Composable fun ArDirectionOverlay() { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) val cameraNode = rememberARCameraNode(engine)
ARScene( modifier = Modifier.fillMaxSize(), nodes = listOf(cameraNode, arrowNode), planeRenderer = false, // 바닥 감지 평면 끄기 onFrame = { // 카메라 앞 1미터에 화살표 위치시키기 arrowNode.position = cameraNode.pose.transformPoint(floatArrayOf(0f, 0f, -1f)) // 화살표 방향을 카메라 방향과 일치시키기 arrowNode.rotation = cameraNode.rotation } ) }
3단계: 의존성 문제 해결하기
코드를 작성하고 의존성을 추가하는 과정에서 문제가 발생했습니다. 처음에는 com.google.ar:core만 추가하면 되는 줄 알았지만, sceneview-android 라이브러리가 필수적이라는 것을 깨달았습니다.
질문: 나는 이미 com.google.ar:core를 추가했는데, 따로 추가할 필요는 없는 건가?
이에 대한 답변은 명확했습니다. com.google.ar:core는 AR 기능의 핵심 '엔진'이고, sceneview-android는 이 엔진을 장착하여 Jetpack Compose에서 바로 운전할 수 있게 만든 '완성된 자동차'와 같다는 것입니다. sceneview-android를 사용하면 복잡한 렌더링과 UI 통합을 매우 쉽게 처리할 수 있습니다.
하지만 sceneview-android를 추가하자마자 더 큰 문제에 부딪혔습니다.
빌드 오류: Duplicate class ... found in modules ...
빌드 시 수많은 'Duplicate class' 오류가 발생했습니다. 오류 로그는 com.google.ar.sceneform.rendering.Color 같은 클래스가 서로 다른 두 라이브러리에서 중복으로 발견되었다고 알려주고 있었습니다.
Duplicate class com.google.ar.sceneform.rendering.Color found in modules rendering-1.17.1.aar (com.google.ar.sceneform:rendering:1.17.1) and sceneview-2.3.0.aar (io.github.sceneview:sceneview:2.3.0) ... (수많은 중복 클래스 오류)
원인 및 해결
원인은 명확했습니다. 제 프로젝트에 구글의 **오래된 Sceneform 라이브러리**와 새로운 SceneView 라이브러리가 동시에 포함되어 있었기 때문입니다. SceneView는 Sceneform의 업그레이드 버전이므로, 두 라이브러리 안에는 이름이 똑같은 클래스들이 가득했습니다.
해결 방법은 간단했습니다. build.gradle 파일에서 오래된 Sceneform 관련 의존성을 모두 제거하는 것이었습니다.
// build.gradle (Groovy)
dependencies { // ...
// 아래와 같은 오래된 Sceneform 의존성들을 모두 삭제! // implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1' // implementation 'com.google.ar.sceneform:rendering:1.17.1'
// 최신 SceneView 라이브러리만 남겨둡니다. implementation("io.github.sceneview:arsceneview:2.3.0")
// ... }
오래된 의존성을 제거하고 Gradle을 다시 동기화하자, 빌드 오류는 마법처럼 사라졌습니다.
결론
Jetpack Compose 환경에서 ARCore를 연동하는 것은 sceneview-android 라이브러리 덕분에 생각보다 어렵지 않았습니다. 특히 의존성 충돌 문제는 초기에 겪기 쉬운 함정이지만, 라이브러리 간의 관계만 잘 이해하면 쉽게 해결할 수 있었습니다. 이제 지도 위에 내가 바라보는 방향을 알려주는 AR 화살표가 성공적으로 나타납니다!
#billcorea #운동동아리관리앱
🏸 श्नीडल, बैडमिंटन क्लबों के लिए एक आवश्यक ऐप!
👉 मैच खेलें - स्कोर रिकॉर्ड करें और विरोधियों को खोजें 🎉
कहीं भी, अकेले, दोस्तों के साथ, या क्लब में बिल्कुल सही! 🤝
अगर आपको बैडमिंटन पसंद है तो इसे जरूर ट्राई करें
ऐप पर जाएं 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
// Android 12 이상에서 추가 권한 확인 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissions = new String[] { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.NEARBY_WIFI_DEVICES }; }
// 권한이 없는 경우 요청 ActivityCompat.requestPermissions(this, permissions, 1001); }
2. 광고 시작 (Advertiser 역할)
/** * 다른 기기들이 이 기기를 발견할 수 있도록 광고를 시작하는 메소드 */ private void startAdvertising() { // 광고 옵션 설정 AdvertisingOptions advertisingOptions = new AdvertisingOptions.Builder() .setStrategy(Strategy.P2P_CLUSTER) // 1:N 연결 전략 .build();
// 연결된 기기의 이름 (다른 기기에서 보여질 이름) String localEndpointName = Build.MODEL; // 기기 모델명 사용
Nearby.getConnectionsClient(this) .startAdvertising( localEndpointName, // 광고할 기기 이름 SERVICE_ID, // 서비스 ID connectionLifecycleCallback, // 연결 생명주기 콜백 advertisingOptions // 광고 옵션 ) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void unused) { // 광고 시작 성공 isAdvertising = true; updateStatus("광고 시작됨 - 다른 기기에서 발견 가능"); updateUI(); Log.d(TAG, "광고 시작 성공"); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // 광고 시작 실패 updateStatus("광고 시작 실패: " + e.getMessage()); Log.e(TAG, "광고 시작 실패", e); } }); }
3. 기기 탐색 시작 (Discoverer 역할)
/** * 근처의 광고 중인 기기들을 탐색하는 메소드 */ private void startDiscovery() { // 탐색 옵션 설정 DiscoveryOptions discoveryOptions = new DiscoveryOptions.Builder() .setStrategy(Strategy.P2P_CLUSTER) // 광고와 동일한 전략 사용 .build();
Nearby.getConnectionsClient(this) .startDiscovery( SERVICE_ID, // 찾을 서비스 ID endpointDiscoveryCallback, // 기기 발견 콜백 discoveryOptions // 탐색 옵션 ) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void unused) { // 탐색 시작 성공 isDiscovering = true; updateStatus("탐색 시작됨 - 근처 기기 검색 중..."); updateUI(); Log.d(TAG, "탐색 시작 성공"); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // 탐색 시작 실패 updateStatus("탐색 시작 실패: " + e.getMessage()); Log.e(TAG, "탐색 시작 실패", e); } }); }
4. 기기 발견 콜백
/** * 기기 발견 시 호출되는 콜백 */ private final EndpointDiscoveryCallback endpointDiscoveryCallback = new EndpointDiscoveryCallback() { @Override public void onEndpointFound(@NonNull String endpointId, @NonNull DiscoveredEndpointInfo info) { // 새로운 기기 발견! Log.d(TAG, "기기 발견: " + info.getEndpointName()); updateStatus("기기 발견: " + info.getEndpointName() + " - 연결 시도 중...");
// 자동으로 연결 요청 보내기 requestConnection(endpointId, info.getEndpointName()); }
@Override public void onEndpointLost(@NonNull String endpointId) { // 기기 연결 끊어짐 Log.d(TAG, "기기 연결 끊어짐: " + endpointId); updateStatus("기기 연결이 끊어졌습니다."); } };
/** * 발견된 기기에 연결을 요청하는 메소드 */ private void requestConnection(String endpointId, String endpointName) { Nearby.getConnectionsClient(this) .requestConnection( Build.MODEL, // 내 기기 이름 endpointId, // 연결할 기기 ID connectionLifecycleCallback // 연결 생명주기 콜백 ) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void unused) { // 연결 요청 전송 성공 Log.d(TAG, "연결 요청 전송됨: " + endpointName); updateStatus("연결 요청 전송됨: " + endpointName); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // 연결 요청 실패 Log.e(TAG, "연결 요청 실패", e); updateStatus("연결 요청 실패: " + e.getMessage()); } }); }
5. 연결 생명주기 관리
/** * 연결의 생명주기를 관리하는 콜백 */ private final ConnectionLifecycleCallback connectionLifecycleCallback = new ConnectionLifecycleCallback() { @Override public void onConnectionInitiated(@NonNull String endpointId, @NonNull ConnectionInfo connectionInfo) { // 연결이 시작됨 - 사용자에게 승인/거절 선택권 제공 Log.d(TAG, "연결 요청 받음: " + connectionInfo.getEndpointName());
// 자동으로 연결 승인 (실제 앱에서는 사용자 확인 받는 것이 좋음) Nearby.getConnectionsClient(MainActivity.this) .acceptConnection(endpointId, payloadCallback);
/** * 메시지를 전송하는 메소드 */ private void sendMessage(String message) { if (connectedEndpointId.isEmpty()) { updateStatus("연결된 기기가 없습니다."); return; }
// 텍스트 메시지를 바이트 배열로 변환 Payload bytesPayload = Payload.fromBytes(message.getBytes());
// 메시지 전송 Nearby.getConnectionsClient(this) .sendPayload(connectedEndpointId, bytesPayload) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void unused) { // 전송 성공 Log.d(TAG, "메시지 전송 성공: " + message); appendMessage("나: " + message); etMessage.setText(""); // 입력 필드 초기화 } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // 전송 실패 Log.e(TAG, "메시지 전송 실패", e); updateStatus("메시지 전송 실패: " + e.getMessage()); } }); }
/** * 데이터 수신을 처리하는 콜백 */ private final PayloadCallback payloadCallback = new PayloadCallback() { @Override public void onPayloadReceived(@NonNull String endpointId, @NonNull Payload payload) { // 데이터 수신됨 if (payload.getType() == Payload.Type.BYTES) { // 바이트 데이터인 경우 (텍스트 메시지) String receivedMessage = new String(payload.asBytes()); Log.d(TAG, "메시지 수신: " + receivedMessage);
// UI 업데이트는 메인 스레드에서 실행 runOnUiThread(() -> { appendMessage("상대방: " + receivedMessage); }); } }
@Override public void onPayloadTransferUpdate(@NonNull String endpointId, @NonNull PayloadTransferUpdate update) { // 전송 상태 업데이트 (파일 전송 시 진행률 표시 등에 사용) if (update.getStatus() == PayloadTransferUpdate.Status.SUCCESS) { Log.d(TAG, "페이로드 전송 완료"); } else if (update.getStatus() == PayloadTransferUpdate.Status.FAILURE) { Log.e(TAG, "페이로드 전송 실패"); } } };
7. UI 업데이트 및 정리 메소드
/** * UI 상태를 업데이트하는 메소드 */ private void updateUI() { runOnUiThread(() -> { // 연결 상태에 따라 버튼 활성화/비활성화 btnSendMessage.setEnabled(!connectedEndpointId.isEmpty()); btnStartAdvertising.setEnabled(!isAdvertising); btnStartDiscovery.setEnabled(!isDiscovering); }); }
/** * 상태 메시지를 업데이트하는 메소드 */ private void updateStatus(String message) { runOnUiThread(() -> { tvStatus.setText(message); }); }
<!-- 메시지 입력 --> <EditText android:id="@+id/etMessage" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="보낼 메시지를 입력하세요" android:layout_marginTop="16dp" />
// 파일 정보를 먼저 전송 (파일명, 크기 등) String fileInfo = "FILE:" + getFileName(fileUri) + ":" + getFileSize(fileUri); Payload infoPayload = Payload.fromBytes(fileInfo.getBytes());
// 정보 먼저 전송 Nearby.getConnectionsClient(this) .sendPayload(connectedEndpointId, infoPayload);
// 그 다음 파일 전송 Nearby.getConnectionsClient(this) .sendPayload(connectedEndpointId, filePayload);
📊 연결 전략 비교 P2P_CLUSTER: 1:N 연결, 많은 기기와 동시 연결 P2P_STAR: 1:1 연결, 안정적이고 빠른 속도 P2P_POINT_TO_POINT: 1:1 전용, 최고 성능
3. 에러 처리 및 디버깅
/** * 상세한 에러 처리를 포함한 연결 메소드 */ private void connectWithErrorHandling(String endpointId) { Nearby.getConnectionsClient(this) .requestConnection(Build.MODEL, endpointId, connectionLifecycleCallback) .addOnFailureListener(exception -> { // 구체적인 에러 타입별 처리 if (exception instanceof ApiException) { ApiException apiException = (ApiException) exception; switch (apiException.getStatusCode()) { case ConnectionsStatusCodes.STATUS_ENDPOINT_UNKNOWN: updateStatus("기기를 찾을 수 없습니다."); break; case ConnectionsStatusCodes.STATUS_NETWORK_NOT_CONNECTED: updateStatus("네트워크에 연결되지 않았습니다."); break; case ConnectionsStatusCodes.STATUS_BLUETOOTH_ERROR: updateStatus("블루투스 오류가 발생했습니다."); break; default: updateStatus("연결 오류: " + apiException.getStatusCode()); break; } } }); }
4. 보안 강화
/** * 연결 시 보안 인증을 추가하는 예제 */ private void secureConnectionHandling(String endpointId, ConnectionInfo info) { // 연결 토큰 검증 (실제 앱에서는 더 복잡한 인증 로직 구현) String authToken = info.getAuthenticationDigits();
// 사용자에게 인증 토큰 확인 요청 new AlertDialog.Builder(this) .setTitle("연결 확인") .setMessage("다음 기기와 연결하시겠습니까?\n" + "기기명: " + info.getEndpointName() + "\n" + "인증 코드: " + authToken) .setPositiveButton("승인", (dialog, which) -> { // 연결 승인 Nearby.getConnectionsClient(this) .acceptConnection(endpointId, payloadCallback); }) .setNegativeButton("거절", (dialog, which) -> { // 연결 거절 Nearby.getConnectionsClient(this) .rejectConnection(endpointId); }) .show(); }
🚀 성능 최적화 팁
1. 배터리 최적화
/** * 배터리 효율적인 설정 */ private void optimizedAdvertising() { AdvertisingOptions options = new AdvertisingOptions.Builder() .setStrategy(Strategy.P2P_CLUSTER) // 저전력 모드 사용 .setLowPower(true) .build();
// 일정 시간 후 자동으로 광고 중지 Handler handler = new Handler(); handler.postDelayed(() -> { stopAdvertising(); updateStatus("배터리 절약을 위해 광고가 중지되었습니다."); }, 60000); // 1분 후 중지 }
2. 연결 품질 모니터링
/** * 연결 품질을 모니터링하는 클래스 */ public class ConnectionQualityMonitor { private long lastMessageTime = 0; private int failedMessages = 0; private static final long TIMEOUT_MS = 5000; // 5초 타임아웃
public void onMessageSent() { lastMessageTime = System.currentTimeMillis(); }
public void onMessageFailed() { failedMessages++; if (failedMessages > 3) { // 연결 품질이 나쁘다고 판단, 재연결 시도 reconnectWithBetterStrategy(); } }
private void reconnectWithBetterStrategy() { // 더 안정적인 전략으로 재연결 // 예: P2P_STAR 전략 사용 } }
🔧 문제해결 가이드
⚠️ 자주 발생하는 문제들
1. 기기를 발견하지 못하는 경우:
- 위치 권한이 허용되었는지 확인
- 블루투스와 WiFi가 켜져있는지 확인
- 같은 SERVICE_ID를 사용하는지 확인
2. 연결이 자주 끊어지는 경우:
- 기기 간 거리가 너무 멀지 않은지 확인
- 다른 무선 신호 간섭이 없는지 확인
- 적절한 연결 전략을 선택했는지 확인
3. 메시지가 전송되지 않는 경우:
- 연결 상태를 확인
- 페이로드 크기 제한 확인 (최대 32KB)
- 네트워크 상태 확인
디버깅을 위한 로그 설정
/** * 상세한 로깅을 위한 헬퍼 클래스 */ public class NearbyLogger { private static final String TAG = "NearbyConnections";
public static void logConnectionState(String endpointId, String state) { Log.d(TAG, String.format("연결 상태 변경 - ID: %s, 상태: %s, 시간: %s", endpointId, state, new Date())); }
/** * 연결 상태를 시각적으로 표시하는 메소드 */ private void updateConnectionStatus(boolean isConnected) { runOnUiThread(() -> { if (isConnected) { // 연결됨 - 녹색 표시 tvStatus.setBackgroundColor(ContextCompat.getColor(this, R.color.green_light)); tvStatus.setTextColor(ContextCompat.getColor(this, R.color.green_dark)); tvStatus.setText("🟢 연결됨 - 메시지 전송 가능"); } else { // 연결 안됨 - 회색 표시 tvStatus.setBackgroundColor(ContextCompat.getColor(this, R.color.grey_light)); tvStatus.setTextColor(ContextCompat.getColor(this, R.color.grey_dark)); tvStatus.setText("⚪ 연결 대기중"); } }); }
Firebase Storage로 이미지 업로드하기 — 갤러리/카메라 선택 + 업로드 진행률 표시
적용해본 예시 이미지
이 글은 Jetpack Compose로 만든 안드로이드 앱에서 Firebase Storage에 이미지를 업로드하는 전 과정을 다룹니다. 사용자는 갤러리에서 이미지 선택 혹은 카메라로 촬영한 사진을 업로드할 수 있고, 업로드 진행률과 다운로드 URL을 바로 확인할 수 있습니다. 초보자분들도 그대로 따라 하면 동작하도록 전체 코드와 함께 단계별로 설명했습니다.
dependencies { // Firebase BOM: 버전 하나로 일괄 관리 implementation(platform("com.google.firebase:firebase-bom:33.1.2")) // KTX 사용 시 -ktx 종속성 권장 implementation("com.google.firebase:firebase-storage-ktx")
// Compose 필수 의존성 (버전은 프로젝트에 맞게 조정 가능) implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.compose.ui:ui:1.6.8") implementation("androidx.compose.material3:material3:1.2.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
// 이미지 미리보기(선택) implementation("io.coil-kt:coil-compose:2.6.0") }
Tip: Firebase BOM은 수시로 업데이트됩니다. 새 프로젝트에선 최신 BOM을 사용하세요.
@Composable fun ImagePickerWithCamera(onImageSelected: (Uri) -> Unit) { val context = LocalContext.current
// 카메라 촬영을 위한 임시 파일 & Uri val imageFile = remember { File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg") } val imageUri = FileProvider.getUriForFile( context, "${context.packageName}.provider", imageFile )
val cameraLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture() ) { success -> if (success) onImageSelected(imageUri) }
val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri -> uri?.let(onImageSelected) }
fun uploadImageToFirebaseWithProgress( uri: Uri, onProgress: (Int) -> Unit, onResult: (String?) -> Unit ) { val storageRef = Firebase.storage.reference val fileRef = storageRef.child("uploads/${System.currentTimeMillis()}.jpg")
val uploadTask = fileRef.putFile(uri) uploadTask .addOnProgressListener { snap -> val p = (100.0 * snap.bytesTransferred / snap.totalByteCount).toInt() onProgress(p) } .addOnSuccessListener { fileRef.downloadUrl.addOnSuccessListener { url -> onResult(url.toString()) } } .addOnFailureListener { e -> e.printStackTrace() onResult(null) } }
초보자 포인트:
uploads/ 폴더 아래에 타임스탬프 기반 파일명을 사용해 중복을 피합니다.
성공 시 URL은 공유 가능한 다운로드 링크입니다. (규칙에 따라 접근 제한 가능)
6) 완성 화면(Compose) 구성
버튼 한 번으로 선택/촬영 후 업로드까지 이어지고, 이미지 미리보기와 진행률, 최종 URL을 보여주는 간단한 화면입니다.
@Composable fun UploadImageScreenWithCamera() { var imageUri by remember { mutableStateOf<Uri?>(null) } var uploadedUrl by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf(0) }
fun uploadImageToFirebaseWithProgress( uri: Uri, onProgress: (Int) -> Unit, onResult: (String?) -> Unit ) { val storageRef = Firebase.storage.reference val fileRef = storageRef.child("uploads/${System.currentTimeMillis()}.jpg")
@Composable fun UploadImageScreenWithCamera() { var imageUri by remember { mutableStateOf<Uri?>(null) } var uploadedUrl by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf(0) }
<주변검색 주변찾기 앱, 동작감지기 앱 : 이하 사용자앱으로 표시 >:는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.
○ 이 개인정보처리방침은 2022년 4월 1부터 적용됩니다.
제1조(개인정보의 처리 목적)
<사용자 앱>은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.
○ 제공되는 앱의 사용자 확인을 위해서 만 사용 됩니다.
제2조(개인정보의 처리 및 보유 기간)
① <사용자 앱>은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.
② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.
1.<앱 사용자 회원가입 및 관리>
<앱 사용자 회원가입 및 관리>와 관련한 개인정보는 수집.이용에 관한 동의일로부터<이 앱의 사용기간 동안>까지 위 이용목적을 위하여 보유.이용됩니다.
보유근거 : 이 앱의 사용자 확인을 위해서 사용 됩니다.
2 <앱 사용자 위치정보 : 백그라운드 위치 포함>
백그라운드 위치정보 : 이 앱의 기본 기능인 도착 알림 발송등을 위해서 백그라운드 에서 위치 정보를 활용 합니다. 사용자의 위치 정보는 앱의 기능 활용을 위해서만 사용 되며, 앱 외부로 전송 하지 않습니다.
3 <Health Data 의 사용>
이 앱은 사용자의 폰이나, 워치를 통해 사용자의 Health Data 가 측정 됩니다. 측정된 Health Data 는 이 앱에서 사용자의 신체상태을 표시 하는 데만 사용 됩니다.
측정된 Health Data 을 앱 이외의 공간(온라인전송 포함)으로 복사 되거나, 제공 되지 않습니다.
제3조(개인정보의 제3자 제공)
① <사용자 앱>은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.
② < billcorea >은(는) 다음과 같이 개인정보를 제3자에게 제공하지 않습니다.
제4조(개인정보처리 위탁)
① <사용자 앱>은(는) 원활한 개인정보 업무처리를 위하여 다음과 같이 개인정보 처리업무를 위탁하지 않습니다.
② 위탁업무의 내용이나 수탁자가 변경될 경우에는 지체없이 본 개인정보 처리방침을 통하여 공개하도록 하겠습니다.
제5조(정보주체와 법정대리인의 권리·의무 및 그 행사방법)
① 정보주체는 우리집에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.
② 제1항에 따른 권리 행사는우리집에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 우리집은(는) 이에 대해 지체 없이 조치하겠습니다.
③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 “개인정보 처리 방법에 관한 고시(제2020-7호)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.
④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.
⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.
⑥ billcorea 은(는) 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.
제6조(처리하는 개인정보의 항목 작성)
①<사용자 앱>은(는) 다음의 개인정보 항목을 처리하고 있습니다.
1<앱 사용자 회원가입 및 관리 >
필수항목 : 식별기호(uuid token), 이메일주소,
소셜 로그인 : 이메일주소, 별명(별칭), 프로필 이미지 링크
2<앱 사용자 위치 정보>
앱 사용중 위치 정보 및 백그라운드 위치 정보 포함.
3<Health Data>
앱 사용중 사용자의 심박수, 걸음수의 정보.
제7조(개인정보의 파기)
① <사용자 앱> 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.
② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.
1. 법령 근거 : 관련법규 적용 사항 없음
2. 보존하는 개인정보 항목 : 없음
③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.
1. 파기절차 <사용자 앱>은(는) 파기 사유가 발생한 개인정보를 선정하고, < billcorea > 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.
2. 앱의 사용자 설정에서 로그인 관련 정보 삭제 버튼을 이용하여 삭제 할 수 있습니다.
제8조(개인정보의 안전성 확보 조치)
<사용자 앱>은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.
1. 내부관리계획의 수립 및 시행
개인정보의 안전한 처리를 위하여 내부관리계획을 수립하고 시행하고 있습니다.
2. 개인정보에 대한 접근 제한
개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.
3. 비인가자에 대한 출입 통제
개인정보를 보관하고 있는 물리적 보관 장소를 별도로 두고 이에 대해 출입통제 절차를 수립, 운영하고 있습니다.
제9조(개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항)
<사용자 앱> 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용하지 않습니다.
제10조 (개인정보 보호책임자)
① <사용자 앱> 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.
▶ 개인정보 보호책임자
성명 :강동엽
직책 : manager
직급 : manager
연락처 : 0504-0662-8122, help@billcorea.com
※ 개인정보 보호 담당부서로 연결됩니다.
▶ 개인정보 보호 담당부서
부서명 : manager
담당자 : 강동엽
연락처 : 0504-0662-8122, help@billcorea.com
② 정보주체께서는 우리집 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. 우리집 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.
제11조(개인정보 열람청구) 정보주체는 「개인정보 보호법」 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
<사용자 앱>은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.
▶ 개인정보 열람청구 접수·처리 부서
부서명 : manager
담당자 : 강동엽
연락처 : 0504-0662-8122, help@billcorea.com
제12조(권익침해 구제방법)
정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.
「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.
※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.