Files
sam-react-prod/src/components/quality/EquipmentManagement/actions.ts
유병철 ca5a9325c6 feat: 급여관리 개선 + 설비관리 신규 + 팝업관리/카드관리/가격표 개선
- 급여관리: 상세/등록 다이얼로그 리팩토링, actions/types 확장
- 설비관리: 설비현황/점검/수리 4개 페이지 신규 추가
- 팝업관리: PopupDetail/PopupForm 개선
- 카드관리: CardForm 개선
- IntegratedListTemplateV2, SearchFilter, useColumnSettings 개선
- CLAUDE.md: 페이지 모드 라우팅 패턴 규칙 추가
- 공통 페이지 패턴 가이드 확장
2026-03-12 21:48:37 +09:00

589 lines
19 KiB
TypeScript

'use server';
/**
* 설비관리 Server Actions
*
* API Endpoints:
* - GET /api/v1/equipment - 목록 조회
* - POST /api/v1/equipment - 등록
* - GET /api/v1/equipment/{id} - 상세 조회
* - PUT /api/v1/equipment/{id} - 수정
* - DELETE /api/v1/equipment/{id} - 삭제
* - GET /api/v1/equipment/options - 드롭다운 옵션
* - GET /api/v1/equipment/stats - 통계
* - GET /api/v1/equipment/{id}/templates - 점검 템플릿 조회
* - POST /api/v1/equipment/{id}/templates - 점검항목 추가
* - PUT /api/v1/equipment/templates/{id} - 점검항목 수정
* - DELETE /api/v1/equipment/templates/{id} - 점검항목 삭제
* - POST /api/v1/equipment/{id}/templates/copy - 주기 복사
* - GET /api/v1/equipment/inspections - 점검 그리드 데이터
* - PATCH /api/v1/equipment/inspections/toggle - 셀 클릭 토글
* - PATCH /api/v1/equipment/inspections/set-result - 결과 직접 설정
* - DELETE /api/v1/equipment/inspections/reset - 점검 초기화
* - PATCH /api/v1/equipment/inspections/notes - 점검 메모 수정
* - GET /api/v1/equipment/repairs - 수리이력 목록
* - POST /api/v1/equipment/repairs - 수리이력 등록
* - DELETE /api/v1/equipment/repairs/{id} - 수리이력 삭제
*/
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type {
EquipmentApiData,
EquipmentPhotoApi,
EquipmentOptionsApi,
EquipmentStatsApi,
EquipmentRepairApi,
InspectionTemplateApi,
Equipment,
EquipmentOptions,
EquipmentStats,
EquipmentRepair,
InspectionTemplate,
EquipmentFormData,
RepairFormData,
InspectionTemplateFormData,
EquipmentStatus,
InspectionCycle,
InspectionResult,
PaginationMeta,
ManagerOption,
} from './types';
// ===== API → Frontend 변환 =====
function transformEquipment(api: EquipmentApiData): Equipment {
return {
id: String(api.id),
equipmentCode: api.equipment_code || '',
name: api.name || '',
equipmentType: api.equipment_type || '',
specification: api.specification || '',
manufacturer: api.manufacturer || '',
modelName: api.model_name || '',
serialNo: api.serial_no || '',
location: api.location || '',
productionLine: api.production_line || '',
purchaseDate: api.purchase_date || '',
installDate: api.install_date || '',
purchasePrice: api.purchase_price || '',
usefulLife: api.useful_life,
status: api.status || 'active',
disposedDate: api.disposed_date || '',
managerId: api.manager_id,
subManagerId: api.sub_manager_id,
managerName: api.manager?.name || '',
subManagerName: api.subManager?.name || '',
memo: api.memo || '',
isActive: api.is_active,
sortOrder: api.sort_order,
photos: (api.photos || []).map((p) => ({
id: p.id,
displayName: p.display_name,
filePath: p.file_path,
fileSize: p.file_size,
mimeType: p.mime_type,
createdAt: p.created_at,
})),
};
}
function transformRepair(api: EquipmentRepairApi): EquipmentRepair {
return {
id: String(api.id),
equipmentId: api.equipment_id,
repairDate: api.repair_date || '',
repairType: api.repair_type,
repairHours: api.repair_hours,
description: api.description || '',
cost: api.cost || '',
vendor: api.vendor || '',
repairedBy: api.repaired_by,
repairerName: api.repairer?.name || '',
memo: api.memo || '',
equipmentCode: api.equipment?.equipment_code || '',
equipmentName: api.equipment?.name || '',
};
}
function transformTemplate(api: InspectionTemplateApi): InspectionTemplate {
return {
id: api.id,
equipmentId: api.equipment_id,
inspectionCycle: api.inspection_cycle,
itemNo: api.item_no || '',
checkPoint: api.check_point || '',
checkItem: api.check_item || '',
checkTiming: api.check_timing || '',
checkFrequency: api.check_frequency || '',
checkMethod: api.check_method || '',
sortOrder: api.sort_order,
isActive: api.is_active,
};
}
function transformOptions(api: EquipmentOptionsApi): EquipmentOptions {
return {
equipmentTypes: api.equipment_types || [],
productionLines: api.production_lines || [],
statuses: api.statuses || {},
equipmentList: (api.equipment_list || []).map((e) => ({
id: e.id,
equipmentCode: e.equipment_code,
name: e.name,
equipmentType: e.equipment_type,
productionLine: e.production_line,
})),
};
}
// ===== Frontend → API 변환 =====
function transformFormToApi(data: EquipmentFormData): Record<string, unknown> {
return {
equipment_code: data.equipmentCode,
name: data.name,
equipment_type: data.equipmentType || null,
specification: data.specification || null,
manufacturer: data.manufacturer || null,
model_name: data.modelName || null,
serial_no: data.serialNo || null,
location: data.location || null,
production_line: data.productionLine || null,
purchase_date: data.purchaseDate || null,
install_date: data.installDate || null,
purchase_price: data.purchasePrice ? Number(data.purchasePrice) : null,
useful_life: data.usefulLife ? Number(data.usefulLife) : null,
status: data.status || 'active',
manager_id: data.managerId ? Number(data.managerId) : null,
sub_manager_id: data.subManagerId ? Number(data.subManagerId) : null,
memo: data.memo || null,
};
}
function transformRepairFormToApi(data: RepairFormData): Record<string, unknown> {
return {
equipment_id: Number(data.equipmentId),
repair_date: data.repairDate,
repair_type: data.repairType || null,
repair_hours: data.repairHours ? Number(data.repairHours) : null,
description: data.description || null,
cost: data.cost ? Number(data.cost) : null,
vendor: data.vendor || null,
repaired_by: data.repairedBy ? Number(data.repairedBy) : null,
memo: data.memo || null,
};
}
// ===== 설비 CRUD =====
interface PaginatedEquipmentResponse {
data: EquipmentApiData[];
current_page: number;
last_page: number;
per_page: number;
total: number;
}
export async function getEquipmentList(params?: {
page?: number;
perPage?: number;
search?: string;
status?: EquipmentStatus | 'all';
productionLine?: string;
equipmentType?: string;
}): Promise<{
success: boolean;
data: Equipment[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
const defaultPagination: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const result = await executeServerAction<PaginatedEquipmentResponse>({
url: buildApiUrl('/api/v1/equipment', {
page: params?.page,
per_page: params?.perPage || 20,
search: params?.search,
status: params?.status !== 'all' ? params?.status : undefined,
production_line: params?.productionLine !== 'all' ? params?.productionLine : undefined,
equipment_type: params?.equipmentType !== 'all' ? params?.equipmentType : undefined,
}),
errorMessage: '설비 목록 조회에 실패했습니다.',
});
if (!result.success) {
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: (d?.data || []).map(transformEquipment),
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
export async function getEquipmentDetail(id: string): Promise<ActionResult<Equipment>> {
const result = await executeServerAction<EquipmentApiData>({
url: buildApiUrl(`/api/v1/equipment/${id}`),
errorMessage: '설비 상세 조회에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
return result.data
? { success: true, data: transformEquipment(result.data) }
: { success: false, error: '설비 데이터를 찾을 수 없습니다.' };
}
export async function createEquipment(data: EquipmentFormData): Promise<ActionResult<Equipment>> {
const result = await executeServerAction<EquipmentApiData>({
url: buildApiUrl('/api/v1/equipment'),
method: 'POST',
body: transformFormToApi(data),
errorMessage: '설비 등록에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
return result.data
? { success: true, data: transformEquipment(result.data) }
: { success: true };
}
export async function updateEquipment(id: string, data: EquipmentFormData): Promise<ActionResult<Equipment>> {
const result = await executeServerAction<EquipmentApiData>({
url: buildApiUrl(`/api/v1/equipment/${id}`),
method: 'PUT',
body: transformFormToApi(data),
errorMessage: '설비 수정에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
return result.data
? { success: true, data: transformEquipment(result.data) }
: { success: true };
}
export async function deleteEquipment(id: string): Promise<ActionResult> {
return executeServerAction({
url: buildApiUrl(`/api/v1/equipment/${id}`),
method: 'DELETE',
errorMessage: '설비 삭제에 실패했습니다.',
});
}
// ===== 옵션 / 통계 =====
export async function getEquipmentOptions(): Promise<ActionResult<EquipmentOptions>> {
const result = await executeServerAction<EquipmentOptionsApi>({
url: buildApiUrl('/api/v1/equipment/options'),
errorMessage: '설비 옵션 조회에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
return result.data
? { success: true, data: transformOptions(result.data) }
: { success: false, error: '옵션 데이터를 찾을 수 없습니다.' };
}
export async function getEquipmentStats(): Promise<ActionResult<EquipmentStats>> {
const result = await executeServerAction<EquipmentStatsApi>({
url: buildApiUrl('/api/v1/equipment/stats'),
errorMessage: '설비 통계 조회에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
if (!result.data) return { success: false, error: '통계 데이터를 찾을 수 없습니다.' };
const d = result.data;
return {
success: true,
data: {
total: d.total,
active: d.active,
idle: d.idle,
disposed: d.disposed,
inspectionStats: d.inspection_stats
? {
targetCount: d.inspection_stats.target_count,
completedCount: d.inspection_stats.completed_count,
issueCount: d.inspection_stats.issue_count,
}
: undefined,
typeDistribution: d.type_distribution
? d.type_distribution.map((t) => ({
equipmentType: t.equipment_type,
count: t.count,
}))
: undefined,
},
};
}
// ===== 점검 템플릿 =====
export async function getInspectionTemplates(
equipmentId: string,
cycle?: InspectionCycle
): Promise<ActionResult<InspectionTemplate[]>> {
const result = await executeServerAction<InspectionTemplateApi[]>({
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates`, {
cycle,
}),
errorMessage: '점검항목 조회에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
// API가 객체 배열이 아닌 경우 방어 (예: 문자열 배열 반환 시)
const rawData = result.data || [];
const validData = rawData.filter((item): item is InspectionTemplateApi => typeof item === 'object' && item !== null && 'id' in item);
return {
success: true,
data: validData.map(transformTemplate),
};
}
export async function createInspectionTemplate(
equipmentId: string,
data: InspectionTemplateFormData
): Promise<ActionResult<InspectionTemplate>> {
const result = await executeServerAction<InspectionTemplateApi>({
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates`),
method: 'POST',
body: {
inspection_cycle: data.inspectionCycle,
item_no: data.itemNo,
check_point: data.checkPoint,
check_item: data.checkItem,
check_timing: data.checkTiming || null,
check_frequency: data.checkFrequency || null,
check_method: data.checkMethod || null,
},
errorMessage: '점검항목 추가에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
return result.data
? { success: true, data: transformTemplate(result.data) }
: { success: true };
}
export async function deleteInspectionTemplate(templateId: number): Promise<ActionResult> {
return executeServerAction({
url: buildApiUrl(`/api/v1/equipment/templates/${templateId}`),
method: 'DELETE',
errorMessage: '점검항목 삭제에 실패했습니다.',
});
}
export async function copyInspectionTemplates(
equipmentId: string,
sourceCycle: InspectionCycle,
targetCycles: InspectionCycle[]
): Promise<ActionResult<{ copied: number; skipped: number }>> {
return executeServerAction({
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates/copy`),
method: 'POST',
body: {
source_cycle: sourceCycle,
target_cycles: targetCycles,
},
errorMessage: '점검항목 복사에 실패했습니다.',
});
}
// ===== 점검 그리드 =====
export async function getInspectionGrid(params?: {
cycle?: InspectionCycle;
period?: string;
productionLine?: string;
equipmentId?: number;
}): Promise<ActionResult<unknown>> {
return executeServerAction({
url: buildApiUrl('/api/v1/equipment/inspections', {
cycle: params?.cycle || 'daily',
period: params?.period,
production_line: params?.productionLine !== 'all' ? params?.productionLine : undefined,
equipment_id: params?.equipmentId,
}),
errorMessage: '점검 데이터 조회에 실패했습니다.',
});
}
export async function toggleInspectionResult(params: {
equipmentId: number;
templateItemId: number;
checkDate: string;
cycle?: InspectionCycle;
}): Promise<ActionResult<{ result: InspectionResult; symbol: string }>> {
return executeServerAction({
url: buildApiUrl('/api/v1/equipment/inspections/toggle'),
method: 'PATCH',
body: {
equipment_id: params.equipmentId,
template_item_id: params.templateItemId,
check_date: params.checkDate,
cycle: params.cycle || 'daily',
},
errorMessage: '점검 결과 변경에 실패했습니다.',
});
}
export async function resetInspections(params: {
equipmentId?: number;
cycle?: InspectionCycle;
period?: string;
}): Promise<ActionResult<{ deleted_count: number }>> {
return executeServerAction({
url: buildApiUrl('/api/v1/equipment/inspections/reset'),
method: 'DELETE',
body: {
equipment_id: params.equipmentId,
cycle: params.cycle || 'daily',
period: params.period,
},
errorMessage: '점검 초기화에 실패했습니다.',
});
}
// ===== 수리이력 =====
interface PaginatedRepairResponse {
data: EquipmentRepairApi[];
current_page: number;
last_page: number;
per_page: number;
total: number;
}
export async function getRepairList(params?: {
page?: number;
perPage?: number;
search?: string;
equipmentId?: string;
repairType?: string;
dateFrom?: string;
dateTo?: string;
}): Promise<{
success: boolean;
data: EquipmentRepair[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
const defaultPagination: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const result = await executeServerAction<PaginatedRepairResponse>({
url: buildApiUrl('/api/v1/equipment/repairs', {
page: params?.page,
per_page: params?.perPage || 20,
search: params?.search,
equipment_id: params?.equipmentId !== 'all' ? params?.equipmentId : undefined,
repair_type: params?.repairType !== 'all' ? params?.repairType : undefined,
date_from: params?.dateFrom,
date_to: params?.dateTo,
}),
errorMessage: '수리이력 목록 조회에 실패했습니다.',
});
if (!result.success) {
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: (d?.data || []).map(transformRepair),
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
export async function createRepair(data: RepairFormData): Promise<ActionResult<EquipmentRepair>> {
const result = await executeServerAction<EquipmentRepairApi>({
url: buildApiUrl('/api/v1/equipment/repairs'),
method: 'POST',
body: transformRepairFormToApi(data),
errorMessage: '수리이력 등록에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
return result.data
? { success: true, data: transformRepair(result.data) }
: { success: true };
}
export async function deleteRepair(id: string): Promise<ActionResult> {
return executeServerAction({
url: buildApiUrl(`/api/v1/equipment/repairs/${id}`),
method: 'DELETE',
errorMessage: '수리이력 삭제에 실패했습니다.',
});
}
// ===== 설비 사진 =====
export async function uploadEquipmentPhoto(equipmentId: string, file: File): Promise<ActionResult<EquipmentPhotoApi>> {
const formData = new FormData();
formData.append('file', file);
return executeServerAction<EquipmentPhotoApi>({
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/photos`),
method: 'POST',
body: formData,
errorMessage: '사진 업로드에 실패했습니다.',
});
}
export async function deleteEquipmentPhoto(equipmentId: string, fileId: number): Promise<ActionResult> {
return executeServerAction({
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/photos/${fileId}`),
method: 'DELETE',
errorMessage: '사진 삭제에 실패했습니다.',
});
}
// ===== 직원 목록 (관리자 선택용) =====
interface EmployeeApiData {
user_id?: number;
user?: { id: number; name: string };
name?: string;
department?: { name: string };
tenant_user_profile?: { department?: { name: string }; position?: { name: string } };
position_key?: string;
}
interface PaginatedEmployeeResponse {
data: EmployeeApiData[];
}
export async function getManagerOptions(): Promise<ManagerOption[]> {
const result = await executeServerAction<PaginatedEmployeeResponse>({
url: buildApiUrl('/api/v1/employees', { per_page: 100, status: 'active' }),
errorMessage: '직원 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data?.data) return [];
return result.data.data
.map((emp) => ({
id: String(emp.user?.id || emp.user_id),
name: emp.user?.name || emp.name || '',
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
}))
.filter((emp) => emp.name && emp.id && emp.id !== 'undefined');
}