iOS 웹뷰 이미지 저장 기능 추가 (네이티브 브릿지)
- iOS 환경에서 이미지 다운로드가 안 되는 문제 해결 - 네이티브 앱 브릿지를 통한 이미지 저장 기능 구현 - iOS만 네이티브 브릿지 사용, Android/웹은 기존 방식 유지 변경사항: - BridgeMessageType에 SAVE_IMAGE 타입 추가 - SaveImageRequest, SaveImageResponse 인터페이스 정의 - appBridge.saveImage() 메서드 구현 - cash-receipt-sample.tsx에서 iOS 환경 감지 및 네이티브 저장 처리 - 네이티브 앱 개발자를 위한 구현 가이드 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
448
NATIVE_IMAGE_SAVE_GUIDE.md
Normal file
448
NATIVE_IMAGE_SAVE_GUIDE.md
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
# 네이티브 앱 이미지 저장 기능 구현 가이드
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
웹뷰에서 생성한 이미지(현금영수증 등)를 iOS/Android 네이티브 환경에서 갤러리에 저장하는 기능 구현 가이드입니다.
|
||||||
|
|
||||||
|
## 웹 → 네이티브 통신 스펙
|
||||||
|
|
||||||
|
### 메시지 타입
|
||||||
|
```
|
||||||
|
type: 'saveImage'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 요청 데이터 구조
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
type: 'saveImage',
|
||||||
|
data: {
|
||||||
|
imageData: string, // base64 인코딩된 이미지 데이터 (순수 base64, data URL 헤더 제외)
|
||||||
|
fileName?: string, // 파일명 (optional, 기본값: timestamp)
|
||||||
|
mimeType?: string // MIME 타입 (optional, 기본값: 'image/png')
|
||||||
|
},
|
||||||
|
callbackId: string // 콜백 식별자
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 응답 데이터 구조
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean, // 성공 여부
|
||||||
|
data?: {
|
||||||
|
filePath?: string // 저장된 파일 경로 (optional)
|
||||||
|
},
|
||||||
|
error?: string, // 에러 메시지 (실패 시)
|
||||||
|
callbackId: string // 요청 시 받은 callbackId
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## iOS (Swift) 구현 예시
|
||||||
|
|
||||||
|
### 1. WKWebView 메시지 핸들러 등록
|
||||||
|
```swift
|
||||||
|
import WebKit
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
class WebViewController: UIViewController, WKScriptMessageHandler {
|
||||||
|
var webView: WKWebView!
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.userContentController.add(self, name: "bridge")
|
||||||
|
|
||||||
|
webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
view.addSubview(webView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContentController(
|
||||||
|
_ userContentController: WKUserContentController,
|
||||||
|
didReceive message: WKScriptMessage
|
||||||
|
) {
|
||||||
|
guard let dict = message.body as? [String: Any],
|
||||||
|
let type = dict["type"] as? String,
|
||||||
|
let callbackId = dict["callbackId"] as? String else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case "saveImage":
|
||||||
|
handleSaveImage(data: dict["data"] as? [String: Any], callbackId: callbackId)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 이미지 저장 로직 구현
|
||||||
|
```swift
|
||||||
|
extension WebViewController {
|
||||||
|
func handleSaveImage(data: [String: Any]?, callbackId: String) {
|
||||||
|
guard let data = data,
|
||||||
|
let imageDataString = data["imageData"] as? String,
|
||||||
|
let imageData = Data(base64Encoded: imageDataString),
|
||||||
|
let image = UIImage(data: imageData) else {
|
||||||
|
sendResponse(
|
||||||
|
success: false,
|
||||||
|
error: "Invalid image data",
|
||||||
|
callbackId: callbackId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileName = data["fileName"] as? String ?? "image_\(Date().timeIntervalSince1970).png"
|
||||||
|
|
||||||
|
// 사진 라이브러리 권한 확인
|
||||||
|
checkPhotoLibraryPermission { [weak self] granted in
|
||||||
|
guard granted else {
|
||||||
|
self?.sendResponse(
|
||||||
|
success: false,
|
||||||
|
error: "Photo library permission denied",
|
||||||
|
callbackId: callbackId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사진 저장
|
||||||
|
PHPhotoLibrary.shared().performChanges({
|
||||||
|
PHAssetCreationRequest.creationRequestForAsset(from: image)
|
||||||
|
}) { success, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if success {
|
||||||
|
self?.sendResponse(
|
||||||
|
success: true,
|
||||||
|
data: ["filePath": fileName],
|
||||||
|
callbackId: callbackId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self?.sendResponse(
|
||||||
|
success: false,
|
||||||
|
error: error?.localizedDescription ?? "Failed to save image",
|
||||||
|
callbackId: callbackId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPhotoLibraryPermission(completion: @escaping (Bool) -> Void) {
|
||||||
|
let status = PHPhotoLibrary.authorizationStatus()
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case .authorized, .limited:
|
||||||
|
completion(true)
|
||||||
|
case .notDetermined:
|
||||||
|
PHPhotoLibrary.requestAuthorization { newStatus in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(newStatus == .authorized || newStatus == .limited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendResponse(
|
||||||
|
success: Bool,
|
||||||
|
data: [String: Any]? = nil,
|
||||||
|
error: String? = nil,
|
||||||
|
callbackId: String
|
||||||
|
) {
|
||||||
|
var response: [String: Any] = [
|
||||||
|
"success": success,
|
||||||
|
"callbackId": callbackId
|
||||||
|
]
|
||||||
|
|
||||||
|
if let data = data {
|
||||||
|
response["data"] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
response["error"] = error
|
||||||
|
}
|
||||||
|
|
||||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: response),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||||
|
webView.evaluateJavaScript("window.postMessage(\(jsonString), '*')")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Info.plist 권한 설정
|
||||||
|
```xml
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>현금영수증 이미지를 저장하기 위해 사진 라이브러리 접근 권한이 필요합니다.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>현금영수증 이미지를 저장하기 위해 사진 라이브러리 접근 권한이 필요합니다.</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android (Kotlin) 구현 예시
|
||||||
|
|
||||||
|
### 1. WebView 인터페이스 설정
|
||||||
|
```kotlin
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var webView: WebView
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
webView = findViewById(R.id.webView)
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.addJavascriptInterface(WebAppInterface(this, webView), "AndroidBridge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebAppInterface(
|
||||||
|
private val context: Context,
|
||||||
|
private val webView: WebView
|
||||||
|
) {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun postMessage(message: String) {
|
||||||
|
try {
|
||||||
|
val json = JSONObject(message)
|
||||||
|
val type = json.getString("type")
|
||||||
|
val callbackId = json.getString("callbackId")
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"saveImage" -> {
|
||||||
|
val data = json.optJSONObject("data")
|
||||||
|
handleSaveImage(data, callbackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 이미지 저장 로직 구현
|
||||||
|
```kotlin
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class WebAppInterface(
|
||||||
|
private val context: Context,
|
||||||
|
private val webView: WebView
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val PERMISSION_REQUEST_CODE = 1001
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSaveImage(data: JSONObject?, callbackId: String) {
|
||||||
|
if (data == null) {
|
||||||
|
sendResponse(false, null, "No data provided", callbackId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageDataString = data.optString("imageData")
|
||||||
|
val fileName = data.optString("fileName", "image_${System.currentTimeMillis()}.png")
|
||||||
|
|
||||||
|
if (imageDataString.isEmpty()) {
|
||||||
|
sendResponse(false, null, "Invalid image data", callbackId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val imageBytes = Base64.decode(imageDataString, Base64.DEFAULT)
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||||
|
|
||||||
|
if (bitmap == null) {
|
||||||
|
sendResponse(false, null, "Failed to decode image", callbackId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 10 (Q) 이상
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
saveImageToGalleryQ(bitmap, fileName, callbackId)
|
||||||
|
} else {
|
||||||
|
// Android 9 이하 - 권한 확인 필요
|
||||||
|
if (checkStoragePermission()) {
|
||||||
|
saveImageToGalleryLegacy(bitmap, fileName, callbackId)
|
||||||
|
} else {
|
||||||
|
requestStoragePermission()
|
||||||
|
sendResponse(false, null, "Storage permission required", callbackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendResponse(false, null, e.message ?: "Unknown error", callbackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 10 (Q) 이상 - Scoped Storage 사용
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun saveImageToGalleryQ(bitmap: Bitmap, fileName: String, callbackId: String) {
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/CashReceipts")
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
|
if (imageUri == null) {
|
||||||
|
sendResponse(false, null, "Failed to create image file", callbackId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val outputStream: OutputStream? = resolver.openOutputStream(imageUri)
|
||||||
|
outputStream?.use {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
|
sendResponse(true, JSONObject().apply {
|
||||||
|
put("filePath", imageUri.toString())
|
||||||
|
}, null, callbackId)
|
||||||
|
} ?: run {
|
||||||
|
sendResponse(false, null, "Failed to open output stream", callbackId)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendResponse(false, null, e.message ?: "Failed to save image", callbackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 9 이하 - Legacy Storage 사용
|
||||||
|
private fun saveImageToGalleryLegacy(bitmap: Bitmap, fileName: String, callbackId: String) {
|
||||||
|
val imagesDir = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_PICTURES
|
||||||
|
)
|
||||||
|
val imageFile = File(imagesDir, "CashReceipts/$fileName")
|
||||||
|
|
||||||
|
try {
|
||||||
|
imageFile.parentFile?.mkdirs()
|
||||||
|
FileOutputStream(imageFile).use {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 갤러리에 스캔하여 즉시 표시되도록 함
|
||||||
|
MediaScannerConnection.scanFile(
|
||||||
|
context,
|
||||||
|
arrayOf(imageFile.absolutePath),
|
||||||
|
arrayOf("image/png"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
sendResponse(true, JSONObject().apply {
|
||||||
|
put("filePath", imageFile.absolutePath)
|
||||||
|
}, null, callbackId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendResponse(false, null, e.message ?: "Failed to save image", callbackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkStoragePermission(): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestStoragePermission() {
|
||||||
|
if (context is Activity) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendResponse(
|
||||||
|
success: Boolean,
|
||||||
|
data: JSONObject?,
|
||||||
|
error: String?,
|
||||||
|
callbackId: String
|
||||||
|
) {
|
||||||
|
val response = JSONObject().apply {
|
||||||
|
put("success", success)
|
||||||
|
put("callbackId", callbackId)
|
||||||
|
data?.let { put("data", it) }
|
||||||
|
error?.let { put("error", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
(context as? Activity)?.runOnUiThread {
|
||||||
|
val script = "window.postMessage(${response}, '*')"
|
||||||
|
webView.evaluateJavascript(script, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. AndroidManifest.xml 권한 설정
|
||||||
|
```xml
|
||||||
|
<!-- Android 9 이하용 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스트 방법
|
||||||
|
|
||||||
|
### 웹 콘솔에서 테스트
|
||||||
|
```javascript
|
||||||
|
// 네이티브 브릿지 테스트
|
||||||
|
if (window.appBridge?.isNativeEnvironment()) {
|
||||||
|
console.log('Native environment detected');
|
||||||
|
|
||||||
|
// 테스트 이미지 저장
|
||||||
|
const testImageData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
|
|
||||||
|
appBridge.saveImage({
|
||||||
|
imageData: testImageData,
|
||||||
|
fileName: 'test.png',
|
||||||
|
mimeType: 'image/png'
|
||||||
|
}).then(result => {
|
||||||
|
console.log('Save result:', result);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주의사항
|
||||||
|
|
||||||
|
1. **권한 처리**: iOS와 Android 모두 사진 라이브러리 접근 권한이 필요합니다.
|
||||||
|
2. **에러 핸들링**: 네트워크 오류, 권한 거부, 저장소 부족 등의 경우를 처리해야 합니다.
|
||||||
|
3. **파일명 중복**: 동일한 파일명이 있을 경우 처리 로직이 필요합니다.
|
||||||
|
4. **메모리 관리**: 대용량 이미지의 경우 메모리 관리에 주의해야 합니다.
|
||||||
|
5. **Android Scoped Storage**: Android 10 이상에서는 Scoped Storage를 사용해야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 추가 개선 사항
|
||||||
|
|
||||||
|
1. **진행 상태 표시**: 대용량 이미지 저장 시 로딩 인디케이터 추가
|
||||||
|
2. **저장 위치 커스터마이징**: 사용자가 저장 위치를 선택할 수 있도록 개선
|
||||||
|
3. **공유 기능**: 저장 후 바로 공유할 수 있는 옵션 추가
|
||||||
|
4. **포맷 선택**: PNG 외에 JPEG 등 다른 포맷 지원
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 문의
|
||||||
|
구현 중 문제가 발생하면 웹 개발팀에 문의해주세요.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IMAGE_ROOT } from '@/shared/constants/common';
|
import { IMAGE_ROOT } from '@/shared/constants/common';
|
||||||
import { snackBar } from '@/shared/lib';
|
import { snackBar, appBridge } from '@/shared/lib';
|
||||||
import { toPng } from 'html-to-image';
|
import { toPng } from 'html-to-image';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import '@/shared/ui/assets/css/style-tax-invoice.css';
|
import '@/shared/ui/assets/css/style-tax-invoice.css';
|
||||||
@@ -31,17 +31,46 @@ export const CashReceiptSample = ({
|
|||||||
}: CashReceiptSampleProps) => {
|
}: CashReceiptSampleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = async () => {
|
||||||
const section = document.getElementById('image-section') as HTMLElement;
|
const section = document.getElementById('image-section') as HTMLElement;
|
||||||
toPng(section).then((image) => {
|
|
||||||
const link = document.createElement('a');
|
try {
|
||||||
link.download = 'downloadImage.png';
|
const imageDataUrl = await toPng(section);
|
||||||
link.href = image;
|
|
||||||
link.click();
|
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
||||||
snackBar(t('common.imageRequested'), function(){
|
if (appBridge.isIOS()) {
|
||||||
//onClickToClose();
|
try {
|
||||||
});
|
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
||||||
});
|
const base64Data = imageDataUrl.split(',')[1];
|
||||||
|
const fileName = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
|
||||||
|
|
||||||
|
const result = await appBridge.saveImage({
|
||||||
|
imageData: base64Data,
|
||||||
|
fileName: fileName,
|
||||||
|
mimeType: 'image/png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
snackBar(t('common.imageSaved'));
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to save image');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Native image save failed:', error);
|
||||||
|
snackBar(t('common.imageSaveFailed'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크)
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
|
||||||
|
link.href = imageDataUrl;
|
||||||
|
link.click();
|
||||||
|
snackBar(t('common.imageRequested'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image generation failed:', error);
|
||||||
|
snackBar(t('common.imageGenerationFailed'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onClickToClose = () => {
|
const onClickToClose = () => {
|
||||||
setCashReceiptSampleOn(false);
|
setCashReceiptSampleOn(false);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
AppBridgeResponse,
|
AppBridgeResponse,
|
||||||
BridgeMessageType,
|
BridgeMessageType,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
ShareContent
|
ShareContent,
|
||||||
|
SaveImageRequest,
|
||||||
|
SaveImageResponse
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
class AppBridge {
|
class AppBridge {
|
||||||
@@ -171,6 +173,11 @@ class AppBridge {
|
|||||||
return this.sendMessage(BridgeMessageType.UPDATE_ALARM_COUNT, { count });
|
return this.sendMessage(BridgeMessageType.UPDATE_ALARM_COUNT, { count });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이미지 저장 (iOS/Android 네이티브)
|
||||||
|
async saveImage(request: SaveImageRequest): Promise<SaveImageResponse> {
|
||||||
|
return this.sendMessage<SaveImageResponse>(BridgeMessageType.SAVE_IMAGE, request);
|
||||||
|
}
|
||||||
|
|
||||||
// 네이티브 환경 체크
|
// 네이티브 환경 체크
|
||||||
isNativeEnvironment(): boolean {
|
isNativeEnvironment(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ export enum BridgeMessageType {
|
|||||||
SET_NOTIFICATION_SETTING = 'setNotificationSetting',
|
SET_NOTIFICATION_SETTING = 'setNotificationSetting',
|
||||||
|
|
||||||
// 알림 링크 확인
|
// 알림 링크 확인
|
||||||
CHECK_ALARM_LINK = 'checkAlarmLink'
|
CHECK_ALARM_LINK = 'checkAlarmLink',
|
||||||
|
|
||||||
|
// 이미지 저장
|
||||||
|
SAVE_IMAGE = 'saveImage'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceInfo {
|
export interface DeviceInfo {
|
||||||
@@ -118,4 +121,16 @@ export interface NotificationSettingResponse {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
needsPermission?: boolean;
|
needsPermission?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveImageRequest {
|
||||||
|
imageData: string; // base64 encoded image data
|
||||||
|
fileName?: string; // 파일명 (optional, default: timestamp)
|
||||||
|
mimeType?: string; // MIME 타입 (optional, default: 'image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveImageResponse {
|
||||||
|
success: boolean;
|
||||||
|
filePath?: string; // 저장된 파일 경로
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user