react-native-rectangle-doc-scanner 3.91.0 → 3.93.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/DocScanner.js +31 -1
- package/dist/FullDocScanner.js +61 -56
- package/package.json +1 -1
- package/src/DocScanner.tsx +33 -1
- package/src/FullDocScanner.tsx +61 -62
package/dist/DocScanner.js
CHANGED
|
@@ -76,11 +76,20 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
76
76
|
const [detectedRectangle, setDetectedRectangle] = (0, react_1.useState)(null);
|
|
77
77
|
const lastRectangleRef = (0, react_1.useRef)(null);
|
|
78
78
|
const captureOriginRef = (0, react_1.useRef)('auto');
|
|
79
|
+
const rectangleClearTimeoutRef = (0, react_1.useRef)(null);
|
|
79
80
|
(0, react_1.useEffect)(() => {
|
|
80
81
|
if (!autoCapture) {
|
|
81
82
|
setIsAutoCapturing(false);
|
|
82
83
|
}
|
|
83
84
|
}, [autoCapture]);
|
|
85
|
+
// Cleanup timeout on unmount
|
|
86
|
+
(0, react_1.useEffect)(() => {
|
|
87
|
+
return () => {
|
|
88
|
+
if (rectangleClearTimeoutRef.current) {
|
|
89
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
84
93
|
const normalizedQuality = (0, react_1.useMemo)(() => {
|
|
85
94
|
if (react_native_1.Platform.OS === 'ios') {
|
|
86
95
|
// iOS expects 0-1
|
|
@@ -281,7 +290,28 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
281
290
|
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
282
291
|
}
|
|
283
292
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
284
|
-
|
|
293
|
+
const hasValidRectangle = isGoodRectangle && rectangleOnScreen;
|
|
294
|
+
// 그리드를 표시할 조건: 좋은 품질의 사각형이 화면에 있을 때만
|
|
295
|
+
if (hasValidRectangle) {
|
|
296
|
+
// 기존 타임아웃 클리어
|
|
297
|
+
if (rectangleClearTimeoutRef.current) {
|
|
298
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
299
|
+
}
|
|
300
|
+
setDetectedRectangle(payload);
|
|
301
|
+
// 500ms 후에 그리드 자동 클리어 (새로운 이벤트가 없으면)
|
|
302
|
+
rectangleClearTimeoutRef.current = setTimeout(() => {
|
|
303
|
+
setDetectedRectangle(null);
|
|
304
|
+
rectangleClearTimeoutRef.current = null;
|
|
305
|
+
}, 500);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// 즉시 클리어
|
|
309
|
+
if (rectangleClearTimeoutRef.current) {
|
|
310
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
311
|
+
rectangleClearTimeoutRef.current = null;
|
|
312
|
+
}
|
|
313
|
+
setDetectedRectangle(null);
|
|
314
|
+
}
|
|
285
315
|
onRectangleDetect?.(payload);
|
|
286
316
|
}, [autoCapture, minStableFrames, onRectangleDetect]);
|
|
287
317
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -110,7 +110,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
110
110
|
const [rectangleDetected, setRectangleDetected] = (0, react_1.useState)(false);
|
|
111
111
|
const [rectangleHint, setRectangleHint] = (0, react_1.useState)(false);
|
|
112
112
|
const [flashEnabled, setFlashEnabled] = (0, react_1.useState)(false);
|
|
113
|
-
const [
|
|
113
|
+
const [rotationDegrees, setRotationDegrees] = (0, react_1.useState)(0);
|
|
114
114
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
115
115
|
const docScannerRef = (0, react_1.useRef)(null);
|
|
116
116
|
const captureModeRef = (0, react_1.useRef)(null);
|
|
@@ -329,54 +329,57 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
329
329
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
330
330
|
setFlashEnabled(prev => !prev);
|
|
331
331
|
}, []);
|
|
332
|
-
const handleRotateImage = (0, react_1.useCallback)(
|
|
333
|
-
if (
|
|
332
|
+
const handleRotateImage = (0, react_1.useCallback)((degrees) => {
|
|
333
|
+
if (!croppedImageData)
|
|
334
334
|
return;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
335
|
+
setRotationDegrees(prev => {
|
|
336
|
+
const newRotation = (prev + degrees + 360) % 360;
|
|
337
|
+
return newRotation;
|
|
338
|
+
});
|
|
339
|
+
}, [croppedImageData]);
|
|
340
|
+
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
341
|
+
if (!croppedImageData)
|
|
342
|
+
return;
|
|
343
|
+
// 회전이 필요한 경우 실제로 이미지를 회전
|
|
344
|
+
if (rotationDegrees !== 0) {
|
|
345
|
+
try {
|
|
346
|
+
const rotatedImage = await react_native_image_crop_picker_1.default.openCropper({
|
|
347
|
+
path: croppedImageData.path,
|
|
348
|
+
mediaType: 'photo',
|
|
349
|
+
cropping: true,
|
|
350
|
+
freeStyleCropEnabled: true,
|
|
351
|
+
includeBase64: true,
|
|
352
|
+
compressImageQuality: 0.9,
|
|
353
|
+
cropperToolbarTitle: ' ',
|
|
354
|
+
cropperChooseText: '완료',
|
|
355
|
+
cropperCancelText: '취소',
|
|
356
|
+
cropperRotateButtonsHidden: false,
|
|
357
|
+
});
|
|
358
|
+
onResult({
|
|
359
|
+
path: rotatedImage.path,
|
|
360
|
+
base64: rotatedImage.data ?? undefined,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
console.error('[FullDocScanner] Image rotation error:', error);
|
|
365
|
+
// 에러 발생 시 원본 이미지 전송
|
|
366
|
+
onResult({
|
|
367
|
+
path: croppedImageData.path,
|
|
368
|
+
base64: croppedImageData.base64,
|
|
369
|
+
});
|
|
363
370
|
}
|
|
364
371
|
}
|
|
365
|
-
|
|
366
|
-
setIsRotating(false);
|
|
367
|
-
}
|
|
368
|
-
}, [isRotating, croppedImageData, emitError]);
|
|
369
|
-
const handleConfirm = (0, react_1.useCallback)(() => {
|
|
370
|
-
if (croppedImageData) {
|
|
372
|
+
else {
|
|
371
373
|
onResult({
|
|
372
374
|
path: croppedImageData.path,
|
|
373
375
|
base64: croppedImageData.base64,
|
|
374
376
|
});
|
|
375
377
|
}
|
|
376
|
-
}, [croppedImageData, onResult]);
|
|
378
|
+
}, [croppedImageData, rotationDegrees, onResult]);
|
|
377
379
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
378
380
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
379
381
|
setCroppedImageData(null);
|
|
382
|
+
setRotationDegrees(0);
|
|
380
383
|
setProcessing(false);
|
|
381
384
|
setRectangleDetected(false);
|
|
382
385
|
setRectangleHint(false);
|
|
@@ -422,30 +425,18 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
422
425
|
}
|
|
423
426
|
if (captureReady) {
|
|
424
427
|
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
425
|
-
console.log('[FullDocScanner] Rectangle timeout - clearing detection');
|
|
426
428
|
setRectangleDetected(false);
|
|
427
429
|
});
|
|
428
430
|
setRectangleDetected(true);
|
|
429
431
|
}
|
|
430
|
-
else
|
|
432
|
+
else {
|
|
433
|
+
// 그리드가 없거나 품질이 좋지 않으면 즉시 상태 해제
|
|
431
434
|
if (rectangleCaptureTimeoutRef.current) {
|
|
432
435
|
clearTimeout(rectangleCaptureTimeoutRef.current);
|
|
433
436
|
rectangleCaptureTimeoutRef.current = null;
|
|
434
437
|
}
|
|
435
438
|
setRectangleDetected(false);
|
|
436
439
|
}
|
|
437
|
-
else if (rectangleDetected) {
|
|
438
|
-
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
439
|
-
console.log('[FullDocScanner] Rectangle timeout - clearing detection');
|
|
440
|
-
setRectangleDetected(false);
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
console.log('[FullDocScanner] Rectangle detection update', {
|
|
444
|
-
lastDetectionType: event.lastDetectionType,
|
|
445
|
-
stableCounter,
|
|
446
|
-
hasRectangle,
|
|
447
|
-
captureReady,
|
|
448
|
-
});
|
|
449
440
|
}, [rectangleDetected]);
|
|
450
441
|
(0, react_1.useEffect)(() => () => {
|
|
451
442
|
if (rectangleCaptureTimeoutRef.current) {
|
|
@@ -459,14 +450,17 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
459
450
|
croppedImageData ? (
|
|
460
451
|
// check_DP: Show confirmation screen
|
|
461
452
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
|
|
462
|
-
react_1.default.createElement(react_native_1.View, { style: styles.
|
|
463
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style:
|
|
453
|
+
react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
|
|
454
|
+
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
455
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
|
|
465
456
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
|
|
466
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style:
|
|
457
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(90), accessibilityLabel: "\uC624\uB978\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
|
|
467
458
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
|
|
468
459
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
|
|
469
|
-
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style:
|
|
460
|
+
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: [
|
|
461
|
+
styles.previewImage,
|
|
462
|
+
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|
|
463
|
+
], resizeMode: "contain" }),
|
|
470
464
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
|
|
471
465
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
|
|
472
466
|
react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
|
|
@@ -625,6 +619,17 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
625
619
|
gap: 12,
|
|
626
620
|
zIndex: 10,
|
|
627
621
|
},
|
|
622
|
+
rotateButtonsCenter: {
|
|
623
|
+
position: 'absolute',
|
|
624
|
+
top: 60,
|
|
625
|
+
left: 0,
|
|
626
|
+
right: 0,
|
|
627
|
+
flexDirection: 'row',
|
|
628
|
+
justifyContent: 'center',
|
|
629
|
+
alignItems: 'center',
|
|
630
|
+
gap: 12,
|
|
631
|
+
zIndex: 10,
|
|
632
|
+
},
|
|
628
633
|
rotateButtonTop: {
|
|
629
634
|
flexDirection: 'row',
|
|
630
635
|
alignItems: 'center',
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -142,6 +142,7 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
142
142
|
const [detectedRectangle, setDetectedRectangle] = useState<RectangleDetectEvent | null>(null);
|
|
143
143
|
const lastRectangleRef = useRef<Rectangle | null>(null);
|
|
144
144
|
const captureOriginRef = useRef<'auto' | 'manual'>('auto');
|
|
145
|
+
const rectangleClearTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
145
146
|
|
|
146
147
|
useEffect(() => {
|
|
147
148
|
if (!autoCapture) {
|
|
@@ -149,6 +150,15 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
149
150
|
}
|
|
150
151
|
}, [autoCapture]);
|
|
151
152
|
|
|
153
|
+
// Cleanup timeout on unmount
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
return () => {
|
|
156
|
+
if (rectangleClearTimeoutRef.current) {
|
|
157
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
152
162
|
const normalizedQuality = useMemo(() => {
|
|
153
163
|
if (Platform.OS === 'ios') {
|
|
154
164
|
// iOS expects 0-1
|
|
@@ -385,7 +395,29 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
388
|
-
|
|
398
|
+
const hasValidRectangle = isGoodRectangle && rectangleOnScreen;
|
|
399
|
+
|
|
400
|
+
// 그리드를 표시할 조건: 좋은 품질의 사각형이 화면에 있을 때만
|
|
401
|
+
if (hasValidRectangle) {
|
|
402
|
+
// 기존 타임아웃 클리어
|
|
403
|
+
if (rectangleClearTimeoutRef.current) {
|
|
404
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
405
|
+
}
|
|
406
|
+
setDetectedRectangle(payload);
|
|
407
|
+
// 500ms 후에 그리드 자동 클리어 (새로운 이벤트가 없으면)
|
|
408
|
+
rectangleClearTimeoutRef.current = setTimeout(() => {
|
|
409
|
+
setDetectedRectangle(null);
|
|
410
|
+
rectangleClearTimeoutRef.current = null;
|
|
411
|
+
}, 500);
|
|
412
|
+
} else {
|
|
413
|
+
// 즉시 클리어
|
|
414
|
+
if (rectangleClearTimeoutRef.current) {
|
|
415
|
+
clearTimeout(rectangleClearTimeoutRef.current);
|
|
416
|
+
rectangleClearTimeoutRef.current = null;
|
|
417
|
+
}
|
|
418
|
+
setDetectedRectangle(null);
|
|
419
|
+
}
|
|
420
|
+
|
|
389
421
|
onRectangleDetect?.(payload);
|
|
390
422
|
},
|
|
391
423
|
[autoCapture, minStableFrames, onRectangleDetect],
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -152,7 +152,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
152
152
|
const [rectangleDetected, setRectangleDetected] = useState(false);
|
|
153
153
|
const [rectangleHint, setRectangleHint] = useState(false);
|
|
154
154
|
const [flashEnabled, setFlashEnabled] = useState(false);
|
|
155
|
-
const [
|
|
155
|
+
const [rotationDegrees, setRotationDegrees] = useState(0);
|
|
156
156
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
157
157
|
const docScannerRef = useRef<DocScannerHandle | null>(null);
|
|
158
158
|
const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
|
|
@@ -440,59 +440,58 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
440
440
|
setFlashEnabled(prev => !prev);
|
|
441
441
|
}, []);
|
|
442
442
|
|
|
443
|
-
const handleRotateImage = useCallback(
|
|
444
|
-
if (
|
|
443
|
+
const handleRotateImage = useCallback((degrees: -90 | 90) => {
|
|
444
|
+
if (!croppedImageData) return;
|
|
445
445
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
446
|
+
setRotationDegrees(prev => {
|
|
447
|
+
const newRotation = (prev + degrees + 360) % 360;
|
|
448
|
+
return newRotation;
|
|
449
|
+
});
|
|
450
|
+
}, [croppedImageData]);
|
|
449
451
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
mediaType: 'photo',
|
|
453
|
-
cropping: true,
|
|
454
|
-
freeStyleCropEnabled: true,
|
|
455
|
-
includeBase64: true,
|
|
456
|
-
compressImageQuality: 0.9,
|
|
457
|
-
cropperToolbarTitle: degrees === 90 ? '↻' : '↺',
|
|
458
|
-
cropperChooseText: '완료',
|
|
459
|
-
cropperCancelText: '취소',
|
|
460
|
-
cropperRotateButtonsHidden: false,
|
|
461
|
-
});
|
|
452
|
+
const handleConfirm = useCallback(async () => {
|
|
453
|
+
if (!croppedImageData) return;
|
|
462
454
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
} finally {
|
|
480
|
-
setIsRotating(false);
|
|
481
|
-
}
|
|
482
|
-
}, [isRotating, croppedImageData, emitError]);
|
|
455
|
+
// 회전이 필요한 경우 실제로 이미지를 회전
|
|
456
|
+
if (rotationDegrees !== 0) {
|
|
457
|
+
try {
|
|
458
|
+
const rotatedImage = await ImageCropPicker.openCropper({
|
|
459
|
+
path: croppedImageData.path,
|
|
460
|
+
mediaType: 'photo',
|
|
461
|
+
cropping: true,
|
|
462
|
+
freeStyleCropEnabled: true,
|
|
463
|
+
includeBase64: true,
|
|
464
|
+
compressImageQuality: 0.9,
|
|
465
|
+
cropperToolbarTitle: ' ',
|
|
466
|
+
cropperChooseText: '완료',
|
|
467
|
+
cropperCancelText: '취소',
|
|
468
|
+
cropperRotateButtonsHidden: false,
|
|
469
|
+
});
|
|
483
470
|
|
|
484
|
-
|
|
485
|
-
|
|
471
|
+
onResult({
|
|
472
|
+
path: rotatedImage.path,
|
|
473
|
+
base64: rotatedImage.data ?? undefined,
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error('[FullDocScanner] Image rotation error:', error);
|
|
477
|
+
// 에러 발생 시 원본 이미지 전송
|
|
478
|
+
onResult({
|
|
479
|
+
path: croppedImageData.path,
|
|
480
|
+
base64: croppedImageData.base64,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
486
484
|
onResult({
|
|
487
485
|
path: croppedImageData.path,
|
|
488
486
|
base64: croppedImageData.base64,
|
|
489
487
|
});
|
|
490
488
|
}
|
|
491
|
-
}, [croppedImageData, onResult]);
|
|
489
|
+
}, [croppedImageData, rotationDegrees, onResult]);
|
|
492
490
|
|
|
493
491
|
const handleRetake = useCallback(() => {
|
|
494
492
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
495
493
|
setCroppedImageData(null);
|
|
494
|
+
setRotationDegrees(0);
|
|
496
495
|
setProcessing(false);
|
|
497
496
|
setRectangleDetected(false);
|
|
498
497
|
setRectangleHint(false);
|
|
@@ -544,29 +543,17 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
544
543
|
|
|
545
544
|
if (captureReady) {
|
|
546
545
|
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
547
|
-
console.log('[FullDocScanner] Rectangle timeout - clearing detection');
|
|
548
546
|
setRectangleDetected(false);
|
|
549
547
|
});
|
|
550
548
|
setRectangleDetected(true);
|
|
551
|
-
} else
|
|
549
|
+
} else {
|
|
550
|
+
// 그리드가 없거나 품질이 좋지 않으면 즉시 상태 해제
|
|
552
551
|
if (rectangleCaptureTimeoutRef.current) {
|
|
553
552
|
clearTimeout(rectangleCaptureTimeoutRef.current);
|
|
554
553
|
rectangleCaptureTimeoutRef.current = null;
|
|
555
554
|
}
|
|
556
555
|
setRectangleDetected(false);
|
|
557
|
-
} else if (rectangleDetected) {
|
|
558
|
-
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
559
|
-
console.log('[FullDocScanner] Rectangle timeout - clearing detection');
|
|
560
|
-
setRectangleDetected(false);
|
|
561
|
-
});
|
|
562
556
|
}
|
|
563
|
-
|
|
564
|
-
console.log('[FullDocScanner] Rectangle detection update', {
|
|
565
|
-
lastDetectionType: event.lastDetectionType,
|
|
566
|
-
stableCounter,
|
|
567
|
-
hasRectangle,
|
|
568
|
-
captureReady,
|
|
569
|
-
});
|
|
570
557
|
}, [rectangleDetected]);
|
|
571
558
|
|
|
572
559
|
useEffect(
|
|
@@ -586,12 +573,11 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
586
573
|
{croppedImageData ? (
|
|
587
574
|
// check_DP: Show confirmation screen
|
|
588
575
|
<View style={styles.confirmationContainer}>
|
|
589
|
-
{/*
|
|
590
|
-
<View style={styles.
|
|
576
|
+
{/* 회전 버튼들 - 가운데 정렬 */}
|
|
577
|
+
<View style={styles.rotateButtonsCenter}>
|
|
591
578
|
<TouchableOpacity
|
|
592
|
-
style={
|
|
579
|
+
style={styles.rotateButtonTop}
|
|
593
580
|
onPress={() => handleRotateImage(-90)}
|
|
594
|
-
disabled={isRotating}
|
|
595
581
|
accessibilityLabel="왼쪽으로 90도 회전"
|
|
596
582
|
accessibilityRole="button"
|
|
597
583
|
>
|
|
@@ -599,9 +585,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
599
585
|
<Text style={styles.rotateButtonLabel}>좌로 90°</Text>
|
|
600
586
|
</TouchableOpacity>
|
|
601
587
|
<TouchableOpacity
|
|
602
|
-
style={
|
|
588
|
+
style={styles.rotateButtonTop}
|
|
603
589
|
onPress={() => handleRotateImage(90)}
|
|
604
|
-
disabled={isRotating}
|
|
605
590
|
accessibilityLabel="오른쪽으로 90도 회전"
|
|
606
591
|
accessibilityRole="button"
|
|
607
592
|
>
|
|
@@ -611,7 +596,10 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
611
596
|
</View>
|
|
612
597
|
<Image
|
|
613
598
|
source={{ uri: croppedImageData.path }}
|
|
614
|
-
style={
|
|
599
|
+
style={[
|
|
600
|
+
styles.previewImage,
|
|
601
|
+
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|
|
602
|
+
]}
|
|
615
603
|
resizeMode="contain"
|
|
616
604
|
/>
|
|
617
605
|
<View style={styles.confirmationButtons}>
|
|
@@ -854,6 +842,17 @@ const styles = StyleSheet.create({
|
|
|
854
842
|
gap: 12,
|
|
855
843
|
zIndex: 10,
|
|
856
844
|
},
|
|
845
|
+
rotateButtonsCenter: {
|
|
846
|
+
position: 'absolute',
|
|
847
|
+
top: 60,
|
|
848
|
+
left: 0,
|
|
849
|
+
right: 0,
|
|
850
|
+
flexDirection: 'row',
|
|
851
|
+
justifyContent: 'center',
|
|
852
|
+
alignItems: 'center',
|
|
853
|
+
gap: 12,
|
|
854
|
+
zIndex: 10,
|
|
855
|
+
},
|
|
857
856
|
rotateButtonTop: {
|
|
858
857
|
flexDirection: 'row',
|
|
859
858
|
alignItems: 'center',
|