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.
- package/dist/FullDocScanner.d.ts +6 -1
- package/dist/FullDocScanner.js +137 -9
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +197 -15
package/dist/FullDocScanner.d.ts
CHANGED
|
@@ -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: (
|
|
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>;
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -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
|
-
|
|
382
|
-
onResult({
|
|
396
|
+
// 현재 사진을 capturedPhotos에 추가
|
|
397
|
+
const currentPhoto = {
|
|
383
398
|
path: croppedImageData.path,
|
|
384
399
|
base64: croppedImageData.base64,
|
|
385
400
|
rotationDegrees: rotationNormalized,
|
|
386
|
-
}
|
|
387
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
package/src/FullDocScanner.tsx
CHANGED
|
@@ -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: (
|
|
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
|
-
|
|
511
|
-
onResult({
|
|
535
|
+
// 현재 사진을 capturedPhotos에 추가
|
|
536
|
+
const currentPhoto: FullDocScannerResult = {
|
|
512
537
|
path: croppedImageData.path,
|
|
513
538
|
base64: croppedImageData.base64,
|
|
514
539
|
rotationDegrees: rotationNormalized,
|
|
515
|
-
}
|
|
516
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
});
|