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>
This commit is contained in:
143
frontend/src/api.ts
Normal file
143
frontend/src/api.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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}`;
|
||||
Reference in New Issue
Block a user