kintex/mobile/components/tickets/QrPlaceholder.tsx

123 lines
3.6 KiB
TypeScript

/*
* QR 자리표시(placeholder) — 실제 스캔 가능한 QR 생성 라이브러리 미설치(GAP).
* react-native-svg로 시드 문자열 기반 결정론적 모듈 그리드 + 파인더 패턴을 그려
* "QR처럼 보이는" 미리보기를 제공한다. 스캔 불가 — 상시 "샘플" 라벨 노출.
* 실제 QR은 M10 티켓 백엔드 + QR 인코더 도입 시 교체(갭: _workspace/port_mobile_m15.md).
*/
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Svg, { Rect } from 'react-native-svg';
import { colors, radius } from '../../theme';
interface Props {
seed: string;
size?: number;
quiet?: boolean; // 여백(quiet zone) 포함 여부
}
// 결정론적 해시(문자열 → 32bit) — 시드로 모듈 on/off 결정
function hashAt(seed: string, i: number): number {
let h = 2166136261 ^ i;
for (let k = 0; k < seed.length; k++) {
h ^= seed.charCodeAt(k);
h = Math.imul(h, 16777619);
}
return (h >>> 0) % 100;
}
const MODULES = 25; // 25x25 격자
export function QrPlaceholder({ seed, size = 192, quiet = true }: Props) {
const cells = useMemo(() => {
const out: { x: number; y: number }[] = [];
for (let y = 0; y < MODULES; y++) {
for (let x = 0; x < MODULES; x++) {
if (isFinderZone(x, y)) continue; // 파인더 영역은 별도 렌더
if (hashAt(seed, y * MODULES + x) < 48) out.push({ x, y });
}
}
return out;
}, [seed]);
const pad = quiet ? 2 : 0;
const total = MODULES + pad * 2;
const cell = size / total;
return (
<View
accessibilityRole="image"
accessibilityLabel="입장 QR 코드 (샘플)"
style={[styles.wrap, { width: size, height: size }]}
>
<Svg width={size} height={size}>
<Rect x={0} y={0} width={size} height={size} fill={colors.white} />
{cells.map((c, idx) => (
<Rect
key={idx}
x={(c.x + pad) * cell}
y={(c.y + pad) * cell}
width={cell}
height={cell}
fill={colors.neutral900}
/>
))}
{/* 3개 파인더 패턴(좌상·우상·좌하) */}
<Finder x={pad} y={pad} cell={cell} />
<Finder x={pad + MODULES - 7} y={pad} cell={cell} />
<Finder x={pad} y={pad + MODULES - 7} cell={cell} />
</Svg>
<View style={styles.sampleTag}>
<Text style={styles.sampleText}></Text>
</View>
</View>
);
}
function isFinderZone(x: number, y: number): boolean {
const inTL = x < 8 && y < 8;
const inTR = x >= MODULES - 8 && y < 8;
const inBL = x < 8 && y >= MODULES - 8;
return inTL || inTR || inBL;
}
function Finder({ x, y, cell }: { x: number; y: number; cell: number }) {
return (
<>
<Rect x={x * cell} y={y * cell} width={cell * 7} height={cell * 7} fill={colors.neutral900} />
<Rect
x={(x + 1) * cell}
y={(y + 1) * cell}
width={cell * 5}
height={cell * 5}
fill={colors.white}
/>
<Rect
x={(x + 2) * cell}
y={(y + 2) * cell}
width={cell * 3}
height={cell * 3}
fill={colors.neutral900}
/>
</>
);
}
const styles = StyleSheet.create({
wrap: {
borderRadius: radius.sm,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
sampleTag: {
position: 'absolute',
right: 4,
bottom: 4,
backgroundColor: colors.aiAccent,
borderRadius: radius.sm,
paddingHorizontal: 6,
paddingVertical: 2,
},
sampleText: { color: colors.white, fontSize: 10, fontWeight: '700' },
});