Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions api/be1.js

This file was deleted.

8 changes: 0 additions & 8 deletions api/be3.js

This file was deleted.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"lodash": "^4.18.1",
"picomatch@^2.3.1": "^2.3.2",
"picomatch@^4.0.3": "^4.0.4",
"postcss": "^8.5.10",
"yaml": "^2.8.3"
}
},
Expand Down Expand Up @@ -64,7 +65,7 @@
"@tanstack/react-router": "^1.166.3",
"@vanilla-extract/css": "^1.18.0",
"antd": "^6.3.2",
"axios": "1.15.1",
"axios": "1.15.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"zustand": "5.0.12"
Expand Down
19 changes: 10 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/features/scan-url/api/uploadErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('isCaptchaRequiredUploadError', () => {
expect(isCaptchaRequiredUploadError(error)).toBe(false);
});

it('returns false for 429 responses without captcha markers', () => {
it('returns true for plain 429 upload responses', () => {
const error = new ApiError({
data: {
message: 'Too many requests.',
Expand All @@ -49,6 +49,6 @@ describe('isCaptchaRequiredUploadError', () => {
statusCode: 429,
});

expect(isCaptchaRequiredUploadError(error)).toBe(false);
expect(isCaptchaRequiredUploadError(error)).toBe(true);
});
});
23 changes: 1 addition & 22 deletions src/features/scan-url/api/uploadErrors.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
import { isApiError } from '@/shared/api/errors/apiError';
import { pickString } from '@/shared/api/responseAccess/payloadAccess';

function normalizeValue(value: string | null | undefined): string {
return value?.trim().toLowerCase() ?? '';
}

export function isCaptchaRequiredUploadError(error: unknown): boolean {
if (!isApiError(error) || error.statusCode !== 429) {
return false;
}

const errorCode = normalizeValue(
pickString(error.data, ['error_code', 'errorCode', 'code', 'status']),
);
const payloadMessage = pickString(error.data, [
'message',
'detail',
'error',
'error_description',
'description',
]);
const message = normalizeValue([error.message, payloadMessage].filter(Boolean).join(' '));

return errorCode.includes('captcha') || message.includes('captcha') || message.includes('캡차');
return isApiError(error) && error.statusCode === 429;
}
7 changes: 6 additions & 1 deletion src/pages/QRScan/QRScanPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,12 @@ export default function QRScanPage() {
{isCapturing ? '촬영 중...' : 'QR 스캔하기'}
</button>

<button className={styles.secondaryButton} onClick={handleOpenGallery} type="button">
<button
className={styles.secondaryButton}
disabled={isCapturing}
onClick={handleOpenGallery}
type="button"
>
<span aria-hidden className={styles.buttonIcon}>
<UploadGlyphIcon />
</span>
Expand Down
93 changes: 64 additions & 29 deletions src/pages/QRScan/hooks/useQRScanPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export function useQRScanPage(): UseQRScanPageReturn {
const latestPhotoUrlRef = useRef<string | null>(null);
const flashTimerRef = useRef<number | null>(null);
const isMountedRef = useRef(true);
const isScanSubmittingRef = useRef(false);
const [cameraStatus, setCameraStatus] = useState<CameraStatus>('loading');
const [cameraStatusText, setCameraStatusText] = useState('후면 카메라를 연결하는 중입니다.');
const [captureRecord, setCaptureRecord] = useState<CaptureRecord | null>(null);
Expand Down Expand Up @@ -208,6 +209,24 @@ export function useQRScanPage(): UseQRScanPageReturn {
setCaptureRecord(nextRecord);
}, []);

const beginScanSubmission = useCallback(() => {
if (isScanSubmittingRef.current) {
return false;
}

isScanSubmittingRef.current = true;
setIsCapturing(true);
return true;
}, []);

const finishScanSubmission = useCallback(() => {
isScanSubmittingRef.current = false;

if (isMountedRef.current) {
setIsCapturing(false);
}
}, []);

const startCamera = useCallback(async () => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return;
Expand Down Expand Up @@ -425,7 +444,9 @@ export function useQRScanPage(): UseQRScanPageReturn {
return;
}

setIsCapturing(true);
if (!beginScanSubmission()) {
return;
}

try {
const canvas = document.createElement('canvas');
Expand Down Expand Up @@ -471,12 +492,12 @@ export function useQRScanPage(): UseQRScanPageReturn {
console.error('Failed to capture or upload QR scan image.', error);
showApiError(message, error, 'QR 이미지 업로드에 실패했습니다.');
} finally {
if (isMountedRef.current) {
setIsCapturing(false);
}
finishScanSubmission();
}
}, [
beginScanSubmission,
cameraStatus,
finishScanSubmission,
message,
startCamera,
submitScanFile,
Expand All @@ -485,8 +506,12 @@ export function useQRScanPage(): UseQRScanPageReturn {
]);

const handleOpenGallery = useCallback(() => {
if (isCapturing) {
return;
}

fileInputRef.current?.click();
}, []);
}, [isCapturing]);

const handleGalleryFileChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
Expand All @@ -496,36 +521,46 @@ export function useQRScanPage(): UseQRScanPageReturn {
return;
}

const photoUrl = URL.createObjectURL(selectedFile);
if (!beginScanSubmission()) {
event.target.value = '';
return;
}

updateCaptureRecord({
badgeLabel: 'UPLOAD',
capturedAt: formatCapturedAt(new Date()),
headline: selectedFile.name,
photoUrl,
summary: '갤러리 이미지를 불러와 최근 기록에 반영했습니다.',
});
let uploadStarted = false;

setIsCapturing(true);
try {
const photoUrl = URL.createObjectURL(selectedFile);

updateCaptureRecord({
badgeLabel: 'UPLOAD',
capturedAt: formatCapturedAt(new Date()),
headline: selectedFile.name,
photoUrl,
summary: '갤러리 이미지를 불러와 최근 기록에 반영했습니다.',
});

void submitScanFile({
file: selectedFile,
fileName: selectedFile.name,
onSuccessMessage: '이미지를 업로드하고 분석을 시작했습니다.',
})
.catch((error) => {
console.error('Failed to upload QR scan image from gallery.', error);
showApiError(message, error, '갤러리 이미지 업로드에 실패했습니다.');
void submitScanFile({
file: selectedFile,
fileName: selectedFile.name,
onSuccessMessage: '이미지를 업로드하고 분석을 시작했습니다.',
})
.finally(() => {
if (isMountedRef.current) {
setIsCapturing(false);
}
});
.catch((error) => {
console.error('Failed to upload QR scan image from gallery.', error);
showApiError(message, error, '갤러리 이미지 업로드에 실패했습니다.');
})
.finally(() => {
finishScanSubmission();
});

event.target.value = '';
uploadStarted = true;
event.target.value = '';
} finally {
if (!uploadStarted) {
finishScanSubmission();
}
}
},
[message, submitScanFile, updateCaptureRecord],
[beginScanSubmission, finishScanSubmission, message, submitScanFile, updateCaptureRecord],
);

return {
Expand Down
4 changes: 2 additions & 2 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"rewrites": [
{
"source": "/be1/:path*",
"destination": "/api/be1?path=:path*"
"destination": "/api/be1/:path*"
},
{
"source": "/be3/:path*",
"destination": "/api/be3?path=:path*"
"destination": "/api/be3/:path*"
},
{
"source": "/(.*)",
Expand Down
Loading