init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
+83
View File
@@ -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;
+16
View File
@@ -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;
+27
View File
@@ -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;
+97
View File
@@ -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;
+52
View File
@@ -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;
+7
View File
@@ -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';
+20
View File
@@ -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;
+32
View File
@@ -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;
+44
View File
@@ -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;
+56
View File
@@ -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;
+29
View File
@@ -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;
+12
View File
@@ -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;