react-native-attachment-kit 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +40 -2
  2. package/android/CMakeLists.txt +3 -0
  3. package/android/build.gradle +11 -2
  4. package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
  5. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
  6. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
  7. package/ios/RNAttachmentKit.swift +133 -4
  8. package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
  9. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
  10. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +727 -415
  11. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +12 -1
  12. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  13. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +12 -1
  14. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  15. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  16. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
  17. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  18. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +727 -415
  19. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +12 -1
  20. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +12 -1
  22. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +727 -415
  23. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +12 -1
  24. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  25. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +12 -1
  26. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  27. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  28. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +20 -20
  29. package/package.json +1 -1
  30. package/src/index.ts +1 -1
  31. package/src/types.ts +34 -1
@@ -57,6 +57,8 @@ class RNAttachmentKit: RCTEventEmitter {
57
57
  /// 동영상 변환 진행 HUD 를 띄울지 — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만
58
58
  /// (앱이 onProcessingStart / onVideoProgress / onImageProcessed 로 말풍선에 진행 상황을 그리는 구조면 HUD 없이)
59
59
  private var galleryShowsVideoHUD = true
60
+ /// 사진 썸네일 (GalleryOptions.thumbnail). nil 이면 만들지 않음 (기존과 같음)
61
+ private var galleryThumbnail: PhotoThumbnailOptions?
60
62
 
61
63
  // MARK: - Module Setup
62
64
 
@@ -240,6 +242,9 @@ class RNAttachmentKit: RCTEventEmitter {
240
242
  func openMapViewer(_ options: NSDictionary,
241
243
  resolve: @escaping RCTPromiseResolveBlock,
242
244
  reject: @escaping RCTPromiseRejectBlock) -> Void {
245
+ guard LicenseGate.isLicensed else {
246
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
247
+ }
243
248
  guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
244
249
  let lng = RNAttachmentKit.cgFloat(options["longitude"]) else {
245
250
  reject("ERROR", "latitude and longitude are required", nil)
@@ -308,6 +313,9 @@ class RNAttachmentKit: RCTEventEmitter {
308
313
  func renderLocationThumbnail(_ options: NSDictionary,
309
314
  resolve: @escaping RCTPromiseResolveBlock,
310
315
  reject: @escaping RCTPromiseRejectBlock) -> Void {
316
+ guard LicenseGate.isLicensed else {
317
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
318
+ }
311
319
  guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
312
320
  let lng = RNAttachmentKit.cgFloat(options["longitude"]),
313
321
  let w = RNAttachmentKit.cgFloat(options["width"]), w > 0 else {
@@ -366,6 +374,9 @@ class RNAttachmentKit: RCTEventEmitter {
366
374
  func reverseGeocode(_ options: NSDictionary,
367
375
  resolve: @escaping RCTPromiseResolveBlock,
368
376
  reject: @escaping RCTPromiseRejectBlock) -> Void {
377
+ guard LicenseGate.isLicensed else {
378
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
379
+ }
369
380
  let lat = (options["latitude"] as? NSNumber)?.doubleValue ?? .nan
370
381
  let lng = (options["longitude"] as? NSNumber)?.doubleValue ?? .nan
371
382
  let lang = (options["languageCode"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? "en"
@@ -383,6 +394,10 @@ class RNAttachmentKit: RCTEventEmitter {
383
394
  resolve: @escaping RCTPromiseResolveBlock,
384
395
  reject: @escaping RCTPromiseRejectBlock) -> Void {
385
396
  let dict = config as? [String: Any] ?? [:]
397
+ // 라이선스 키가 없어도 설정은 받아 둠 (AI 버튼은 보이고, 누르면 "라이선스 필요" 안내)
398
+ if !LicenseGate.isLicensed, (dict["enabled"] as? Bool) == true {
399
+ NSLog("[RNAttachmentKit] AI needs a license key — the AI buttons show a license notice until init() gets a valid key")
400
+ }
386
401
  DispatchQueue.main.async {
387
402
  AIBilling.apply(dict)
388
403
  AIBilling.onUsage = { [weak self] body in self?.sendEvent(withName: "onAIUsage", body: body) }
@@ -421,6 +436,10 @@ class RNAttachmentKit: RCTEventEmitter {
421
436
  let cacheFiles: [String]? = (options["cacheFiles"] as? [Any])?.map { ($0 as? String) ?? "" }
422
437
  // videos: images 와 같은 순서. 항목이 있으면 그 페이지는 동영상 (images[i] 는 포스터)
423
438
  let viewerVideos: [ViewerVideoItem?] = (options["videos"] as? [Any])?.map { ViewerVideoItem(dictionary: $0) } ?? []
439
+ // 동영상 페이지는 라이선스 키가 있어야
440
+ if !LicenseGate.isLicensed, viewerVideos.contains(where: { $0 != nil }) {
441
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
442
+ }
424
443
  let themeColorHex = options["themeColor"] as? String
425
444
  let languageCode = options["languageCode"] as? String ?? "en"
426
445
  let viewerTitle = options["title"] as? String
@@ -679,6 +698,7 @@ class RNAttachmentKit: RCTEventEmitter {
679
698
  var parsedMaxHeight: Int? = nil
680
699
  if let maxW = options["maxWidth"] as? Int { parsedMaxWidth = maxW }
681
700
  if let maxH = options["maxHeight"] as? Int { parsedMaxHeight = maxH }
701
+ let parsedThumbnail = PhotoThumbnailOptions(options["thumbnail"])
682
702
 
683
703
  DispatchQueue.main.async {
684
704
  guard let rootViewController = self.getRootViewController() else {
@@ -716,7 +736,7 @@ class RNAttachmentKit: RCTEventEmitter {
716
736
 
717
737
  editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
718
738
  editorVC?.dismiss(animated: true) {
719
- self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
739
+ self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, thumbnail: parsedThumbnail, resolve: { resultArr in
720
740
  if let arr = resultArr as? [[String: Any]], let first = arr.first {
721
741
  resolve(first)
722
742
  } else {
@@ -760,7 +780,7 @@ class RNAttachmentKit: RCTEventEmitter {
760
780
 
761
781
  editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
762
782
  editorVC?.dismiss(animated: true) {
763
- self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
783
+ self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, thumbnail: parsedThumbnail, resolve: { resultArr in
764
784
  var resultDict = (resultArr as? [[String: Any]])?.first ?? (resultArr as? [String: Any]) ?? [:]
765
785
  // For remote URLs, we preserve originalUri if editedFileUri isn't provided
766
786
  let originalRemoteUrl = (options["editedFileUri"] as? String) ?? uri
@@ -868,6 +888,8 @@ class RNAttachmentKit: RCTEventEmitter {
868
888
  // maxWidth, maxHeight 옵션 (미설정 또는 0이면 원본 크기 유지)
869
889
  galleryVC.maxWidth = (options["maxWidth"] as? Int) ?? 0
870
890
  galleryVC.maxHeight = (options["maxHeight"] as? Int) ?? 0
891
+ // 사진 썸네일 (동영상 포스터 규칙은 그대로 — video.posterFormat, 긴 변 512)
892
+ self.galleryThumbnail = PhotoThumbnailOptions(options["thumbnail"])
871
893
 
872
894
  // outputFormat: "webp" | "jpg" | "png" (기본값: "webp")
873
895
  let fmt = (options["outputFormat"] as? String)?.lowercased() ?? "webp"
@@ -973,6 +995,7 @@ class RNAttachmentKit: RCTEventEmitter {
973
995
  // 동영상: mediaType "photo"(기본) | "video" | "all" + video 옵션 (지시서 3-1)
974
996
  let mediaTypeOption = (options["mediaType"] as? String)?.lowercased() ?? "photo"
975
997
  galleryVC.mediaType = ["photo", "video", "all"].contains(mediaTypeOption) ? mediaTypeOption : "photo"
998
+ // 동영상은 라이선스 키가 있어야 — 없으면 갤러리가 사진만 보여 줌 (GalleryBaseViewController.viewDidLoad)
976
999
  let videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
977
1000
  let asyncProcessing = options["asyncProcessing"] as? Bool ?? false
978
1001
  self.galleryShowsVideoHUD = ((options["video"] as? [String: Any])?["showProgressHUD"] as? Bool) ?? !asyncProcessing
@@ -1004,6 +1027,7 @@ class RNAttachmentKit: RCTEventEmitter {
1004
1027
  let galleryFormat = galleryVC.outputFormat // "webp" | "jpg" | "png"
1005
1028
  let galleryAccent = galleryVC.parsedThemeColor
1006
1029
  let galleryLanguage = galleryVC.languageCode
1030
+ let galleryThumb = self.galleryThumbnail
1007
1031
  galleryVC.onImagesSelected = { [weak self, weak galleryVC] selectedImages in
1008
1032
  guard let self = self else { resolve([]); return }
1009
1033
 
@@ -1176,6 +1200,9 @@ class RNAttachmentKit: RCTEventEmitter {
1176
1200
  result["fileExtension"] = meta.fileExtension
1177
1201
  result["fileSize"] = meta.fileSize
1178
1202
  }
1203
+ // 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서
1204
+ result.merge(self.makePhotoThumbnail(targetImage, options: galleryThumb, index: index,
1205
+ timestamp: timestamp, tempDir: tempDir)) { $1 }
1179
1206
 
1180
1207
  resultsLock.lock()
1181
1208
  results.append(result)
@@ -1227,6 +1254,9 @@ class RNAttachmentKit: RCTEventEmitter {
1227
1254
  result["fileExtension"] = meta.fileExtension
1228
1255
  result["fileSize"] = meta.fileSize
1229
1256
  }
1257
+ // 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서
1258
+ result.merge(self.makePhotoThumbnail(targetImage, options: galleryThumb, index: index,
1259
+ timestamp: timestamp, tempDir: tempDir)) { $1 }
1230
1260
 
1231
1261
  resultsLock.lock()
1232
1262
  results.append(result)
@@ -1621,6 +1651,66 @@ class RNAttachmentKit: RCTEventEmitter {
1621
1651
  }
1622
1652
  }
1623
1653
 
1654
+ // MARK: - Photo thumbnail (GalleryOptions.thumbnail / EditorOptions.thumbnail)
1655
+
1656
+ /// 결과 크기와 원본에서 잘라 쓸 영역 (픽셀). Android PhotoThumbnail.layout 과 같은 계산
1657
+ /// - fit: ratio = min(maxW/w, maxH/h, 1) → round(w·ratio) × round(h·ratio), 자르지 않음
1658
+ /// - cover: 상자 비율로 가운데를 자른 뒤 maxW×maxH 로 줄임. 원본이 상자보다 작으면 확대하지 않고 자른 크기 그대로
1659
+ static func photoThumbnailLayout(width w: Int, height h: Int, options o: PhotoThumbnailOptions) -> (crop: CGRect, size: CGSize) {
1660
+ let dw = Double(w), dh = Double(h)
1661
+ if !o.cover {
1662
+ let r = min(Double(o.maxWidth) / dw, Double(o.maxHeight) / dh, 1)
1663
+ return (CGRect(x: 0, y: 0, width: w, height: h),
1664
+ CGSize(width: max(1, (dw * r).rounded()), height: max(1, (dh * r).rounded())))
1665
+ }
1666
+ let aspect = Double(o.maxWidth) / Double(o.maxHeight)
1667
+ let cropW: Double, cropH: Double
1668
+ if dw / dh > aspect { cropH = dh; cropW = dh * aspect } else { cropW = dw; cropH = dw / aspect }
1669
+ let left = ((dw - cropW) / 2).rounded(), top = ((dh - cropH) / 2).rounded()
1670
+ let crop = CGRect(x: left, y: top, width: min(dw - left, cropW.rounded()), height: min(dh - top, cropH.rounded()))
1671
+ let size = cropW > Double(o.maxWidth)
1672
+ ? CGSize(width: o.maxWidth, height: o.maxHeight)
1673
+ : CGSize(width: max(1, cropW.rounded()), height: max(1, cropH.rounded()))
1674
+ return (crop, size)
1675
+ }
1676
+
1677
+ /// 사진 썸네일을 만들어 결과에 넣을 필드(thumbnailUri/Width/Height/FileSize)를 돌려줌. options 가 없거나 실패하면 빈 딕셔너리.
1678
+ /// image 는 결과 사진(uri)을 만든 것과 같은 UIImage — 원본을 다시 읽지 않음. 새로 그려 인코딩하므로 EXIF·GPS 는 들어가지 않음
1679
+ private func makePhotoThumbnail(_ image: UIImage, options: PhotoThumbnailOptions?, index: Int,
1680
+ timestamp: Int, tempDir: String) -> [String: Any] {
1681
+ guard let o = options else { return [:] }
1682
+ // 방향(imageOrientation) 반영된 픽셀 크기
1683
+ let w = Int((image.size.width * image.scale).rounded()), h = Int((image.size.height * image.scale).rounded())
1684
+ guard w > 0, h > 0 else { return [:] }
1685
+ let (crop, size) = Self.photoThumbnailLayout(width: w, height: h, options: o)
1686
+ let format = UIGraphicsImageRendererFormat()
1687
+ format.scale = 1
1688
+ format.opaque = false
1689
+ format.preferredRange = .standard
1690
+ let sx = size.width / crop.width, sy = size.height / crop.height
1691
+ let thumb = UIGraphicsImageRenderer(size: size, format: format).image { ctx in
1692
+ ctx.cgContext.interpolationQuality = .high
1693
+ image.draw(in: CGRect(x: -crop.minX * sx, y: -crop.minY * sy, width: CGFloat(w) * sx, height: CGFloat(h) * sy))
1694
+ }
1695
+ // 색 프로필(sRGB ICC, 약 3KB)을 붙이지 않도록 장치 RGB 로 태그 — 수 KB 짜리 썸네일에선 용량의 절반 가까이라.
1696
+ // 프로필이 없는 이미지는 sRGB 로 읽히므로 색은 같음
1697
+ let untagged = thumb.cgImage.flatMap { $0.copy(colorSpace: CGColorSpaceCreateDeviceRGB()) }.map { UIImage(cgImage: $0) } ?? thumb
1698
+ // webp 인코딩이 안 되면 jpg 로
1699
+ var data: Data? = nil
1700
+ var ext = "jpg"
1701
+ if o.format == "webp", let d = encodeWebP(image: untagged, quality: o.quality) { data = d; ext = "webp" }
1702
+ if data == nil { data = untagged.jpegData(compressionQuality: o.quality); ext = "jpg" }
1703
+ guard let bytes = data else { return [:] }
1704
+ let url = URL(fileURLWithPath: (tempDir as NSString).appendingPathComponent("turbo_picker_thumb_\(timestamp)_\(index).\(ext)"))
1705
+ do { try bytes.write(to: url, options: [.atomic]) } catch { return [:] }
1706
+ return [
1707
+ "thumbnailUri": url.absoluteString,
1708
+ "thumbnailWidth": Int(size.width),
1709
+ "thumbnailHeight": Int(size.height),
1710
+ "thumbnailFileSize": bytes.count,
1711
+ ]
1712
+ }
1713
+
1624
1714
  /// 이미지를 지정 포맷으로 저장하고 file:// URI 반환
1625
1715
  /// - Parameters:
1626
1716
  /// - image: 저장할 UIImage
@@ -1806,7 +1896,7 @@ class RNAttachmentKit: RCTEventEmitter {
1806
1896
  }
1807
1897
 
1808
1898
 
1809
- private func processEditorResult(asset: PHAsset?, croppedImage: UIImage?, filterState: FilterState, caption: String, galleryVC: GalleryViewController?, targetMaxWidth: Int? = nil, targetMaxHeight: Int? = nil, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
1899
+ private func processEditorResult(asset: PHAsset?, croppedImage: UIImage?, filterState: FilterState, caption: String, galleryVC: GalleryViewController?, targetMaxWidth: Int? = nil, targetMaxHeight: Int? = nil, thumbnail: PhotoThumbnailOptions? = nil, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
1810
1900
  let tempDir = prepareCacheDirectory()
1811
1901
  // 동영상 1장 편집 → 전송: 편집 화면에서 고른 품질·음소거로 VideoExporter (사진 경로와 같은 결과 형태)
1812
1902
  if let phAsset = asset, phAsset.mediaType == .video {
@@ -1816,6 +1906,8 @@ class RNAttachmentKit: RCTEventEmitter {
1816
1906
  let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
1817
1907
  let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
1818
1908
  let format = galleryVC?.outputFormat ?? "webp"
1909
+ // openEditor 는 자기 옵션, 갤러리 편집 경로는 openGallery 의 thumbnail
1910
+ let thumbOptions = thumbnail ?? (galleryVC != nil ? galleryThumbnail : nil)
1819
1911
 
1820
1912
  DispatchQueue.global(qos: .userInitiated).async {
1821
1913
  let photoManager = PhotoManager.shared
@@ -1901,6 +1993,9 @@ class RNAttachmentKit: RCTEventEmitter {
1901
1993
  result["fileExtension"] = finalMeta.fileExtension
1902
1994
  result["fileSize"] = finalMeta.fileSize
1903
1995
  }
1996
+ // 썸네일: 크롭·그리기·필터가 반영된 결과(uri 와 같은 비트맵)에서
1997
+ result.merge(self.makePhotoThumbnail(finalImageForFormat, options: thumbOptions, index: 0,
1998
+ timestamp: timestamp, tempDir: tempDir)) { $1 }
1904
1999
 
1905
2000
  DispatchQueue.main.async {
1906
2001
  self.currentGalleryVC = nil
@@ -1916,6 +2011,7 @@ class RNAttachmentKit: RCTEventEmitter {
1916
2011
  let maxWidth = galleryVC.maxWidth
1917
2012
  let maxHeight = galleryVC.maxHeight
1918
2013
  let format = galleryVC.outputFormat
2014
+ let thumbOptions = galleryThumbnail
1919
2015
 
1920
2016
  DispatchQueue.global(qos: .userInitiated).async {
1921
2017
  let photoManager = PhotoManager.shared
@@ -1962,7 +2058,8 @@ class RNAttachmentKit: RCTEventEmitter {
1962
2058
  result["fileSize"] = meta.fileSize
1963
2059
  }
1964
2060
 
1965
- if (maxWidth > 0 || maxHeight > 0), let resizedImage = self.resizeImage(finalImage, maxWidth: maxWidth, maxHeight: maxHeight) {
2061
+ let resizedForThumb = (maxWidth > 0 || maxHeight > 0) ? self.resizeImage(finalImage, maxWidth: maxWidth, maxHeight: maxHeight) : nil
2062
+ if let resizedImage = resizedForThumb {
1966
2063
  if let resizedUri = self.saveImage(resizedImage, format: format, prefix: "resized", index: index, timestamp: timestamp, tempDir: tempDir) {
1967
2064
  result["uri"] = resizedUri
1968
2065
  result["width"] = Int(resizedImage.size.width)
@@ -1973,6 +2070,8 @@ class RNAttachmentKit: RCTEventEmitter {
1973
2070
  result["fileSize"] = resizedMeta.fileSize
1974
2071
  }
1975
2072
  }
2073
+ result.merge(self.makePhotoThumbnail(resizedForThumb ?? finalImage, options: thumbOptions, index: index,
2074
+ timestamp: timestamp, tempDir: tempDir)) { $1 }
1976
2075
 
1977
2076
  resultsLock.lock()
1978
2077
  results.append(result)
@@ -2018,6 +2117,29 @@ class RNAttachmentKit: RCTEventEmitter {
2018
2117
  }
2019
2118
  }
2020
2119
 
2120
+ /// GalleryOptions.thumbnail / EditorOptions.thumbnail
2121
+ struct PhotoThumbnailOptions {
2122
+ let maxWidth: Int
2123
+ let maxHeight: Int
2124
+ /// true = "cover" (상자를 꽉 채우고 가운데 기준으로 자름), false = "fit"
2125
+ let cover: Bool
2126
+ let quality: CGFloat
2127
+ /// "webp" | "jpg"
2128
+ let format: String
2129
+
2130
+ /// RN 옵션 → 값. maxWidth 가 없거나 0 이하면 nil (썸네일 안 만듦)
2131
+ init?(_ raw: Any?) {
2132
+ guard let d = raw as? NSDictionary, let mw = (d["maxWidth"] as? NSNumber)?.intValue, mw > 0 else { return nil }
2133
+ maxWidth = mw
2134
+ let mh = (d["maxHeight"] as? NSNumber)?.intValue ?? 0
2135
+ maxHeight = mh > 0 ? mh : mw
2136
+ cover = (d["mode"] as? String)?.lowercased() == "cover"
2137
+ quality = CGFloat(min(max((d["quality"] as? NSNumber)?.doubleValue ?? 0.8, 0), 1))
2138
+ let f = (d["format"] as? String)?.lowercased()
2139
+ format = (f == "jpg" || f == "jpeg") ? "jpg" : "webp"
2140
+ }
2141
+ }
2142
+
2021
2143
  // MARK: - UIColor Extension for Hex Support
2022
2144
  extension UIColor {
2023
2145
  convenience init?(hexString: String) {
@@ -2059,6 +2181,10 @@ extension RNAttachmentKit {
2059
2181
  resolve: @escaping RCTPromiseResolveBlock,
2060
2182
  reject: @escaping RCTPromiseRejectBlock) -> Void {
2061
2183
  let o = options as? [String: Any] ?? [:]
2184
+ // 동영상 항목은 라이선스 키가 있어야
2185
+ if !LicenseGate.isLicensed, (o["items"] as? [[String: Any]] ?? []).compactMap({ MediaGalleryItem(dictionary: $0) }).contains(where: { $0.isVideo }) {
2186
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
2187
+ }
2062
2188
  DispatchQueue.main.async {
2063
2189
  let config = MediaGalleryConfig(dictionary: o)
2064
2190
  let galleryId = config.galleryId
@@ -2140,6 +2266,9 @@ extension RNAttachmentKit {
2140
2266
  let galleryId = p["galleryId"] as? String ?? ""
2141
2267
  let raw = p["items"] as? [[String: Any]] ?? []
2142
2268
  let items = raw.compactMap { MediaGalleryItem(dictionary: $0) }
2269
+ if !LicenseGate.isLicensed, items.contains(where: { $0.isVideo }) {
2270
+ reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
2271
+ }
2143
2272
  let page = MediaGalleryPage(items: items, nextCursor: p["nextCursor"] as? String,
2144
2273
  hasMore: (p["hasMore"] as? NSNumber)?.boolValue ?? false)
2145
2274
  DispatchQueue.main.async {
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>RNAttachmentKit.framework/RNAttachmentKit</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>RNAttachmentKit.framework</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>RNAttachmentKit.framework/RNAttachmentKit</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>RNAttachmentKit.framework</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>