Files
sam-manage/app/Services/FlowTester/DependencyResolver.php
hskwon 60618ddd04 feat: 견적 시뮬레이터 개선 및 FlowTester 조건 평가기 추가
- 견적 시뮬레이터 UI 레이아웃 개선 (가로 배치, 반응형)
- FlowTester ConditionEvaluator 클래스 추가 (조건부 실행 지원)
- FormulaEvaluatorService 기능 확장
- DependencyResolver 의존성 해결 로직 개선
- PushDeviceToken 모델 확장 (FCM 토큰 관리)
- QuoteFormula API 엔드포인트 추가
- FlowTester 가이드 모달 업데이트
2025-12-23 23:41:37 +09:00

233 lines
6.8 KiB
PHP

<?php
namespace App\Services\FlowTester;
use Exception;
/**
* 의존성 정렬기 (Topological Sort)
*
* 플로우 스텝의 dependsOn 속성을 분석하여
* 올바른 실행 순서를 결정합니다.
*
* 지원하는 의존성 형식:
* 1. 단순 문자열: "step_id" - 해당 스텝이 성공해야 실행
* 2. 조건부 객체: {"step": "step_id", "onlyIf": "success|failure|executed|skipped"}
* - success: 해당 스텝이 성공했을 때만 실행 (기본값)
* - failure: 해당 스텝이 실패했을 때만 실행
* - executed: 해당 스텝이 실행되었을 때만 실행 (성공/실패 무관, 스킵 제외)
* - skipped: 해당 스텝이 스킵되었을 때만 실행
* - any: 해당 스텝의 결과와 무관하게 순서만 보장
*/
class DependencyResolver
{
/**
* 의존성 기반 실행 순서 결정
*
* @param array $steps 스텝 정의 배열
* @return array 정렬된 스텝 ID 배열
*
* @throws Exception 순환 의존성 발견 시
*/
public function resolve(array $steps): array
{
$graph = [];
$inDegree = [];
$stepMap = [];
// 그래프 초기화
foreach ($steps as $step) {
$id = $step['id'];
$graph[$id] = [];
$inDegree[$id] = 0;
$stepMap[$id] = $step;
}
// 간선 추가 (의존성)
foreach ($steps as $step) {
$id = $step['id'];
$deps = $step['dependsOn'] ?? [];
foreach ($deps as $dep) {
// 조건부 의존성 또는 단순 문자열 지원
$depId = $this->extractDependencyId($dep);
if (! isset($graph[$depId])) {
throw new Exception("Unknown dependency: '{$depId}' in step '{$id}'");
}
$graph[$depId][] = $id;
$inDegree[$id]++;
}
}
// Kahn's Algorithm
$queue = [];
foreach ($inDegree as $id => $degree) {
if ($degree === 0) {
$queue[] = $id;
}
}
$sorted = [];
while (! empty($queue)) {
$current = array_shift($queue);
$sorted[] = $current;
foreach ($graph[$current] as $neighbor) {
$inDegree[$neighbor]--;
if ($inDegree[$neighbor] === 0) {
$queue[] = $neighbor;
}
}
}
if (count($sorted) !== count($steps)) {
// 순환 의존성 발견 - 어떤 스텝들이 문제인지 파악
$remaining = array_diff(array_keys($graph), $sorted);
throw new Exception('Circular dependency detected in steps: '.implode(', ', $remaining));
}
return $sorted;
}
/**
* 의존성에서 스텝 ID 추출
*
* @param mixed $dependency 의존성 (문자열 또는 조건부 객체)
* @return string 스텝 ID
*/
private function extractDependencyId(mixed $dependency): string
{
if (is_string($dependency)) {
return $dependency;
}
if (is_array($dependency) && isset($dependency['step'])) {
return $dependency['step'];
}
throw new Exception('Invalid dependency format: '.json_encode($dependency));
}
/**
* 의존성 조건 파싱
*
* @param mixed $dependency 의존성 (문자열 또는 조건부 객체)
* @return array ['stepId' => string, 'condition' => string]
*/
public function parseDependency(mixed $dependency): array
{
if (is_string($dependency)) {
return [
'stepId' => $dependency,
'condition' => 'success', // 기본값: 성공 시에만
];
}
if (is_array($dependency)) {
return [
'stepId' => $dependency['step'] ?? '',
'condition' => $dependency['onlyIf'] ?? 'success',
];
}
return [
'stepId' => '',
'condition' => 'success',
];
}
/**
* 의존성 그래프 시각화 (디버깅용)
*
* @param array $steps 스텝 정의 배열
* @return array 의존성 정보
*/
public function visualize(array $steps): array
{
$result = [];
foreach ($steps as $step) {
$id = $step['id'];
$deps = $step['dependsOn'] ?? [];
$result[$id] = [
'name' => $step['name'] ?? $id,
'depends_on' => $deps,
'depended_by' => [],
];
}
// 역방향 의존성 추가
foreach ($steps as $step) {
$id = $step['id'];
$deps = $step['dependsOn'] ?? [];
foreach ($deps as $dep) {
if (isset($result[$dep])) {
$result[$dep]['depended_by'][] = $id;
}
}
}
return $result;
}
/**
* 의존성 유효성 검사
*
* @param array $steps 스텝 정의 배열
* @return array ['valid' => bool, 'errors' => array]
*/
public function validate(array $steps): array
{
$errors = [];
$stepIds = array_column($steps, 'id');
// 중복 ID 체크
$duplicates = array_diff_assoc($stepIds, array_unique($stepIds));
if (! empty($duplicates)) {
$errors[] = 'Duplicate step IDs found: '.implode(', ', array_unique($duplicates));
}
// 존재하지 않는 의존성 체크
foreach ($steps as $step) {
$id = $step['id'];
$deps = $step['dependsOn'] ?? [];
foreach ($deps as $dep) {
// 조건부 의존성 지원
$parsed = $this->parseDependency($dep);
$depId = $parsed['stepId'];
if (! in_array($depId, $stepIds)) {
$errors[] = "Step '{$id}' depends on unknown step '{$depId}'";
}
// 자기 참조 체크
if ($depId === $id) {
$errors[] = "Step '{$id}' cannot depend on itself";
}
// 조건부 의존성 조건값 검증
$validConditions = ['success', 'failure', 'executed', 'skipped', 'any'];
if (! in_array($parsed['condition'], $validConditions)) {
$errors[] = "Step '{$id}': Invalid dependency condition '{$parsed['condition']}'. Valid values: ".implode(', ', $validConditions);
}
}
}
// 순환 의존성 체크
try {
$this->resolve($steps);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
return [
'valid' => empty($errors),
'errors' => $errors,
];
}
}