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
+5
View File
@@ -0,0 +1,5 @@
export const assignments = [
{ assignmentId: 1, equipmentId: 1, userId: 3, userName: '김현장', assignedDate: '2026-02-01', returnedDate: null, condOnAssign: '미개봉 신품', condOnReturn: null },
{ assignmentId: 2, equipmentId: 3, userId: 4, userName: '박파견', assignedDate: '2026-02-15', returnedDate: null, condOnAssign: '신품', condOnReturn: null },
{ assignmentId: 3, equipmentId: 6, userId: 5, userName: '이전문', assignedDate: '2026-01-15', returnedDate: null, condOnAssign: '미개봉 신품', condOnReturn: null },
];
+7
View File
@@ -0,0 +1,7 @@
export const comments = [
{ commentId: 1, ticketId: 1, userId: 3, userName: '김현장', commentText: '오라클 실행 계획 첨부합니다. Full Scan 발생 구간 확인 부탁드립니다.', attachedFileUrl: '/mock/exec_plan.png', createdAt: '2026-05-20T09:30:00Z' },
{ commentId: 2, ticketId: 1, userId: 5, userName: '이전문 (본사)', commentText: '인덱스 힌트 사용해보세요: /*+ INDEX(emp IDX_EMP_DEPT) */\n통계 정보 갱신도 필요해 보입니다.', attachedFileUrl: null, createdAt: '2026-05-20T10:15:00Z' },
{ commentId: 3, ticketId: 1, userId: 3, userName: '김현장', commentText: '힌트 적용 후 3초→0.5초로 개선되었습니다! ✅ 감사합니다.', attachedFileUrl: null, createdAt: '2026-05-20T11:00:00Z' },
{ commentId: 4, ticketId: 3, userId: 6, userName: '최개발', commentText: '빌드 로그 첨부합니다. Dockerfile 45번째 라인 에러.', attachedFileUrl: '/mock/build_log.txt', createdAt: '2026-05-18T11:30:00Z' },
{ commentId: 5, ticketId: 3, userId: 5, userName: '이전문 (본사)', commentText: 'Node 버전 mismatch 문제로 확인됩니다. .nvmrc 파일 확인해보세요.', attachedFileUrl: null, createdAt: '2026-05-18T14:00:00Z' },
];
+16
View File
@@ -0,0 +1,16 @@
export const commonCodes = [
{ codeId: 1, groupCode: 'EMP_ROLE', codeValue: 'ADMIN', codeName: '최고관리자', sortOrder: 1, isActive: 'Y' },
{ codeId: 2, groupCode: 'EMP_ROLE', codeValue: 'MANAGER', codeName: '본사관리자', sortOrder: 2, isActive: 'Y' },
{ codeId: 3, groupCode: 'EMP_ROLE', codeValue: 'EMPLOYEE', codeName: '파견직원', sortOrder: 3, isActive: 'Y' },
{ codeId: 4, groupCode: 'EMP_STATUS', codeValue: 'ACTIVE', codeName: '재직', sortOrder: 1, isActive: 'Y' },
{ codeId: 5, groupCode: 'EMP_STATUS', codeValue: 'ON_LEAVE', codeName: '휴직', sortOrder: 2, isActive: 'Y' },
{ codeId: 6, groupCode: 'EMP_STATUS', codeValue: 'RESIGNED', codeName: '퇴사', sortOrder: 3, isActive: 'Y' },
{ codeId: 7, groupCode: 'TICKET_CATEGORY', codeValue: 'BUG', codeName: '버그', sortOrder: 1, isActive: 'Y' },
{ codeId: 8, groupCode: 'TICKET_CATEGORY', codeValue: 'FEATURE', codeName: '기능', sortOrder: 2, isActive: 'Y' },
{ codeId: 9, groupCode: 'TICKET_CATEGORY', codeValue: 'SUPPORT', codeName: '지원', sortOrder: 3, isActive: 'Y' },
{ codeId: 10, groupCode: 'TICKET_CATEGORY', codeValue: 'QA', codeName: '품질', sortOrder: 4, isActive: 'Y' },
{ codeId: 11, groupCode: 'EQUIP_TYPE', codeValue: 'LAPTOP', codeName: '노트북', sortOrder: 1, isActive: 'Y' },
{ codeId: 12, groupCode: 'EQUIP_TYPE', codeValue: 'MONITOR', codeName: '모니터', sortOrder: 2, isActive: 'Y' },
{ codeId: 13, groupCode: 'EQUIP_TYPE', codeValue: 'TEST_DEVICE', codeName: '테스트장비', sortOrder: 3, isActive: 'Y' },
{ codeId: 14, groupCode: 'EQUIP_TYPE', codeValue: 'ACCESSORY', codeName: '액세서리', sortOrder: 4, isActive: 'Y' },
];
+4
View File
@@ -0,0 +1,4 @@
export const dispatchHistories = [
{ dispatchId: 1, empId: 3, projectId: 1, startDate: '2025-01-01', endDate: null, roleTitle: 'BACKEND', contributionRate: 100, individualTech: 'Java 17, Spring Boot, Oracle', performedTasks: '차세대 결제 시스템 API 개발, 성능 튜닝', achievements: '결제 처리 시간 40% 개선', clientFeedback: null, internalRating: null },
{ dispatchId: 2, empId: 4, projectId: 2, startDate: '2024-06-01', endDate: null, roleTitle: 'DBA', contributionRate: 100, individualTech: 'Oracle 11g, SQL Tuning, DataGuard', performedTasks: '운영 DB 모니터링, 장애 대응, 쿼리 튜닝', achievements: '월간 장애 건수 60% 감소', clientFeedback: null, internalRating: null },
];
+9
View File
@@ -0,0 +1,9 @@
export const employees = [
{ empId: 1, loginId: 'admin', name: '김관리', email: 'admin@hq.com', roleCode: 'ADMIN' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'WAITING' as const, joinedDate: '2020-01-01', leftDate: null },
{ empId: 2, loginId: 'manager1', name: '이매니저', email: 'manager@hq.com', roleCode: 'MANAGER' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'WAITING' as const, joinedDate: '2021-03-15', leftDate: null },
{ empId: 3, loginId: 'kim', name: '김현장', email: 'kim@client.com', roleCode: 'EMPLOYEE' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'DISPATCHED' as const, joinedDate: '2022-06-01', leftDate: null },
{ empId: 4, loginId: 'park', name: '박파견', email: 'park@client.com', roleCode: 'EMPLOYEE' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'DISPATCHED' as const, joinedDate: '2023-02-10', leftDate: null },
{ empId: 5, loginId: 'lee', name: '이전문', email: 'lee@hq.com', roleCode: 'MANAGER' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'WAITING' as const, joinedDate: '2019-11-20', leftDate: null },
{ empId: 6, loginId: 'choi', name: '최개발', email: 'choi@client.com', roleCode: 'EMPLOYEE' as const, empStatus: 'ACTIVE' as const, dispatchStatus: 'DISPATCHED' as const, joinedDate: '2023-08-05', leftDate: null },
{ empId: 7, loginId: 'jung', name: '정휴직', email: 'jung@hq.com', roleCode: 'EMPLOYEE' as const, empStatus: 'ON_LEAVE' as const, dispatchStatus: 'WAITING' as const, joinedDate: '2021-07-01', leftDate: null },
];
+8
View File
@@ -0,0 +1,8 @@
export const equipment = [
{ equipmentId: 1, equipmentType: 'LAPTOP' as const, manufacturer: 'Apple', modelName: 'MacBook Pro 16 M3', serialNumber: 'F2G8H3J9K1', assetCode: 'HQ-LP-2026-0001', specifications: { cpu: 'M3 Pro', ram: '36GB', ssd: '512GB' }, status: 'IN_USE' as const, purchaseDate: '2026-01-15', warrantyExpDate: '2028-01-15', version: 0 },
{ equipmentId: 2, equipmentType: 'LAPTOP' as const, manufacturer: 'Apple', modelName: 'MacBook Pro 14 M3', serialNumber: 'A1B2C3D4E5', assetCode: 'HQ-LP-2026-0002', specifications: { cpu: 'M3', ram: '18GB', ssd: '512GB' }, status: 'AVAILABLE' as const, purchaseDate: '2026-02-01', warrantyExpDate: '2028-02-01', version: 0 },
{ equipmentId: 3, equipmentType: 'MONITOR' as const, manufacturer: 'LG', modelName: '27UN880 Ergo', serialNumber: 'LG-27U-001', assetCode: 'HQ-MO-2026-0001', specifications: { size: '27"', resolution: '4K UHD', panel: 'IPS' }, status: 'IN_USE' as const, purchaseDate: '2026-01-20', warrantyExpDate: '2028-01-20', version: 0 },
{ equipmentId: 4, equipmentType: 'LAPTOP' as const, manufacturer: 'Samsung', modelName: 'Galaxy Book4 Pro', serialNumber: 'SAMSUNG-001', assetCode: 'HQ-LP-2026-0003', specifications: { cpu: 'Ultra 7', ram: '16GB', ssd: '256GB' }, status: 'REPAIRING' as const, purchaseDate: '2026-03-01', warrantyExpDate: '2028-03-01', version: 0 },
{ equipmentId: 5, equipmentType: 'MONITOR' as const, manufacturer: 'Dell', modelName: 'U2723QE', serialNumber: 'DELL-U27-002', assetCode: 'HQ-MO-2026-0002', specifications: { size: '27"', resolution: '4K', panel: 'IPS Black' }, status: 'AVAILABLE' as const, purchaseDate: '2026-04-01', warrantyExpDate: '2028-04-01', version: 0 },
{ equipmentId: 6, equipmentType: 'ACCESSORY' as const, manufacturer: 'Logitech', modelName: 'MX Master 3S', serialNumber: 'LOG-MX-001', assetCode: 'HQ-AC-2026-0001', specifications: { type: 'Mouse', connection: 'Bluetooth' }, status: 'IN_USE' as const, purchaseDate: '2026-01-10', warrantyExpDate: '2027-01-10', version: 0 },
];
+3
View File
@@ -0,0 +1,3 @@
export const maintenances = [
{ maintenanceId: 1, equipmentId: 4, reportedBy: 3, maintenanceType: 'REPAIR' as const, issueDescription: '키보드 침수 작동 불가', actionTaken: '공식 서비스센터 키보드 상판 교체 완료', cost: 250000, reportedDate: '2026-05-10', resolvedDate: '2026-05-15' },
];
+6
View File
@@ -0,0 +1,6 @@
export const notifications = [
{ notificationId: 1, type: 'TICKET', title: 'SR 티켓 배정됨', message: 'SR-2026-003 CI/CD 배포 실패 건이 이전문 매니저에게 배정되었습니다.', isRead: false, createdAt: '2026-05-21T09:00:00Z' },
{ notificationId: 2, type: 'REPORT', title: '보고서 반려', message: '주간 보고서(WR-2026-05-W1)가 반려되었습니다. 피드백을 확인해주세요.', isRead: false, createdAt: '2026-05-20T14:00:00Z' },
{ notificationId: 3, type: 'TICKET', title: 'SLA 임박 ⚠️', message: 'SR-2026-001 JVM 메모리 릭 건 SLA 데드라인이 4시간 남았습니다.', isRead: false, createdAt: '2026-05-21T07:00:00Z' },
{ notificationId: 4, type: 'EQUIPMENT', title: '장비 수리 완료', message: 'Galaxy Book4 Pro 키보드 수리가 완료되어 창고 입고되었습니다.', isRead: true, createdAt: '2026-05-19T16:00:00Z' },
];
+5
View File
@@ -0,0 +1,5 @@
export const projects = [
{ projectId: 1, projectType: 'SI' as const, projectName: '삼성페이 고도화 (SI)', contractCompany: '삼성전자', endUserCompany: '삼성전자', contractType: 'TURN_KEY' as const, status: 'ONGOING' as const, startDate: '2025-01-01', endDate: '2026-12-31', workLocationType: 'ON_SITE' as const, workAddress: '서울시 서초구', techEnvironment: 'Java 17, Spring Boot, Oracle 19c', mainTaskDesc: '삼성페이 차세대 시스템 구축', internalManagerId: 1 },
{ projectId: 2, projectType: 'SM' as const, projectName: 'LG CNS SM 운영', contractCompany: 'LG CNS', endUserCompany: 'LG CNS', contractType: 'MM' as const, status: 'ONGOING' as const, startDate: '2024-01-01', endDate: '2026-06-30', workLocationType: 'HYBRID' as const, workAddress: '서울시 강남구', techEnvironment: 'Java 11, Spring, Oracle 11g', mainTaskDesc: 'LG CNS 시스템 유지보수', internalManagerId: 2 },
{ projectId: 3, projectType: 'SM' as const, projectName: 'Kakao Maintenance', contractCompany: '카카오', endUserCompany: '카카오', contractType: 'MM' as const, status: 'ONGOING' as const, startDate: '2024-06-01', endDate: '2025-12-31', workLocationType: 'REMOTE' as const, workAddress: '경기도 판교', techEnvironment: 'Kotlin, Spring Cloud, MySQL', mainTaskDesc: '카카오 서비스 운영 유지보수', internalManagerId: 2 },
];
+5
View File
@@ -0,0 +1,5 @@
export const reports = [
{ reportId: 1, dispatchId: 1, userId: 3, userName: '김현장', reportType: 'WEEKLY' as const, targetDate: '2026-05-15', completedTasks: 'SR 3건 해결 완료 (DB튜닝, 배포파이프라인, 모니터링)', plannedTasks: '차주 성능 테스트 진행, 인덱스 리뷰', issuesAndRisks: '납기 일정 협의 필요', supportRequested: null, status: 'APPROVED' as const, reviewerId: 2, reviewerName: '이매니저', reviewComment: '수고하셨습니다.', createdAt: '2026-05-15T10:00:00Z', updatedAt: '2026-05-16T09:00:00Z' },
{ reportId: 2, dispatchId: 1, userId: 3, userName: '김현장', reportType: 'WEEKLY' as const, targetDate: '2026-05-22', completedTasks: '오라클 튜닝 2건, 장애 대응 1건', plannedTasks: 'DBA 정기 점검, 교육 수강', issuesAndRisks: null, supportRequested: null, status: 'SUBMITTED' as const, reviewerId: null, reviewerName: null, reviewComment: null, createdAt: '2026-05-22T10:00:00Z', updatedAt: '2026-05-22T10:00:00Z' },
{ reportId: 3, dispatchId: 2, userId: 4, userName: '박파견', reportType: 'WEEKLY' as const, targetDate: '2026-05-08', completedTasks: 'DB 모니터링, 장애 대응 2건', plannedTasks: '인덱스 튜닝 작업 예정', issuesAndRisks: '장비 SSD 불량 교체 필요', supportRequested: '노트북 SSD 교체 요청', status: 'REJECTED' as const, reviewerId: 2, reviewerName: '이매니저', reviewComment: '완료 실적에 SR 연동 내용 상세 보강 요망', createdAt: '2026-05-08T09:00:00Z', updatedAt: '2026-05-09T14:00:00Z' },
];
+9
View File
@@ -0,0 +1,9 @@
export const tickets = [
{ ticketId: 1, projectId: 1, requesterId: 3, requesterName: '김현장', assigneeId: null, assigneeName: null, category: 'BUG' as const, priority: 'CRITICAL' as const, title: 'JVM 메모리 릭 및 Heap Dump 분석 요청', description: 'Prod 서비스 JVM 메모리 사용량 지속 증가, Full GC 빈도 높음', status: 'OPEN' as const, dueDate: '2026-05-22', resolvedAt: null, resolutionNote: null, slaWarnFlag: 'N' as const, createdAt: '2026-05-20T09:00:00Z', updatedAt: '2026-05-20T09:00:00Z' },
{ ticketId: 2, projectId: 2, requesterId: 4, requesterName: '박파견', assigneeId: null, assigneeName: null, category: 'SUPPORT' as const, priority: 'HIGH' as const, title: '사내망 방화벽 정책 가이드 필요', description: '파견지 사내망 연동을 위한 방화벽 정책 가이드 요청', status: 'OPEN' as const, dueDate: '2026-05-25', resolvedAt: null, resolutionNote: null, slaWarnFlag: 'N' as const, createdAt: '2026-05-19T14:00:00Z', updatedAt: '2026-05-19T14:00:00Z' },
{ ticketId: 3, projectId: 1, requesterId: 6, requesterName: '최개발', assigneeId: 5, assigneeName: '이전문', category: 'BUG' as const, priority: 'CRITICAL' as const, title: 'CI/CD 파이프라인 staging 배포 실패', description: 'Staging 환경 배포 시 Docker build 실패', status: 'ASSIGNED' as const, dueDate: '2026-05-22', resolvedAt: null, resolutionNote: null, slaWarnFlag: 'N' as const, createdAt: '2026-05-18T11:00:00Z', updatedAt: '2026-05-19T09:00:00Z' },
{ ticketId: 4, projectId: 3, requesterId: 4, requesterName: '박파견', assigneeId: 2, assigneeName: '이매니저', category: 'SUPPORT' as const, priority: 'NORMAL' as const, title: 'MacBook Pro 배터리 교체 요청', description: '배터리 성능 78%로 교체 주기 도래', status: 'IN_PROGRESS' as const, dueDate: '2026-05-30', resolvedAt: null, resolutionNote: null, slaWarnFlag: 'N' as const, createdAt: '2026-05-17T10:00:00Z', updatedAt: '2026-05-18T15:00:00Z' },
{ ticketId: 5, projectId: 2, requesterId: 3, requesterName: '김현장', assigneeId: 5, assigneeName: '이전문', category: 'SUPPORT' as const, priority: 'HIGH' as const, title: 'VPN 접속 연장 요청', description: '원격 접속 VPN 인증서 만료 예정', status: 'RESOLVED' as const, dueDate: '2026-05-20', resolvedAt: '2026-05-19T17:00:00Z', resolutionNote: 'VPN 인증서 재발급 완료', slaWarnFlag: 'N' as const, createdAt: '2026-05-16T08:00:00Z', updatedAt: '2026-05-19T17:00:00Z' },
{ ticketId: 6, projectId: 1, requesterId: 6, requesterName: '최개발', assigneeId: 5, assigneeName: '이전문', category: 'BUG' as const, priority: 'HIGH' as const, title: 'DB 커넥션 풀 고갈 문제 분석', description: '특정 시간대에 DB 커넥션 풀이 고갈되는 현상', status: 'CLOSED' as const, dueDate: '2026-05-18', resolvedAt: '2026-05-17T12:00:00Z', resolutionNote: 'Connection leak, 코드 수정 완료', slaWarnFlag: 'N' as const, createdAt: '2026-05-15T09:00:00Z', updatedAt: '2026-05-17T12:00:00Z' },
{ ticketId: 7, projectId: 1, requesterId: 3, requesterName: '김현장', assigneeId: null, assigneeName: null, category: 'QA' as const, priority: 'HIGH' as const, title: '오라클 쿼리 튜닝 필요 (Full Scan)', description: '실행 계획 상 Full Scan 발생, 인덱스 튜닝 필요', status: 'OPEN' as const, dueDate: '2026-05-28', resolvedAt: null, resolutionNote: null, slaWarnFlag: 'N' as const, createdAt: '2026-05-21T08:00:00Z', updatedAt: '2026-05-21T08:00:00Z' },
];
+8
View File
@@ -0,0 +1,8 @@
export const training = [
{ progressId: 1, userId: 3, courseName: 'Next.js Enterprise Architecture', progressRate: 75, status: 'ONGOING' as const, updatedAt: '2026-05-15T10:00:00Z' },
{ progressId: 2, userId: 3, courseName: 'SpringBoot JPA Masterclass', progressRate: 100, status: 'COMPLETED' as const, updatedAt: '2026-04-20T10:00:00Z' },
{ progressId: 3, userId: 3, courseName: 'Oracle SQL Tuning & Optimization', progressRate: 30, status: 'ONGOING' as const, updatedAt: '2026-05-18T10:00:00Z' },
{ progressId: 4, userId: 3, courseName: 'Kubernetes for DevOps Engineers', progressRate: 0, status: 'NOT_STARTED' as const, updatedAt: '2026-05-01T10:00:00Z' },
{ progressId: 5, userId: 3, courseName: 'React 19 Advanced Patterns', progressRate: 100, status: 'COMPLETED' as const, updatedAt: '2026-03-15T10:00:00Z' },
{ progressId: 6, userId: 3, courseName: 'Cloud Architecture on AWS', progressRate: 0, status: 'NOT_STARTED' as const, updatedAt: '2026-05-01T10:00:00Z' },
];
+535
View File
@@ -0,0 +1,535 @@
import MockAdapter from 'axios-mock-adapter';
import type { AxiosInstance } from 'axios';
import type { RoleCode } from '@/types';
import { employees as _employees } from './data/employees';
import { projects as _projects } from './data/projects';
import { dispatchHistories as _dispatchHistories } from './data/dispatchHistories';
import { tickets as _tickets } from './data/tickets';
import { comments as _comments } from './data/comments';
import { equipment as _equipment } from './data/equipment';
import { assignments as _assignments } from './data/assignments';
import { maintenances as _maintenances } from './data/maintenances';
import { reports as _reports } from './data/reports';
import { training as _training } from './data/training';
import { notifications as _notifications } from './data/notifications';
import { commonCodes as _commonCodes } from './data/commonCodes';
interface MockUser {
loginId: string;
password: string;
name: string;
role: RoleCode;
}
const MOCK_USERS: MockUser[] = [
{ loginId: 'admin', password: 'admin123', name: 'Admin', role: 'ADMIN' },
{ loginId: 'manager', password: 'manager123', name: 'Manager Kim', role: 'MANAGER' },
{ loginId: 'employee', password: 'emp123', name: 'Employee Lee', role: 'EMPLOYEE' },
];
function paginate<T>(arr: T[], page = 0, size = 20) {
const start = page * size;
const content = arr.slice(start, start + size);
return { content, totalElements: arr.length, totalPages: Math.ceil(arr.length / size), number: page, size };
}
let initialized = false;
export function setupMocks(instance: AxiosInstance) {
if (typeof window === 'undefined' || initialized) return;
initialized = true;
const mock = new MockAdapter(instance, { delayResponse: 200 });
const s: Record<string, any[]> = {
employees: _employees,
projects: _projects,
dispatchHistories: _dispatchHistories,
tickets: _tickets,
comments: _comments,
equipment: _equipment,
assignments: _assignments,
maintenances: _maintenances,
reports: _reports,
training: _training,
notifications: _notifications,
commonCodes: _commonCodes,
};
/* Auth */
mock.onPost('/auth/login').reply((config) => {
const { loginId, password } = JSON.parse(config.data);
const mockUser = MOCK_USERS.find((u) => u.loginId === loginId && u.password === password);
if (!mockUser) {
return [401, { success: false, message: '아이디 또는 비밀번호가 일치하지 않습니다.', errorCode: 'AUTH_001' }];
}
const emp = s.employees.find((e: any) => e.loginId === loginId) || s.employees[0];
const accessToken = `mock-jwt-${mockUser.loginId}-${Date.now()}`;
const refreshToken = `mock-refresh-${mockUser.loginId}-${Date.now()}`;
return [200, { success: true, message: '로그인 성공', data: { accessToken, refreshToken, employee: emp } }];
});
mock.onPost('/auth/refresh').reply((config) => {
const { refreshToken } = JSON.parse(config.data);
if (!refreshToken || !refreshToken.startsWith('mock-refresh-')) {
return [401, { success: false, message: '유효하지 않은 리프레시 토큰입니다.', errorCode: 'AUTH_002' }];
}
const loginId = refreshToken.split('-')[2];
const emp = s.employees.find((e: any) => e.loginId === loginId) || s.employees[0];
const accessToken = `mock-jwt-${loginId}-${Date.now()}`;
const newRefreshToken = `mock-refresh-${loginId}-${Date.now()}`;
return [200, { success: true, message: '토큰 갱신 성공', data: { accessToken, refreshToken: newRefreshToken, employee: emp } }];
});
mock.onGet('/employees/me').reply((config) => {
const authHeader = config.headers?.Authorization;
if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
const token = authHeader.replace('Bearer ', '');
const loginId = token.split('-')[2];
const emp = s.employees.find((e: any) => e.loginId === loginId);
if (emp) return [200, { success: true, message: 'OK', data: emp }];
}
return [200, { success: true, message: 'OK', data: s.employees[0] }];
});
mock.onGet('/employees').reply((config) => [200, { success: true, message: 'OK', data: paginate(s.employees, config.params?.page, config.params?.size) }]);
mock.onGet(/\/employees\/search/).reply((config) => {
const keyword = (config.params?.keyword || '').toLowerCase();
const filtered = s.employees.filter((e: any) => e.name.toLowerCase().includes(keyword) || e.email.toLowerCase().includes(keyword));
return [200, { success: true, message: 'OK', data: filtered.map((e: any) => ({ empId: e.empId, name: e.name, roleCode: e.roleCode, dispatchStatus: e.dispatchStatus, email: e.email })) }];
});
mock.onGet(/\/employees\/\d+$/).reply((config) => {
const id = parseInt(config.url?.match(/\/employees\/(\d+)$/)?.[1] || '0');
const emp = s.employees.find((e: any) => e.empId === id);
if (!emp) return [404, { success: false, message: 'Not found' }];
return [200, { success: true, message: 'OK', data: emp }];
});
mock.onGet(/\/employees\/\d+\/dispatch-histories/).reply(() => [200, { success: true, message: 'OK', data: s.dispatchHistories }]);
/* Tickets */
mock.onGet('/tickets').reply((config) => {
let result = [...s.tickets];
const { category, priority, status, searchKeyword } = config.params || {};
if (category && category !== 'ALL') result = result.filter((t: any) => t.category === category);
if (priority && priority !== 'ALL') result = result.filter((t: any) => t.priority === priority);
if (status && status !== 'ALL') result = result.filter((t: any) => t.status === status);
if (searchKeyword) result = result.filter((t: any) => t.title.toLowerCase().includes(searchKeyword.toLowerCase()));
return [200, { success: true, message: 'OK', data: paginate(result, config.params?.page, config.params?.size) }];
});
mock.onGet(/\/tickets\/(\d+)$/).reply((config) => {
const id = parseInt(config.url?.match(/\/tickets\/(\d+)$/)?.[1] || '0');
const ticket = s.tickets.find((t: any) => t.ticketId === id);
if (!ticket) return [404, { success: false, message: 'Not found' }];
ticket.isImminent = ticket.dueDate ? new Date(ticket.dueDate).getTime() - Date.now() < 86400000 : false;
return [200, { success: true, message: 'OK', data: ticket }];
});
mock.onPost(/\/tickets\/(\d+)\/comments/).reply((config) => {
const ticketId = parseInt(config.url?.match(/\/tickets\/(\d+)\/comments/)?.[1] || '0');
const { commentText, attachedFileUrl } = JSON.parse(config.data);
const newComment = { commentId: Date.now(), ticketId, userId: 1, userName: 'Admin User (본사)', commentText, attachedFileUrl: attachedFileUrl || null, createdAt: new Date().toISOString() };
return [201, { success: true, message: '등록 완료', data: newComment }];
});
mock.onGet(/\/tickets\/(\d+)\/comments/).reply((config) => {
const ticketId = parseInt(config.url?.match(/\/tickets\/(\d+)\/comments/)?.[1] || '0');
return [200, { success: true, message: 'OK', data: s.comments.filter((c: any) => c.ticketId === ticketId) }];
});
mock.onPut(/\/tickets\/(\d+)\/status/).reply((config) => {
const id = parseInt(config.url?.match(/\/tickets\/(\d+)\/status/)?.[1] || '0');
const ticket = s.tickets.find((t: any) => t.ticketId === id);
if (ticket) ticket.status = config.params?.status || ticket.status;
return [200, { success: true, message: '상태 변경 완료', data: ticket }];
});
mock.onPut(/\/tickets\/(\d+)\/resolve/).reply((config) => {
const id = parseInt(config.url?.match(/\/tickets\/(\d+)\/resolve/)?.[1] || '0');
const ticket = s.tickets.find((t: any) => t.ticketId === id);
if (ticket) { ticket.status = 'RESOLVED'; ticket.resolutionNote = JSON.parse(config.data).resolutionNote; ticket.resolvedAt = new Date().toISOString(); }
return [200, { success: true, message: '해결 처리 완료', data: ticket }];
});
mock.onDelete(/\/tickets\/(\d+)$/).reply(204);
/* Equipment */
mock.onGet('/equipment').reply((config) => {
let result = [...s.equipment];
const { type, status, manufacturer } = config.params || {};
if (type) result = result.filter((e: any) => e.equipmentType === type);
if (status) result = result.filter((e: any) => e.status === status);
if (manufacturer) result = result.filter((e: any) => e.manufacturer === manufacturer);
return [200, { success: true, message: 'OK', data: paginate(result, config.params?.page, config.params?.size) }];
});
mock.onGet(/\/equipment\/(\d+)$/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)$/)?.[1] || '0');
const equip = s.equipment.find((e: any) => e.equipmentId === id);
if (!equip) return [404, { success: false, message: 'Not found' }];
return [200, { success: true, message: 'OK', data: equip }];
});
mock.onPost('/equipment').reply((config) => {
const body = JSON.parse(config.data);
const newEquip = { equipmentId: Date.now(), ...body, version: 0 };
s.equipment.push(newEquip);
return [201, { success: true, message: '등록 완료', data: newEquip }];
});
mock.onPost(/\/equipment\/(\d+)\/assign/).reply((config) => {
const equipId = parseInt(config.url?.match(/\/equipment\/(\d+)\/assign/)?.[1] || '0');
const { assigneeId, conditionNote } = JSON.parse(config.data);
const equip = s.equipment.find((e: any) => e.equipmentId === equipId);
if (!equip || equip.status !== 'AVAILABLE') return [400, { success: false, message: '지급 가능한 재고 상태가 아닙니다.', errorCode: 'EQP_ERR_001' }];
equip.status = 'IN_USE';
const assignment = { assignmentId: Date.now(), equipmentId: equipId, userId: assigneeId, userName: s.employees.find((e: any) => e.empId === assigneeId)?.name || '', assignedDate: new Date().toISOString().split('T')[0], returnedDate: null, condOnAssign: conditionNote, condOnReturn: null };
s.assignments.push(assignment);
return [200, { success: true, message: '장비 지급 완료', data: assignment }];
});
mock.onPut(/\/equipment\/(\d+)\/return/).reply((config) => {
const equipId = parseInt(config.url?.match(/\/equipment\/(\d+)\/return/)?.[1] || '0');
const { conditionNote } = JSON.parse(config.data);
const equip = s.equipment.find((e: any) => e.equipmentId === equipId);
if (equip) equip.status = 'AVAILABLE';
const active = s.assignments.find((a: any) => a.equipmentId === equipId && !a.returnedDate);
if (active) { active.returnedDate = new Date().toISOString().split('T')[0]; active.condOnReturn = conditionNote; }
return [200, { success: true, message: '반납 완료', data: active }];
});
mock.onGet('/equipment/summary').reply(() => {
const counts: Record<string, number> = {};
s.equipment.forEach((e: any) => { counts[e.status] = (counts[e.status] || 0) + 1; });
counts.ALL = Object.values(counts).reduce((a: number, b: any) => a + (typeof b === 'number' ? b : 0), 0);
return [200, { success: true, message: 'OK', data: { counts } }];
});
/* Reports */
mock.onGet('/reports').reply((config) => {
let result = [...s.reports];
const { reportType, status } = config.params || {};
if (reportType) result = result.filter((r: any) => r.reportType === reportType);
if (status) result = result.filter((r: any) => r.status === status);
return [200, { success: true, message: 'OK', data: paginate(result, config.params?.page, config.params?.size) }];
});
mock.onPost('/reports').reply((config) => {
const body = JSON.parse(config.data);
const newReport = { reportId: Date.now(), ...body, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
s.reports.push(newReport);
return [201, { success: true, message: '보고서 생성 완료', data: newReport }];
});
mock.onPut(/\/reports\/(\d+)\/review/).reply((config) => {
const id = parseInt(config.url?.match(/\/reports\/(\d+)\/review/)?.[1] || '0');
const { status, reviewComment } = JSON.parse(config.data);
const report = s.reports.find((r: any) => r.reportId === id);
if (report) { report.status = status; report.reviewComment = reviewComment || null; }
return [200, { success: true, message: '결재 처리 완료', data: report }];
});
mock.onGet('/reports/sr-auto-complete').reply(() => [200, { success: true, message: 'OK', data: ['SR-2026-042: 오라클 인덱스 튜닝 완료', 'SR-2026-043: 배포 파이프라인 오류 수정'] }]);
/* Training */
mock.onGet('/training/me').reply(() => [200, { success: true, message: 'OK', data: s.training }]);
mock.onGet('/training/summary').reply(() => {
const byCourse: Record<string, any> = {};
s.training.forEach((t: any) => {
if (!byCourse[t.courseName]) byCourse[t.courseName] = { total: 0, completed: 0, totalRate: 0 };
byCourse[t.courseName].total++;
if (t.status === 'COMPLETED') byCourse[t.courseName].completed++;
byCourse[t.courseName].totalRate += t.progressRate;
});
const summary = Object.entries(byCourse).map(([courseName, v]) => ({ courseName, totalEnrolled: v.total, completedCount: v.completed, avgProgress: Math.round(v.totalRate / v.total) }));
return [200, { success: true, message: 'OK', data: summary }];
});
mock.onPut(/\/training\/(\d+)$/).reply((config) => {
const id = parseInt(config.url?.match(/\/training\/(\d+)/)?.[1] || '0');
const body = JSON.parse(config.data);
const prog = s.training.find((t: any) => t.progressId === id);
if (prog) { if (body.progressRate !== undefined) prog.progressRate = body.progressRate; if (body.status) prog.status = body.status; }
return [200, { success: true, message: 'OK', data: prog }];
});
/* Notifications */
mock.onGet('/notifications/unread').reply(() => [200, { success: true, message: 'OK', data: { count: s.notifications.filter((n: any) => !n.isRead).length } }]);
mock.onGet('/notifications').reply(() => [200, { success: true, message: 'OK', data: s.notifications }]);
mock.onPut(/\/notifications\/(\d+)\/read$/).reply((config) => {
const id = parseInt(config.url?.match(/\/notifications\/(\d+)\/read/)?.[1] || '0');
const n = s.notifications.find((x: any) => x.notificationId === id);
if (n) n.isRead = true;
return [200, { success: true, message: 'OK', data: null }];
});
/* Parking */
mock.onGet('/health').reply(() => {
const now = new Date();
const day = now.getDay();
const hour = now.getHours();
const parked = day === 0 || day === 6 || hour < 8 || hour >= 20;
return [200, { success: true, message: 'OK', data: { parked } }];
});
mock.onPost('/wake').reply(() => new Promise((resolve) => {
setTimeout(() => {
resolve([200, { success: true, message: '관리자에게 기동 요청이 전송되었습니다. iLO4 원격 부팅이 진행됩니다.', data: { success: true, message: '관리자에게 기동 요청이 전송되었습니다. iLO4 원격 부팅이 진행됩니다.', estimatedWakeTime: '약 5~10분 소요' } }]);
}, 3000);
}));
/* Projects */
mock.onGet('/projects').reply((config) => {
let result = [...s.projects];
const { status } = config.params || {};
if (status) result = result.filter((p: any) => p.status === status);
return [200, { success: true, message: 'OK', data: result }];
});
mock.onGet(/\/projects\/\d+/).reply((config) => {
const id = parseInt(config.url?.match(/\/projects\/(\d+)/)?.[1] || '0');
const project = s.projects.find((p: any) => p.projectId === id);
if (!project) return [404, { success: false, message: 'Not found' }];
return [200, { success: true, message: 'OK', data: project }];
});
mock.onPost('/projects').reply((config) => {
const body = JSON.parse(config.data);
const newProject = { projectId: Date.now(), ...body, createdAt: new Date().toISOString() };
s.projects.push(newProject);
return [201, { success: true, message: '프로젝트가 등록되었습니다', data: newProject }];
});
mock.onPut(/\/projects\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/projects\/(\d+)/)?.[1] || '0');
const body = JSON.parse(config.data);
const project = s.projects.find((p: any) => p.projectId === id);
if (!project) return [404, { success: false, message: 'Not found' }];
Object.assign(project, body);
return [200, { success: true, message: '프로젝트가 수정되었습니다', data: project }];
});
mock.onDelete(/\/projects\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/projects\/(\d+)/)?.[1] || '0');
const idx = s.projects.findIndex((p: any) => p.projectId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.projects.splice(idx, 1);
return [200, { success: true, message: '프로젝트가 삭제되었습니다', data: null }];
});
/* Common Codes */
mock.onGet('/common-codes').reply((config) => {
let result = [...s.commonCodes];
const { groupCode } = config.params || {};
if (groupCode) result = result.filter((c: any) => c.groupCode === groupCode);
return [200, { success: true, message: 'OK', data: result }];
});
mock.onGet('/common-codes/groups').reply(() => {
const groups = [...new Set(s.commonCodes.map((c: any) => c.groupCode))];
return [200, { success: true, message: 'OK', data: groups }];
});
mock.onPost('/common-codes').reply((config) => {
const body = JSON.parse(config.data);
const newCode = { codeId: Date.now(), ...body };
s.commonCodes.push(newCode);
return [200, { success: true, message: '생성 완료', data: newCode }];
});
mock.onPut(/\/common-codes\/\d+/).reply((config) => {
const id = parseInt(config.url?.match(/\/common-codes\/(\d+)/)?.[1] || '0');
const body = JSON.parse(config.data);
const idx = s.commonCodes.findIndex((c: any) => c.codeId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.commonCodes[idx] = { ...s.commonCodes[idx], ...body };
return [200, { success: true, message: '수정 완료', data: s.commonCodes[idx] }];
});
mock.onDelete(/\/common-codes\/\d+/).reply((config) => {
const id = parseInt(config.url?.match(/\/common-codes\/(\d+)/)?.[1] || '0');
const idx = s.commonCodes.findIndex((c: any) => c.codeId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.commonCodes.splice(idx, 1);
return [200, { success: true, message: '삭제 완료', data: null }];
});
/* Calendar Events */
mock.onGet('/calendar/events').reply(() => {
const events: any[] = [];
const now = new Date();
// Ticket due dates
s.tickets.forEach((t: any) => {
if (t.dueDate) events.push({ eventType: 'TICKET_DUE', sourceId: t.ticketId, title: t.title, eventDate: t.dueDate.split('T')[0], color: '#3B82F6', linkUrl: `/issues/${t.ticketId}` });
});
return [200, { success: true, message: 'OK', data: events }];
});
/* Employee CRUD */
mock.onPost('/employees').reply((config) => {
const body = JSON.parse(config.data);
const newEmp = { empId: Date.now(), ...body, password: 'encoded', createdAt: new Date().toISOString() };
s.employees.push(newEmp);
return [200, { success: true, message: '직원이 생성되었습니다', data: newEmp }];
});
mock.onPut(/\/employees\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/employees\/(\d+)/)?.[1] || '0');
const body = JSON.parse(config.data);
const emp = s.employees.find((e: any) => e.empId === id);
if (!emp) return [404, { success: false, message: 'Not found' }];
Object.assign(emp, body);
return [200, { success: true, message: '직원이 수정되었습니다', data: emp }];
});
mock.onDelete(/\/employees\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/employees\/(\d+)/)?.[1] || '0');
const idx = s.employees.findIndex((e: any) => e.empId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.employees.splice(idx, 1);
return [200, { success: true, message: '직원이 삭제되었습니다', data: null }];
});
mock.onPut('/employees/me/password').reply(() => [200, { success: true, message: '비밀번호가 변경되었습니다', data: null }]);
mock.onGet(/\/employees\/(\d+)\/portfolio/).reply((config) => {
const id = parseInt(config.url?.match(/\/employees\/(\d+)\/portfolio/)?.[1] || '0');
const emp = s.employees.find((e: any) => e.empId === id);
const dispatchList = s.dispatchHistories.filter((d: any) => d.empId === id);
const portfolio = dispatchList.map((d: any) => ({
dispatchHistory: d,
projectName: s.projects.find((p: any) => p.projectId === d.projectId)?.projectName || '',
reports: s.reports.filter((r: any) => r.dispatchId === d.dispatchId),
resolvedTickets: s.tickets.filter((t: any) => {
const project = s.projects.find((p: any) => p.projectId === d.projectId);
return project && t.projectId === project.projectId;
}),
}));
return [200, { success: true, message: 'OK', data: portfolio }];
});
mock.onPut(/\/employees\/(\d+)\/dispatch-histories\/(\d+)\/achievements/).reply(() => [200, { success: true, message: 'OK', data: null }]);
/* Dispatch History CRUD */
mock.onGet('/dispatch-histories').reply((config) => {
let result = [...s.dispatchHistories];
const { empId, projectId } = config.params || {};
if (empId) result = result.filter((d: any) => d.empId === Number(empId));
if (projectId) result = result.filter((d: any) => d.projectId === Number(projectId));
return [200, { success: true, message: 'OK', data: { content: result, totalElements: result.length, totalPages: 1, number: 0, size: result.length } }];
});
mock.onGet(/\/dispatch-histories\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/dispatch-histories\/(\d+)/)?.[1] || '0');
const dh = s.dispatchHistories.find((d: any) => d.dispatchId === id);
if (!dh) return [404, { success: false, message: 'Not found' }];
return [200, { success: true, message: 'OK', data: dh }];
});
mock.onGet('/dispatch-histories/active').reply(() => [200, { success: true, message: 'OK', data: s.dispatchHistories.filter((d: any) => !d.endDate) }]);
mock.onPut(/\/dispatch-histories\/(\d+)\/end/).reply((config) => {
const id = parseInt(config.url?.match(/\/dispatch-histories\/(\d+)\/end/)?.[1] || '0');
const dh = s.dispatchHistories.find((d: any) => d.dispatchId === id);
if (dh) dh.endDate = new Date().toISOString().split('T')[0];
return [200, { success: true, message: '파견이 종료되었습니다', data: dh }];
});
/* Tickets - additional */
mock.onPost('/tickets').reply((config) => {
const body = JSON.parse(config.data);
const newTicket = { ticketId: Date.now(), ...body, status: 'OPEN', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
s.tickets.push(newTicket);
return [200, { success: true, message: '티켓이 생성되었습니다', data: newTicket }];
});
mock.onDelete(/\/tickets\/(\d+)\/comments\/(\d+)/).reply(() => [200, { success: true, message: '삭제 완료', data: null }]);
/* Equipment - additional */
mock.onPut(/\/equipment\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)/)?.[1] || '0');
const body = JSON.parse(config.data);
const equip = s.equipment.find((e: any) => e.equipmentId === id);
if (!equip) return [404, { success: false, message: 'Not found' }];
Object.assign(equip, body);
return [200, { success: true, message: '수정 완료', data: equip }];
});
mock.onDelete(/\/equipment\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)/)?.[1] || '0');
const idx = s.equipment.findIndex((e: any) => e.equipmentId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.equipment.splice(idx, 1);
return [200, { success: true, message: '삭제 완료', data: null }];
});
mock.onGet(/\/equipment\/(\d+)\/assignments/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)\/assignments/)?.[1] || '0');
return [200, { success: true, message: 'OK', data: s.assignments.filter((a: any) => a.equipmentId === id) }];
});
mock.onGet(/\/equipment\/(\d+)\/maintenances/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)\/maintenances/)?.[1] || '0');
return [200, { success: true, message: 'OK', data: s.maintenances.filter((m: any) => m.equipmentId === id) }];
});
mock.onPost(/\/equipment\/(\d+)\/maintenance/).reply((config) => {
const id = parseInt(config.url?.match(/\/equipment\/(\d+)\/maintenance/)?.[1] || '0');
const body = JSON.parse(config.data);
const newMaint = { maintenanceId: Date.now(), equipmentId: id, ...body, reportedDate: new Date().toISOString().split('T')[0] };
s.maintenances.push(newMaint);
return [200, { success: true, message: '등록 완료', data: newMaint }];
});
/* Reports - additional */
mock.onGet(/\/reports\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/reports\/(\d+)/)?.[1] || '0');
const report = s.reports.find((r: any) => r.reportId === id);
if (!report) return [404, { success: false, message: 'Not found' }];
return [200, { success: true, message: 'OK', data: report }];
});
mock.onPut(/\/reports\/(\d+)\/status/).reply((config) => {
const id = parseInt(config.url?.match(/\/reports\/(\d+)\/status/)?.[1] || '0');
const { status } = config.params || {};
const report = s.reports.find((r: any) => r.reportId === id);
if (report) report.status = status || report.status;
return [200, { success: true, message: '상태 변경 완료', data: report }];
});
mock.onDelete(/\/reports\/(\d+)/).reply((config) => {
const id = parseInt(config.url?.match(/\/reports\/(\d+)/)?.[1] || '0');
const idx = s.reports.findIndex((r: any) => r.reportId === id);
if (idx === -1) return [404, { success: false, message: 'Not found' }];
s.reports.splice(idx, 1);
return [200, { success: true, message: '삭제 완료', data: null }];
});
mock.onGet('/reports/summary').reply(() => [200, { success: true, message: 'OK', data: { submitted: s.reports.filter((r: any) => r.status === 'SUBMITTED').length, approved: s.reports.filter((r: any) => r.status === 'APPROVED').length, rejected: s.reports.filter((r: any) => r.status === 'REJECTED').length, draft: s.reports.filter((r: any) => r.status === 'DRAFT').length } }]);
/* Projects - my */
mock.onGet('/projects/my').reply((config) => {
const empId = 3; // default employee
const dispatchProjects = s.dispatchHistories
.filter((d: any) => d.empId === empId)
.map((d: any) => s.projects.find((p: any) => p.projectId === d.projectId))
.filter(Boolean);
return [200, { success: true, message: 'OK', data: dispatchProjects }];
});
/* Training */
mock.onGet('/training').reply((config) => {
const { userId } = config.params || {};
let result = [...s.training];
if (userId) result = result.filter((t: any) => t.userId === Number(userId));
return [200, { success: true, message: 'OK', data: result }];
});
/* File upload */
mock.onPost('/files/upload').reply(() => [200, { success: true, message: '업로드 완료', data: { url: '/uploads/mock-file.png', name: 'mock-file.png' } }]);
mock.onAny().passThrough();
}