Files
acs/frontend/src/api.ts
unknown f0c30d8005 Initial commit: IT센터 출입자관리시스템 (ACS)
방문자 사전신청·승인, 입·출입 체크인/아웃, QR 배지, 재실현황,
블랙리스트, 대시보드 통계, 방문 리포트(엑셀)까지 7단계 전 기능 구현.

- backend: Spring Boot 3.4.5 / Java 21 (JDK 26 빌드), 세션 인증, JPA, H2/PostgreSQL, POI, ZXing, Flyway
- frontend: React 19 / Vite 6 / TypeScript
- infra: Docker Compose (db·app·web nginx), Flyway V1__init, Python 사용자 시드
- docs: 워크플로우 / 시퀀스 다이어그램(Mermaid) / 이슈·유의사항

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:59:41 +09:00

144 lines
5.0 KiB
TypeScript

import {
AccessAction,
AccessRecord,
ApiResponse,
BlacklistCreate,
BlacklistItem,
ChangePasswordRequest,
CurrentUser,
ExcelImportResult,
InsideVisitor,
LoginRequest,
StatsSummary,
PublicPass,
VisitRequestCreate,
VisitRequestView,
Zone,
} from './types';
const BASE = '/api';
/** Endpoints whose 401 must NOT trigger a redirect (login probe / public pages). */
const NO_REDIRECT_ON_401 = ['/auth/me', '/auth/login', '/auth/logout', '/public/'];
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
...init,
});
// Session expired / not authenticated on a protected call → send to login.
if (
res.status === 401 &&
!NO_REDIRECT_ON_401.some((p) => path.startsWith(p)) &&
typeof window !== 'undefined' &&
!['/login', '/kiosk'].includes(window.location.pathname) &&
!window.location.pathname.startsWith('/pass/')
) {
window.location.assign('/login');
}
let body: ApiResponse<T> | null = null;
try {
body = (await res.json()) as ApiResponse<T>;
} catch {
// non-JSON (shouldn't happen with our envelope)
}
if (!res.ok || !body) {
throw new Error(body?.message || `HTTP ${res.status}`);
}
return body.data as T;
}
function jsonInit(method: string, payload: unknown): RequestInit {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
};
}
// ===== Auth =====
export const login = (req: LoginRequest) =>
request<CurrentUser>('/auth/login', jsonInit('POST', req));
export const logout = () =>
request<string>('/auth/logout', { method: 'POST' });
export const getCurrentUser = () => request<CurrentUser>('/auth/me');
export const changePassword = (req: ChangePasswordRequest) =>
request<string>('/auth/change-password', jsonInit('POST', req));
// ===== Zones =====
export const listZones = () => request<Zone[]>('/zones');
// ===== Visit requests =====
export const listVisitRequests = () =>
request<VisitRequestView[]>('/visit-requests');
export const listPendingRequests = () =>
request<VisitRequestView[]>('/visit-requests/pending');
export const getVisitRequest = (id: number) =>
request<VisitRequestView>(`/visit-requests/${id}`);
export const createVisitRequest = (req: VisitRequestCreate) =>
request<VisitRequestView>('/visit-requests', jsonInit('POST', req));
export const cancelVisitRequest = (id: number) =>
request<VisitRequestView>(`/visit-requests/${id}/cancel`, { method: 'POST' });
export const uploadVisitRequests = (file: File) => {
const form = new FormData();
form.append('file', file);
return request<ExcelImportResult>('/visit-requests/upload', {
method: 'POST',
body: form,
});
};
// ===== Approvals =====
export const approveRequest = (id: number, comment?: string) =>
request<VisitRequestView>(`/approvals/${id}/approve`, jsonInit('POST', { comment }));
export const rejectRequest = (id: number, comment?: string) =>
request<VisitRequestView>(`/approvals/${id}/reject`, jsonInit('POST', { comment }));
// ===== Access console (check-in / out, currently inside) =====
export const searchApprovedForCheckIn = (q: string) =>
request<VisitRequestView[]>(`/access/search?q=${encodeURIComponent(q)}`);
export const checkIn = (payload: { qrToken?: string; visitRequestId?: number; gateId?: string }) =>
request<AccessAction>('/access/check-in', jsonInit('POST', payload));
export const checkOut = (payload: { qrToken?: string; visitRequestId?: number; gateId?: string }) =>
request<AccessAction>('/access/check-out', jsonInit('POST', payload));
export const listInside = () => request<InsideVisitor[]>('/access/inside');
export const listTodayAccess = () => request<AccessRecord[]>('/access/today');
// ===== Passes / badge =====
export const getPass = (id: number) => request<VisitRequestView>(`/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<PublicPass>(`/public/passes/${token}`);
export const publicPassQrUrl = (token: string) => `/api/public/passes/${token}/qr.png`;
export const publicCheckIn = (token: string) =>
request<AccessAction>(`/public/passes/${token}/check-in`, { method: 'POST' });
export const publicCheckOut = (token: string) =>
request<AccessAction>(`/public/passes/${token}/check-out`, { method: 'POST' });
// ===== Stats =====
export const getStatsSummary = () => request<StatsSummary>('/stats/summary');
// ===== Blacklist (ADMIN) =====
export const listBlacklist = () => request<BlacklistItem[]>('/blacklist');
export const addBlacklist = (req: BlacklistCreate) =>
request<BlacklistItem>('/blacklist', jsonInit('POST', req));
export const deleteBlacklist = (id: number) =>
request<string>(`/blacklist/${id}`, { method: 'DELETE' });
// ===== Reports =====
export const reportDownloadUrl = (from: string, to: string) =>
`/api/reports/visits.xlsx?from=${from}&to=${to}`;