react-native-rectangle-doc-scanner 3.105.0 → 3.107.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/README.md +1 -1
- package/SETUP.md +2 -0
- package/dist/FullDocScanner.js +89 -19
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +158 -48
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ Manual capture exposes an imperative `capture()` method via `ref`. Children rend
|
|
|
120
120
|
## Convenience APIs
|
|
121
121
|
|
|
122
122
|
- `CropEditor` – wraps `react-native-perspective-image-cropper` for manual corner adjustment.
|
|
123
|
-
- `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow.
|
|
123
|
+
- `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow. If the host app links either `expo-image-manipulator` or `react-native-image-rotate`, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
|
|
124
124
|
|
|
125
125
|
## License
|
|
126
126
|
|
package/SETUP.md
CHANGED
|
@@ -117,6 +117,8 @@ function MyComponent() {
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
> 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 또는 `react-native-image-rotate` 네이티브 모듈이 연결되어 있을 때만 노출됩니다. 둘 중 하나라도 없으면 버튼이 자동으로 숨겨지고 원본 각도로 결과가 반환됩니다.
|
|
121
|
+
|
|
120
122
|
## API 변경사항
|
|
121
123
|
|
|
122
124
|
### FullDocScannerResult (변경됨)
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -42,9 +42,27 @@ const react_native_1 = require("react-native");
|
|
|
42
42
|
const react_native_image_picker_1 = require("react-native-image-picker");
|
|
43
43
|
const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
|
|
44
44
|
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
45
|
-
const ImageManipulator = __importStar(require("expo-image-manipulator"));
|
|
46
45
|
const DocScanner_1 = require("./DocScanner");
|
|
46
|
+
let ImageManipulator = null;
|
|
47
|
+
let ImageRotate = null;
|
|
48
|
+
try {
|
|
49
|
+
ImageManipulator = require('expo-image-manipulator');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.', error);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const rotateModule = require('react-native-image-rotate');
|
|
56
|
+
ImageRotate = (rotateModule?.default ?? rotateModule);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.warn('[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.', error);
|
|
60
|
+
}
|
|
61
|
+
const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
|
|
62
|
+
const isImageRotateAvailable = !!ImageRotate?.rotateImage;
|
|
63
|
+
const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
|
|
47
64
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
65
|
+
const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
48
66
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
49
67
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
50
68
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -344,9 +362,29 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
344
362
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
345
363
|
setFlashEnabled(prev => !prev);
|
|
346
364
|
}, []);
|
|
365
|
+
const rotateImageWithFallback = (0, react_1.useCallback)((uri, angle) => {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
if (!ImageRotate?.rotateImage) {
|
|
368
|
+
reject(new Error('react-native-image-rotate unavailable'));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
ImageRotate.rotateImage(uri, angle, (rotatedUri) => resolve(rotatedUri), (error) => {
|
|
372
|
+
const message = typeof error === 'string'
|
|
373
|
+
? error
|
|
374
|
+
: error && typeof error === 'object' && 'message' in error
|
|
375
|
+
? String(error.message)
|
|
376
|
+
: 'Unknown rotation error';
|
|
377
|
+
reject(new Error(message));
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}, []);
|
|
347
381
|
const handleRotateImage = (0, react_1.useCallback)((degrees) => {
|
|
382
|
+
if (!isImageRotationSupported) {
|
|
383
|
+
console.warn('[FullDocScanner] Image rotation requested but no rotation module is available.');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
348
386
|
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
349
|
-
setRotationDegrees(prev => {
|
|
387
|
+
setRotationDegrees((prev) => {
|
|
350
388
|
const newRotation = prev + degrees;
|
|
351
389
|
// -360 ~ 360 범위로 정규화
|
|
352
390
|
if (newRotation <= -360)
|
|
@@ -357,9 +395,10 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
357
395
|
});
|
|
358
396
|
}, []);
|
|
359
397
|
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
360
|
-
if (!croppedImageData)
|
|
398
|
+
if (!croppedImageData) {
|
|
361
399
|
return;
|
|
362
|
-
|
|
400
|
+
}
|
|
401
|
+
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
363
402
|
if (rotationDegrees === 0) {
|
|
364
403
|
onResult({
|
|
365
404
|
path: croppedImageData.path,
|
|
@@ -367,31 +406,62 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
367
406
|
});
|
|
368
407
|
return;
|
|
369
408
|
}
|
|
370
|
-
// 회전이 있으면 실제 파일 회전 처리
|
|
371
409
|
try {
|
|
372
410
|
setProcessing(true);
|
|
373
411
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
374
412
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
if (rotationNormalized === 0) {
|
|
414
|
+
onResult({
|
|
415
|
+
path: croppedImageData.path,
|
|
416
|
+
base64: croppedImageData.base64,
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
|
|
421
|
+
// expo-image-manipulator로 이미지 회전
|
|
422
|
+
const result = await ImageManipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
|
|
423
|
+
compress: 0.9,
|
|
424
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
425
|
+
base64: true,
|
|
426
|
+
});
|
|
427
|
+
onResult({
|
|
428
|
+
path: result.uri,
|
|
429
|
+
base64: result.base64,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (isImageRotateAvailable && ImageRotate?.rotateImage) {
|
|
434
|
+
const sourceUri = ensureFileUri(croppedImageData.path);
|
|
435
|
+
const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
|
|
436
|
+
let base64Result = croppedImageData.base64;
|
|
437
|
+
try {
|
|
438
|
+
base64Result = await react_native_fs_1.default.readFile(stripFileUri(rotatedUri), 'base64');
|
|
439
|
+
}
|
|
440
|
+
catch (readError) {
|
|
441
|
+
console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
|
|
442
|
+
base64Result = undefined;
|
|
443
|
+
}
|
|
444
|
+
onResult({
|
|
445
|
+
path: rotatedUri,
|
|
446
|
+
base64: base64Result,
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.warn('[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.');
|
|
383
451
|
onResult({
|
|
384
|
-
path:
|
|
385
|
-
base64:
|
|
452
|
+
path: croppedImageData.path,
|
|
453
|
+
base64: croppedImageData.base64,
|
|
386
454
|
});
|
|
387
455
|
}
|
|
388
456
|
catch (error) {
|
|
389
457
|
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
390
|
-
setProcessing(false);
|
|
391
458
|
const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : '';
|
|
392
459
|
react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
393
460
|
}
|
|
394
|
-
|
|
461
|
+
finally {
|
|
462
|
+
setProcessing(false);
|
|
463
|
+
}
|
|
464
|
+
}, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
|
|
395
465
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
396
466
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
397
467
|
setCroppedImageData(null);
|
|
@@ -466,13 +536,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
466
536
|
croppedImageData ? (
|
|
467
537
|
// check_DP: Show confirmation screen
|
|
468
538
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
|
|
469
|
-
react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
|
|
539
|
+
isImageRotationSupported ? (react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
|
|
470
540
|
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" },
|
|
471
541
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
|
|
472
542
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
|
|
473
543
|
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" },
|
|
474
544
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
|
|
475
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
|
|
545
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0")))) : null,
|
|
476
546
|
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: [
|
|
477
547
|
styles.previewImage,
|
|
478
548
|
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import { launchImageLibrary } from 'react-native-image-picker';
|
|
13
13
|
import ImageCropPicker from 'react-native-image-crop-picker';
|
|
14
14
|
import RNFS from 'react-native-fs';
|
|
15
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
16
15
|
import { DocScanner } from './DocScanner';
|
|
17
16
|
import type { CapturedDocument } from './types';
|
|
18
17
|
import type {
|
|
@@ -22,7 +21,44 @@ import type {
|
|
|
22
21
|
RectangleDetectEvent,
|
|
23
22
|
} from './DocScanner';
|
|
24
23
|
|
|
24
|
+
type ImageManipulatorModule = typeof import('expo-image-manipulator');
|
|
25
|
+
type ImageRotateModule = {
|
|
26
|
+
rotateImage: (
|
|
27
|
+
uri: string,
|
|
28
|
+
angle: number,
|
|
29
|
+
onSuccess: (uri: string) => void,
|
|
30
|
+
onError: (error: unknown) => void,
|
|
31
|
+
) => void;
|
|
32
|
+
} | null;
|
|
33
|
+
|
|
34
|
+
let ImageManipulator: ImageManipulatorModule | null = null;
|
|
35
|
+
let ImageRotate: ImageRotateModule = null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(
|
|
41
|
+
'[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.',
|
|
42
|
+
error,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const rotateModule = require('react-native-image-rotate');
|
|
48
|
+
ImageRotate = (rotateModule?.default ?? rotateModule) as ImageRotateModule;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(
|
|
51
|
+
'[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.',
|
|
52
|
+
error,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
|
|
57
|
+
const isImageRotateAvailable = !!ImageRotate?.rotateImage;
|
|
58
|
+
const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
|
|
59
|
+
|
|
25
60
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
61
|
+
const ensureFileUri = (value: string) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
26
62
|
|
|
27
63
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
28
64
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
@@ -456,21 +492,57 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
456
492
|
setFlashEnabled(prev => !prev);
|
|
457
493
|
}, []);
|
|
458
494
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
495
|
+
const rotateImageWithFallback = useCallback((uri: string, angle: number) => {
|
|
496
|
+
return new Promise<string>((resolve, reject) => {
|
|
497
|
+
if (!ImageRotate?.rotateImage) {
|
|
498
|
+
reject(new Error('react-native-image-rotate unavailable'));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
ImageRotate.rotateImage(
|
|
503
|
+
uri,
|
|
504
|
+
angle,
|
|
505
|
+
(rotatedUri) => resolve(rotatedUri),
|
|
506
|
+
(error) => {
|
|
507
|
+
const message =
|
|
508
|
+
typeof error === 'string'
|
|
509
|
+
? error
|
|
510
|
+
: error && typeof error === 'object' && 'message' in error
|
|
511
|
+
? String((error as any).message)
|
|
512
|
+
: 'Unknown rotation error';
|
|
513
|
+
reject(new Error(message));
|
|
514
|
+
},
|
|
515
|
+
);
|
|
467
516
|
});
|
|
468
517
|
}, []);
|
|
469
518
|
|
|
519
|
+
const handleRotateImage = useCallback(
|
|
520
|
+
(degrees: -90 | 90) => {
|
|
521
|
+
if (!isImageRotationSupported) {
|
|
522
|
+
console.warn(
|
|
523
|
+
'[FullDocScanner] Image rotation requested but no rotation module is available.',
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
529
|
+
setRotationDegrees((prev) => {
|
|
530
|
+
const newRotation = prev + degrees;
|
|
531
|
+
// -360 ~ 360 범위로 정규화
|
|
532
|
+
if (newRotation <= -360) return newRotation + 360;
|
|
533
|
+
if (newRotation >= 360) return newRotation - 360;
|
|
534
|
+
return newRotation;
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
[],
|
|
538
|
+
);
|
|
539
|
+
|
|
470
540
|
const handleConfirm = useCallback(async () => {
|
|
471
|
-
if (!croppedImageData)
|
|
541
|
+
if (!croppedImageData) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
472
544
|
|
|
473
|
-
// 회전이 없으면 기존 데이터 그대로 전달
|
|
545
|
+
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
474
546
|
if (rotationDegrees === 0) {
|
|
475
547
|
onResult({
|
|
476
548
|
path: croppedImageData.path,
|
|
@@ -479,39 +551,75 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
479
551
|
return;
|
|
480
552
|
}
|
|
481
553
|
|
|
482
|
-
// 회전이 있으면 실제 파일 회전 처리
|
|
483
554
|
try {
|
|
484
555
|
setProcessing(true);
|
|
485
556
|
|
|
486
557
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
487
558
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
488
559
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
560
|
+
if (rotationNormalized === 0) {
|
|
561
|
+
onResult({
|
|
562
|
+
path: croppedImageData.path,
|
|
563
|
+
base64: croppedImageData.base64,
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
|
|
569
|
+
// expo-image-manipulator로 이미지 회전
|
|
570
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
571
|
+
croppedImageData.path,
|
|
572
|
+
[{ rotate: rotationNormalized }],
|
|
573
|
+
{
|
|
574
|
+
compress: 0.9,
|
|
575
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
576
|
+
base64: true,
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
onResult({
|
|
581
|
+
path: result.uri,
|
|
582
|
+
base64: result.base64,
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isImageRotateAvailable && ImageRotate?.rotateImage) {
|
|
588
|
+
const sourceUri = ensureFileUri(croppedImageData.path);
|
|
589
|
+
const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
|
|
590
|
+
let base64Result = croppedImageData.base64;
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
base64Result = await RNFS.readFile(stripFileUri(rotatedUri), 'base64');
|
|
594
|
+
} catch (readError) {
|
|
595
|
+
console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
|
|
596
|
+
base64Result = undefined;
|
|
497
597
|
}
|
|
498
|
-
);
|
|
499
598
|
|
|
500
|
-
|
|
599
|
+
onResult({
|
|
600
|
+
path: rotatedUri,
|
|
601
|
+
base64: base64Result,
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
501
605
|
|
|
502
|
-
|
|
606
|
+
console.warn(
|
|
607
|
+
'[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.',
|
|
608
|
+
);
|
|
503
609
|
onResult({
|
|
504
|
-
path:
|
|
505
|
-
base64:
|
|
610
|
+
path: croppedImageData.path,
|
|
611
|
+
base64: croppedImageData.base64,
|
|
506
612
|
});
|
|
507
613
|
} catch (error) {
|
|
508
614
|
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
509
|
-
setProcessing(false);
|
|
510
615
|
|
|
511
|
-
const errorMessage =
|
|
616
|
+
const errorMessage =
|
|
617
|
+
error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
|
|
512
618
|
Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
619
|
+
} finally {
|
|
620
|
+
setProcessing(false);
|
|
513
621
|
}
|
|
514
|
-
}, [croppedImageData, rotationDegrees, onResult]);
|
|
622
|
+
}, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
|
|
515
623
|
|
|
516
624
|
const handleRetake = useCallback(() => {
|
|
517
625
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
@@ -599,26 +707,28 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
599
707
|
// check_DP: Show confirmation screen
|
|
600
708
|
<View style={styles.confirmationContainer}>
|
|
601
709
|
{/* 회전 버튼들 - 가운데 정렬 */}
|
|
602
|
-
|
|
603
|
-
<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
710
|
+
{isImageRotationSupported ? (
|
|
711
|
+
<View style={styles.rotateButtonsCenter}>
|
|
712
|
+
<TouchableOpacity
|
|
713
|
+
style={styles.rotateButtonTop}
|
|
714
|
+
onPress={() => handleRotateImage(-90)}
|
|
715
|
+
accessibilityLabel="왼쪽으로 90도 회전"
|
|
716
|
+
accessibilityRole="button"
|
|
717
|
+
>
|
|
718
|
+
<Text style={styles.rotateIconText}>↺</Text>
|
|
719
|
+
<Text style={styles.rotateButtonLabel}>좌로 90°</Text>
|
|
720
|
+
</TouchableOpacity>
|
|
721
|
+
<TouchableOpacity
|
|
722
|
+
style={styles.rotateButtonTop}
|
|
723
|
+
onPress={() => handleRotateImage(90)}
|
|
724
|
+
accessibilityLabel="오른쪽으로 90도 회전"
|
|
725
|
+
accessibilityRole="button"
|
|
726
|
+
>
|
|
727
|
+
<Text style={styles.rotateIconText}>↻</Text>
|
|
728
|
+
<Text style={styles.rotateButtonLabel}>우로 90°</Text>
|
|
729
|
+
</TouchableOpacity>
|
|
730
|
+
</View>
|
|
731
|
+
) : null}
|
|
622
732
|
<Image
|
|
623
733
|
source={{ uri: croppedImageData.path }}
|
|
624
734
|
style={[
|