Skip to content
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"prettier": "^3.8.1",
"secretlint": "^11.3.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite": "^7.3.2",
"vitest": "^3.2.4"
},
"dependencies": {
Expand Down
34 changes: 17 additions & 17 deletions pnpm-lock.yaml

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

159 changes: 124 additions & 35 deletions src/pages/QRScan/QRScanPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Link } from '@tanstack/react-router';
import { Link, useNavigate } from '@tanstack/react-router';

import { qrIconByTone } from '@/shared/icon/resultIcons';
import AppHeader from '@/shared/ui/app-header';

import type { ScanListStatus } from '@/pages/ScanList/types/scanListPage.types';

import { useQRScanPage } from './hooks/useQRScanPage';
import * as styles from './styles/qrScanPage.css';

Expand Down Expand Up @@ -49,33 +51,79 @@ function ViewGlyphIcon() {
);
}

function HistoryPrevIcon() {
return (
<svg aria-hidden="true" fill="none" viewBox="0 0 20 20">
<path
d="m11.75 4.5-5 5 5 5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}

function HistoryNextIcon() {
return (
<svg aria-hidden="true" fill="none" viewBox="0 0 20 20">
<path
d="m8.25 4.5 5 5-5 5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}

const resultRouteByStatus: Record<
ScanListStatus,
'/result/safe' | '/result/warning' | '/result/critical'
> = {
safe: '/result/safe',
warning: '/result/warning',
critical: '/result/critical',
};

const statusLabelByTone: Record<ScanListStatus, string> = {
safe: '안전',
warning: '주의',
critical: '위험',
};

const statusSummaryByTone: Record<ScanListStatus, string> = {
safe: '안전으로 판정된 스캔 이력입니다.',
warning: '주의가 필요한 스캔 이력입니다.',
critical: '위험으로 분류된 스캔 이력입니다.',
};

export default function QRScanPage() {
const navigate = useNavigate();
const {
cameraStatus,
cameraStatusText,
captureRecord,
canNavigateHistory,
fileInputRef,
handleCapturePhoto,
handleGalleryFileChange,
handleOpenGallery,
handleShowNextHistoryItem,
handleShowPreviousHistoryItem,
isCapturing,
isFlashVisible,
recentScanItem,
videoRef,
} = useQRScanPage();

const recentActivity = captureRecord ?? {
badgeLabel: 'LIVE',
capturedAt:
cameraStatus === 'ready' ? '실시간 프리뷰 활성화됨' : '카메라 연결을 확인해 주세요.',
headline:
cameraStatus === 'ready'
? '후면 카메라 화면이 실시간으로 표시되고 있습니다.'
: '후면 카메라 연결 후 현재 화면을 촬영할 수 있습니다.',
photoUrl: null,
summary:
cameraStatus === 'ready'
? '초록색 버튼을 누르면 현재 후면 카메라 프레임이 촬영됩니다.'
: cameraStatusText,
const handleOpenRecentScanResult = () => {
if (!recentScanItem) {
return;
}

void navigate({ to: resultRouteByStatus[recentScanItem.status] });
};

return (
Expand Down Expand Up @@ -147,7 +195,9 @@ export default function QRScanPage() {

<header className={styles.intro}>
<h1 className={styles.title}>QR 스캔하기</h1>
<p className={styles.description}>안전한 스캔을 위해 QR을 프레임 안에 맞춰 주세요</p>
<p className={styles.description}>
안전한 스캔을 위해 QR 코드를 프레임 안에 맞춰 주세요.
</p>
<p className={`${styles.cameraStatusText} ${styles.cameraStatusTone[cameraStatus]}`}>
{cameraStatusText}
</p>
Expand Down Expand Up @@ -188,7 +238,7 @@ export default function QRScanPage() {
/>

<p className={styles.helperText}>
라이브 카메라 프리뷰가 준비되면 초록색 버튼으로 현재 화면을 바로 촬영할 수 있습니다.
라이브 카메라 미리보기가 준비되면 초록색 버튼으로 현재 화면을 바로 촬영할 수 있습니다.
</p>
</section>

Expand All @@ -197,32 +247,71 @@ export default function QRScanPage() {
<h2 className={styles.sectionTitle} id="recent-scan-title">
최근 스캔 기록
</h2>
<Link className={styles.viewAllLink} to="/scan-history">
<Link className={styles.viewAllLink} to="/scan-list">
전체보기
</Link>
</div>

<article className={styles.historyCard}>
<span className={styles.historyBadge}>
<span aria-hidden className={styles.historyBadgeIcon}>
<ViewGlyphIcon />
<button
aria-label="이전 스캔 이력 보기"
className={`${styles.historyNavButton} ${styles.historyNavButtonLeft}`}
disabled={!canNavigateHistory}
onClick={handleShowPreviousHistoryItem}
type="button"
>
<span aria-hidden className={styles.historyNavIcon}>
<HistoryPrevIcon />
</span>
{recentActivity.badgeLabel}
</span>
</button>

<p className={styles.historyDate}>{recentActivity.capturedAt}</p>
<p className={styles.historyHeadline}>{recentActivity.headline}</p>
<p className={styles.historyStatus}>{recentActivity.summary}</p>

{recentActivity.photoUrl ? (
<div className={styles.historyThumbnail}>
<img
alt="최근 촬영 또는 업로드한 이미지 미리보기"
className={styles.historyThumbnailImage}
src={recentActivity.photoUrl}
/>
{recentScanItem ? (
<button
aria-label={`${statusLabelByTone[recentScanItem.status]} 결과 페이지로 이동`}
className={`${styles.historyOpenButton} ${styles.historyCardInteractive}`}
onClick={handleOpenRecentScanResult}
type="button"
Comment on lines +269 to +273

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

aria-label 때문에 카드 본문 정보가 보조기기에서 사라질 수 있습니다.

Line 270의 aria-label이 버튼 안의 가시 텍스트(스캔 시각, URL, 상태 설명)를 accessible name으로 덮어써서, 스크린리더 사용자는 어떤 기록을 여는지 알기 어렵습니다. 이 버튼은 aria-label을 제거해 본문을 그대로 읽히게 하거나, aria-labelledby/aria-describedby로 “결과 페이지로 이동”만 보조 설명으로 붙이는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/QRScan/QRScanPage.tsx` around lines 269 - 273, The current button
uses aria-label which overrides the visible card text (recentScanItem details)
and hides it from assistive tech; update the button (the element with classNames
styles.historyOpenButton/styles.historyCardInteractive and onClick handler
handleOpenRecentScanResult) by removing the aria-label and either leaving the
visible content as the accessible name or replace aria-label with
aria-labelledby/aria-describedby that references the card text nodes (e.g., IDs
for the scan time/URL/status rendered from recentScanItem and the
statusLabelByTone mapping) so screen readers announce the card content plus a
short auxiliary “결과 페이지로 이동” hint.

>
<div className={styles.historyContent}>
<span
className={`${styles.historyBadge} ${styles.historyBadgeTone[recentScanItem.status]}`}
>
<span aria-hidden className={styles.historyBadgeIcon}>
<ViewGlyphIcon />
</span>
{statusLabelByTone[recentScanItem.status]}
</span>

<p className={styles.historyDate}>{recentScanItem.scannedAt}</p>
<p className={styles.historyHeadline}>{recentScanItem.url}</p>
<p
className={`${styles.historyStatus} ${styles.historyStatusTone[recentScanItem.status]}`}
>
{statusSummaryByTone[recentScanItem.status]}
</p>
</div>
</button>
) : (
<div className={styles.historyContent}>
<p className={styles.historyDate}>스캔 이력이 없습니다.</p>
<p className={styles.historyHeadline}>
최근 스캔 이력이 생기면 이곳에 가장 최신 항목이 표시됩니다.
</p>
<p className={styles.historyStatus}>전체보기에서 스캔 목록을 확인할 수 있습니다.</p>
</div>
) : null}
)}

<button
aria-label="다음 스캔 이력 보기"
className={`${styles.historyNavButton} ${styles.historyNavButtonRight}`}
disabled={!canNavigateHistory}
onClick={handleShowNextHistoryItem}
type="button"
>
<span aria-hidden className={styles.historyNavIcon}>
<HistoryNextIcon />
</span>
</button>
</article>
</section>
</div>
Expand Down
Loading
Loading