Files
sam-api/app/Services/Production/PrefixResolver.php
권혁성 4dd38ab14d feat: [생산지시] 전용 API + 자재투입/공정 개선
- ProductionOrder 전용 엔드포인트 (목록/통계/상세)
- 재고생산 보조공정 일반 워크플로우에서 분리
- 자재투입 replace 모드 + bom_group_key 개별 저장
- 공정단계 options 컬럼 추가 (검사 설정/범위)
- 셔터박스 prefix isStandard 파라미터 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:57:59 +09:00

306 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 결정
*
* CF/CL/CP/CB 품목은 모든 길이에 등록되어 있으므로 boxSize 무관하게 적용.
* top_cover, fin_cover는 전용 품목 없이 XX(하부BASE/상부/마구리) 공용.
*
* @param string $partType 'front', 'lintel', 'inspection', 'rear_corner', 'top_cover', 'fin_cover'
* @return string prefix
*/
public function resolveShutterBoxPrefix(string $partType): string
{
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);
}
}