feat: 단가관리 페이지 마이그레이션 및 HR 관리 기능 추가
## 단가관리 (Pricing Management) - 단가 목록 페이지 (IntegratedListTemplateV2 공통 템플릿 적용) - 단가 등록/수정 폼 (원가/마진 자동 계산) - 이력 조회, 수정 이력, 최종 확정 다이얼로그 - 판매관리 > 단가관리 네비게이션 메뉴 추가 ## HR 관리 (Human Resources) - 사원관리 (목록, 등록, 수정, 상세, CSV 업로드) - 부서관리 (트리 구조) - 근태관리 (기본 구조) ## 품목관리 개선 - Radix UI Select controlled mode 버그 수정 (key prop 적용) - DynamicItemForm 파일 업로드 지원 - 수정 페이지 데이터 로딩 개선 ## 문서화 - 단가관리 마이그레이션 체크리스트 - HR 관리 구현 체크리스트 - Radix UI Select 버그 수정 가이드 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
111
src/lib/utils/materialTransform.ts
Normal file
111
src/lib/utils/materialTransform.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Material(SM, RM, CS) 데이터 변환 유틸리티
|
||||
*
|
||||
* 프론트엔드 폼 데이터 ↔ 백엔드 API 데이터 변환
|
||||
* 2025-12-05: 등록/수정 페이지 공통 사용
|
||||
*/
|
||||
|
||||
import type { DynamicFormData } from '@/components/items/DynamicItemForm/types';
|
||||
|
||||
// Material 타입 상수
|
||||
export const MATERIAL_TYPES = ['SM', 'RM', 'CS'] as const;
|
||||
export type MaterialType = typeof MATERIAL_TYPES[number];
|
||||
|
||||
/**
|
||||
* Material 타입인지 확인
|
||||
*/
|
||||
export function isMaterialType(itemType: string | null | undefined): boolean {
|
||||
return itemType ? MATERIAL_TYPES.includes(itemType as MaterialType) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 폼 데이터에서 standard_* 필드들을 추출하여 options 배열로 변환
|
||||
*
|
||||
* 입력: { standard_1: "옵션1-1", standard_3: "옵션3-2", name: "품목명", ... }
|
||||
* 출력: { options: [{label: "standard_1", value: "옵션1-1"}, {label: "standard_3", value: "옵션3-2"}], ... }
|
||||
*/
|
||||
export function convertStandardFieldsToOptions(data: DynamicFormData): {
|
||||
options: Array<{ label: string; value: string }>;
|
||||
specification: string;
|
||||
remainingData: DynamicFormData;
|
||||
} {
|
||||
const options: Array<{ label: string; value: string }> = [];
|
||||
const specValues: string[] = [];
|
||||
const remainingData: DynamicFormData = {};
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// standard_로 시작하는 필드 또는 옵션 관련 필드 탐지
|
||||
const isStandardField = key.startsWith('standard_') ||
|
||||
key.startsWith('option_') ||
|
||||
/^[0-9]+_standard_/.test(key) || // "{id}_standard_1" 형식
|
||||
/^[0-9]+_option_/.test(key); // "{id}_option_1" 형식
|
||||
|
||||
if (isStandardField && value && typeof value === 'string' && value.trim()) {
|
||||
// standard_* 필드는 options 배열로 변환
|
||||
options.push({ label: key, value: value.trim() });
|
||||
specValues.push(value.trim());
|
||||
} else {
|
||||
// 나머지 필드는 그대로 유지
|
||||
remainingData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 선택된 옵션 값들을 '-'로 연결하여 specification 생성
|
||||
const specification = specValues.join('-');
|
||||
|
||||
return { options, specification, remainingData };
|
||||
}
|
||||
|
||||
/**
|
||||
* 백엔드 API 응답의 options 배열을 프론트엔드 폼 필드로 변환
|
||||
*
|
||||
* 입력: options: [{label: "standard_1", value: "옵션1-1"}, {label: "standard_3", value: "옵션3-2"}]
|
||||
* 출력: { standard_1: "옵션1-1", standard_3: "옵션3-2" }
|
||||
*/
|
||||
export function convertOptionsToStandardFields(
|
||||
options: Array<{ label: string; value: string }> | null | undefined
|
||||
): DynamicFormData {
|
||||
const formData: DynamicFormData = {};
|
||||
|
||||
if (options && Array.isArray(options)) {
|
||||
options.forEach((opt) => {
|
||||
if (opt.label && opt.value) {
|
||||
formData[opt.label] = opt.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material 저장 데이터 변환 (등록/수정 공통)
|
||||
*
|
||||
* 프론트엔드 폼 데이터를 백엔드 Material API 형식으로 변환
|
||||
*/
|
||||
export function transformMaterialDataForSave(
|
||||
data: DynamicFormData,
|
||||
itemType: string
|
||||
): DynamicFormData {
|
||||
// standard_* 필드들을 options 배열로 변환
|
||||
const { options, specification, remainingData } = convertStandardFieldsToOptions(data);
|
||||
|
||||
// Material 품목코드 생성: 품목명-규격(옵션조합)
|
||||
const materialName = (remainingData.name || remainingData.item_name || '') as string;
|
||||
const materialCode = remainingData.code ||
|
||||
(specification ? `${materialName}-${specification}` : materialName);
|
||||
|
||||
return {
|
||||
...remainingData,
|
||||
// Material API 필드명 매핑
|
||||
material_type: (remainingData.product_type as string) || itemType,
|
||||
remarks: remainingData.note as string, // note → remarks 변환
|
||||
material_code: materialCode,
|
||||
specification: specification || null, // 옵션 조합값을 specification으로 저장
|
||||
options: options.length > 0 ? options : null, // options 배열로 저장
|
||||
// 불필요한 필드 제거
|
||||
code: undefined,
|
||||
product_type: undefined,
|
||||
note: undefined,
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ShoppingCart,
|
||||
Receipt,
|
||||
Factory,
|
||||
DollarSign,
|
||||
LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -35,6 +36,9 @@ export const iconMap: Record<string, LucideIcon> = {
|
||||
// 생산관리 관련 아이콘
|
||||
factory: Factory,
|
||||
production: Factory,
|
||||
// 단가관리 관련 아이콘
|
||||
dollar: DollarSign,
|
||||
pricing: DollarSign,
|
||||
};
|
||||
|
||||
// API 메뉴 데이터 타입
|
||||
|
||||
Reference in New Issue
Block a user