diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 2712a09..41a2351 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -149,12 +149,12 @@ export const getPass = (id: number) => request(`/passes/${id}` export const passQrUrl = (id: number) => `/api/passes/${id}/qr.png`; // ===== Public pass + self-service kiosk (no login, token-based) ===== -export const getPublicPass = (token: string) => request(`/public/passes/${token}`); -export const publicPassQrUrl = (token: string) => `/api/public/passes/${token}/qr.png`; +export const getPublicPass = (token: string) => request(`/public/passes/${encodeURIComponent(token)}`); +export const publicPassQrUrl = (token: string) => `/api/public/passes/${encodeURIComponent(token)}/qr.png`; export const publicCheckIn = (token: string) => - request(`/public/passes/${token}/check-in`, { method: 'POST' }); + request(`/public/passes/${encodeURIComponent(token)}/check-in`, { method: 'POST' }); export const publicCheckOut = (token: string) => - request(`/public/passes/${token}/check-out`, { method: 'POST' }); + request(`/public/passes/${encodeURIComponent(token)}/check-out`, { method: 'POST' }); // ===== Stats ===== export const getStatsSummary = () => request('/stats/summary'); diff --git a/frontend/src/pages/KioskPage.tsx b/frontend/src/pages/KioskPage.tsx index 0583fb7..0f7dac8 100644 --- a/frontend/src/pages/KioskPage.tsx +++ b/frontend/src/pages/KioskPage.tsx @@ -5,6 +5,27 @@ import { formatVisitRange } from '../status'; import { useQrScanner } from '../useQrScanner'; import { playChime } from '../chime'; +function extractQrToken(raw: string): string { + const text = raw.trim(); + if (!text) return ''; + + try { + const url = new URL(text); + const passMatch = url.pathname.match(/\/pass\/([^/?#]+)/); + if (passMatch?.[1]) return decodeURIComponent(passMatch[1]); + + const apiMatch = url.pathname.match(/\/public\/passes\/([^/?#]+)/); + if (apiMatch?.[1]) return decodeURIComponent(apiMatch[1]); + } catch { + // Not a URL; fall through to path/token parsing. + } + + const pathMatch = text.match(/\/pass\/([^/?#]+)/) ?? text.match(/\/public\/passes\/([^/?#]+)/); + if (pathMatch?.[1]) return decodeURIComponent(pathMatch[1]); + + return text; +} + /** * Public entrance kiosk (no login). The visitor scans their phone QR, the * approved pass is shown, and they tap 입장/퇴장 to self check-in/out. @@ -19,7 +40,7 @@ export const KioskPage: React.FC = () => { const [manual, setManual] = useState(''); const resolveToken = async (raw: string) => { - const t = raw.trim(); + const t = extractQrToken(raw); if (!t || busy || token) return; setError(null); scanner.stop();