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'); } }