react-native-attachment-kit 1.0.5 → 1.0.6

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 (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +100 -3
  3. package/android/build.gradle +14 -0
  4. package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
  5. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +280 -13
  6. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +282 -13
  7. package/ios/RNAttachmentKit.m +18 -0
  8. package/ios/RNAttachmentKit.swift +421 -9
  9. package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
  10. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
  11. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +21 -5
  12. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Info.plist +0 -0
  13. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +33275 -7562
  14. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +193 -5
  15. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  16. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +193 -5
  17. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  18. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  19. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +15 -15
  20. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  21. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
  22. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +42 -10
  23. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Info.plist +0 -0
  24. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +33275 -7562
  25. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +193 -5
  26. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  27. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +193 -5
  28. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +33275 -7562
  29. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +193 -5
  30. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  31. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +193 -5
  32. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  33. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  34. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +25 -25
  35. package/package.json +1 -1
  36. package/react-native-attachment-kit.podspec +1 -1
  37. package/src/NativeAttachmentKit.ts +5 -0
  38. package/src/index.ts +118 -7
  39. package/src/types.ts +173 -0
@@ -6,7 +6,7 @@
6
6
  <dict>
7
7
  <key>Assets.car</key>
8
8
  <data>
9
- 24XBU3LEblGjGTOAqjhqZ+mB2U0=
9
+ RhTH3FdNHWutYB3O0httBcss21E=
10
10
  </data>
11
11
  <key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
12
12
  <data>
@@ -22,51 +22,51 @@
22
22
  </data>
23
23
  <key>Headers/RNAttachmentKit-Swift.h</key>
24
24
  <data>
25
- 4lqGZdGnjvCCDxNRFc2GgyyzqyE=
25
+ SA2Uv/xUxXBayyyXE8H2RjgdHQA=
26
26
  </data>
27
27
  <key>Info.plist</key>
28
28
  <data>
29
- 0sC7XVCqX43alae4JC2p56Wh1F4=
29
+ VGVHsdYWxmrpkpVKbSFeY69ecnY=
30
30
  </data>
31
31
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
32
32
  <data>
33
- yzVs+k0n+fAWBNbxtMQPWwrjRN0=
33
+ pQhFtv1R/nVZA5UMRIUtRAYUPfE=
34
34
  </data>
35
35
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
36
36
  <data>
37
- 3d+7crlvM8P5/XPlmAcyIbpS6zs=
37
+ QViRQdPRlGSnejc+FkklUFb1xzo=
38
38
  </data>
39
39
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
40
40
  <data>
41
- 3Y+Yb6iItYSkNObvNxCJpXncvHQ=
41
+ 13waPrbWcS1Vv7tMDqh9IORrbTU=
42
42
  </data>
43
43
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
44
44
  <data>
45
- 3d+7crlvM8P5/XPlmAcyIbpS6zs=
45
+ QViRQdPRlGSnejc+FkklUFb1xzo=
46
46
  </data>
47
47
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
48
48
  <data>
49
- pTw5/RKAzDXtx8rwalHKzC1QVrw=
49
+ srdAd3yXJz+UBhI0hq2En+76JBQ=
50
50
  </data>
51
51
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
52
52
  <data>
53
- yzVs+k0n+fAWBNbxtMQPWwrjRN0=
53
+ pQhFtv1R/nVZA5UMRIUtRAYUPfE=
54
54
  </data>
55
55
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
56
56
  <data>
57
- YZtIbnfcDasieXiHsoxa0r1/Hkc=
57
+ 4Bhyu9pqG8QPG8JDzN15Es69LgQ=
58
58
  </data>
59
59
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
60
60
  <data>
61
- 174LWJPXzjBzOETdSc+BKgsj/ig=
61
+ 9aHuRlxlcUm5c6hoS03l0x+rd+8=
62
62
  </data>
63
63
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
64
64
  <data>
65
- YZtIbnfcDasieXiHsoxa0r1/Hkc=
65
+ 4Bhyu9pqG8QPG8JDzN15Es69LgQ=
66
66
  </data>
67
67
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
68
68
  <data>
69
- brFa0/Ksy3av4Ce56nRbOQSY1c0=
69
+ GRAT4UVHZ+7RlMTxrfKaS0xKzRo=
70
70
  </data>
71
71
  <key>Modules/module.modulemap</key>
72
72
  <data>
@@ -227,7 +227,7 @@
227
227
  <dict>
228
228
  <key>hash2</key>
229
229
  <data>
230
- hcxV/4NZu79SXIpwsHu8Fo8WVAZLQfuKhITugdLFV+I=
230
+ 0N1A4LGshpc4ca3QAtjAqMx7M6KC4+eiQAKHcKRnUB4=
231
231
  </data>
232
232
  </dict>
233
233
  <key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
@@ -255,77 +255,77 @@
255
255
  <dict>
256
256
  <key>hash2</key>
257
257
  <data>
258
- jP+jiNnXhkOKTRJb5kKT4Skcl3imtlmW8htzvrG/9B8=
258
+ CIsiqD4QUiiFinw84KecZtVO9t4RF+3C362HoLNyZiA=
259
259
  </data>
260
260
  </dict>
261
261
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
262
262
  <dict>
263
263
  <key>hash2</key>
264
264
  <data>
265
- wPEN6QtxVqoCbCp/z3rYhGT5QOE2vKZR2uHf1sFkKcQ=
265
+ 1zvKPqPypcR4T+4Rom2iyPxya6nAybDNWacrlzCtmHk=
266
266
  </data>
267
267
  </dict>
268
268
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
269
269
  <dict>
270
270
  <key>hash2</key>
271
271
  <data>
272
- PCJzsbciCPQjQ+8d1NaEqra2tG6IhnKGxArWy0tMCNU=
272
+ bQBBzPNGBXPENE5stiQHn43HpKljGLBGsmytx74osv8=
273
273
  </data>
274
274
  </dict>
275
275
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
276
276
  <dict>
277
277
  <key>hash2</key>
278
278
  <data>
279
- IqlfotL45Jo1H46fYK8QlNdsrWP+Wc4omyD8Ei0M38Y=
279
+ c5sCwczWvgTgfE4P3/QZXLg4Ju1YyiBkHe8mlkacRzs=
280
280
  </data>
281
281
  </dict>
282
282
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
283
283
  <dict>
284
284
  <key>hash2</key>
285
285
  <data>
286
- PCJzsbciCPQjQ+8d1NaEqra2tG6IhnKGxArWy0tMCNU=
286
+ bQBBzPNGBXPENE5stiQHn43HpKljGLBGsmytx74osv8=
287
287
  </data>
288
288
  </dict>
289
289
  <key>Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
290
290
  <dict>
291
291
  <key>hash2</key>
292
292
  <data>
293
- +69q+F1mVqbDKIgKGmPaw7Oh0Evf2i4ESrGeEAa4b94=
293
+ jN0azSsNiu9msGjnni3J+edf0PMv4n919UpLDnJhFfU=
294
294
  </data>
295
295
  </dict>
296
296
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
297
297
  <dict>
298
298
  <key>hash2</key>
299
299
  <data>
300
- wPEN6QtxVqoCbCp/z3rYhGT5QOE2vKZR2uHf1sFkKcQ=
300
+ 1zvKPqPypcR4T+4Rom2iyPxya6nAybDNWacrlzCtmHk=
301
301
  </data>
302
302
  </dict>
303
303
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
304
304
  <dict>
305
305
  <key>hash2</key>
306
306
  <data>
307
- +c23Df2P+F8jTw46r7Qo0C11WzQMkRn3zQP3nOBtOng=
307
+ ruMrsqQu7c5vBjEKp/hzHz82QST/UnwbWCTHD3uGgqM=
308
308
  </data>
309
309
  </dict>
310
310
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
311
311
  <dict>
312
312
  <key>hash2</key>
313
313
  <data>
314
- oh5aRAAJiHhK6wJJLdK4eEzb5vdFcETRW28KyIXHj0g=
314
+ l+6rzGzrcNz6C1xQixmxJnTj6bp8QFLvi01FhSTniOA=
315
315
  </data>
316
316
  </dict>
317
317
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
318
318
  <dict>
319
319
  <key>hash2</key>
320
320
  <data>
321
- +c23Df2P+F8jTw46r7Qo0C11WzQMkRn3zQP3nOBtOng=
321
+ ruMrsqQu7c5vBjEKp/hzHz82QST/UnwbWCTHD3uGgqM=
322
322
  </data>
323
323
  </dict>
324
324
  <key>Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
325
325
  <dict>
326
326
  <key>hash2</key>
327
327
  <data>
328
- hC7shtU5k+++O06+9Yl5UBbBKbXd0idJRMcEokgi3Co=
328
+ EqFFC6hS93F/fIA+wRumib7y+P0lyAk64livq+vjDuE=
329
329
  </data>
330
330
  </dict>
331
331
  <key>Modules/module.modulemap</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-attachment-kit",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Native attachment kit for React Native — gallery picker, editor, image viewer and media gallery (Turbo Module)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -26,7 +26,7 @@ Pod::Spec.new do |s|
26
26
  }
27
27
 
28
28
  s.vendored_frameworks = "ios/RNAttachmentKit.xcframework"
29
- s.frameworks = "MapKit", "CoreLocation", "UniformTypeIdentifiers" # 첨부 메뉴 > 위치·파일 탭
29
+ s.frameworks = "MapKit", "CoreLocation", "UniformTypeIdentifiers", "Vision", "CryptoKit", "CoreMotion" # 위치·파일 탭 / AI 편집 사진 분석·캐시 / 카메라 기기 방향
30
30
 
31
31
  # s.dependency "SDWebImageWebPCoder"
32
32
 
@@ -7,6 +7,8 @@ export interface Spec extends TurboModule {
7
7
  openEditor(options: Object): Promise<Object>;
8
8
  openViewer(options: Object): Promise<void>;
9
9
  closeGallery(): Promise<boolean>;
10
+ /** 동영상 변환 취소. index -1 = 남은 동영상 전부, processingId "" = 가장 최근 처리 */
11
+ cancelVideoProcessing(index: number, processingId: string): Promise<boolean>;
10
12
 
11
13
  // RCTEventEmitter 필수 규격 (Turbo Module에서 이벤트 방출 시 필수)
12
14
  addListener(eventName: string): void;
@@ -26,6 +28,9 @@ export interface Spec extends TurboModule {
26
28
  reverseGeocode(options: Object): Promise<Object>;
27
29
  clearReverseGeocodeCache(): Promise<void>;
28
30
 
31
+ // AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트
32
+ setAIConfig(config: Object): Promise<void>;
33
+
29
34
  // 미디어 갤러리 (사진 모아보기)
30
35
  openMediaGallery(options: Object): Promise<Object>;
31
36
  closeMediaGallery(galleryId: string): Promise<void>;
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NativeModules, Platform, TurboModuleRegistry, NativeEventEmitter, DeviceEventEmitter } from "react-native"
2
- import type { ImageResult, GalleryOptions, EditorOptions, SelectionChangeEvent, ViewerOptions, MediaGalleryOptions, MediaGalleryAppendPayload, MediaGalleryRemovePayload, Appearance } from "./types"
2
+ import type { ImageResult, GalleryOptions, EditorOptions, SelectionChangeEvent, ViewerOptions, MediaGalleryOptions, MediaGalleryAppendPayload, MediaGalleryRemovePayload, Appearance, AIConfig, AIUsageEvent, AIChargeRequiredEvent } from "./types"
3
3
 
4
4
  const LINKING_ERROR = `The package 'react-native-attachment-kit' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) + "- You rebuilt the app after installing the package\n" + "- You are not using Expo Go\n"
5
5
 
@@ -78,6 +78,8 @@ function alreadyOpenError(what: string): Error {
78
78
  * Android pickImagesPromise) 중복 호출하면 앞 호출의 Promise 가 영영 끝나지 않습니다 → JS 에서 먼저 막습니다.
79
79
  */
80
80
  let _galleryOpen = false
81
+ /** 어느 openGallery 호출이 _galleryOpen 을 잡고 있는지 — 처리 시작(onProcessingStart)에 먼저 풀고, 늦게 끝난 앞 호출이 새 호출의 잠금을 풀지 않게 */
82
+ let _galleryOpenToken = 0
81
83
  /** openEditor 도 Android 가 editImagePromise 하나만 보관하므로 같은 보호가 필요 */
82
84
  let _editorOpen = false
83
85
  /**
@@ -86,6 +88,8 @@ let _editorOpen = false
86
88
  */
87
89
  let _asyncImgProcessedSub: { remove: () => void } | null = null
88
90
 
91
+ export type { AIConfig, AIUsageEvent, AIChargeRequiredEvent } from "./types"
92
+ export type { VideoOptions, VideoProgressEvent, ViewerVideo, ProcessingStartEvent, ProcessingStartItem } from "./types"
89
93
  export type { SelectionChangeEvent, Appearance, AttachLocationResult, MapViewerOptions, ReverseGeocodeOptions, ReverseGeocodeResult, ReverseGeocodeErrorCode, AttachFileResult, AttachFileFailure, LocationThumbnailOptions, LocationThumbnailResult } from "./types"
90
94
  export type { MediaGalleryItem, MediaGalleryOptions, MediaGalleryGridSize, MediaGalleryGridSizeChangedEvent, MediaGalleryTheme, MediaGalleryViewerOptions, MediaGalleryDeleteOption, MediaGalleryItemDeletedEvent, MediaGalleryNeedPageEvent, MediaGalleryClosedEvent, MediaGalleryAppendPayload, MediaGalleryRemovePayload } from "./types"
91
95
 
@@ -115,6 +119,23 @@ export interface RNAttachmentKit {
115
119
  /** reverseGeocode 캐시 비우기 */
116
120
  clearReverseGeocodeCache(): Promise<void>
117
121
  closeGallery(): Promise<boolean>
122
+ /**
123
+ * processingId: 어느 보내기인지 (onProcessingStart 의 값). 생략하면 가장 최근 보내기
124
+ *
125
+ * 보내는 중인 동영상 변환 취소 (HUD 없이 말풍선에서 취소할 때). index 생략 = 남은 동영상 전부.
126
+ * 취소된 동영상은 onImageProcessed·결과에 { error: "CANCELLED", cancelled: true } 로 오고 나머지는 계속 처리됩니다.
127
+ * 진행 중인 처리가 없으면 false
128
+ */
129
+ cancelVideoProcessing(index?: number, processingId?: string): Promise<boolean>
130
+ /**
131
+ * AI 기능 on/off · 포인트 · 무료 횟수 · 1회 차감 포인트 (넘긴 값만 바뀜). 앱 시작 때 한 번, 잔액이 바뀔 때마다(충전·서버 동기화) 다시 호출.
132
+ * AI 화면이 열려 있으면 바로 반영됩니다.
133
+ */
134
+ setAIConfig(config: AIConfig): Promise<void>
135
+ /** AI 를 한 번 쓸 때마다 (무료 횟수 사용 또는 포인트 차감) — 앱이 서버에 반영. 해제 함수를 돌려줌 */
136
+ addAIUsageListener(listener: (event: AIUsageEvent) => void): () => void
137
+ /** 포인트가 모자라 사용자가 '충전하기'를 눌렀을 때 — 앱이 충전 화면으로 안내 (필요하면 closeGallery() 로 시트도 닫기). 해제 함수를 돌려줌 */
138
+ addAIChargeRequiredListener(listener: (event: AIChargeRequiredEvent) => void): () => void
118
139
  addSelectionChangeListener(listener: (event: SelectionChangeEvent) => void): () => void
119
140
  getDefaultAnimationConfig(): Promise<{ galleryOpen: number, galleryClose: number, editorOpen: number, editorClose: number, viewerOpen: number, viewerClose: number }>
120
141
  injectImageCache(urlString: string, localPath: string): Promise<boolean>
@@ -167,6 +188,22 @@ const AttachmentKit: RNAttachmentKit = {
167
188
  throw alreadyOpenError("Gallery")
168
189
  }
169
190
  _galleryOpen = true
191
+ const myToken = ++_galleryOpenToken
192
+ // 갤러리 화면이 닫히고 처리가 시작되면(onProcessingStart) 바로 잠금을 풂 — 동영상을 변환하는 동안에도
193
+ // 다시 열어 보낼 수 있게. Promise 는 지금처럼 처리가 다 끝나야 resolve
194
+ let myProcessingId: string | null = null
195
+ const subscriptionOpenLock = eventEmitter.addListener("onProcessingStart", (e: any) => {
196
+ if (myProcessingId) return
197
+ myProcessingId = typeof e?.processingId === "string" ? e.processingId : ""
198
+ if (_galleryOpenToken === myToken) _galleryOpen = false
199
+ })
200
+ /** 이 호출의 이벤트인지 — processingId 가 붙은 이벤트는 내 처리의 것만, 안 붙은(옛 네이티브·사진 1장 편집) 이벤트는 처리 ID 를 모를 때만 */
201
+ const isMine = (e: any) => {
202
+ const id = e?.processingId
203
+ if (typeof id !== "string" || id === "") return !myProcessingId
204
+ return myProcessingId === id
205
+ }
206
+ const mine = <T,>(fn?: (e: T) => void) => fn ? (e: T) => { if (isMine(e)) fn(e) } : undefined
170
207
 
171
208
  // 콜백 이벤트 임시 리스너 등록
172
209
  let subscriptionSelChange: any = null
@@ -182,16 +219,33 @@ const AttachmentKit: RNAttachmentKit = {
182
219
  try { _asyncImgProcessedSub.remove() } catch (e) { /* noop */ }
183
220
  _asyncImgProcessedSub = null
184
221
  }
185
- subscriptionImgProcessed = eventEmitter.addListener("onImageProcessed", options.onImageProcessed)
222
+ subscriptionImgProcessed = eventEmitter.addListener("onImageProcessed", mine(options.onImageProcessed)!)
186
223
  _asyncImgProcessedSub = subscriptionImgProcessed
187
224
  } else {
188
- subscriptionImgProcessed = eventEmitter.addListener("onImageProcessed", options.onImageProcessed)
225
+ subscriptionImgProcessed = eventEmitter.addListener("onImageProcessed", mine(options.onImageProcessed)!)
189
226
  }
190
227
  }
191
228
  let subscriptionLocation: any = null
192
229
  if (options.onLocationSelected) {
193
230
  subscriptionLocation = eventEmitter.addListener("onLocationSelected", options.onLocationSelected)
194
231
  }
232
+ // 동영상 변환 진행률 (video.onProgress)
233
+ let subscriptionVideoProgress: any = null
234
+ if (options.video?.onProgress) {
235
+ subscriptionVideoProgress = eventEmitter.addListener("onVideoProgress", mine(options.video.onProgress)!)
236
+ }
237
+ // 처리 시작 (보낸 직후 한 번, 결과보다 먼저)
238
+ let subscriptionProcessingStart: any = null
239
+ if (options.onProcessingStart) {
240
+ // 내 처리 시작만 (앞 보내기의 시작 이벤트는 이 호출 전에 이미 지나감)
241
+ const onStart = options.onProcessingStart
242
+ let started = false
243
+ subscriptionProcessingStart = eventEmitter.addListener("onProcessingStart", (e: any) => {
244
+ if (started) return
245
+ started = true
246
+ onStart(e)
247
+ })
248
+ }
195
249
  let subscriptionFiles: any = null
196
250
  if (options.onFilesSelected) {
197
251
  const onFiles = options.onFilesSelected
@@ -201,19 +255,26 @@ const AttachmentKit: RNAttachmentKit = {
201
255
 
202
256
  try {
203
257
  // 이벤트 콜백은 네이티브로 전달하지 않음
204
- const { onSelectionChange, onImageProcessed, onLocationSelected, onFilesSelected, ...restOptions } = options
205
- const nativeOptions = {
258
+ const { onSelectionChange, onImageProcessed, onLocationSelected, onFilesSelected, onProcessingStart, video, ...restOptions } = options
259
+ const nativeOptions: any = {
206
260
  themeColor: _defaultThemeColor,
207
261
  languageCode: _defaultLanguageCode,
208
262
  appearance: _defaultAppearance,
209
263
  ...restOptions,
210
264
  }
265
+ if (video) {
266
+ // onProgress 콜백은 네이티브로 넘기지 않음
267
+ const { onProgress, ...videoRest } = video
268
+ nativeOptions.video = videoRest
269
+ }
211
270
  nativeOptions.languageCode = normalizeLanguageCode(nativeOptions.languageCode)
212
271
  nativeOptions.appearance = normalizeAppearance(nativeOptions.appearance)
213
272
  const result = await RNAttachmentKitModule.openGallery(nativeOptions)
214
273
  return result
215
274
  } finally {
216
- _galleryOpen = false
275
+ // 처리 시작 때 이미 풀었고 그 뒤 새 호출이 잡았을 수 있음 → 내 잠금일 때만
276
+ if (_galleryOpenToken === myToken) _galleryOpen = false
277
+ subscriptionOpenLock.remove()
217
278
  // 갤러리가 닫히면 onSelectionChange 리스너 제거
218
279
  if (subscriptionSelChange) {
219
280
  subscriptionSelChange.remove()
@@ -225,6 +286,12 @@ const AttachmentKit: RNAttachmentKit = {
225
286
  if (subscriptionFiles) {
226
287
  subscriptionFiles.remove()
227
288
  }
289
+ if (subscriptionVideoProgress) {
290
+ subscriptionVideoProgress.remove()
291
+ }
292
+ if (subscriptionProcessingStart) {
293
+ subscriptionProcessingStart.remove()
294
+ }
228
295
  // asyncProcessing 모드가 아닐 때만 onImageProcessed 리스너 제거 (async 모드에선 백그라운드 처리를 위해 유지해야 함)
229
296
  // 호출부에서 수동으로 제거하거나, 최종 완료 시 정리하는 구조가 필요할 수 있음
230
297
  if (!options.asyncProcessing && subscriptionImgProcessed) {
@@ -380,7 +447,22 @@ const AttachmentKit: RNAttachmentKit = {
380
447
  mergedOptions.addressLanguageCode = options.languageCode || _rawLanguageCode || _defaultLanguageCode
381
448
  mergedOptions.languageCode = normalizeLanguageCode(mergedOptions.languageCode)
382
449
  mergedOptions.appearance = normalizeAppearance(mergedOptions.appearance)
383
- await RNAttachmentKitModule.openMapViewer(mergedOptions)
450
+ // 함수는 네이티브로 못 넘김 — 닫기 시작은 이벤트로 받음 (Android 는 RCTDeviceEventEmitter 로도 올 수 있어 둘 다 듣고 한 번만)
451
+ const onWillClose = options.onViewerWillClose
452
+ delete mergedOptions.onViewerWillClose
453
+ let willCloseSent = false
454
+ const willCloseHandler = (e: any) => {
455
+ if (willCloseSent) return
456
+ willCloseSent = true
457
+ onWillClose?.({ durationMs: typeof e?.durationMs === 'number' ? e.durationMs : 0 })
458
+ }
459
+ const subs: any[] = [eventEmitter.addListener("onMapViewerWillClose", willCloseHandler)]
460
+ if (Platform.OS === "android") subs.push(DeviceEventEmitter.addListener("onMapViewerWillClose", willCloseHandler))
461
+ try {
462
+ await RNAttachmentKitModule.openMapViewer(mergedOptions)
463
+ } finally {
464
+ subs.forEach(s => s.remove())
465
+ }
384
466
  },
385
467
 
386
468
  updateMapViewerSourceRect: async (rect: import("./types").SourceRect | null): Promise<void> => {
@@ -439,6 +521,35 @@ const AttachmentKit: RNAttachmentKit = {
439
521
  return RNAttachmentKitModule.closeGallery()
440
522
  },
441
523
 
524
+ cancelVideoProcessing: async (index?: number, processingId?: string): Promise<boolean> => {
525
+ if (!RNAttachmentKitModule) {
526
+ throw new Error(LINKING_ERROR)
527
+ }
528
+ if (typeof RNAttachmentKitModule.cancelVideoProcessing !== "function") return false // 옛 네이티브 빌드
529
+ return RNAttachmentKitModule.cancelVideoProcessing(typeof index === "number" && index >= 0 ? index : -1, processingId ?? "")
530
+ },
531
+
532
+ setAIConfig: async (config: AIConfig): Promise<void> => {
533
+ if (!RNAttachmentKitModule) {
534
+ throw new Error(LINKING_ERROR)
535
+ }
536
+ if (typeof RNAttachmentKitModule.setAIConfig !== "function") return // 옛 네이티브 빌드
537
+ // undefined 는 "바꾸지 않음" 이므로 빼고 보냄 (null 은 "비움")
538
+ const payload: Record<string, unknown> = {}
539
+ Object.entries(config ?? {}).forEach(([k, v]) => { if (v !== undefined) payload[k] = v })
540
+ return RNAttachmentKitModule.setAIConfig(payload)
541
+ },
542
+
543
+ addAIUsageListener: (listener: (event: AIUsageEvent) => void) => {
544
+ const subscription = eventEmitter.addListener("onAIUsage", listener as any)
545
+ return () => subscription.remove()
546
+ },
547
+
548
+ addAIChargeRequiredListener: (listener: (event: AIChargeRequiredEvent) => void) => {
549
+ const subscription = eventEmitter.addListener("onAIChargeRequired", listener as any)
550
+ return () => subscription.remove()
551
+ },
552
+
442
553
  addSelectionChangeListener: (listener: (event: SelectionChangeEvent) => void) => {
443
554
  const subscription = eventEmitter.addListener("onSelectionChange", listener)
444
555
 
package/src/types.ts CHANGED
@@ -20,6 +20,94 @@ export interface ImageResult {
20
20
  fileName?: string // 파일명
21
21
  fileExtension?: string // 확장자
22
22
  fileSize?: number // 파일 크기 (bytes)
23
+ /** "image"(기본) | "video" — mediaType: "all"/"video" 로 열었을 때 동영상이면 "video" */
24
+ mediaType?: "image" | "video"
25
+ /** 동영상: 길이(초, 소수) */
26
+ duration?: number
27
+ /** 동영상: 포스터 썸네일 file:// (video.posterFormat 형식 — 기본 webp, 긴 변 512) */
28
+ thumbnailUri?: string
29
+ thumbnailWidth?: number
30
+ thumbnailHeight?: number
31
+ /** 동영상: 전송본 코덱·평균 비트레이트(bps) */
32
+ videoCodec?: "h264" | "hevc" | string
33
+ videoBitrate?: number
34
+ /** 편집(trim/crop/overlay/음소거/품질 변경)했는지. false 면 원본을 그대로(리먹스) 보낸 것 */
35
+ edited?: boolean
36
+ /** 동영상 변환에 실패했을 때 (uri 없음). 사진·정상 동영상엔 없음. 앱이 취소했으면 "CANCELLED" */
37
+ error?: string
38
+ /** 앱이 cancelVideoProcessing 으로 취소한 동영상 (uri 없음) */
39
+ cancelled?: boolean
40
+ }
41
+
42
+ /**
43
+ * onProcessingStart — 갤러리에서 보낸 직후, 처리(사진 리사이즈·동영상 변환)를 시작하기 전에 한 번.
44
+ * 앱은 이걸로 말풍선 자리를 먼저 만들고(비율·포스터·길이), 동영상은 onVideoProgress 로 진행률을 그린 뒤
45
+ * onImageProcessed(index 같음)가 오면 실제 파일로 바꿉니다.
46
+ */
47
+ export interface ProcessingStartEvent {
48
+ /**
49
+ * 이번 보내기의 ID. onVideoProgress / onImageProcessed 에도 같은 값이 붙음.
50
+ * 동영상을 변환하는 동안에도 갤러리를 다시 열어 또 보낼 수 있으므로, 이벤트를 전역 리스너로 받는 앱은 이 값으로 구분
51
+ */
52
+ processingId: string
53
+ total: number
54
+ items: ProcessingStartItem[]
55
+ }
56
+
57
+ export interface ProcessingStartItem {
58
+ index: number
59
+ mediaType: "image" | "video"
60
+ /** ph://… (iOS) / content://… (Android) */
61
+ originalUri?: string
62
+ /** 예상 결과 크기 (사진: 리사이즈 후, 동영상: 품질·크롭 반영). 비율용 */
63
+ width: number
64
+ height: number
65
+ /** 동영상: 보낼 길이(초, 구간 반영) */
66
+ duration?: number
67
+ /** 동영상: 포스터 file:// (video.posterFormat 형식 — 기본 webp, 크롭·그리기 반영) — 변환이 끝나기 전 말풍선에 */
68
+ thumbnailUri?: string
69
+ thumbnailWidth?: number
70
+ thumbnailHeight?: number
71
+ }
72
+
73
+ /** 동영상 옵션 (GalleryOptions.video). mediaType 이 "video"/"all" 일 때만 의미 */
74
+ export interface VideoOptions {
75
+ /** 고를 수 있는 최대 길이(초). 기본 600 (10분). 넘는 동영상은 누르면 안내만. 0 = 제한 없음 */
76
+ maxDurationSec?: number
77
+ /** 전송 결과 최대 용량(MB). 기본 100. 추정치가 넘으면 더 낮은 품질로 자동 조정. 0 = 제한 없음 */
78
+ maxFileSizeMB?: number
79
+ /** 해상도 단계 (세로 px). 기본 [360, 480, 720, 1080]. 원본보다 큰 단계는 쓰지 않음(원본 유지) */
80
+ qualities?: number[]
81
+ /** 기본 품질 (세로 px). 기본 720. 원본이 더 작으면 원본 그대로 */
82
+ defaultQuality?: number
83
+ /** 구간 자르기 허용. 기본 true (편집 화면이 생기는 P2 부터) */
84
+ allowTrim?: boolean
85
+ /** 음소거 버튼 표시. 기본 true (P2 부터) */
86
+ allowMute?: boolean
87
+ /** 코덱. 기본 "h264". "hevc" 는 용량이 작지만 수신 기기 호환을 확인한 경우만 */
88
+ codec?: "h264" | "hevc"
89
+ /**
90
+ * 동영상 포스터(썸네일) 파일 형식. 기본 "webp" (jpg 보다 작음). "jpg" 로 하면 예전처럼 jpg.
91
+ * onProcessingStart / onImageProcessed 의 thumbnailUri 확장자가 이 값을 따름 (webp 인코딩이 안 되는 드문 경우엔 jpg 로 대신하니, 확장자는 uri 로 확인)
92
+ */
93
+ posterFormat?: "webp" | "jpg"
94
+ /** 전송 전 변환 진행률 (동영상마다 0~1, 1% 이상 바뀔 때·최소 0.1초 간격) */
95
+ onProgress?: (e: VideoProgressEvent) => void
96
+ /**
97
+ * 변환하는 동안 화면 가운데 진행 HUD(취소 버튼 포함)를 띄울지.
98
+ * 기본: asyncProcessing 이 아니면 true, asyncProcessing: true 면 false —
99
+ * 이벤트(onProcessingStart / onVideoProgress / onImageProcessed)로 말풍선에 진행 상황을 그리는 앱은 HUD 없이 씁니다.
100
+ * HUD 가 없을 때 취소는 AttachmentKit.cancelVideoProcessing(index)
101
+ */
102
+ showProgressHUD?: boolean
103
+ }
104
+
105
+ export interface VideoProgressEvent {
106
+ index: number
107
+ total: number
108
+ progress: number
109
+ /** 어느 보내기의 진행률인지 (onProcessingStart 의 processingId) */
110
+ processingId?: string
23
111
  }
24
112
 
25
113
  export interface SourceRect {
@@ -33,7 +121,10 @@ export interface GalleryOptions {
33
121
  maxSelection?: number
34
122
  maxWidth?: number // 최대 width
35
123
  maxHeight?: number // 최대 height
124
+ /** 기본 "photo" = 사진만(동영상 권한도 요청 안 함). "all" = 사진+동영상 날짜순 혼합, "video" = 동영상만 */
36
125
  mediaType?: "photo" | "video" | "all"
126
+ /** 동영상 옵션 (mediaType 이 video/all 일 때) */
127
+ video?: VideoOptions
37
128
  quality?: number
38
129
  allItemsText?: string
39
130
  selectedItemsText?: string
@@ -61,6 +152,8 @@ export interface GalleryOptions {
61
152
  asyncProcessing?: boolean // 완전 비동기 이벤트 리턴 여부
62
153
  onSelectionChange?: (event: SelectionChangeEvent) => void
63
154
  onImageProcessed?: (event: any) => void
155
+ /** 처리 시작 (보낸 직후 한 번) — 모든 자리의 미디어 종류·예상 크기·동영상 포스터. ProcessingStartEvent 참고 */
156
+ onProcessingStart?: (event: ProcessingStartEvent) => void
64
157
  /**
65
158
  * 갤러리 아래에 첨부 메뉴(갤러리 / 위치 / 파일)를 보여줍니다. 기본 false.
66
159
  * 위치 탭: iOS 는 Apple 지도(키 필요 없음), Android 는 Google 지도 — 앱의 AndroidManifest 에
@@ -266,12 +359,29 @@ export interface MapViewerOptions {
266
359
  sourceImageUri?: string
267
360
  /** 말풍선 이미지 라운드 (dp/pt, 기본 12) */
268
361
  sourceCornerRadius?: number
362
+ /**
363
+ * 닫기 애니메이션이 "시작"될 때 (durationMs = 닫기 애니메이션 길이). 프로미스는 다 닫힌 뒤에 끝나므로,
364
+ * 키보드 복귀처럼 닫힘과 동시에 하고 싶은 일은 여기서. Android 는 이 시점에 뷰어 창이 입력 포커스를 내려놓음
365
+ */
366
+ onViewerWillClose?: (e: { durationMs: number }) => void
269
367
  /** 마커·버튼 색 (기본: setDefaults 의 themeColor) */
270
368
  themeColor?: string
271
369
  languageCode?: LanguageCode
272
370
  appearance?: Appearance
273
371
  }
274
372
 
373
+ /** 뷰어의 동영상 페이지 정보 (ViewerOptions.videos) */
374
+ export interface ViewerVideo {
375
+ /** http(s) 또는 file:// */
376
+ uri: string
377
+ /** 초. 모르면 생략 (불러온 뒤 채움) */
378
+ duration?: number
379
+ /** 절대 경로(file:// 없음). 파일이 있으면 그걸로 재생, 없으면 스트리밍하고 다 받으면 이 경로에 저장 */
380
+ cacheFile?: string
381
+ width?: number
382
+ height?: number
383
+ }
384
+
275
385
  export interface ViewerOptions {
276
386
  images: string[]
277
387
  placeholderImages?: string[]
@@ -281,6 +391,12 @@ export interface ViewerOptions {
281
391
  * (앱이 캐시를 한 곳에서 관리하기 위한 옵션 — 항목이 null/빈 문자열이면 해당 사진은 기존 방식)
282
392
  */
283
393
  cacheFiles?: (string | null)[]
394
+ /**
395
+ * images 와 같은 순서. 항목이 있으면 그 페이지는 동영상 (null/생략 = 사진) — cacheFiles 와 같은 "평행 배열".
396
+ * 동영상 페이지에서 images[i] 는 포스터(썸네일) 이미지로 쓰입니다 (열기·닫기 애니메이션, 로딩 중 표시).
397
+ * 지원: iOS 뷰어, Android 는 useOverlayViewer: true 일 때 (그 외 Android 뷰어는 포스터만 보임)
398
+ */
399
+ videos?: (ViewerVideo | null)[]
284
400
  initialIndex?: number
285
401
  themeColor?: string // Added themeColor for Android
286
402
  /**
@@ -414,6 +530,14 @@ export interface MediaGalleryItem {
414
530
  isMine?: boolean
415
531
  width?: number
416
532
  height?: number
533
+ /** 생략 = 'image'. 'video' 면 imageUri/thumbUri 는 포스터, 재생은 videoUri */
534
+ mediaType?: 'image' | 'video'
535
+ /** 동영상 원본 (http(s) 또는 file://) */
536
+ videoUri?: string
537
+ /** 동영상 캐시 경로 (ViewerVideo.cacheFile 과 같은 의미) */
538
+ videoCacheFile?: string
539
+ /** 초 — 그리드 칸의 "▶ 0:12" 배지 */
540
+ duration?: number
417
541
  }
418
542
 
419
543
  export interface MediaGalleryDeleteOption {
@@ -534,3 +658,52 @@ export interface MediaGalleryRemovePayload {
534
658
  galleryId: string
535
659
  groupIds: string[]
536
660
  }
661
+
662
+ /**
663
+ * AI 기능 (갤러리의 AI 생성 · 편집 화면의 AI 편집) 설정과 잔액. `AttachmentKit.setAIConfig` 로 넘기며, 넘긴 값만 바뀝니다.
664
+ * 앱 서버가 진짜 장부이고 모듈은 화면 표시·사용 가능 판단용 사본을 들고 있습니다.
665
+ * 사용 순서: 구독자의 남은 무료 횟수(freeCount, 예: 월 30회) → 포인트(points, 1회 costPerUse) → 모자라면 충전 안내(onAIChargeRequired).
666
+ * 성공한 요청만 차감되고(실패·취소·안전 차단은 무료), 차감할 때마다 onAIUsage 가 옵니다.
667
+ */
668
+ export interface AIConfig {
669
+ /** AI 기능 on/off. 기본 false — 꺼져 있으면 갤러리·편집 화면에 AI 버튼이 보이지 않음 */
670
+ enabled?: boolean
671
+ /** 현재 포인트 */
672
+ points?: number
673
+ /** 구독자의 남은 무료 횟수 (예: 월 30회 중 남은 것 — 기간·횟수는 앱/서버가 정함). 0 보다 크면 포인트보다 먼저 씀 */
674
+ freeCount?: number
675
+ /** 1회 차감 포인트. 0 이면 과금 없음 (무제한) */
676
+ costPerUse?: number
677
+ /** 화질 옵션 "고화질(1K)" 1회 차감 포인트. 생략/null 이면 costPerUse */
678
+ highQualityCost?: number | null
679
+ /** 운영: 앱 서버(프록시) 주소. 모듈이 Gemini generateContent 본문을 이 주소로 보내고 서버가 키를 붙여 전달 (헤더 X-AI-Model = 모델 이름) */
680
+ endpoint?: string | null
681
+ /** endpoint 로 보낼 때 붙일 헤더 (예: { Authorization: "Bearer …" }) */
682
+ headers?: Record<string, string>
683
+ /** 테스트 전용 Gemini 키 (앱에 넣어 배포하지 말 것). endpoint 가 있으면 무시 */
684
+ apiKey?: string | null
685
+ }
686
+
687
+ /** AI 를 한 번 성공적으로 썼을 때 (서버에 차감을 반영하는 데 사용) */
688
+ export interface AIUsageEvent {
689
+ /** "generate" = 갤러리 AI 생성, "edit" = 편집 화면 AI 편집 */
690
+ kind: "generate" | "edit"
691
+ /** 무료 횟수를 썼으면 true (이때 pointsDeducted = 0) */
692
+ usedFreeCount: boolean
693
+ /** 이번에 차감한 포인트 */
694
+ pointsDeducted: number
695
+ /** 고화질(1K)로 만들었는지 */
696
+ highQuality: boolean
697
+ /** 차감 후 모듈이 들고 있는 값 */
698
+ points: number
699
+ freeCount: number
700
+ }
701
+
702
+ /** 포인트가 모자라 사용자가 '충전하기'를 눌렀을 때 (모듈은 AI 화면을 닫음 → 앱이 충전 화면으로 안내) */
703
+ export interface AIChargeRequiredEvent {
704
+ kind: "generate" | "edit"
705
+ /** 1회에 필요한 포인트 */
706
+ required: number
707
+ points: number
708
+ freeCount: number
709
+ }