Files
sam-api/app/Services/NumberingService.php
권혁성 78851ec04a feat: 테넌트별 채번 규칙 시스템 구현
- numbering_rules 테이블: JSON 패턴 기반 채번 규칙 저장 (tenant별)
- numbering_sequences 테이블: MySQL UPSERT 기반 atomic 시퀀스 관리
- NumberingService: generate/preview/nextSequence 핵심 서비스
- QuoteNumberService: NumberingService 우선, 폴백 QT{YYYYMMDD}{NNNN}
- OrderService: NumberingService 우선 (pair_code 지원), 폴백 ORD{YYYYMMDD}{NNNN}
- StoreOrderRequest: pair_code 필드 추가
- NumberingRuleSeeder: tenant_id=287 견적(KD-PR)/수주(KD-{pairCode}) 규칙

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

191 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\NumberingRule;
use Illuminate\Support\Facades\DB;
class NumberingService extends Service
{
/**
* 채번 규칙 기반 번호 생성
*
* @param string $documentType 문서유형 (quote, order, sale, work_order, material_receipt)
* @param array $params 외부 파라미터 (pair_code, product_category 등)
* @return string|null 생성된 번호 (규칙 없으면 null → 호출자가 기존 로직 사용)
*/
public function generate(string $documentType, array $params = []): ?string
{
$tenantId = $this->tenantId();
$rule = NumberingRule::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('document_type', $documentType)
->where('is_active', true)
->first();
if (! $rule) {
return null;
}
$segments = $rule->pattern;
$result = '';
$scopeKey = '';
foreach ($segments as $segment) {
switch ($segment['type']) {
case 'static':
$result .= $segment['value'];
break;
case 'separator':
$result .= $segment['value'];
break;
case 'date':
$result .= now()->format($segment['format']);
break;
case 'param':
$value = $params[$segment['key']] ?? $segment['default'] ?? '';
$result .= $value;
$scopeKey = $value;
break;
case 'mapping':
$inputValue = $params[$segment['key']] ?? '';
$value = $segment['map'][$inputValue] ?? $segment['default'] ?? '';
$result .= $value;
$scopeKey = $value;
break;
case 'sequence':
$periodKey = match ($rule->reset_period) {
'daily' => now()->format('ymd'),
'monthly' => now()->format('Ym'),
'yearly' => now()->format('Y'),
'never' => 'all',
default => now()->format('ymd'),
};
$nextSeq = $this->nextSequence(
$tenantId,
$documentType,
$scopeKey,
$periodKey
);
$result .= str_pad(
(string) $nextSeq,
$rule->sequence_padding,
'0',
STR_PAD_LEFT
);
break;
}
}
return $result;
}
/**
* 미리보기 (시퀀스 증가 없이 다음 번호 예측)
*/
public function preview(string $documentType, array $params = []): ?array
{
$tenantId = $this->tenantId();
$rule = NumberingRule::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('document_type', $documentType)
->where('is_active', true)
->first();
if (! $rule) {
return null;
}
$segments = $rule->pattern;
$result = '';
$scopeKey = '';
foreach ($segments as $segment) {
switch ($segment['type']) {
case 'static':
case 'separator':
$result .= $segment['value'];
break;
case 'date':
$result .= now()->format($segment['format']);
break;
case 'param':
$value = $params[$segment['key']] ?? $segment['default'] ?? '';
$result .= $value;
$scopeKey = $value;
break;
case 'mapping':
$inputValue = $params[$segment['key']] ?? '';
$value = $segment['map'][$inputValue] ?? $segment['default'] ?? '';
$result .= $value;
$scopeKey = $value;
break;
case 'sequence':
$periodKey = match ($rule->reset_period) {
'daily' => now()->format('ymd'),
'monthly' => now()->format('Ym'),
'yearly' => now()->format('Y'),
'never' => 'all',
default => now()->format('ymd'),
};
$currentSeq = (int) DB::table('numbering_sequences')
->where('tenant_id', $tenantId)
->where('document_type', $documentType)
->where('scope_key', $scopeKey)
->where('period_key', $periodKey)
->value('last_sequence');
$result .= str_pad(
(string) ($currentSeq + 1),
$rule->sequence_padding,
'0',
STR_PAD_LEFT
);
break;
}
}
return [
'preview_number' => $result,
'document_type' => $documentType,
'rule_name' => $rule->rule_name,
];
}
/**
* Atomic sequence increment (MySQL UPSERT)
*/
private function nextSequence(
int $tenantId,
string $documentType,
string $scopeKey,
string $periodKey
): int {
DB::statement(
'INSERT INTO numbering_sequences
(tenant_id, document_type, scope_key, period_key, last_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
last_sequence = last_sequence + 1,
updated_at = NOW()',
[$tenantId, $documentType, $scopeKey, $periodKey]
);
return (int) DB::table('numbering_sequences')
->where('tenant_id', $tenantId)
->where('document_type', $documentType)
->where('scope_key', $scopeKey)
->where('period_key', $periodKey)
->value('last_sequence');
}
}