react-native-rectangle-doc-scanner 3.112.0 → 3.115.0

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.
@@ -20,9 +20,13 @@ export interface FullDocScannerStrings {
20
20
  retake?: string;
21
21
  confirm?: string;
22
22
  cropTitle?: string;
23
+ first?: string;
24
+ second?: string;
25
+ secondBtn?: string;
26
+ secondPrompt?: string;
23
27
  }
24
28
  export interface FullDocScannerProps {
25
- onResult: (result: FullDocScannerResult) => void;
29
+ onResult: (results: FullDocScannerResult[]) => void;
26
30
  onClose?: () => void;
27
31
  detectionConfig?: DetectionConfig;
28
32
  overlayColor?: string;
@@ -35,5 +39,6 @@ export interface FullDocScannerProps {
35
39
  enableGallery?: boolean;
36
40
  cropWidth?: number;
37
41
  cropHeight?: number;
42
+ type?: 'business';
38
43
  }
39
44
  export declare const FullDocScanner: React.FC<FullDocScannerProps>;
@@ -115,7 +115,7 @@ const normalizeCapturedDocument = (document) => {
115
115
  croppedPath: document.croppedPath ? stripFileUri(document.croppedPath) : null,
116
116
  };
117
117
  };
118
- const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, strings, minStableFrames, onError, enableGallery = true, cropWidth = 1200, cropHeight = 1600, }) => {
118
+ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, strings, minStableFrames, onError, enableGallery = true, cropWidth = 1200, cropHeight = 1600, type, }) => {
119
119
  const [processing, setProcessing] = (0, react_1.useState)(false);
120
120
  const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
121
121
  const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
@@ -123,12 +123,21 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
123
123
  const [rectangleHint, setRectangleHint] = (0, react_1.useState)(false);
124
124
  const [flashEnabled, setFlashEnabled] = (0, react_1.useState)(false);
125
125
  const [rotationDegrees, setRotationDegrees] = (0, react_1.useState)(0);
126
+ const [capturedPhotos, setCapturedPhotos] = (0, react_1.useState)([]);
127
+ const [currentPhotoIndex, setCurrentPhotoIndex] = (0, react_1.useState)(0);
128
+ const [previewPhotoIndex, setPreviewPhotoIndex] = (0, react_1.useState)(0);
126
129
  const resolvedGridColor = gridColor ?? overlayColor;
127
130
  const docScannerRef = (0, react_1.useRef)(null);
128
131
  const captureModeRef = (0, react_1.useRef)(null);
129
132
  const captureInProgressRef = (0, react_1.useRef)(false);
130
133
  const rectangleCaptureTimeoutRef = (0, react_1.useRef)(null);
131
134
  const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
135
+ const currentPhotoIndexRef = (0, react_1.useRef)(currentPhotoIndex);
136
+ const isBusinessMode = type === 'business';
137
+ const maxPhotos = isBusinessMode ? 2 : 1;
138
+ (0, react_1.useEffect)(() => {
139
+ currentPhotoIndexRef.current = currentPhotoIndex;
140
+ }, [currentPhotoIndex]);
132
141
  const mergedStrings = (0, react_1.useMemo)(() => ({
133
142
  captureHint: strings?.captureHint,
134
143
  manualHint: strings?.manualHint,
@@ -138,6 +147,10 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
138
147
  retake: strings?.retake ?? 'Retake',
139
148
  confirm: strings?.confirm ?? 'Confirm',
140
149
  cropTitle: strings?.cropTitle ?? 'Crop Document',
150
+ first: strings?.first ?? 'Front',
151
+ second: strings?.second ?? 'Back',
152
+ secondBtn: strings?.secondBtn ?? 'Capture Back Side?',
153
+ secondPrompt: strings?.secondPrompt ?? strings?.secondBtn ?? 'Capture Back Side?',
141
154
  }), [strings]);
142
155
  const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
143
156
  console.error('[FullDocScanner] error', error);
@@ -177,6 +190,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
177
190
  hasBase64: !!croppedImage.data,
178
191
  });
179
192
  setProcessing(false);
193
+ setPreviewPhotoIndex(currentPhotoIndexRef.current);
180
194
  // Show confirmation screen
181
195
  setCroppedImageData({
182
196
  path: croppedImage.path,
@@ -223,6 +237,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
223
237
  console.warn('[FullDocScanner] Missing capture mode for capture result, ignoring');
224
238
  return;
225
239
  }
240
+ const previewIndex = currentPhotoIndexRef.current;
241
+ setPreviewPhotoIndex(previewIndex);
226
242
  const normalizedDoc = normalizeCapturedDocument(document);
227
243
  if (captureMode === 'no-grid') {
228
244
  console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
@@ -377,16 +393,69 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
377
393
  // 회전 각도 정규화 (0, 90, 180, 270)
378
394
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
379
395
  console.log('[FullDocScanner] Confirm - rotation degrees:', rotationDegrees, 'normalized:', rotationNormalized);
380
- // 원본 이미지와 회전 각도를 함께 반환
381
- // tdb 앱에서 expo-image-manipulator를 사용해서 회전 처리
382
- onResult({
396
+ // 현재 사진을 capturedPhotos에 추가
397
+ const currentPhoto = {
383
398
  path: croppedImageData.path,
384
399
  base64: croppedImageData.base64,
385
400
  rotationDegrees: rotationNormalized,
386
- });
387
- }, [croppedImageData, rotationDegrees, onResult]);
401
+ };
402
+ const updatedPhotos = [...capturedPhotos, currentPhoto];
403
+ console.log('[FullDocScanner] Photos captured:', updatedPhotos.length, 'of', maxPhotos);
404
+ // Business 모드이고 아직 첫 번째 사진만 찍은 경우
405
+ if (isBusinessMode && updatedPhotos.length === 1) {
406
+ // 두 번째 사진 촬영 여부를 물어봄 (UI에서 버튼으로 표시)
407
+ setCapturedPhotos(updatedPhotos);
408
+ setCurrentPhotoIndex(1);
409
+ // 확인 화면을 유지하고 "뒷면 촬영" 버튼을 표시
410
+ return;
411
+ }
412
+ // 모든 사진 촬영 완료 - 결과 반환
413
+ console.log('[FullDocScanner] All photos captured, returning results');
414
+ onResult(updatedPhotos);
415
+ }, [croppedImageData, rotationDegrees, capturedPhotos, isBusinessMode, maxPhotos, onResult]);
416
+ const handleCaptureSecondPhoto = (0, react_1.useCallback)(() => {
417
+ console.log('[FullDocScanner] Capturing second photo');
418
+ // 확인 화면을 닫고 카메라로 돌아감
419
+ setCroppedImageData(null);
420
+ setRotationDegrees(0);
421
+ setProcessing(false);
422
+ setRectangleDetected(false);
423
+ setRectangleHint(false);
424
+ captureModeRef.current = null;
425
+ captureInProgressRef.current = false;
426
+ if (rectangleCaptureTimeoutRef.current) {
427
+ clearTimeout(rectangleCaptureTimeoutRef.current);
428
+ rectangleCaptureTimeoutRef.current = null;
429
+ }
430
+ if (rectangleHintTimeoutRef.current) {
431
+ clearTimeout(rectangleHintTimeoutRef.current);
432
+ rectangleHintTimeoutRef.current = null;
433
+ }
434
+ if (docScannerRef.current?.reset) {
435
+ docScannerRef.current.reset();
436
+ }
437
+ }, []);
438
+ const handleSkipSecondPhoto = (0, react_1.useCallback)(() => {
439
+ console.log('[FullDocScanner] Skipping second photo');
440
+ // 첫 번째 사진만 반환
441
+ onResult(capturedPhotos);
442
+ }, [capturedPhotos, onResult]);
388
443
  const handleRetake = (0, react_1.useCallback)(() => {
389
444
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
445
+ if (isBusinessMode) {
446
+ if (currentPhotoIndex === 1 && previewPhotoIndex === 0 && capturedPhotos.length === 1) {
447
+ console.log('[FullDocScanner] Retake detected on front preview after confirmation - reverting to front capture');
448
+ setCapturedPhotos([]);
449
+ setCurrentPhotoIndex(0);
450
+ setPreviewPhotoIndex(0);
451
+ }
452
+ else if (previewPhotoIndex === 1 && capturedPhotos.length === 2) {
453
+ console.log('[FullDocScanner] Retake detected on back preview after final confirmation - removing back photo');
454
+ setCapturedPhotos(capturedPhotos.slice(0, 1));
455
+ setCurrentPhotoIndex(1);
456
+ setPreviewPhotoIndex(1);
457
+ }
458
+ }
390
459
  setCroppedImageData(null);
391
460
  setRotationDegrees(0);
392
461
  setProcessing(false);
@@ -406,7 +475,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
406
475
  if (docScannerRef.current?.reset) {
407
476
  docScannerRef.current.reset();
408
477
  }
409
- }, []);
478
+ }, [capturedPhotos, currentPhotoIndex, isBusinessMode, previewPhotoIndex]);
410
479
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
411
480
  const stableCounter = event.stableCounter ?? 0;
412
481
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
@@ -459,6 +528,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
459
528
  croppedImageData ? (
460
529
  // check_DP: Show confirmation screen
461
530
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
531
+ isBusinessMode && (react_1.default.createElement(react_native_1.View, { style: styles.photoHeader },
532
+ react_1.default.createElement(react_native_1.Text, { style: styles.photoHeaderText }, previewPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second))),
462
533
  isImageRotationSupported() ? (react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
463
534
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(-90), accessibilityLabel: "\uC67C\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
464
535
  react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
@@ -470,11 +541,23 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
470
541
  styles.previewImage,
471
542
  { transform: [{ rotate: `${rotationDegrees}deg` }] }
472
543
  ], resizeMode: "contain" }),
544
+ isBusinessMode &&
545
+ capturedPhotos.length === 1 &&
546
+ currentPhotoIndex === 1 &&
547
+ previewPhotoIndex === 0 &&
548
+ mergedStrings.secondPrompt ? (react_1.default.createElement(react_native_1.Text, { style: styles.confirmationPromptText }, mergedStrings.secondPrompt)) : null,
473
549
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
474
550
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
475
551
  react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
476
- react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleConfirm, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
477
- react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm))))) : (react_1.default.createElement(react_native_1.View, { style: styles.flex },
552
+ isBusinessMode &&
553
+ capturedPhotos.length === 1 &&
554
+ currentPhotoIndex === 1 &&
555
+ previewPhotoIndex === 0 ? (react_1.default.createElement(react_1.default.Fragment, null,
556
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleCaptureSecondPhoto, accessibilityLabel: mergedStrings.secondBtn, accessibilityRole: "button" },
557
+ react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.secondBtn)),
558
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.skipButton], onPress: handleSkipSecondPhoto, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
559
+ react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm)))) : (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleConfirm, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
560
+ react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm)))))) : (react_1.default.createElement(react_native_1.View, { style: styles.flex },
478
561
  react_1.default.createElement(DocScanner_1.DocScanner, { ref: docScannerRef, autoCapture: false, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture, onRectangleDetect: handleRectangleDetect, showManualCaptureButton: false, enableTorch: flashEnabled },
479
562
  react_1.default.createElement(react_native_1.View, { style: styles.overlayTop, pointerEvents: "box-none" },
480
563
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
@@ -484,6 +567,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
484
567
  ], onPress: handleFlashToggle, disabled: processing, accessibilityLabel: "Toggle flash", accessibilityRole: "button" },
485
568
  react_1.default.createElement(react_native_1.View, { style: styles.iconContainer },
486
569
  react_1.default.createElement(react_native_1.Text, { style: styles.iconText }, "\u26A1\uFE0F"))),
570
+ isBusinessMode && (react_1.default.createElement(react_native_1.View, { style: styles.cameraHeaderContainer },
571
+ react_1.default.createElement(react_native_1.Text, { style: styles.cameraHeaderText }, currentPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second))),
487
572
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.iconButton, onPress: handleClose, accessibilityLabel: mergedStrings.cancel, accessibilityRole: "button" },
488
573
  react_1.default.createElement(react_native_1.View, { style: styles.iconContainer },
489
574
  react_1.default.createElement(react_native_1.Text, { style: styles.closeIconText }, "\u00D7")))),
@@ -664,6 +749,14 @@ const styles = react_native_1.StyleSheet.create({
664
749
  width: '100%',
665
750
  height: '80%',
666
751
  },
752
+ confirmationPromptText: {
753
+ color: '#fff',
754
+ fontSize: 18,
755
+ fontWeight: '600',
756
+ textAlign: 'center',
757
+ paddingHorizontal: 32,
758
+ marginTop: 24,
759
+ },
667
760
  confirmationButtons: {
668
761
  flexDirection: 'row',
669
762
  justifyContent: 'center',
@@ -687,9 +780,44 @@ const styles = react_native_1.StyleSheet.create({
687
780
  confirmButtonPrimary: {
688
781
  backgroundColor: '#3170f3',
689
782
  },
783
+ skipButton: {
784
+ backgroundColor: 'rgba(100,100,100,0.8)',
785
+ },
690
786
  confirmButtonText: {
691
787
  color: '#fff',
692
788
  fontSize: 18,
693
789
  fontWeight: '600',
694
790
  },
791
+ photoHeader: {
792
+ position: 'absolute',
793
+ top: 20,
794
+ left: 0,
795
+ right: 0,
796
+ alignItems: 'center',
797
+ zIndex: 5,
798
+ },
799
+ photoHeaderText: {
800
+ color: '#fff',
801
+ fontSize: 20,
802
+ fontWeight: 'bold',
803
+ backgroundColor: 'rgba(0,0,0,0.6)',
804
+ paddingHorizontal: 24,
805
+ paddingVertical: 8,
806
+ borderRadius: 20,
807
+ },
808
+ cameraHeaderContainer: {
809
+ position: 'absolute',
810
+ left: 0,
811
+ right: 0,
812
+ alignItems: 'center',
813
+ },
814
+ cameraHeaderText: {
815
+ color: '#fff',
816
+ fontSize: 18,
817
+ fontWeight: 'bold',
818
+ backgroundColor: 'rgba(0,0,0,0.7)',
819
+ paddingHorizontal: 20,
820
+ paddingVertical: 6,
821
+ borderRadius: 16,
822
+ },
695
823
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.112.0",
3
+ "version": "3.115.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -135,10 +135,14 @@ export interface FullDocScannerStrings {
135
135
  retake?: string;
136
136
  confirm?: string;
137
137
  cropTitle?: string;
138
+ first?: string;
139
+ second?: string;
140
+ secondBtn?: string;
141
+ secondPrompt?: string;
138
142
  }
139
143
 
140
144
  export interface FullDocScannerProps {
141
- onResult: (result: FullDocScannerResult) => void;
145
+ onResult: (results: FullDocScannerResult[]) => void;
142
146
  onClose?: () => void;
143
147
  detectionConfig?: DetectionConfig;
144
148
  overlayColor?: string;
@@ -151,6 +155,7 @@ export interface FullDocScannerProps {
151
155
  enableGallery?: boolean;
152
156
  cropWidth?: number;
153
157
  cropHeight?: number;
158
+ type?: 'business';
154
159
  }
155
160
 
156
161
  export const FullDocScanner: React.FC<FullDocScannerProps> = ({
@@ -167,6 +172,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
167
172
  enableGallery = true,
168
173
  cropWidth = 1200,
169
174
  cropHeight = 1600,
175
+ type,
170
176
  }) => {
171
177
  const [processing, setProcessing] = useState(false);
172
178
  const [croppedImageData, setCroppedImageData] = useState<{path: string; base64?: string} | null>(null);
@@ -175,12 +181,23 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
175
181
  const [rectangleHint, setRectangleHint] = useState(false);
176
182
  const [flashEnabled, setFlashEnabled] = useState(false);
177
183
  const [rotationDegrees, setRotationDegrees] = useState(0);
184
+ const [capturedPhotos, setCapturedPhotos] = useState<FullDocScannerResult[]>([]);
185
+ const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
186
+ const [previewPhotoIndex, setPreviewPhotoIndex] = useState(0);
178
187
  const resolvedGridColor = gridColor ?? overlayColor;
179
188
  const docScannerRef = useRef<DocScannerHandle | null>(null);
180
189
  const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
181
190
  const captureInProgressRef = useRef(false);
182
191
  const rectangleCaptureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
183
192
  const rectangleHintTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
193
+ const currentPhotoIndexRef = useRef(currentPhotoIndex);
194
+
195
+ const isBusinessMode = type === 'business';
196
+ const maxPhotos = isBusinessMode ? 2 : 1;
197
+
198
+ useEffect(() => {
199
+ currentPhotoIndexRef.current = currentPhotoIndex;
200
+ }, [currentPhotoIndex]);
184
201
 
185
202
  const mergedStrings = useMemo(
186
203
  () => ({
@@ -192,6 +209,10 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
192
209
  retake: strings?.retake ?? 'Retake',
193
210
  confirm: strings?.confirm ?? 'Confirm',
194
211
  cropTitle: strings?.cropTitle ?? 'Crop Document',
212
+ first: strings?.first ?? 'Front',
213
+ second: strings?.second ?? 'Back',
214
+ secondBtn: strings?.secondBtn ?? 'Capture Back Side?',
215
+ secondPrompt: strings?.secondPrompt ?? strings?.secondBtn ?? 'Capture Back Side?',
195
216
  }),
196
217
  [strings],
197
218
  );
@@ -248,6 +269,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
248
269
 
249
270
  setProcessing(false);
250
271
 
272
+ setPreviewPhotoIndex(currentPhotoIndexRef.current);
273
+
251
274
  // Show confirmation screen
252
275
  setCroppedImageData({
253
276
  path: croppedImage.path,
@@ -310,6 +333,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
310
333
  return;
311
334
  }
312
335
 
336
+ const previewIndex = currentPhotoIndexRef.current;
337
+ setPreviewPhotoIndex(previewIndex);
338
+
313
339
  const normalizedDoc = normalizeCapturedDocument(document);
314
340
 
315
341
  if (captureMode === 'no-grid') {
@@ -506,17 +532,76 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
506
532
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
507
533
  console.log('[FullDocScanner] Confirm - rotation degrees:', rotationDegrees, 'normalized:', rotationNormalized);
508
534
 
509
- // 원본 이미지와 회전 각도를 함께 반환
510
- // tdb 앱에서 expo-image-manipulator를 사용해서 회전 처리
511
- onResult({
535
+ // 현재 사진을 capturedPhotos에 추가
536
+ const currentPhoto: FullDocScannerResult = {
512
537
  path: croppedImageData.path,
513
538
  base64: croppedImageData.base64,
514
539
  rotationDegrees: rotationNormalized,
515
- });
516
- }, [croppedImageData, rotationDegrees, onResult]);
540
+ };
541
+
542
+ const updatedPhotos = [...capturedPhotos, currentPhoto];
543
+ console.log('[FullDocScanner] Photos captured:', updatedPhotos.length, 'of', maxPhotos);
544
+
545
+ // Business 모드이고 아직 첫 번째 사진만 찍은 경우
546
+ if (isBusinessMode && updatedPhotos.length === 1) {
547
+ // 두 번째 사진 촬영 여부를 물어봄 (UI에서 버튼으로 표시)
548
+ setCapturedPhotos(updatedPhotos);
549
+ setCurrentPhotoIndex(1);
550
+ // 확인 화면을 유지하고 "뒷면 촬영" 버튼을 표시
551
+ return;
552
+ }
553
+
554
+ // 모든 사진 촬영 완료 - 결과 반환
555
+ console.log('[FullDocScanner] All photos captured, returning results');
556
+ onResult(updatedPhotos);
557
+ }, [croppedImageData, rotationDegrees, capturedPhotos, isBusinessMode, maxPhotos, onResult]);
558
+
559
+ const handleCaptureSecondPhoto = useCallback(() => {
560
+ console.log('[FullDocScanner] Capturing second photo');
561
+ // 확인 화면을 닫고 카메라로 돌아감
562
+ setCroppedImageData(null);
563
+ setRotationDegrees(0);
564
+ setProcessing(false);
565
+ setRectangleDetected(false);
566
+ setRectangleHint(false);
567
+ captureModeRef.current = null;
568
+ captureInProgressRef.current = false;
569
+ if (rectangleCaptureTimeoutRef.current) {
570
+ clearTimeout(rectangleCaptureTimeoutRef.current);
571
+ rectangleCaptureTimeoutRef.current = null;
572
+ }
573
+ if (rectangleHintTimeoutRef.current) {
574
+ clearTimeout(rectangleHintTimeoutRef.current);
575
+ rectangleHintTimeoutRef.current = null;
576
+ }
577
+ if (docScannerRef.current?.reset) {
578
+ docScannerRef.current.reset();
579
+ }
580
+ }, []);
581
+
582
+ const handleSkipSecondPhoto = useCallback(() => {
583
+ console.log('[FullDocScanner] Skipping second photo');
584
+ // 첫 번째 사진만 반환
585
+ onResult(capturedPhotos);
586
+ }, [capturedPhotos, onResult]);
517
587
 
518
588
  const handleRetake = useCallback(() => {
519
589
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
590
+
591
+ if (isBusinessMode) {
592
+ if (currentPhotoIndex === 1 && previewPhotoIndex === 0 && capturedPhotos.length === 1) {
593
+ console.log('[FullDocScanner] Retake detected on front preview after confirmation - reverting to front capture');
594
+ setCapturedPhotos([]);
595
+ setCurrentPhotoIndex(0);
596
+ setPreviewPhotoIndex(0);
597
+ } else if (previewPhotoIndex === 1 && capturedPhotos.length === 2) {
598
+ console.log('[FullDocScanner] Retake detected on back preview after final confirmation - removing back photo');
599
+ setCapturedPhotos(capturedPhotos.slice(0, 1));
600
+ setCurrentPhotoIndex(1);
601
+ setPreviewPhotoIndex(1);
602
+ }
603
+ }
604
+
520
605
  setCroppedImageData(null);
521
606
  setRotationDegrees(0);
522
607
  setProcessing(false);
@@ -536,7 +621,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
536
621
  if (docScannerRef.current?.reset) {
537
622
  docScannerRef.current.reset();
538
623
  }
539
- }, []);
624
+ }, [capturedPhotos, currentPhotoIndex, isBusinessMode, previewPhotoIndex]);
540
625
 
541
626
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
542
627
  const stableCounter = event.stableCounter ?? 0;
@@ -600,6 +685,15 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
600
685
  {croppedImageData ? (
601
686
  // check_DP: Show confirmation screen
602
687
  <View style={styles.confirmationContainer}>
688
+ {/* 헤더 - 앞면/뒷면 표시 */}
689
+ {isBusinessMode && (
690
+ <View style={styles.photoHeader}>
691
+ <Text style={styles.photoHeaderText}>
692
+ {previewPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second}
693
+ </Text>
694
+ </View>
695
+ )}
696
+
603
697
  {/* 회전 버튼들 - 가운데 정렬 */}
604
698
  {isImageRotationSupported() ? (
605
699
  <View style={styles.rotateButtonsCenter}>
@@ -631,6 +725,13 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
631
725
  ]}
632
726
  resizeMode="contain"
633
727
  />
728
+ {isBusinessMode &&
729
+ capturedPhotos.length === 1 &&
730
+ currentPhotoIndex === 1 &&
731
+ previewPhotoIndex === 0 &&
732
+ mergedStrings.secondPrompt ? (
733
+ <Text style={styles.confirmationPromptText}>{mergedStrings.secondPrompt}</Text>
734
+ ) : null}
634
735
  <View style={styles.confirmationButtons}>
635
736
  <TouchableOpacity
636
737
  style={[styles.confirmButton, styles.retakeButton]}
@@ -640,14 +741,40 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
640
741
  >
641
742
  <Text style={styles.confirmButtonText}>{mergedStrings.retake}</Text>
642
743
  </TouchableOpacity>
643
- <TouchableOpacity
644
- style={[styles.confirmButton, styles.confirmButtonPrimary]}
645
- onPress={handleConfirm}
646
- accessibilityLabel={mergedStrings.confirm}
647
- accessibilityRole="button"
648
- >
649
- <Text style={styles.confirmButtonText}>{mergedStrings.confirm}</Text>
650
- </TouchableOpacity>
744
+
745
+ {/* Business 모드이고 첫 번째 사진을 찍은 후: 뒷면 촬영 버튼 또는 확인 버튼 */}
746
+ {isBusinessMode &&
747
+ capturedPhotos.length === 1 &&
748
+ currentPhotoIndex === 1 &&
749
+ previewPhotoIndex === 0 ? (
750
+ <>
751
+ <TouchableOpacity
752
+ style={[styles.confirmButton, styles.confirmButtonPrimary]}
753
+ onPress={handleCaptureSecondPhoto}
754
+ accessibilityLabel={mergedStrings.secondBtn}
755
+ accessibilityRole="button"
756
+ >
757
+ <Text style={styles.confirmButtonText}>{mergedStrings.secondBtn}</Text>
758
+ </TouchableOpacity>
759
+ <TouchableOpacity
760
+ style={[styles.confirmButton, styles.skipButton]}
761
+ onPress={handleSkipSecondPhoto}
762
+ accessibilityLabel={mergedStrings.confirm}
763
+ accessibilityRole="button"
764
+ >
765
+ <Text style={styles.confirmButtonText}>{mergedStrings.confirm}</Text>
766
+ </TouchableOpacity>
767
+ </>
768
+ ) : (
769
+ <TouchableOpacity
770
+ style={[styles.confirmButton, styles.confirmButtonPrimary]}
771
+ onPress={handleConfirm}
772
+ accessibilityLabel={mergedStrings.confirm}
773
+ accessibilityRole="button"
774
+ >
775
+ <Text style={styles.confirmButtonText}>{mergedStrings.confirm}</Text>
776
+ </TouchableOpacity>
777
+ )}
651
778
  </View>
652
779
  </View>
653
780
  ) : (
@@ -667,6 +794,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
667
794
  enableTorch={flashEnabled}
668
795
  >
669
796
  <View style={styles.overlayTop} pointerEvents="box-none">
797
+ {/* 좌측: 플래시 버튼 */}
670
798
  <TouchableOpacity
671
799
  style={[
672
800
  styles.iconButton,
@@ -682,6 +810,17 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
682
810
  <Text style={styles.iconText}>⚡️</Text>
683
811
  </View>
684
812
  </TouchableOpacity>
813
+
814
+ {/* 중앙: 앞면/뒷면 헤더 */}
815
+ {isBusinessMode && (
816
+ <View style={styles.cameraHeaderContainer}>
817
+ <Text style={styles.cameraHeaderText}>
818
+ {currentPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second}
819
+ </Text>
820
+ </View>
821
+ )}
822
+
823
+ {/* 우측: 닫기 버튼 */}
685
824
  <TouchableOpacity
686
825
  style={styles.iconButton}
687
826
  onPress={handleClose}
@@ -907,6 +1046,14 @@ const styles = StyleSheet.create({
907
1046
  width: '100%',
908
1047
  height: '80%',
909
1048
  },
1049
+ confirmationPromptText: {
1050
+ color: '#fff',
1051
+ fontSize: 18,
1052
+ fontWeight: '600',
1053
+ textAlign: 'center',
1054
+ paddingHorizontal: 32,
1055
+ marginTop: 24,
1056
+ },
910
1057
  confirmationButtons: {
911
1058
  flexDirection: 'row',
912
1059
  justifyContent: 'center',
@@ -930,9 +1077,44 @@ const styles = StyleSheet.create({
930
1077
  confirmButtonPrimary: {
931
1078
  backgroundColor: '#3170f3',
932
1079
  },
1080
+ skipButton: {
1081
+ backgroundColor: 'rgba(100,100,100,0.8)',
1082
+ },
933
1083
  confirmButtonText: {
934
1084
  color: '#fff',
935
1085
  fontSize: 18,
936
1086
  fontWeight: '600',
937
1087
  },
1088
+ photoHeader: {
1089
+ position: 'absolute',
1090
+ top: 20,
1091
+ left: 0,
1092
+ right: 0,
1093
+ alignItems: 'center',
1094
+ zIndex: 5,
1095
+ },
1096
+ photoHeaderText: {
1097
+ color: '#fff',
1098
+ fontSize: 20,
1099
+ fontWeight: 'bold',
1100
+ backgroundColor: 'rgba(0,0,0,0.6)',
1101
+ paddingHorizontal: 24,
1102
+ paddingVertical: 8,
1103
+ borderRadius: 20,
1104
+ },
1105
+ cameraHeaderContainer: {
1106
+ position: 'absolute',
1107
+ left: 0,
1108
+ right: 0,
1109
+ alignItems: 'center',
1110
+ },
1111
+ cameraHeaderText: {
1112
+ color: '#fff',
1113
+ fontSize: 18,
1114
+ fontWeight: 'bold',
1115
+ backgroundColor: 'rgba(0,0,0,0.7)',
1116
+ paddingHorizontal: 20,
1117
+ paddingVertical: 6,
1118
+ borderRadius: 16,
1119
+ },
938
1120
  });