- 견적 시뮬레이터 UI 레이아웃 개선 (가로 배치, 반응형) - FlowTester ConditionEvaluator 클래스 추가 (조건부 실행 지원) - FormulaEvaluatorService 기능 확장 - DependencyResolver 의존성 해결 로직 개선 - PushDeviceToken 모델 확장 (FCM 토큰 관리) - QuoteFormula API 엔드포인트 추가 - FlowTester 가이드 모달 업데이트
233 lines
6.8 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|