refactor: 품목관리 시스템 리팩토링 및 Sales 페이지 추가
DynamicItemForm 개선: - 품목코드 자동생성 기능 추가 - 조건부 표시 로직 개선 - 불필요한 컴포넌트 정리 (DynamicField, DynamicSection 등) - 타입 시스템 단순화 새로운 기능: - Sales 페이지 마이그레이션 (견적관리, 거래처관리) - 공통 컴포넌트 추가 (atoms, molecules, organisms, templates) 문서화: - 구현 문서 및 참조 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
377
src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts
Normal file
377
src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 품목코드/품목명 자동생성 유틸리티
|
||||
*
|
||||
* MVP용 프론트엔드 구현 - 하드코딩 내역은 추후 백엔드 API로 이관 필요
|
||||
*
|
||||
* @see claudedocs/item-master/[REF] item-code-hardcoding.md
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// [하드코딩] 영문약어 매핑 테이블
|
||||
// TODO: 추후 백엔드 API 또는 품목기준관리에서 설정 가능하도록 변경
|
||||
// ============================================
|
||||
export const ITEM_CODE_PREFIX_MAP: Record<string, string> = {
|
||||
// 부품 - 조립품
|
||||
'가이드레일': 'GR',
|
||||
'케이스': 'CASE',
|
||||
'브라켓': 'BRK',
|
||||
|
||||
// 부품 - 구매품
|
||||
'모터': 'MOTOR',
|
||||
'제어기': 'CTL',
|
||||
'전동개폐기': 'OPENER',
|
||||
'스위치': 'SW',
|
||||
'센서': 'SENSOR',
|
||||
'리모컨': 'REMOTE',
|
||||
|
||||
// 부품 - 절곡품
|
||||
'레일': 'RAIL',
|
||||
'커버': 'COVER',
|
||||
'플레이트': 'PLATE',
|
||||
|
||||
// 제품
|
||||
'스크린': 'SCREEN',
|
||||
'셔터': 'SHUTTER',
|
||||
'방화스크린': 'FIRE-SCR',
|
||||
'롤스크린': 'ROLL-SCR',
|
||||
|
||||
// 원자재
|
||||
'알루미늄': 'ALU',
|
||||
'스틸': 'STEEL',
|
||||
'철판': 'STEEL',
|
||||
|
||||
// 부자재/소모품
|
||||
'볼트': 'BOLT',
|
||||
'너트': 'NUT',
|
||||
'와셔': 'WASHER',
|
||||
'나사': 'SCREW',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// [하드코딩] 절곡품 코드 체계
|
||||
// TODO: 추후 품목기준관리에서 설정 가능하도록 변경
|
||||
// ============================================
|
||||
export const BENDING_CODE_SYSTEM = {
|
||||
// 품목명코드 (category2)
|
||||
품목명코드: {
|
||||
'R': '가이드레일',
|
||||
'S': '스크린',
|
||||
'C': '케이스',
|
||||
'B': '박스',
|
||||
'T': '트림',
|
||||
'L': '라스틱',
|
||||
'G': '기타',
|
||||
} as Record<string, string>,
|
||||
|
||||
// 종류코드 (category3)
|
||||
종류코드: {
|
||||
'M': '마감',
|
||||
'T': '티',
|
||||
'C': '채널',
|
||||
'D': '단면',
|
||||
'S': '상부',
|
||||
'U': '하부',
|
||||
'F': '플랫',
|
||||
'P': '피스',
|
||||
'L': '리드',
|
||||
'B': '브라켓',
|
||||
'E': '엔드',
|
||||
'I': '이음',
|
||||
'A': '각재',
|
||||
} as Record<string, string>,
|
||||
|
||||
// 길이코드 매핑 (mm → 코드)
|
||||
길이코드: {
|
||||
1219: '12',
|
||||
2438: '24',
|
||||
3000: '30',
|
||||
3500: '35',
|
||||
4000: '40',
|
||||
4150: '41',
|
||||
4200: '42',
|
||||
4300: '43',
|
||||
} as Record<number, string>,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// [하드코딩] 조립품 설치유형 매핑
|
||||
// TODO: 추후 품목기준관리에서 설정 가능하도록 변경
|
||||
// ============================================
|
||||
export const INSTALLATION_TYPE_MAP: Record<string, string> = {
|
||||
'standard': '표준형',
|
||||
'top': '상부형',
|
||||
'bottom': '하부형',
|
||||
'side': '측면형',
|
||||
'custom': '맞춤형',
|
||||
};
|
||||
|
||||
/**
|
||||
* 품목명에서 영문약어 추출
|
||||
* @param itemName 품목명 (한글)
|
||||
* @returns 영문약어 또는 기본값
|
||||
*/
|
||||
export function getItemCodePrefix(itemName: string): string {
|
||||
// 정확한 매칭 먼저 시도
|
||||
if (ITEM_CODE_PREFIX_MAP[itemName]) {
|
||||
return ITEM_CODE_PREFIX_MAP[itemName];
|
||||
}
|
||||
|
||||
// 부분 매칭 시도 (품목명에 키워드가 포함된 경우)
|
||||
for (const [keyword, prefix] of Object.entries(ITEM_CODE_PREFIX_MAP)) {
|
||||
if (itemName.includes(keyword)) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 품목명의 첫 글자들로 생성 (임시)
|
||||
// 예: "새로운품목" → "ITEM" (기본값)
|
||||
return 'ITEM';
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 품목 목록에서 다음 순번 계산
|
||||
* @param existingCodes 기존 품목코드 배열 (예: ["GR-001", "GR-002"])
|
||||
* @param prefix 영문약어 (예: "GR")
|
||||
* @returns 다음 순번 (예: "003")
|
||||
*/
|
||||
export function getNextSequence(existingCodes: string[], prefix: string): string {
|
||||
const pattern = new RegExp(`^${prefix}-(\\d+)$`, 'i');
|
||||
|
||||
let maxSeq = 0;
|
||||
existingCodes.forEach(code => {
|
||||
const match = code.match(pattern);
|
||||
if (match) {
|
||||
const seq = parseInt(match[1], 10);
|
||||
if (seq > maxSeq) {
|
||||
maxSeq = seq;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return String(maxSeq + 1).padStart(3, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목코드 생성 (영문약어-순번)
|
||||
* @param itemName 품목명 (한글)
|
||||
* @param existingCodes 기존 품목코드 배열
|
||||
* @returns 새 품목코드 (예: "GR-003")
|
||||
*/
|
||||
export function generateItemCode(itemName: string, existingCodes: string[]): string {
|
||||
const prefix = getItemCodePrefix(itemName);
|
||||
const sequence = getNextSequence(existingCodes, prefix);
|
||||
return `${prefix}-${sequence}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 품목코드 생성 (품목명코드 + 종류코드 + 길이코드)
|
||||
* @param category2 품목명코드 (R, S, C 등)
|
||||
* @param category3 종류코드 (M, T, C 등)
|
||||
* @param lengthMm 길이 (mm)
|
||||
* @returns 품목코드 (예: "RC24")
|
||||
*/
|
||||
export function generateBendingItemCode(
|
||||
category2: string,
|
||||
category3: string,
|
||||
lengthMm: number
|
||||
): string {
|
||||
// 길이코드 변환
|
||||
let lengthCode = BENDING_CODE_SYSTEM.길이코드[lengthMm];
|
||||
if (!lengthCode) {
|
||||
// 매핑에 없으면 100으로 나눈 값 사용
|
||||
lengthCode = String(Math.floor(lengthMm / 100)).padStart(2, '0');
|
||||
}
|
||||
|
||||
return `${category2}${category3}${lengthCode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조립품 품목명 생성 (품목명 + 설치유형 + 측면규격*길이코드)
|
||||
* @param itemName 기본 품목명 (가이드레일)
|
||||
* @param installationType 설치유형 (standard → 표준형)
|
||||
* @param sideSpecWidth 측면규격 너비
|
||||
* @param sideSpecHeight 측면규격 높이
|
||||
* @param lengthMm 길이 (mm)
|
||||
* @returns 조합된 품목명 (예: "가이드레일표준형50*60*24")
|
||||
*/
|
||||
export function generateAssemblyItemName(
|
||||
itemName: string,
|
||||
installationType: string,
|
||||
sideSpecWidth?: number,
|
||||
sideSpecHeight?: number,
|
||||
lengthMm?: number
|
||||
): string {
|
||||
const installationTypeKorean = INSTALLATION_TYPE_MAP[installationType] || installationType;
|
||||
|
||||
let result = `${itemName}${installationTypeKorean}`;
|
||||
|
||||
if (sideSpecWidth && sideSpecHeight && lengthMm) {
|
||||
// 길이코드 변환
|
||||
let lengthCode = BENDING_CODE_SYSTEM.길이코드[lengthMm];
|
||||
if (!lengthCode) {
|
||||
lengthCode = String(Math.floor(lengthMm / 100)).padStart(2, '0');
|
||||
}
|
||||
|
||||
result += `${sideSpecWidth}*${sideSpecHeight}*${lengthCode}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 품목명 생성 (품목명 + 종류 + 규격)
|
||||
* @param category2Label 품목명 라벨 (가이드레일)
|
||||
* @param category3Label 종류 라벨 (채널)
|
||||
* @param specification 규격
|
||||
* @returns 조합된 품목명
|
||||
*/
|
||||
export function generateBendingItemName(
|
||||
category2Label: string,
|
||||
category3Label: string,
|
||||
specification?: string
|
||||
): string {
|
||||
let result = `${category2Label} ${category3Label}`;
|
||||
if (specification) {
|
||||
result += ` ${specification}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 구매품 품목명 생성 (품목명 + 규격)
|
||||
* @param itemName 기본 품목명
|
||||
* @param specification 규격
|
||||
* @returns 조합된 품목명 (예: "모터 0.4KW")
|
||||
*/
|
||||
export function generatePurchasedItemName(
|
||||
itemName: string,
|
||||
specification?: string
|
||||
): string {
|
||||
if (specification) {
|
||||
return `${itemName} ${specification}`;
|
||||
}
|
||||
return itemName;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 절곡 부품 품목코드 자동생성 (간소화 버전)
|
||||
// 2025-12-03 추가
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 문자열에서 마지막 괄호 안의 단일 문자 추출
|
||||
* @param str 입력 문자열 (예: "가이드레일(벽면형) (R)")
|
||||
* @returns 괄호 안의 문자 (예: "R") 또는 빈 문자열
|
||||
*/
|
||||
export function extractParenthesisCode(str: string): string {
|
||||
if (!str) return '';
|
||||
|
||||
// 마지막 괄호 안의 내용 추출 (예: "(R)" → "R")
|
||||
const match = str.match(/\(([A-Za-z가-힣])\)\s*$/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
// 대안: 마지막 괄호가 없으면 앞쪽 괄호에서 추출 시도
|
||||
const altMatch = str.match(/\(([A-Za-z가-힣])\)/);
|
||||
return altMatch ? altMatch[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 품목코드 생성 (품목명코드 + 종류코드)
|
||||
* @param itemNameValue 품목명 드롭다운 값 (예: "가이드레일(벽면형) (R)")
|
||||
* @param categoryValue 종류 드롭다운 값 (예: "본체 (M)")
|
||||
* @param shapeLengthValue 모양&길이 드롭다운 값 (예: "2438" 또는 "30")
|
||||
* @returns 품목코드 (예: "RM" 또는 "RM30")
|
||||
*/
|
||||
export function generateBendingItemCodeSimple(
|
||||
itemNameValue: string,
|
||||
categoryValue: string,
|
||||
shapeLengthValue?: string
|
||||
): string {
|
||||
const itemNameCode = extractParenthesisCode(itemNameValue);
|
||||
const categoryCode = extractParenthesisCode(categoryValue);
|
||||
|
||||
if (!itemNameCode && !categoryValue) return '';
|
||||
|
||||
let code = `${itemNameCode}${categoryCode}`;
|
||||
|
||||
// 모양&길이가 있으면 길이 축약 추가
|
||||
if (shapeLengthValue) {
|
||||
// 숫자만 추출
|
||||
const lengthNum = parseInt(shapeLengthValue.replace(/[^0-9]/g, ''), 10);
|
||||
if (lengthNum > 0) {
|
||||
// 100으로 나눈 값 (예: 2438 → 24, 3000 → 30)
|
||||
const lengthCode = Math.floor(lengthNum / 100).toString();
|
||||
code += lengthCode;
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 조립 부품 품목명/규격 자동생성
|
||||
// 2025-12-03 추가
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 조립 부품 품목명 생성 (품목명 + 가로x세로)
|
||||
* @param itemName 선택한 품목명 (가이드레일)
|
||||
* @param sideSpecWidth 측면규격 가로 (mm)
|
||||
* @param sideSpecHeight 측면규격 세로 (mm)
|
||||
* @returns 조합된 품목명 (예: "가이드레일 50x60")
|
||||
*/
|
||||
export function generateAssemblyItemNameSimple(
|
||||
itemName: string,
|
||||
sideSpecWidth?: number | string,
|
||||
sideSpecHeight?: number | string
|
||||
): string {
|
||||
if (!itemName) return '';
|
||||
|
||||
if (sideSpecWidth && sideSpecHeight) {
|
||||
return `${itemName} ${sideSpecWidth}x${sideSpecHeight}`;
|
||||
}
|
||||
|
||||
return itemName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조립 부품 규격 생성 (가로x세로x길이)
|
||||
* @param sideSpecWidth 측면규격 가로 (mm)
|
||||
* @param sideSpecHeight 측면규격 세로 (mm)
|
||||
* @param assemblyLength 길이 (mm) - 네자리 그대로 사용
|
||||
* @returns 조합된 규격 (예: "50x60x2438")
|
||||
*/
|
||||
export function generateAssemblySpecification(
|
||||
sideSpecWidth?: number | string,
|
||||
sideSpecHeight?: number | string,
|
||||
assemblyLength?: number | string
|
||||
): string {
|
||||
if (!sideSpecWidth || !sideSpecHeight || !assemblyLength) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${sideSpecWidth}x${sideSpecHeight}x${assemblyLength}`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 하드코딩 내역 목록 (문서화용)
|
||||
// ============================================
|
||||
export const HARDCODED_ITEMS = {
|
||||
ITEM_CODE_PREFIX_MAP: {
|
||||
description: '품목명 → 영문약어 매핑 테이블',
|
||||
location: 'itemCodeGenerator.ts',
|
||||
migrationTarget: '품목기준관리 API 또는 별도 설정 테이블',
|
||||
},
|
||||
BENDING_CODE_SYSTEM: {
|
||||
description: '절곡품 코드 체계 (품목명코드, 종류코드, 길이코드)',
|
||||
location: 'itemCodeGenerator.ts',
|
||||
migrationTarget: '품목기준관리 API 또는 별도 설정 테이블',
|
||||
},
|
||||
INSTALLATION_TYPE_MAP: {
|
||||
description: '조립품 설치유형 매핑',
|
||||
location: 'itemCodeGenerator.ts',
|
||||
migrationTarget: '품목기준관리 API 또는 별도 설정 테이블',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user