init: SR System — Helpdesk & ITAM Portal
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import axios, { type AxiosInstance, AxiosError } from 'axios';
|
||||
import type { ApiResponse } from '@/types';
|
||||
import { setCookie, removeCookie } from '@/lib/cookie';
|
||||
import { setupMocks } from '@/mocks/handler';
|
||||
|
||||
interface TypedAxios extends AxiosInstance {
|
||||
get<T>(url: string, config?: Record<string, unknown>): Promise<T>;
|
||||
post<T>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<T>;
|
||||
put<T>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<T>;
|
||||
delete<T>(url: string, config?: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
|
||||
interface AxiosRequestConfigWithSentinel {
|
||||
sentinel?: boolean;
|
||||
}
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(res) => res.data,
|
||||
async (err: AxiosError<ApiResponse<null>>) => {
|
||||
const status = err.response?.status;
|
||||
const originalRequest = err.config;
|
||||
const configWithSentinel = originalRequest as typeof originalRequest & AxiosRequestConfigWithSentinel;
|
||||
|
||||
// Treat 401/403 as auth errors -> refresh or redirect to login
|
||||
if ((status === 401 || status === 403) && typeof window !== 'undefined' && originalRequest && !configWithSentinel.sentinel) {
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
configWithSentinel.sentinel = true;
|
||||
const res = await axios.post('/api/v1/auth/refresh', { refreshToken });
|
||||
const { accessToken, refreshToken: newRefresh } = res.data.data;
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
if (newRefresh) localStorage.setItem('refreshToken', newRefresh);
|
||||
setCookie('accessToken', accessToken);
|
||||
configWithSentinel.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return instance(configWithSentinel);
|
||||
}
|
||||
} catch {
|
||||
// refresh failed
|
||||
}
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
removeCookie('accessToken');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject({ errorCode: 'UNAUTHORIZED', message: '인증이 만료되었습니다.', status });
|
||||
}
|
||||
|
||||
if ((status === 502 || status === 503) && typeof window !== 'undefined') {
|
||||
import('@/stores').then(({ useParkingStore }) => {
|
||||
useParkingStore.getState().checkParking();
|
||||
});
|
||||
}
|
||||
|
||||
const errorCode = err.response?.data?.errorCode;
|
||||
const message = err.response?.data?.message || err.message;
|
||||
return Promise.reject({ errorCode, message, status });
|
||||
},
|
||||
);
|
||||
|
||||
const apiClient = instance as TypedAxios;
|
||||
|
||||
// Mock — enabled for development (static import ensures synchronous setup)
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
||||
setupMocks(instance);
|
||||
}
|
||||
|
||||
export default apiClient;
|
||||
@@ -0,0 +1,16 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, CommonCode } from '@/types';
|
||||
|
||||
const commonCodeService = {
|
||||
getByGroup(groupCode: string) {
|
||||
return apiClient.get<ApiResponse<CommonCode[]>>('/common-codes', {
|
||||
params: { groupCode },
|
||||
});
|
||||
},
|
||||
|
||||
getAllGroups() {
|
||||
return apiClient.get<ApiResponse<CommonCode[]>>('/common-codes');
|
||||
},
|
||||
};
|
||||
|
||||
export default commonCodeService;
|
||||
@@ -0,0 +1,27 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Page, DispatchHistory } from '@/types';
|
||||
|
||||
const dispatchHistoryService = {
|
||||
getByEmployee: (empId: number) =>
|
||||
apiClient.get<ApiResponse<DispatchHistory[]>>(`/dispatch-histories/employee/${empId}`),
|
||||
|
||||
getActive: () =>
|
||||
apiClient.get<ApiResponse<DispatchHistory[]>>('/dispatch-histories/active'),
|
||||
|
||||
getAll: (params?: Record<string, unknown>) =>
|
||||
apiClient.get<ApiResponse<Page<DispatchHistory>>>('/dispatch-histories', { params }),
|
||||
|
||||
getById: (id: number) =>
|
||||
apiClient.get<ApiResponse<DispatchHistory>>(`/dispatch-histories/${id}`),
|
||||
|
||||
create: (data: Record<string, unknown>) =>
|
||||
apiClient.post<ApiResponse<DispatchHistory>>('/dispatch-histories', data),
|
||||
|
||||
endDispatch: (id: number) =>
|
||||
apiClient.put<ApiResponse<DispatchHistory>>(`/dispatch-histories/${id}/end`),
|
||||
|
||||
deleteDispatch: (id: number) =>
|
||||
apiClient.delete<ApiResponse<null>>(`/dispatch-histories/${id}`),
|
||||
};
|
||||
|
||||
export default dispatchHistoryService;
|
||||
@@ -0,0 +1,97 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Employee, EmployeeSearchResult, Page, DispatchHistory, Project } from '@/types';
|
||||
|
||||
const employeeService = {
|
||||
login(credentials: { loginId: string; password: string }) {
|
||||
return apiClient.post<ApiResponse<{ accessToken: string; refreshToken: string; employee: Employee }>>('/auth/login', credentials);
|
||||
},
|
||||
|
||||
refresh(refreshToken: string) {
|
||||
return apiClient.post<ApiResponse<{ accessToken: string; refreshToken: string; employee: Employee }>>('/auth/refresh', { refreshToken });
|
||||
},
|
||||
|
||||
getAll(params?: { roleCode?: string; empStatus?: string; dispatchStatus?: string; page?: number; size?: number }) {
|
||||
return apiClient.get<ApiResponse<Page<Employee>>>('/employees', { params });
|
||||
},
|
||||
|
||||
getOne(empId: number) {
|
||||
return apiClient.get<ApiResponse<Employee>>(`/employees/${empId}`);
|
||||
},
|
||||
|
||||
getMe() {
|
||||
return apiClient.get<ApiResponse<Employee>>('/employees/me');
|
||||
},
|
||||
|
||||
search(keyword: string) {
|
||||
return apiClient.get<ApiResponse<EmployeeSearchResult[]>>('/employees/search', {
|
||||
params: { keyword },
|
||||
});
|
||||
},
|
||||
|
||||
create(data: Partial<Employee>) {
|
||||
return apiClient.post<ApiResponse<Employee>>('/employees', data);
|
||||
},
|
||||
|
||||
update(empId: number, data: Partial<Employee>) {
|
||||
return apiClient.put<ApiResponse<Employee>>(`/employees/${empId}`, data);
|
||||
},
|
||||
|
||||
delete(empId: number) {
|
||||
return apiClient.delete<ApiResponse<void>>(`/employees/${empId}`);
|
||||
},
|
||||
|
||||
getDispatchHistories(empId: number) {
|
||||
return apiClient.get<ApiResponse<DispatchHistory[]>>(`/employees/${empId}/dispatch-histories`);
|
||||
},
|
||||
|
||||
getPortfolio(empId: number) {
|
||||
return apiClient.get<ApiResponse<PortfolioEntry[]>>(`/employees/${empId}/portfolio`);
|
||||
},
|
||||
|
||||
updateAchievements(empId: number, dispatchId: number, achievements: string) {
|
||||
return apiClient.put<ApiResponse<void>>(`/employees/${empId}/dispatch-histories/${dispatchId}/achievements`, { achievements });
|
||||
},
|
||||
|
||||
createDispatch(data: CreateDispatchRequest) {
|
||||
return apiClient.post<ApiResponse<DispatchHistory>>('/dispatch-histories', data);
|
||||
},
|
||||
|
||||
endDispatch(dispatchId: number) {
|
||||
return apiClient.put<ApiResponse<DispatchHistory>>(`/dispatch-histories/${dispatchId}/end`);
|
||||
},
|
||||
|
||||
getActiveDispatches() {
|
||||
return apiClient.get<ApiResponse<DispatchHistory[]>>('/dispatch-histories/active');
|
||||
},
|
||||
|
||||
changePassword(currentPassword: string, newPassword: string) {
|
||||
return apiClient.put<ApiResponse<void>>('/employees/me/password', { currentPassword, newPassword });
|
||||
},
|
||||
};
|
||||
|
||||
export interface PortfolioEntry {
|
||||
dispatchId: number;
|
||||
projectName: string;
|
||||
roleTitle: string;
|
||||
contributionRate: number;
|
||||
individualTech: string;
|
||||
performedTasks: string;
|
||||
achievements: string | null;
|
||||
startDate: string;
|
||||
endDate: string | null;
|
||||
internalRating: number | null;
|
||||
reports: { targetDate: string; reportType: string; completedTasks: string }[];
|
||||
resolvedTickets: { ticketId: number; title: string; category: string; resolutionNote: string | null }[];
|
||||
}
|
||||
|
||||
export interface CreateDispatchRequest {
|
||||
employeeId: number;
|
||||
projectId: number;
|
||||
roleTitle: string;
|
||||
contributionRate: number;
|
||||
individualTech: string;
|
||||
performedTasks: string;
|
||||
startDate?: string;
|
||||
}
|
||||
|
||||
export default employeeService;
|
||||
@@ -0,0 +1,52 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Page, Equipment, EquipmentAssignment, EquipmentMaintenance } from '@/types';
|
||||
|
||||
const equipmentService = {
|
||||
getAll(params?: { type?: string; status?: string; manufacturer?: string; page?: number; size?: number }) {
|
||||
return apiClient.get<ApiResponse<Page<Equipment>>>('/equipment', { params });
|
||||
},
|
||||
|
||||
getOne(equipId: number) {
|
||||
return apiClient.get<ApiResponse<Equipment>>(`/equipment/${equipId}`);
|
||||
},
|
||||
|
||||
create(data: Partial<Equipment>) {
|
||||
return apiClient.post<ApiResponse<Equipment>>('/equipment', data);
|
||||
},
|
||||
|
||||
update(equipId: number, data: Partial<Equipment>) {
|
||||
return apiClient.put<ApiResponse<Equipment>>(`/equipment/${equipId}`, data);
|
||||
},
|
||||
|
||||
delete(equipId: number) {
|
||||
return apiClient.delete<ApiResponse<void>>(`/equipment/${equipId}`);
|
||||
},
|
||||
|
||||
assign(equipId: number, assigneeId: number, conditionNote: string) {
|
||||
return apiClient.post<ApiResponse<EquipmentAssignment>>(`/equipment/${equipId}/assign`, {
|
||||
assigneeId, conditionNote,
|
||||
});
|
||||
},
|
||||
|
||||
return_(equipId: number, data: { conditionNote?: string }) {
|
||||
return apiClient.put<ApiResponse<EquipmentAssignment>>(`/equipment/${equipId}/return`, data);
|
||||
},
|
||||
|
||||
getAssignments(equipId: number) {
|
||||
return apiClient.get<ApiResponse<EquipmentAssignment[]>>(`/equipment/${equipId}/assignments`);
|
||||
},
|
||||
|
||||
getMaintenances(equipId: number) {
|
||||
return apiClient.get<ApiResponse<EquipmentMaintenance[]>>(`/equipment/${equipId}/maintenances`);
|
||||
},
|
||||
|
||||
addMaintenance(equipId: number, data: Partial<EquipmentMaintenance>) {
|
||||
return apiClient.post<ApiResponse<EquipmentMaintenance>>(`/equipment/${equipId}/maintenance`, data);
|
||||
},
|
||||
|
||||
getSummary() {
|
||||
return apiClient.get<ApiResponse<Record<string, number>>>('/equipment/summary');
|
||||
},
|
||||
};
|
||||
|
||||
export default equipmentService;
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as apiClient } from './apiClient';
|
||||
export { default as ticketService } from './ticketService';
|
||||
export { default as equipmentService } from './equipmentService';
|
||||
export { default as reportService } from './reportService';
|
||||
export { default as employeeService } from './employeeService';
|
||||
export { default as trainingService } from './trainingService';
|
||||
export { default as notificationService } from './notificationService';
|
||||
@@ -0,0 +1,20 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Notification } from '@/types';
|
||||
|
||||
const notificationService = {
|
||||
getUnreadCount() {
|
||||
return apiClient.get<ApiResponse<{ count: number }>>('/notifications/unread');
|
||||
},
|
||||
|
||||
getAll(page = 0, size = 20) {
|
||||
return apiClient.get<ApiResponse<Notification[]>>('/notifications', {
|
||||
params: { page, size },
|
||||
});
|
||||
},
|
||||
|
||||
markAsRead(notificationId: number) {
|
||||
return apiClient.put<ApiResponse<void>>(`/notifications/${notificationId}/read`);
|
||||
},
|
||||
};
|
||||
|
||||
export default notificationService;
|
||||
@@ -0,0 +1,32 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Project } from '@/types';
|
||||
|
||||
const projectService = {
|
||||
getMyProjects(): Promise<ApiResponse<Project[]>> {
|
||||
return apiClient.get('/projects/my');
|
||||
},
|
||||
|
||||
getProject(projectId: number): Promise<ApiResponse<Project>> {
|
||||
return apiClient.get(`/projects/${projectId}`);
|
||||
},
|
||||
|
||||
getAll(status?: string): Promise<ApiResponse<Project[]>> {
|
||||
return apiClient.get('/projects', {
|
||||
params: status ? { status } : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
create(data: Record<string, unknown>): Promise<ApiResponse<Project>> {
|
||||
return apiClient.post('/projects', data);
|
||||
},
|
||||
|
||||
update(projectId: number, data: Record<string, unknown>): Promise<ApiResponse<Project>> {
|
||||
return apiClient.put(`/projects/${projectId}`, data);
|
||||
},
|
||||
|
||||
deleteProject(projectId: number): Promise<ApiResponse<void>> {
|
||||
return apiClient.delete(`/projects/${projectId}`);
|
||||
},
|
||||
};
|
||||
|
||||
export default projectService;
|
||||
@@ -0,0 +1,44 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Page, ReportSummary, WorkReport } from '@/types';
|
||||
|
||||
const reportService = {
|
||||
getAll(params?: { reportType?: string; status?: string; userId?: number; page?: number; size?: number }) {
|
||||
return apiClient.get<ApiResponse<Page<WorkReport>>>('/reports', { params });
|
||||
},
|
||||
|
||||
getOne(reportId: number) {
|
||||
return apiClient.get<ApiResponse<WorkReport>>(`/reports/${reportId}`);
|
||||
},
|
||||
|
||||
create(data: Partial<WorkReport>) {
|
||||
return apiClient.post<ApiResponse<WorkReport>>('/reports', data);
|
||||
},
|
||||
|
||||
updateStatus(reportId: number, status: string) {
|
||||
return apiClient.put<ApiResponse<WorkReport>>(`/reports/${reportId}/status`, null, {
|
||||
params: { status },
|
||||
});
|
||||
},
|
||||
|
||||
review(reportId: number, status: 'APPROVED' | 'REJECTED', reviewComment: string) {
|
||||
return apiClient.put<ApiResponse<WorkReport>>(`/reports/${reportId}/review`, {
|
||||
status, reviewComment,
|
||||
});
|
||||
},
|
||||
|
||||
delete(reportId: number) {
|
||||
return apiClient.delete<ApiResponse<void>>(`/reports/${reportId}`);
|
||||
},
|
||||
|
||||
getSummary() {
|
||||
return apiClient.get<ApiResponse<ReportSummary>>('/reports/summary');
|
||||
},
|
||||
|
||||
getSrAutoComplete(targetDate: string) {
|
||||
return apiClient.get<ApiResponse<string[]>>('/reports/sr-auto-complete', {
|
||||
params: { targetDate },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default reportService;
|
||||
@@ -0,0 +1,56 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, Page, IssueTicket, TicketComment } from '@/types';
|
||||
|
||||
export interface TicketFilterParams {
|
||||
category?: string;
|
||||
priority?: string;
|
||||
status?: string;
|
||||
searchKeyword?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const ticketService = {
|
||||
getTickets(params?: TicketFilterParams) {
|
||||
return apiClient.get<ApiResponse<Page<IssueTicket>>>('/tickets', { params });
|
||||
},
|
||||
|
||||
getTicket(ticketId: number) {
|
||||
return apiClient.get<ApiResponse<IssueTicket>>(`/tickets/${ticketId}`);
|
||||
},
|
||||
|
||||
createTicket(data: Partial<IssueTicket>) {
|
||||
return apiClient.post<ApiResponse<IssueTicket>>('/tickets', data);
|
||||
},
|
||||
|
||||
updateStatus(ticketId: number, status: string, assigneeId?: number) {
|
||||
return apiClient.put<ApiResponse<IssueTicket>>(`/tickets/${ticketId}/status`, null, {
|
||||
params: { status, assigneeId },
|
||||
});
|
||||
},
|
||||
|
||||
resolveTicket(ticketId: number, resolutionNote: string) {
|
||||
return apiClient.put<ApiResponse<IssueTicket>>(`/tickets/${ticketId}/resolve`, { resolutionNote });
|
||||
},
|
||||
|
||||
deleteTicket(ticketId: number) {
|
||||
return apiClient.delete<ApiResponse<void>>(`/tickets/${ticketId}`);
|
||||
},
|
||||
|
||||
getComments(ticketId: number) {
|
||||
return apiClient.get<ApiResponse<TicketComment[]>>(`/tickets/${ticketId}/comments`);
|
||||
},
|
||||
|
||||
addComment(ticketId: number, commentText: string, attachedFileUrl?: string) {
|
||||
return apiClient.post<ApiResponse<TicketComment>>(`/tickets/${ticketId}/comments`, {
|
||||
commentText,
|
||||
attachedFileUrl,
|
||||
});
|
||||
},
|
||||
|
||||
deleteComment(ticketId: number, commentId: number) {
|
||||
return apiClient.delete<ApiResponse<void>>(`/tickets/${ticketId}/comments/${commentId}`);
|
||||
},
|
||||
};
|
||||
|
||||
export default ticketService;
|
||||
@@ -0,0 +1,29 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, TrainingProgress } from '@/types';
|
||||
|
||||
const trainingService = {
|
||||
getMyProgress() {
|
||||
return apiClient.get<ApiResponse<TrainingProgress[]>>('/training/me');
|
||||
},
|
||||
|
||||
getByUser(userId: number) {
|
||||
return apiClient.get<ApiResponse<TrainingProgress[]>>('/training', {
|
||||
params: { userId },
|
||||
});
|
||||
},
|
||||
|
||||
update(progressId: number, data: { progressRate?: number; status?: string }) {
|
||||
return apiClient.put<ApiResponse<TrainingProgress>>(`/training/${progressId}`, data);
|
||||
},
|
||||
|
||||
getSummary() {
|
||||
return apiClient.get<ApiResponse<Array<{
|
||||
courseName: string;
|
||||
totalEnrolled: number;
|
||||
completedCount: number;
|
||||
avgProgress: number;
|
||||
}>>>('/training/summary');
|
||||
},
|
||||
};
|
||||
|
||||
export default trainingService;
|
||||
@@ -0,0 +1,12 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ApiResponse, WakeRequest, WakeResponse } from '@/types';
|
||||
|
||||
const wakeService = {
|
||||
requestWake: (data: WakeRequest) =>
|
||||
apiClient.post<ApiResponse<WakeResponse>>('/wake', data),
|
||||
|
||||
checkStatus: () =>
|
||||
apiClient.get<ApiResponse<{ parked: boolean }>>('/health'),
|
||||
};
|
||||
|
||||
export default wakeService;
|
||||
Reference in New Issue
Block a user