- PrefixResolver: 제품코드×마감재질→LOT prefix 결정 + BD-XX-NN 코드 생성 - DynamicBomEntry DTO: dynamic_bom JSON 항목 타입 안전 관리 - BendingInfoBuilder 확장: build() 리턴 변경 + buildDynamicBomForItem() 추가 - OrderService: 작업지시 생성 시 per-item dynamic_bom 자동 저장 - WorkOrderService.getMaterials(): dynamic_bom 우선 체크 + N+1 배치 최적화 - WorkOrderService.registerMaterialInput(): work_order_item_id 분기 라우팅 통일 - 단위 테스트 58개 + 통합 테스트 6개 (64 tests / 293 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
308 lines
11 KiB
PHP
308 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Production;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* 절곡 세부품목 LOT Prefix 결정 및 BD-XX-NN 코드 생성
|
|
*
|
|
* 제품코드 + 마감재질 + 가이드타입 → LOT prefix → BD-XX-NN 코드 → items.id
|
|
*/
|
|
class PrefixResolver
|
|
{
|
|
// ─────────────────────────────────────────────────
|
|
// 가이드레일 Prefix 맵
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/** 벽면형(Wall) prefix: partType → prefix (finish는 productCode별 분기) */
|
|
private const WALL_PREFIXES = [
|
|
'finish' => ['KSS' => 'RS', 'KQTS' => 'RS', 'KSE' => 'RE', 'KWE' => 'RE', 'KTE' => 'RS'],
|
|
'body' => 'RM',
|
|
'c_type' => 'RC',
|
|
'd_type' => 'RD',
|
|
'extra_finish' => 'YY',
|
|
'base' => 'XX',
|
|
];
|
|
|
|
/** 측면형(Side) prefix */
|
|
private const SIDE_PREFIXES = [
|
|
'finish' => ['KSS' => 'SS', 'KQTS' => 'SS', 'KSE' => 'SE', 'KWE' => 'SE', 'KTE' => 'SS'],
|
|
'body' => 'SM',
|
|
'c_type' => 'SC',
|
|
'd_type' => 'SD',
|
|
'extra_finish' => 'YY',
|
|
'base' => 'XX',
|
|
];
|
|
|
|
/** 철재(KTE01) body 오버라이드 */
|
|
private const STEEL_BODY_OVERRIDES = [
|
|
'wall' => 'RT',
|
|
'side' => 'ST',
|
|
];
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 하단마감재 Prefix 맵
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/** 하단마감재 main prefix: finishMaterial 기반 */
|
|
private const BOTTOM_BAR_MAIN = [
|
|
'EGI' => 'BE',
|
|
'SUS' => 'BS',
|
|
'STEEL' => 'TS',
|
|
];
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 셔터박스 Prefix 맵
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/** 표준 사이즈(500*380) 셔터박스 prefix */
|
|
private const SHUTTER_STANDARD = [
|
|
'front' => 'CF',
|
|
'lintel' => 'CL',
|
|
'inspection' => 'CP',
|
|
'rear_corner' => 'CB',
|
|
'top_cover' => 'XX',
|
|
'fin_cover' => 'XX',
|
|
];
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 길이코드 매핑
|
|
// ─────────────────────────────────────────────────
|
|
|
|
private const LENGTH_TO_CODE = [
|
|
1219 => '12',
|
|
2438 => '24',
|
|
3000 => '30',
|
|
3500 => '35',
|
|
4000 => '40',
|
|
4150 => '41',
|
|
4200 => '42',
|
|
4300 => '43',
|
|
];
|
|
|
|
/** 연기차단재 전용 길이코드 */
|
|
private const SMOKE_LENGTH_TO_CODE = [
|
|
'w50' => [3000 => '53', 4000 => '54'],
|
|
'w80' => [3000 => '83', 4000 => '84'],
|
|
];
|
|
|
|
/** 파트타입 한글명 */
|
|
private const PART_TYPE_NAMES = [
|
|
'finish' => '마감재',
|
|
'body' => '본체',
|
|
'c_type' => 'C형',
|
|
'd_type' => 'D형',
|
|
'extra_finish' => '별도마감',
|
|
'base' => '하부BASE',
|
|
'main' => '메인',
|
|
'lbar' => 'L-Bar',
|
|
'reinforce' => '보강평철',
|
|
'extra' => '별도마감',
|
|
'front' => '전면부',
|
|
'lintel' => '린텔부',
|
|
'inspection' => '점검구',
|
|
'rear_corner' => '후면코너부',
|
|
'top_cover' => '상부덮개',
|
|
'fin_cover' => '마구리',
|
|
'smoke' => '연기차단재',
|
|
];
|
|
|
|
/** items.id 캐시: code → id */
|
|
private array $itemIdCache = [];
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 가이드레일
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 가이드레일 세부품목의 prefix 결정
|
|
*
|
|
* @param string $partType 'finish', 'body', 'c_type', 'd_type', 'extra_finish', 'base'
|
|
* @param string $guideType 'wall', 'side'
|
|
* @param string $productCode 'KSS01', 'KSE01', 'KWE01', 'KTE01', 'KQTS01'
|
|
* @return string prefix (빈 문자열이면 해당 파트 없음)
|
|
*/
|
|
public function resolveGuideRailPrefix(string $partType, string $guideType, string $productCode): string
|
|
{
|
|
$prefixMap = $guideType === 'wall' ? self::WALL_PREFIXES : self::SIDE_PREFIXES;
|
|
$codePrefix = $this->extractCodePrefix($productCode);
|
|
$isSteel = $codePrefix === 'KTE';
|
|
|
|
// body: 철재 오버라이드
|
|
if ($partType === 'body' && $isSteel) {
|
|
return self::STEEL_BODY_OVERRIDES[$guideType] ?? '';
|
|
}
|
|
|
|
// finish: productCode별 분기
|
|
if ($partType === 'finish') {
|
|
$finishMap = $prefixMap['finish'] ?? [];
|
|
|
|
return $finishMap[$codePrefix] ?? '';
|
|
}
|
|
|
|
// extra_finish, base, c_type, d_type, body: 고정 prefix
|
|
return $prefixMap[$partType] ?? '';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 하단마감재
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 하단마감재 세부품목의 prefix 결정
|
|
*
|
|
* @param string $partType 'main', 'lbar', 'reinforce', 'extra'
|
|
* @param string $productCode 'KSS01', 'KSE01', etc.
|
|
* @param string $finishMaterial 'EGI마감', 'SUS마감'
|
|
* @return string prefix
|
|
*/
|
|
public function resolveBottomBarPrefix(string $partType, string $productCode, string $finishMaterial): string
|
|
{
|
|
if ($partType === 'lbar') {
|
|
return 'LA';
|
|
}
|
|
if ($partType === 'reinforce') {
|
|
return 'HH';
|
|
}
|
|
if ($partType === 'extra') {
|
|
return 'YY';
|
|
}
|
|
|
|
// main: 재질 기반
|
|
$codePrefix = $this->extractCodePrefix($productCode);
|
|
$isSteel = $codePrefix === 'KTE';
|
|
|
|
if ($isSteel) {
|
|
return 'TS';
|
|
}
|
|
|
|
$isSUS = in_array($codePrefix, ['KSS', 'KQTS']);
|
|
|
|
return $isSUS ? 'BS' : 'BE';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 셔터박스
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 셔터박스 세부품목의 prefix 결정
|
|
*
|
|
* @param string $partType 'front', 'lintel', 'inspection', 'rear_corner', 'top_cover', 'fin_cover'
|
|
* @param bool $isStandardSize 500*380인지
|
|
* @return string prefix
|
|
*/
|
|
public function resolveShutterBoxPrefix(string $partType, bool $isStandardSize): string
|
|
{
|
|
if (! $isStandardSize) {
|
|
return 'XX';
|
|
}
|
|
|
|
return self::SHUTTER_STANDARD[$partType] ?? 'XX';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 연기차단재
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 연기차단재 세부품목의 prefix 결정 (항상 GI)
|
|
*/
|
|
public function resolveSmokeBarrierPrefix(): string
|
|
{
|
|
return 'GI';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// 코드 생성 및 조회
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* prefix + 길이(mm) → BD-XX-NN 코드 생성
|
|
*
|
|
* @param string $prefix LOT prefix (RS, RM, etc.)
|
|
* @param int $lengthMm 길이 (mm)
|
|
* @param string|null $smokeCategory 연기차단재 카테고리 ('w50', 'w80')
|
|
* @return string|null BD 코드 (길이코드 변환 실패 시 null)
|
|
*/
|
|
public function buildItemCode(string $prefix, int $lengthMm, ?string $smokeCategory = null): ?string
|
|
{
|
|
$lengthCode = self::lengthToCode($lengthMm, $smokeCategory);
|
|
if ($lengthCode === null) {
|
|
return null;
|
|
}
|
|
|
|
return "BD-{$prefix}-{$lengthCode}";
|
|
}
|
|
|
|
/**
|
|
* BD-XX-NN 코드 → items.id 조회 (캐시)
|
|
*
|
|
* @return int|null items.id (미등록 시 null)
|
|
*/
|
|
public function resolveItemId(string $itemCode, int $tenantId = 287): ?int
|
|
{
|
|
$cacheKey = "{$tenantId}:{$itemCode}";
|
|
|
|
if (isset($this->itemIdCache[$cacheKey])) {
|
|
return $this->itemIdCache[$cacheKey];
|
|
}
|
|
|
|
$id = DB::table('items')
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $itemCode)
|
|
->whereNull('deleted_at')
|
|
->value('id');
|
|
|
|
$this->itemIdCache[$cacheKey] = $id;
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* 길이(mm) → 길이코드 변환
|
|
*
|
|
* @param int $lengthMm 길이 (mm)
|
|
* @param string|null $smokeCategory 연기차단재 카테고리 ('w50', 'w80')
|
|
* @return string|null 길이코드 (변환 불가 시 null)
|
|
*/
|
|
public static function lengthToCode(int $lengthMm, ?string $smokeCategory = null): ?string
|
|
{
|
|
// 연기차단재 전용 코드
|
|
if ($smokeCategory && isset(self::SMOKE_LENGTH_TO_CODE[$smokeCategory][$lengthMm])) {
|
|
return self::SMOKE_LENGTH_TO_CODE[$smokeCategory][$lengthMm];
|
|
}
|
|
|
|
return self::LENGTH_TO_CODE[$lengthMm] ?? null;
|
|
}
|
|
|
|
/**
|
|
* 파트타입 한글명 반환
|
|
*/
|
|
public static function partTypeName(string $partType): string
|
|
{
|
|
return self::PART_TYPE_NAMES[$partType] ?? $partType;
|
|
}
|
|
|
|
/**
|
|
* 캐시 초기화 (테스트 용)
|
|
*/
|
|
public function clearCache(): void
|
|
{
|
|
$this->itemIdCache = [];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────
|
|
// private
|
|
// ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 'KSS01' → 'KSS', 'KQTS01' → 'KQTS' 등 제품코드 prefix 추출
|
|
*/
|
|
private function extractCodePrefix(string $productCode): string
|
|
{
|
|
return preg_replace('/\d+$/', '', $productCode);
|
|
}
|
|
}
|