diff --git a/app/Console/Commands/GenerateSimpleRelationships.php b/app/Console/Commands/GenerateSimpleRelationships.php index c8ed32e..74f8391 100644 --- a/app/Console/Commands/GenerateSimpleRelationships.php +++ b/app/Console/Commands/GenerateSimpleRelationships.php @@ -8,6 +8,7 @@ class GenerateSimpleRelationships extends Command { protected $signature = 'db:generate-simple-relationships'; + protected $description = '기본 논리적 관계 문서 생성'; public function handle() @@ -29,7 +30,7 @@ private function getKnownRelationships(): array 'user_tenants (hasMany)' => 'user_tenants.user_id → users.id', 'user_roles (hasMany)' => 'user_roles.user_id → users.id', 'audit_logs (hasMany)' => 'audit_logs.actor_id → users.id (생성자)', - ] + ], ], 'tenants' => [ 'description' => '테넌트 (회사/조직)', @@ -39,7 +40,7 @@ private function getKnownRelationships(): array 'departments (hasMany)' => 'departments.tenant_id → tenants.id', 'products (hasMany)' => 'products.tenant_id → tenants.id', 'orders (hasMany)' => 'orders.tenant_id → tenants.id', - ] + ], ], 'categories' => [ 'description' => '제품 카테고리 (계층구조)', @@ -48,7 +49,7 @@ private function getKnownRelationships(): array 'children (hasMany)' => 'categories.parent_id → categories.id', 'products (hasMany)' => 'products.category_id → categories.id', 'estimates (hasMany)' => 'estimates.model_set_id → categories.id (논리적)', - ] + ], ], 'products' => [ 'description' => '제품 마스터', @@ -57,7 +58,7 @@ private function getKnownRelationships(): array 'tenant (belongsTo)' => 'products.tenant_id → tenants.id', 'product_components (hasMany)' => 'product_components.parent_product_id → products.id (논리적)', 'order_items (hasMany)' => 'order_items.product_id → products.id', - ] + ], ], 'departments' => [ 'description' => '부서 관리 (계층구조)', @@ -65,7 +66,7 @@ private function getKnownRelationships(): array 'parent (belongsTo)' => 'departments.parent_id → departments.id (논리적)', 'children (hasMany)' => 'departments.parent_id → departments.id (논리적)', 'tenant (belongsTo)' => 'departments.tenant_id → tenants.id', - ] + ], ], 'estimates' => [ 'description' => '견적서 (스냅샷 데이터)', @@ -73,14 +74,14 @@ private function getKnownRelationships(): array 'category (belongsTo)' => 'estimates.model_set_id → categories.id (논리적)', 'tenant (belongsTo)' => 'estimates.tenant_id → tenants.id', 'estimate_items (hasMany)' => 'estimate_items.estimate_id → estimates.id (논리적)', - ] + ], ], 'estimate_items' => [ 'description' => '견적 아이템', 'relationships' => [ 'estimate (belongsTo)' => 'estimate_items.estimate_id → estimates.id (논리적)', 'tenant (belongsTo)' => 'estimate_items.tenant_id → tenants.id', - ] + ], ], 'product_components' => [ 'description' => 'BOM 구성요소 (통합 참조구조)', @@ -88,13 +89,13 @@ private function getKnownRelationships(): array 'parent_product (belongsTo)' => 'product_components.parent_product_id → products.id (논리적)', 'material_or_product (polymorphic)' => 'product_components.ref_id → materials.id OR products.id (ref_type 기반)', 'tenant (belongsTo)' => 'product_components.tenant_id → tenants.id', - ] + ], ], 'classifications' => [ 'description' => '분류 코드', 'relationships' => [ 'tenant (belongsTo)' => 'classifications.tenant_id → tenants.id (논리적)', - ] + ], ], ]; } @@ -145,4 +146,4 @@ private function generateDocument(array $relationships): void File::put(base_path('LOGICAL_RELATIONSHIPS_SIMPLE.md'), $content); $this->info('📄 문서 생성: LOGICAL_RELATIONSHIPS_SIMPLE.md'); } -} \ No newline at end of file +} diff --git a/app/Console/Commands/MakeModelWithRelationships.php b/app/Console/Commands/MakeModelWithRelationships.php index df3a4ba..eede951 100644 --- a/app/Console/Commands/MakeModelWithRelationships.php +++ b/app/Console/Commands/MakeModelWithRelationships.php @@ -9,6 +9,7 @@ class MakeModelWithRelationships extends Command { protected $signature = 'make:model-with-docs {name} {--migration} {--controller} {--resource}'; + protected $description = '모델 생성 후 자동으로 관계 문서 업데이트'; public function handle() @@ -17,9 +18,15 @@ public function handle() // 기본 모델 생성 $options = []; - if ($this->option('migration')) $options['--migration'] = true; - if ($this->option('controller')) $options['--controller'] = true; - if ($this->option('resource')) $options['--resource'] = true; + if ($this->option('migration')) { + $options['--migration'] = true; + } + if ($this->option('controller')) { + $options['--controller'] = true; + } + if ($this->option('resource')) { + $options['--resource'] = true; + } Artisan::call('make:model', array_merge(['name' => $modelName], $options)); $this->info("✅ 모델 생성 완료: {$modelName}"); @@ -36,8 +43,9 @@ private function addRelationshipTemplate(string $modelName): void { $modelPath = app_path("Models/{$modelName}.php"); - if (!File::exists($modelPath)) { + if (! File::exists($modelPath)) { $this->error("모델 파일을 찾을 수 없습니다: {$modelPath}"); + return; } @@ -65,12 +73,12 @@ private function addRelationshipTemplate(string $modelName): void // 클래스 끝 부분에 템플릿 삽입 $content = str_replace( - '}' . PHP_EOL, - $template . PHP_EOL . '}' . PHP_EOL, + '}'.PHP_EOL, + $template.PHP_EOL.'}'.PHP_EOL, $content ); File::put($modelPath, $content); $this->info("📝 관계 템플릿 추가: {$modelName}"); } -} \ No newline at end of file +} diff --git a/app/Console/Commands/PruneAuditLogs.php b/app/Console/Commands/PruneAuditLogs.php index 3d2273a..dd7ef99 100644 --- a/app/Console/Commands/PruneAuditLogs.php +++ b/app/Console/Commands/PruneAuditLogs.php @@ -9,6 +9,7 @@ class PruneAuditLogs extends Command { protected $signature = 'audit:prune {--days=}'; + protected $description = 'Delete audit logs older than given days (default: config(audit.retention_days)).'; public function handle(): int diff --git a/app/Console/Commands/SeedMenuPermissions.php b/app/Console/Commands/SeedMenuPermissions.php index 5f50120..6becd40 100644 --- a/app/Console/Commands/SeedMenuPermissions.php +++ b/app/Console/Commands/SeedMenuPermissions.php @@ -2,21 +2,22 @@ namespace App\Console\Commands; -use Illuminate\Console\Command; use App\Models\Commons\Menu; +use Illuminate\Console\Command; use Spatie\Permission\Models\Permission; use Spatie\Permission\PermissionRegistrar; class SeedMenuPermissions extends Command { protected $signature = 'sam:seed-menu-perms {--tenant=} {--guard=api}'; + protected $description = 'Create missing permissions menu:{id}.{action} for all menus'; public function handle(): int { - $tenant = $this->option('tenant') ? (int)$this->option('tenant') : null; - $guard = $this->option('guard') ?: 'api'; - $actions = config('authz.menu_actions', ['view','create','update','delete','approve']); + $tenant = $this->option('tenant') ? (int) $this->option('tenant') : null; + $guard = $this->option('guard') ?: 'api'; + $actions = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']); $menus = Menu::query() ->when($tenant !== null, fn ($q) => $q->where('tenant_id', $tenant)) @@ -25,13 +26,13 @@ public function handle(): int $count = 0; foreach ($menus as $m) { - app(PermissionRegistrar::class)->setPermissionsTeamId((int)$m->tenant_id); + app(PermissionRegistrar::class)->setPermissionsTeamId((int) $m->tenant_id); foreach ($actions as $act) { Permission::firstOrCreate([ - 'tenant_id' => (int)$m->tenant_id, + 'tenant_id' => (int) $m->tenant_id, 'guard_name' => $guard, - 'name' => "menu:{$m->id}.{$act}", + 'name' => "menu:{$m->id}.{$act}", ]); $count++; } @@ -39,6 +40,7 @@ public function handle(): int app(PermissionRegistrar::class)->forgetCachedPermissions(); $this->info("Ensured {$count} permissions."); + return self::SUCCESS; } } diff --git a/app/Console/Commands/TenantsBootstrap.php b/app/Console/Commands/TenantsBootstrap.php index 1fd05d2..bbd58d4 100644 --- a/app/Console/Commands/TenantsBootstrap.php +++ b/app/Console/Commands/TenantsBootstrap.php @@ -3,9 +3,9 @@ namespace App\Console\Commands; use App\Services\TenantBootstrapper; +use Illuminate\Console\Attributes\AsCommand; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use Illuminate\Console\Attributes\AsCommand; #[AsCommand(name: 'tenants:bootstrap', description: 'Bootstrap menus/capability/categories/settings for tenant(s)')] class TenantsBootstrap extends Command @@ -18,7 +18,7 @@ class TenantsBootstrap extends Command public function handle(TenantBootstrapper $svc): int { - $recipe = (string) $this->option('recipe'); + $recipe = (string) $this->option('recipe'); $tenantId = $this->option('tenant_id'); if ($this->option('all')) { @@ -27,11 +27,13 @@ public function handle(TenantBootstrapper $svc): int $ids = [(int) $tenantId]; } else { $this->error('Provide --tenant_id=ID or --all'); + return self::FAILURE; } if (empty($ids)) { $this->warn('No tenant to bootstrap.'); + return self::SUCCESS; } diff --git a/app/Console/Commands/UpdateLogicalRelationships.php b/app/Console/Commands/UpdateLogicalRelationships.php index 440b560..cd0815e 100644 --- a/app/Console/Commands/UpdateLogicalRelationships.php +++ b/app/Console/Commands/UpdateLogicalRelationships.php @@ -9,6 +9,7 @@ class UpdateLogicalRelationships extends Command { protected $signature = 'db:update-relationships'; + protected $description = '모델에서 논리적 관계를 추출하여 문서 업데이트'; public function handle() @@ -30,16 +31,20 @@ private function extractModelRelationships(): array $modelFiles = File::allFiles($modelPath); foreach ($modelFiles as $file) { - if ($file->getExtension() !== 'php') continue; + if ($file->getExtension() !== 'php') { + continue; + } $className = $this->getClassNameFromFile($file); - if (!$className || !class_exists($className)) continue; + if (! $className || ! class_exists($className)) { + continue; + } try { $reflection = new ReflectionClass($className); // 모델이 Eloquent Model인지 확인 - if (!$reflection->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) { + if (! $reflection->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) { continue; } @@ -50,15 +55,18 @@ private function extractModelRelationships(): array // 테이블 이름 직접 추출 $tableName = $this->getTableNameFromModel($className, $reflection); - if (!$tableName) continue; + if (! $tableName) { + continue; + } $relationships[$tableName] = [ 'model' => $className, - 'relationships' => $this->getModelRelationshipsFromFile($file, $className) + 'relationships' => $this->getModelRelationshipsFromFile($file, $className), ]; } catch (\Exception $e) { - $this->warn("모델 분석 실패: {$className} - " . $e->getMessage()); + $this->warn("모델 분석 실패: {$className} - ".$e->getMessage()); + continue; } } @@ -73,7 +81,7 @@ private function getTableNameFromModel(string $className, ReflectionClass $refle $tableName = strtolower(preg_replace('/(? $type, 'related_model' => '(Polymorphic)', 'foreign_key' => null, - 'local_key' => null + 'local_key' => null, ]; + continue; } @@ -128,7 +137,7 @@ private function getModelRelationshipsFromFile($file, string $className): array 'type' => $type, 'related_model' => $fullyQualifiedClass, 'foreign_key' => null, - 'local_key' => null + 'local_key' => null, ]; } } @@ -196,7 +205,7 @@ private function resolveClassName(string $className, array $useStatements, ?stri // 같은 namespace에 있다고 가정 if ($currentNamespace) { - return $currentNamespace . '\\' . $className; + return $currentNamespace.'\\'.$className; } // 그 외의 경우 그대로 반환 @@ -223,7 +232,7 @@ private function getModelRelationships(ReflectionClass $reflection, $model): arr 'type' => $this->getRelationshipType($result), 'related_model' => get_class($result->getRelated()), 'foreign_key' => $this->getForeignKey($result), - 'local_key' => $this->getLocalKey($result) + 'local_key' => $this->getLocalKey($result), ]; } } catch (\Exception $e) { @@ -243,6 +252,7 @@ private function isRelationshipMethod($result): bool private function getRelationshipType($relation): string { $className = get_class($relation); + return class_basename($className); } @@ -264,15 +274,15 @@ private function getClassNameFromFile($file): ?string { $content = File::get($file->getRealPath()); - if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatches)) { + if (! preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatches)) { return null; } - if (!preg_match('/class\s+(\w+)/', $content, $classMatches)) { + if (! preg_match('/class\s+(\w+)/', $content, $classMatches)) { return null; } - return $namespaceMatches[1] . '\\' . $classMatches[1]; + return $namespaceMatches[1].'\\'.$classMatches[1]; } private function updateLogicalDocument(array $relationships): void @@ -287,7 +297,9 @@ private function updateLogicalDocument(array $relationships): void $content .= "## 📊 모델별 관계 현황\n\n"; foreach ($relationships as $tableName => $info) { - if (empty($info['relationships'])) continue; + if (empty($info['relationships'])) { + continue; + } $content .= "### {$tableName}\n"; $content .= "**모델**: `{$info['model']}`\n\n"; @@ -297,12 +309,14 @@ private function updateLogicalDocument(array $relationships): void // Polymorphic 관계는 특별 표시 if ($rel['related_model'] === '(Polymorphic)') { $content .= "- **{$rel['method']}()**: {$rel['type']} → `(Polymorphic)`\n"; + continue; } // 관련 모델 클래스가 존재하는지 확인 - if (!class_exists($rel['related_model'])) { + if (! class_exists($rel['related_model'])) { $this->warn("모델 클래스가 존재하지 않음: {$rel['related_model']}"); + continue; } @@ -315,7 +329,8 @@ private function updateLogicalDocument(array $relationships): void $content .= "\n"; } catch (\Exception $e) { - $this->warn("관계 처리 실패: {$rel['method']} - " . $e->getMessage()); + $this->warn("관계 처리 실패: {$rel['method']} - ".$e->getMessage()); + continue; } } @@ -326,4 +341,4 @@ private function updateLogicalDocument(array $relationships): void File::put($documentPath, $content); $this->info("📄 문서 업데이트: {$documentPath}"); } -} \ No newline at end of file +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 74fe00c..36c552c 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,10 +2,10 @@ namespace App\Console; +use App\Console\Commands\GenerateSimpleRelationships; +use App\Console\Commands\MakeModelWithRelationships; use App\Console\Commands\PruneAuditLogs; use App\Console\Commands\UpdateLogicalRelationships; -use App\Console\Commands\MakeModelWithRelationships; -use App\Console\Commands\GenerateSimpleRelationships; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; diff --git a/app/Enums/EstimateStatus.php b/app/Enums/EstimateStatus.php index d057b53..2c2a595 100644 --- a/app/Enums/EstimateStatus.php +++ b/app/Enums/EstimateStatus.php @@ -1,4 +1,5 @@ '임시저장', + return match ($this) { + self::Draft => '임시저장', self::Submitted => '제출/확정', self::Approved => '승인', self::Rejected => '반려', @@ -30,17 +31,17 @@ public function label(): string */ enum OrderStatus: string { - case Created = 'created'; - case Confirmed = 'confirmed'; + case Created = 'created'; + case Confirmed = 'confirmed'; case InProgress = 'in_progress'; - case Completed = 'completed'; - case Cancelled = 'cancelled'; + case Completed = 'completed'; + case Cancelled = 'cancelled'; // 한글라벨 등 커스텀 메서드 public function label(): string { - return match($this) { - self::Created => '등록', + return match ($this) { + self::Created => '등록', self::Confirmed => '승인', self::InProgress => '진행중', self::Completed => '완료', diff --git a/app/Enums/OrderStatus.php b/app/Enums/OrderStatus.php index d057b53..2c2a595 100644 --- a/app/Enums/OrderStatus.php +++ b/app/Enums/OrderStatus.php @@ -1,4 +1,5 @@ '임시저장', + return match ($this) { + self::Draft => '임시저장', self::Submitted => '제출/확정', self::Approved => '승인', self::Rejected => '반려', @@ -30,17 +31,17 @@ public function label(): string */ enum OrderStatus: string { - case Created = 'created'; - case Confirmed = 'confirmed'; + case Created = 'created'; + case Confirmed = 'confirmed'; case InProgress = 'in_progress'; - case Completed = 'completed'; - case Cancelled = 'cancelled'; + case Completed = 'completed'; + case Cancelled = 'cancelled'; // 한글라벨 등 커스텀 메서드 public function label(): string { - return match($this) { - self::Created => '등록', + return match ($this) { + self::Created => '등록', self::Confirmed => '승인', self::InProgress => '진행중', self::Completed => '완료', diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b26b795..0e34db7 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -4,6 +4,7 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Illuminate\Support\Facades\Http; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -11,7 +12,6 @@ use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Throwable; -use Illuminate\Support\Facades\Http; class Handler extends ExceptionHandler { @@ -36,16 +36,18 @@ protected function sendSlackException(Throwable $e): void try { $url = env('LOG_SLACK_WEBHOOK_URL'); - if (!$url) return; + if (! $url) { + return; + } $ip = request()?->ip() ?? 'N/A'; // 요청이 없는 경우 대비 Http::post($url, [ - 'text' => "*[Laravel] 예외 발생!*\n" . - "• 메시지: `{$e->getMessage()}`\n" . - "• 위치: `{$e->getFile()}:{$e->getLine()}`\n" . - "• 시간: " . now()->toDateTimeString() . "\n" . - "• IP: `{$ip}`" + 'text' => "*[Laravel] 예외 발생!*\n". + "• 메시지: `{$e->getMessage()}`\n". + "• 위치: `{$e->getFile()}:{$e->getLine()}`\n". + '• 시간: '.now()->toDateTimeString()."\n". + "• IP: `{$ip}`", ]); } catch (Throwable $ex) { logger()->error('슬랙 전송 실패', ['message' => $ex->getMessage()]); diff --git a/app/Helpers/ApiResponse.php b/app/Helpers/ApiResponse.php index b809809..b7a285c 100644 --- a/app/Helpers/ApiResponse.php +++ b/app/Helpers/ApiResponse.php @@ -2,14 +2,14 @@ namespace App\Helpers; -use Illuminate\Support\Facades\DB; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\DB; use Symfony\Component\HttpKernel\Exception\HttpException; class ApiResponse { - - function normalizeFiles(array $laravelFiles): array { + public function normalizeFiles(array $laravelFiles): array + { $files = ['name' => [], 'type' => [], 'tmp_name' => [], 'size' => [], 'fileType' => []]; foreach ($laravelFiles as $file) { $files['name'][] = $file->getClientOriginalName(); @@ -18,10 +18,11 @@ function normalizeFiles(array $laravelFiles): array { $files['size'][] = $file->getSize(); $files['fileType'][] = ''; } + return $files; } - # DebugQuery Helper + // DebugQuery Helper public static function debugQueryLog(): array { $logs = DB::getQueryLog(); @@ -31,17 +32,18 @@ public static function debugQueryLog(): array ->map(function ($log) { $query = $log['query']; foreach ($log['bindings'] as $binding) { - $binding = is_numeric($binding) ? $binding : "'" . addslashes($binding) . "'"; + $binding = is_numeric($binding) ? $binding : "'".addslashes($binding)."'"; $query = preg_replace('/\\?/', $binding, $query, 1); } // \n 제거 $query = str_replace(["\n", "\r"], ' ', $query)." (time: {$log['time']})"; + return trim($query); })->toArray(); } - # ApiResponse Helper + // ApiResponse Helper public static function success( $data = null, string $message = '요청 성공', @@ -52,7 +54,10 @@ public static function success( 'message' => $message, 'data' => $data, ]; - if(!empty($debug)) $response['query'] = $debug; + if (! empty($debug)) { + $response['query'] = $debug; + } + return response()->json($response); } @@ -85,7 +90,7 @@ public static function response($type = '', $query = '', $key = ''): array $debug = app()->environment('local') && request()->is('api/*'); $result = match ($type) { - 'get' => $key ? $query->get()->keyBy($key) : $query->get(), + 'get' => $key ? $query->get()->keyBy($key) : $query->get(), 'getSub' => $query->get(), 'count' => $query->count(), 'first' => $query->first(), @@ -94,7 +99,7 @@ public static function response($type = '', $query = '', $key = ''): array default => null, }; - if($type=='getSub'){ + if ($type == 'getSub') { $array = $result->map(function ($item) { return (array) $item; })->toArray(); @@ -118,8 +123,7 @@ public static function response($type = '', $query = '', $key = ''): array public static function handle( callable $callback, string $responseTitle = '요청' - ): JsonResponse - { + ): JsonResponse { try { $result = $callback(); @@ -133,12 +137,12 @@ public static function handle( // ['error' => 'NO_TENANT', 'code' => 400] // ['code' => 404, 'message' => '데이터 없음'] if (is_array($result) && ( - array_key_exists('error', $result) || - (array_key_exists('code', $result) && is_numeric($result['code']) && (int)$result['code'] >= 400) - ) + array_key_exists('error', $result) || + (array_key_exists('code', $result) && is_numeric($result['code']) && (int) $result['code'] >= 400) + ) ) { - $code = (int)($result['code'] ?? 400); - $message = (string)($result['message'] ?? ($result['error'] ?? ($responseTitle.' 실패'))); + $code = (int) ($result['code'] ?? 400); + $message = (string) ($result['message'] ?? ($result['error'] ?? ($responseTitle.' 실패'))); $details = $result['details'] ?? null; // 에러에도 쿼리 로그 포함되도록 error()가 처리하게 맡김 @@ -147,11 +151,11 @@ public static function handle( // 표준 박스( ['data'=>..., 'query'=>...] ) 하위호환 if (is_array($result) && array_key_exists('data', $result)) { - $data = $result['data']; + $data = $result['data']; $debug = $result['query'] ?? []; } else { // 그냥 도메인 결과만 반환한 경우 - $data = $result; + $data = $result; $debug = (app()->environment('local') && request()->is('api/*')) ? self::debugQueryLog() : []; diff --git a/app/Http/Controllers/Api/V1/AdminController.php b/app/Http/Controllers/Api/V1/AdminController.php index ee4d82d..50e10e4 100644 --- a/app/Http/Controllers/Api/V1/AdminController.php +++ b/app/Http/Controllers/Api/V1/AdminController.php @@ -2,14 +2,13 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\AdminService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class AdminController extends Controller { - public function index(Request $request) { return ApiResponse::handle(function () use ($request) { diff --git a/app/Http/Controllers/Api/V1/ApiController.php b/app/Http/Controllers/Api/V1/ApiController.php index 90a2e9b..f6b952e 100644 --- a/app/Http/Controllers/Api/V1/ApiController.php +++ b/app/Http/Controllers/Api/V1/ApiController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Api\V1; -use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Models\Members\User; use Illuminate\Http\Request; @@ -11,32 +10,27 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; - class ApiController extends Controller { - - public function debugApikey() { $message = 'API Key 인증 성공'; + return response()->json(['message' => $message]); } - - public function login(Request $request) { $userId = $request->input('user_id'); $userPwd = $request->input('user_pwd'); - if (!$userId || !$userPwd) { + if (! $userId || ! $userPwd) { return response()->json(['error' => '아이디 또는 비밀번호 누락'], 400); } - $user = User::where('user_id', $userId)->first(); - if (!$user) { + if (! $user) { return response()->json(['error' => '사용자를 찾을 수 없습니다.'], 404); } @@ -50,11 +44,11 @@ public function login(Request $request) $isValid = strtoupper(hash('sha256', $userPwd)) === strtoupper($user->password); } - if (!$isValid) { + if (! $isValid) { return response()->json(['error' => '아이디 또는 비밀번호가 올바르지 않습니다.'], 401); } - //인증토큰 생성 + // 인증토큰 생성 $token = $user->createToken('front-app')->plainTextToken; // 선택: DB에 신규 token 저장 @@ -70,21 +64,20 @@ public function login(Request $request) public function logout(Request $request) { - //인증토큰 삭제 + // 인증토큰 삭제 $request->user()->currentAccessToken()->delete(); return response()->json(['message' => '로그아웃 완료']); } - public function signup(Request $request) { // 신규 회원 생성 + 역할 부여 지원 $v = Validator::make($request->all(), [ - 'user_id' => 'required|string|max:255|unique:users,user_id', - 'name' => 'required|string|max:255', - 'email' => 'required|email|max:100|unique:users,email', - 'phone' => 'nullable|string|max:30', + 'user_id' => 'required|string|max:255|unique:users,user_id', + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:100|unique:users,email', + 'phone' => 'nullable|string|max:30', 'password' => 'required|string|min:8|max:64', ]); @@ -97,15 +90,14 @@ public function signup(Request $request) return DB::transaction(function () use ($payload) { // 신규 사용자 생성 $user = User::create([ - 'user_id' => $payload['user_id'], - 'name' => $payload['name'], - 'email' => $payload['email'], - 'phone' => $payload['phone'] ?? null, + 'user_id' => $payload['user_id'], + 'name' => $payload['name'], + 'email' => $payload['email'], + 'phone' => $payload['phone'] ?? null, 'password' => $payload['password'], // 캐스트가 알아서 해싱 ]); - return ['user' => $user->only(['id','user_id','name','email','phone'])]; + return ['user' => $user->only(['id', 'user_id', 'name', 'email', 'phone'])]; }); } - } diff --git a/app/Http/Controllers/Api/V1/CategoryController.php b/app/Http/Controllers/Api/V1/CategoryController.php index 1b47506..5767f5a 100644 --- a/app/Http/Controllers/Api/V1/CategoryController.php +++ b/app/Http/Controllers/Api/V1/CategoryController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\CategoryService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class CategoryController extends Controller { diff --git a/app/Http/Controllers/Api/V1/CategoryFieldController.php b/app/Http/Controllers/Api/V1/CategoryFieldController.php index 95b950e..ffec341 100644 --- a/app/Http/Controllers/Api/V1/CategoryFieldController.php +++ b/app/Http/Controllers/Api/V1/CategoryFieldController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use App\Services\CategoryFieldService; use App\Helpers\ApiResponse; +use App\Http\Controllers\Controller; +use App\Services\CategoryFieldService; +use Illuminate\Http\Request; class CategoryFieldController extends Controller { @@ -48,6 +48,7 @@ public function destroy(int $field) { return ApiResponse::handle(function () use ($field) { $this->service->destroy($field); + return 'success'; }, '카테고리 필드 삭제'); } @@ -57,6 +58,7 @@ public function reorder(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->service->reorder($id, $request->input()); + return 'success'; }, '카테고리 필드 정렬 저장'); } diff --git a/app/Http/Controllers/Api/V1/CategoryLogController.php b/app/Http/Controllers/Api/V1/CategoryLogController.php index b99c198..7dc2333 100644 --- a/app/Http/Controllers/Api/V1/CategoryLogController.php +++ b/app/Http/Controllers/Api/V1/CategoryLogController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use App\Services\CategoryLogService; use App\Helpers\ApiResponse; +use App\Http\Controllers\Controller; +use App\Services\CategoryLogService; +use Illuminate\Http\Request; class CategoryLogController extends Controller { diff --git a/app/Http/Controllers/Api/V1/CategoryTemplateController.php b/app/Http/Controllers/Api/V1/CategoryTemplateController.php index cd92cb1..b9e2aa4 100644 --- a/app/Http/Controllers/Api/V1/CategoryTemplateController.php +++ b/app/Http/Controllers/Api/V1/CategoryTemplateController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use App\Services\CategoryTemplateService; use App\Helpers\ApiResponse; +use App\Http\Controllers\Controller; +use App\Services\CategoryTemplateService; +use Illuminate\Http\Request; class CategoryTemplateController extends Controller { @@ -48,6 +48,7 @@ public function destroy(int $tpl) { return ApiResponse::handle(function () use ($tpl) { $this->service->destroy($tpl); + return 'success'; }, '카테고리 템플릿 삭제'); } @@ -57,6 +58,7 @@ public function apply(int $id, int $tpl) { return ApiResponse::handle(function () use ($id, $tpl) { $this->service->apply($id, $tpl); + return 'success'; }, '카테고리 템플릿 적용'); } @@ -73,7 +75,7 @@ public function preview(int $id, int $tpl) public function diff(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->diff($id, (int)$request->query('a'), (int)$request->query('b')); + return $this->service->diff($id, (int) $request->query('a'), (int) $request->query('b')); }, '카테고리 템플릿 비교'); } } diff --git a/app/Http/Controllers/Api/V1/ClassificationController.php b/app/Http/Controllers/Api/V1/ClassificationController.php index 23983f8..011468d 100644 --- a/app/Http/Controllers/Api/V1/ClassificationController.php +++ b/app/Http/Controllers/Api/V1/ClassificationController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\ClassificationService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class ClassificationController extends Controller { @@ -13,27 +13,26 @@ public function __construct(private ClassificationService $service) {} public function index(Request $request) { - return ApiResponse::handle(fn() => $this->service->index($request->all()), '분류 목록 조회'); + return ApiResponse::handle(fn () => $this->service->index($request->all()), '분류 목록 조회'); } public function show(string $id) { - return ApiResponse::handle(fn() => $this->service->show((int)$id), '분류 단건 조회'); + return ApiResponse::handle(fn () => $this->service->show((int) $id), '분류 단건 조회'); } public function store(Request $request) { - return ApiResponse::handle(fn() => $this->service->store($request->all()), '분류 생성'); + return ApiResponse::handle(fn () => $this->service->store($request->all()), '분류 생성'); } public function update(string $id, Request $request) { - return ApiResponse::handle(fn() => $this->service->update((int)$id, $request->all()), '분류 수정'); + return ApiResponse::handle(fn () => $this->service->update((int) $id, $request->all()), '분류 수정'); } public function destroy(string $id) { - return ApiResponse::handle(fn() => $this->service->destroy((int)$id), '분류 삭제'); + return ApiResponse::handle(fn () => $this->service->destroy((int) $id), '분류 삭제'); } - } diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index f8a3f03..f39d0f0 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers\Api\V1; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\ClientService; -use App\Helpers\ApiResponse; use Illuminate\Http\Request; class ClientController extends Controller @@ -20,6 +20,7 @@ public function index(Request $request) { return ApiResponse::handle(function () use ($request) { $data = $this->service->index($request->all()); + return ['data' => $data, 'message' => __('message.fetched')]; }); } @@ -28,6 +29,7 @@ public function show(int $id) { return ApiResponse::handle(function () use ($id) { $data = $this->service->show($id); + return ['data' => $data, 'message' => __('message.fetched')]; }); } @@ -36,6 +38,7 @@ public function store(Request $request) { return ApiResponse::handle(function () use ($request) { $data = $this->service->store($request->all()); + return ['data' => $data, 'message' => __('message.created')]; }); } @@ -44,6 +47,7 @@ public function update(Request $request, int $id) { return ApiResponse::handle(function () use ($request, $id) { $data = $this->service->update($id, $request->all()); + return ['data' => $data, 'message' => __('message.updated')]; }); } @@ -52,6 +56,7 @@ public function destroy(int $id) { return ApiResponse::handle(function () use ($id) { $this->service->destroy($id); + return ['data' => null, 'message' => __('message.deleted')]; }); } @@ -60,6 +65,7 @@ public function toggle(int $id) { return ApiResponse::handle(function () use ($id) { $data = $this->service->toggle($id); + return ['data' => $data, 'message' => __('message.updated')]; }); } diff --git a/app/Http/Controllers/Api/V1/ClientGroupController.php b/app/Http/Controllers/Api/V1/ClientGroupController.php index bfd0d46..1928da6 100644 --- a/app/Http/Controllers/Api/V1/ClientGroupController.php +++ b/app/Http/Controllers/Api/V1/ClientGroupController.php @@ -69,4 +69,4 @@ public function toggle(int $id) return ['data' => $data, 'message' => __('message.updated')]; }); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/CommonController.php b/app/Http/Controllers/Api/V1/CommonController.php index fa7478e..34abbc1 100644 --- a/app/Http/Controllers/Api/V1/CommonController.php +++ b/app/Http/Controllers/Api/V1/CommonController.php @@ -21,18 +21,23 @@ class CommonController * description="테넌트의 활성화된 공통 코드 목록을 조회합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}}}, + * * @OA\Response( * response=200, * description="공통 코드 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="공통코드"), * @OA\Property( * property="data", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="code_group", type="string", example="product_type"), * @OA\Property(property="code", type="string", example="PRODUCT"), * @OA\Property(property="name", type="string", example="제품"), @@ -61,6 +66,7 @@ public static function getComeCode() * description="전체 공통 코드 목록을 조회합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="공통 코드 목록 조회 성공" @@ -69,7 +75,7 @@ public static function getComeCode() */ public function list(Request $request) { - return ApiResponse::handle(function () use ($request) { + return ApiResponse::handle(function () { // Service implementation needed return []; }, __('message.fetched')); @@ -82,13 +88,16 @@ public function list(Request $request) * description="특정 그룹의 공통 코드 목록을 조회합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="group", * in="path", * required=true, * description="코드 그룹", + * * @OA\Schema(type="string", example="product_type") * ), + * * @OA\Response( * response=200, * description="그룹 코드 조회 성공" @@ -97,7 +106,7 @@ public function list(Request $request) */ public function index(Request $request, string $group) { - return ApiResponse::handle(function () use ($group) { + return ApiResponse::handle(function () { // Service implementation needed return []; }, __('message.fetched')); @@ -110,16 +119,20 @@ public function index(Request $request, string $group) * description="새로운 공통 코드를 생성합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="code_group", type="string", example="product_type"), * @OA\Property(property="code", type="string", example="SERVICE"), * @OA\Property(property="name", type="string", example="서비스"), * @OA\Property(property="description", type="string", example="서비스 상품") * ) * ), + * * @OA\Response( * response=201, * description="공통 코드 생성 성공" @@ -127,22 +140,26 @@ public function index(Request $request, string $group) * @OA\Response( * response=409, * description="중복된 공통 코드", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="중복된 공통 코드가 존재합니다.") * ) * ), + * * @OA\Response( * response=422, * description="유효성 검사 실패", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) */ public function store(Request $request) { - return ApiResponse::handle(function () use ($request) { + return ApiResponse::handle(function () { // Service implementation needed return []; }, __('message.settings.common_code_saved')); @@ -155,21 +172,27 @@ public function store(Request $request) * description="기존 공통 코드를 수정합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="공통 코드 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="name", type="string", example="수정된 이름"), * @OA\Property(property="description", type="string", example="수정된 설명") * ) * ), + * * @OA\Response( * response=200, * description="공통 코드 수정 성공" @@ -177,22 +200,26 @@ public function store(Request $request) * @OA\Response( * response=404, * description="공통 코드를 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="해당 공통 코드를 찾을 수 없습니다.") * ) * ), + * * @OA\Response( * response=422, * description="유효성 검사 실패", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) */ public function update(Request $request, int $id) { - return ApiResponse::handle(function () use ($request, $id) { + return ApiResponse::handle(function () { // Service implementation needed return []; }, __('message.updated')); @@ -205,13 +232,16 @@ public function update(Request $request, int $id) * description="공통 코드를 삭제합니다.", * tags={"Settings - Common Codes"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="공통 코드 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="공통 코드 삭제 성공" @@ -219,8 +249,10 @@ public function update(Request $request, int $id) * @OA\Response( * response=404, * description="공통 코드를 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="해당 공통 코드를 찾을 수 없습니다.") * ) @@ -229,7 +261,7 @@ public function update(Request $request, int $id) */ public function destroy(Request $request, int $id) { - return ApiResponse::handle(function () use ($id) { + return ApiResponse::handle(function () { // Service implementation needed return []; }, __('message.deleted')); diff --git a/app/Http/Controllers/Api/V1/DepartmentController.php b/app/Http/Controllers/Api/V1/DepartmentController.php index f5275fd..00b4ae0 100644 --- a/app/Http/Controllers/Api/V1/DepartmentController.php +++ b/app/Http/Controllers/Api/V1/DepartmentController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\DepartmentService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class DepartmentController extends Controller { @@ -39,7 +39,7 @@ public function show(int $id, Request $request) public function update($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->update((int)$id, $request->all()); + return $this->service->update((int) $id, $request->all()); }, '부서 수정'); } @@ -47,7 +47,7 @@ public function update($id, Request $request) public function destroy($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->destroy((int)$id, $request->all()); + return $this->service->destroy((int) $id, $request->all()); }, '부서 삭제'); } @@ -55,7 +55,7 @@ public function destroy($id, Request $request) public function listUsers($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->listUsers((int)$id, $request->all()); + return $this->service->listUsers((int) $id, $request->all()); }, '부서 사용자 목록'); } @@ -63,7 +63,7 @@ public function listUsers($id, Request $request) public function attachUser($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->attachUser((int)$id, $request->all()); + return $this->service->attachUser((int) $id, $request->all()); }, '부서 사용자 배정'); } @@ -71,7 +71,7 @@ public function attachUser($id, Request $request) public function detachUser($id, $user, Request $request) { return ApiResponse::handle(function () use ($id, $user, $request) { - return $this->service->detachUser((int)$id, (int)$user, $request->all()); + return $this->service->detachUser((int) $id, (int) $user, $request->all()); }, '부서 사용자 제거'); } @@ -79,7 +79,7 @@ public function detachUser($id, $user, Request $request) public function setPrimary($id, $user, Request $request) { return ApiResponse::handle(function () use ($id, $user, $request) { - return $this->service->setPrimary((int)$id, (int)$user, $request->all()); + return $this->service->setPrimary((int) $id, (int) $user, $request->all()); }, '주 부서 설정/해제'); } @@ -87,7 +87,7 @@ public function setPrimary($id, $user, Request $request) public function listPermissions($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->listPermissions((int)$id, $request->all()); + return $this->service->listPermissions((int) $id, $request->all()); }, '부서 권한 목록'); } @@ -95,7 +95,7 @@ public function listPermissions($id, Request $request) public function upsertPermissions($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return $this->service->upsertPermissions((int)$id, $request->all()); + return $this->service->upsertPermissions((int) $id, $request->all()); }, '부서 권한 부여/차단'); } @@ -103,7 +103,7 @@ public function upsertPermissions($id, Request $request) public function revokePermissions($id, $permission, Request $request) { return ApiResponse::handle(function () use ($id, $permission, $request) { - return $this->service->revokePermissions((int)$id, (int)$permission, $request->all()); + return $this->service->revokePermissions((int) $id, (int) $permission, $request->all()); }, '부서 권한 제거'); } } diff --git a/app/Http/Controllers/Api/V1/Design/AuditLogController.php b/app/Http/Controllers/Api/V1/Design/AuditLogController.php index 3b6f4ae..e1ff0ad 100644 --- a/app/Http/Controllers/Api/V1/Design/AuditLogController.php +++ b/app/Http/Controllers/Api/V1/Design/AuditLogController.php @@ -17,6 +17,7 @@ public function index(AuditLogIndexRequest $request) { return ApiResponse::handle(function () use ($request) { $filters = $request->validated(); + return $this->service->paginate($filters); }, __('message.fetched')); } diff --git a/app/Http/Controllers/Api/V1/Design/BomCalculationController.php b/app/Http/Controllers/Api/V1/Design/BomCalculationController.php index a01e94c..2577391 100644 --- a/app/Http/Controllers/Api/V1/Design/BomCalculationController.php +++ b/app/Http/Controllers/Api/V1/Design/BomCalculationController.php @@ -2,14 +2,14 @@ namespace App\Http\Controllers\Api\V1\Design; -use App\Http\Controllers\Controller; -use App\Services\Design\BomCalculationService; -use App\Http\Requests\Design\GetEstimateParametersRequest; -use App\Http\Requests\Design\CalculateBomRequest; -use App\Http\Requests\Design\SaveCompanyFormulaRequest; use App\Helpers\ApiResponse; -use Illuminate\Http\Request; +use App\Http\Controllers\Controller; +use App\Http\Requests\Design\CalculateBomRequest; +use App\Http\Requests\Design\GetEstimateParametersRequest; +use App\Http\Requests\Design\SaveCompanyFormulaRequest; +use App\Services\Design\BomCalculationService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class BomCalculationController extends Controller { @@ -30,7 +30,7 @@ public function getEstimateParameters(GetEstimateParametersRequest $request, int return [ 'success' => true, 'data' => $result, - 'message' => __('message.fetched') + 'message' => __('message.fetched'), ]; }); } @@ -46,18 +46,18 @@ public function calculateBom(CalculateBomRequest $request, int $bomTemplateId): $data['company_name'] ?? null ); - if (!$result['success']) { + if (! $result['success']) { return [ 'success' => false, 'message' => __('error.calculation_failed'), - 'error' => $result['error'] + 'error' => $result['error'], ]; } return [ 'success' => true, 'data' => $result['data'], - 'message' => __('message.calculation.completed') + 'message' => __('message.calculation.completed'), ]; }); } @@ -70,7 +70,7 @@ public function getCompanyFormulas(string $companyName): JsonResponse return [ 'success' => true, 'data' => $result, - 'message' => __('message.fetched') + 'message' => __('message.fetched'), ]; }); } @@ -85,7 +85,7 @@ public function saveCompanyFormula(SaveCompanyFormulaRequest $request, string $c return [ 'success' => true, 'data' => $result, - 'message' => __('message.company_formula.created') + 'message' => __('message.company_formula.created'), ]; }); } @@ -95,7 +95,7 @@ public function testFormula(Request $request): JsonResponse return ApiResponse::handle(function () use ($request) { $request->validate([ 'formula_expression' => 'required|string', - 'test_parameters' => 'required|array' + 'test_parameters' => 'required|array', ]); $startTime = microtime(true); @@ -115,10 +115,10 @@ public function testFormula(Request $request): JsonResponse 'formula_expression' => $request->input('formula_expression'), 'input_parameters' => $request->input('test_parameters'), 'result' => $result, - 'execution_time_ms' => round($executionTime, 2) + 'execution_time_ms' => round($executionTime, 2), ], - 'message' => __('message.formula.test_completed') + 'message' => __('message.formula.test_completed'), ]; }); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/Design/BomTemplateController.php b/app/Http/Controllers/Api/V1/Design/BomTemplateController.php index a5070ae..4da2b71 100644 --- a/app/Http/Controllers/Api/V1/Design/BomTemplateController.php +++ b/app/Http/Controllers/Api/V1/Design/BomTemplateController.php @@ -28,10 +28,11 @@ public function upsertTemplate(UpsertRequest $request, int $versionId) { return ApiResponse::handle(function () use ($request, $versionId) { $payload = $request->validated(); + return $this->service->upsertTemplate( modelVersionId: $versionId, name: $payload['name'] ?? 'Main', - isPrimary: (bool)($payload['is_primary'] ?? true), + isPrimary: (bool) ($payload['is_primary'] ?? true), notes: $payload['notes'] ?? null ); }, __('message.created')); @@ -39,7 +40,7 @@ public function upsertTemplate(UpsertRequest $request, int $versionId) public function show(int $templateId) { - return ApiResponse::handle(fn() => $this->service->show($templateId, true), __('message.fetched')); + return ApiResponse::handle(fn () => $this->service->show($templateId, true), __('message.fetched')); } public function replaceItems(ReplaceItemsRequest $request, int $templateId) @@ -47,6 +48,7 @@ public function replaceItems(ReplaceItemsRequest $request, int $templateId) return ApiResponse::handle(function () use ($request, $templateId) { $payload = $request->validated(); $this->service->replaceItems($templateId, $payload['items']); + return null; }, __('message.bom.bulk_upsert')); } @@ -55,6 +57,7 @@ public function diff(int $templateId, DiffRequest $request) { return ApiResponse::handle(function () use ($templateId, $request) { $otherId = $request->validated()['other_template_id']; + return $this->service->diffTemplates($templateId, $otherId); }, __('message.design.template_diff')); } @@ -67,9 +70,10 @@ public function cloneTemplate(int $templateId, CloneRequest $request) templateId: $templateId, targetVersionId: $payload['target_version_id'] ?? null, name: $payload['name'] ?? null, - isPrimary: (bool)($payload['is_primary'] ?? false), + isPrimary: (bool) ($payload['is_primary'] ?? false), notes: $payload['notes'] ?? null ); + return $tpl; }, __('message.design.template_cloned')); } diff --git a/app/Http/Controllers/Api/V1/Design/DesignModelController.php b/app/Http/Controllers/Api/V1/Design/DesignModelController.php index 30059bb..aae554e 100644 --- a/app/Http/Controllers/Api/V1/Design/DesignModelController.php +++ b/app/Http/Controllers/Api/V1/Design/DesignModelController.php @@ -4,10 +4,10 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; -use App\Services\Design\ModelService; use App\Http\Requests\Common\PaginateRequest; use App\Http\Requests\Design\Model\StoreRequest; use App\Http\Requests\Design\Model\UpdateRequest; +use App\Services\Design\ModelService; class DesignModelController extends Controller { @@ -19,11 +19,11 @@ public function __construct( public function index(PaginateRequest $request) { return ApiResponse::handle(function () use ($request) { - $v = $request->validatedOrDefaults(); // page/size 기본값 주입됨 - $q = $v['q'] ?? ''; - $page = (int)($v['page'] ?? 1); - $size = (int)($v['size'] ?? 20); - $sort = $v['sort'] ?? null; // id|code|name|created_at + $v = $request->validatedOrDefaults(); // page/size 기본값 주입됨 + $q = $v['q'] ?? ''; + $page = (int) ($v['page'] ?? 1); + $size = (int) ($v['size'] ?? 20); + $sort = $v['sort'] ?? null; // id|code|name|created_at $order = $v['order'] ?? 'desc'; // asc|desc return $this->service->list($q, $page, $size); @@ -33,7 +33,7 @@ public function index(PaginateRequest $request) public function store(StoreRequest $request) { return ApiResponse::handle( - fn() => $this->service->create($request->validated()), + fn () => $this->service->create($request->validated()), __('message.created') ); } @@ -41,7 +41,7 @@ public function store(StoreRequest $request) public function show(int $id) { return ApiResponse::handle( - fn() => $this->service->find($id), + fn () => $this->service->find($id), __('message.fetched') ); } @@ -49,7 +49,7 @@ public function show(int $id) public function update(UpdateRequest $request, int $id) { return ApiResponse::handle( - fn() => $this->service->update($id, $request->validated()), + fn () => $this->service->update($id, $request->validated()), __('message.updated') ); } @@ -58,6 +58,7 @@ public function destroy(int $id) { return ApiResponse::handle(function () use ($id) { $this->service->delete($id); + return null; }, __('message.deleted')); } diff --git a/app/Http/Controllers/Api/V1/Design/ModelVersionController.php b/app/Http/Controllers/Api/V1/Design/ModelVersionController.php index cf6a18a..ba87854 100644 --- a/app/Http/Controllers/Api/V1/Design/ModelVersionController.php +++ b/app/Http/Controllers/Api/V1/Design/ModelVersionController.php @@ -15,19 +15,20 @@ public function __construct( public function index(int $modelId) { - return ApiResponse::handle(fn() => $this->service->listByModel($modelId), __('message.fetched')); + return ApiResponse::handle(fn () => $this->service->listByModel($modelId), __('message.fetched')); } public function createDraft(CreateDraftRequest $request, int $modelId) { return ApiResponse::handle(function () use ($request, $modelId) { $payload = $request->validated(); + return $this->service->createDraft($modelId, $payload); }, __('message.created')); } public function release(int $versionId) { - return ApiResponse::handle(fn() => $this->service->release($versionId), __('message.updated')); + return ApiResponse::handle(fn () => $this->service->release($versionId), __('message.updated')); } } diff --git a/app/Http/Controllers/Api/V1/EstimateController.php b/app/Http/Controllers/Api/V1/EstimateController.php index 67de0c4..e2a7fa9 100644 --- a/app/Http/Controllers/Api/V1/EstimateController.php +++ b/app/Http/Controllers/Api/V1/EstimateController.php @@ -2,11 +2,11 @@ namespace App\Http\Controllers\Api\V1; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Http\Requests\Estimate\CreateEstimateRequest; use App\Http\Requests\Estimate\UpdateEstimateRequest; use App\Services\Estimate\EstimateService; -use App\Helpers\ApiResponse; use Illuminate\Http\Request; /** @@ -27,6 +27,7 @@ public function __construct(EstimateService $estimateService) * summary="견적 목록 조회", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="status", in="query", description="견적 상태", @OA\Schema(type="string")), * @OA\Parameter(name="customer_name", in="query", description="고객명", @OA\Schema(type="string")), * @OA\Parameter(name="model_set_id", in="query", description="모델셋 ID", @OA\Schema(type="integer")), @@ -34,6 +35,7 @@ public function __construct(EstimateService $estimateService) * @OA\Parameter(name="date_to", in="query", description="종료일", @OA\Schema(type="string", format="date")), * @OA\Parameter(name="search", in="query", description="검색어", @OA\Schema(type="string")), * @OA\Parameter(name="per_page", in="query", description="페이지당 항목수", @OA\Schema(type="integer", default=20)), + * * @OA\Response(response=200, description="성공") * ) */ @@ -42,7 +44,7 @@ public function index(Request $request) $estimates = $this->estimateService->getEstimates($request->all()); return ApiResponse::success([ - 'estimates' => $estimates + 'estimates' => $estimates, ], __('message.fetched')); } @@ -52,7 +54,9 @@ public function index(Request $request) * summary="견적 상세 조회", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="견적 ID", @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="성공") * ) */ @@ -61,7 +65,7 @@ public function show($id) $estimate = $this->estimateService->getEstimateDetail($id); return ApiResponse::success([ - 'estimate' => $estimate + 'estimate' => $estimate, ], __('message.fetched')); } @@ -71,10 +75,13 @@ public function show($id) * summary="견적 생성", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"model_set_id", "estimate_name", "parameters"}, + * * @OA\Property(property="model_set_id", type="integer", description="모델셋 ID"), * @OA\Property(property="estimate_name", type="string", description="견적명"), * @OA\Property(property="customer_name", type="string", description="고객명"), @@ -83,6 +90,7 @@ public function show($id) * @OA\Property(property="notes", type="string", description="비고") * ) * ), + * * @OA\Response(response=201, description="생성 성공") * ) */ @@ -91,7 +99,7 @@ public function store(CreateEstimateRequest $request) $estimate = $this->estimateService->createEstimate($request->validated()); return ApiResponse::success([ - 'estimate' => $estimate + 'estimate' => $estimate, ], __('message.created'), 201); } @@ -101,10 +109,14 @@ public function store(CreateEstimateRequest $request) * summary="견적 수정", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="견적 ID", @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( + * * @OA\Property(property="estimate_name", type="string", description="견적명"), * @OA\Property(property="customer_name", type="string", description="고객명"), * @OA\Property(property="project_name", type="string", description="프로젝트명"), @@ -113,6 +125,7 @@ public function store(CreateEstimateRequest $request) * @OA\Property(property="notes", type="string", description="비고") * ) * ), + * * @OA\Response(response=200, description="수정 성공") * ) */ @@ -121,7 +134,7 @@ public function update(UpdateEstimateRequest $request, $id) $estimate = $this->estimateService->updateEstimate($id, $request->validated()); return ApiResponse::success([ - 'estimate' => $estimate + 'estimate' => $estimate, ], __('message.updated')); } @@ -131,7 +144,9 @@ public function update(UpdateEstimateRequest $request, $id) * summary="견적 삭제", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="견적 ID", @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공") * ) */ @@ -148,17 +163,22 @@ public function destroy($id) * summary="견적 복제", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="견적 ID", @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"estimate_name"}, + * * @OA\Property(property="estimate_name", type="string", description="새 견적명"), * @OA\Property(property="customer_name", type="string", description="고객명"), * @OA\Property(property="project_name", type="string", description="프로젝트명"), * @OA\Property(property="notes", type="string", description="비고") * ) * ), + * * @OA\Response(response=201, description="복제 성공") * ) */ @@ -174,7 +194,7 @@ public function clone(Request $request, $id) $newEstimate = $this->estimateService->cloneEstimate($id, $request->all()); return ApiResponse::success([ - 'estimate' => $newEstimate + 'estimate' => $newEstimate, ], __('message.estimate.cloned'), 201); } @@ -184,15 +204,20 @@ public function clone(Request $request, $id) * summary="견적 상태 변경", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="견적 ID", @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"status"}, + * * @OA\Property(property="status", type="string", enum={"DRAFT","SENT","APPROVED","REJECTED","EXPIRED"}, description="변경할 상태"), * @OA\Property(property="notes", type="string", description="상태 변경 사유") * ) * ), + * * @OA\Response(response=200, description="상태 변경 성공") * ) */ @@ -210,7 +235,7 @@ public function changeStatus(Request $request, $id) ); return ApiResponse::success([ - 'estimate' => $estimate + 'estimate' => $estimate, ], __('message.estimate.status_changed')); } @@ -220,7 +245,9 @@ public function changeStatus(Request $request, $id) * summary="견적 폼 스키마 조회", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="model_set_id", in="path", required=true, description="모델셋 ID", @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="성공") * ) */ @@ -229,7 +256,7 @@ public function getFormSchema($modelSetId) $schema = $this->estimateService->getEstimateFormSchema($modelSetId); return ApiResponse::success([ - 'form_schema' => $schema + 'form_schema' => $schema, ], __('message.fetched')); } @@ -239,14 +266,19 @@ public function getFormSchema($modelSetId) * summary="견적 계산 미리보기", * tags={"Estimate"}, * security={{"bearerAuth": {}}}, + * * @OA\Parameter(name="model_set_id", in="path", required=true, description="모델셋 ID", @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"parameters"}, + * * @OA\Property(property="parameters", type="object", description="견적 파라미터") * ) * ), + * * @OA\Response(response=200, description="계산 성공") * ) */ @@ -263,7 +295,7 @@ public function previewCalculation(Request $request, $modelSetId) ); return ApiResponse::success([ - 'calculation' => $calculation + 'calculation' => $calculation, ], __('message.calculated')); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/FileController.php b/app/Http/Controllers/Api/V1/FileController.php index fec325d..81cd456 100644 --- a/app/Http/Controllers/Api/V1/FileController.php +++ b/app/Http/Controllers/Api/V1/FileController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use App\Services\FileService; use App\Helpers\ApiResponse; +use App\Http\Controllers\Controller; +use App\Services\FileService; +use Illuminate\Http\Request; /** * @OA\Tag( @@ -22,33 +22,43 @@ class FileController extends Controller * description="파일을 업로드합니다.", * tags={"Files"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\MediaType( * mediaType="multipart/form-data", + * * @OA\Schema( * type="object", + * * @OA\Property( * property="files[]", * type="array", + * * @OA\Items(type="string", format="binary"), * description="업로드할 파일들" * ) * ) * ) * ), + * * @OA\Response( * response=201, * description="파일 업로드 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="파일 업로드"), * @OA\Property( * property="data", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="filename", type="string", example="document.pdf"), * @OA\Property(property="path", type="string", example="/uploads/tenant/1/document.pdf"), @@ -57,29 +67,38 @@ class FileController extends Controller * ) * ) * ), + * * @OA\Response( * response=400, * description="파일 업로드 실패", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="파일 업로드에 실패했습니다.") * ) * ), + * * @OA\Response( * response=413, * description="파일 크기 초과", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="파일 크기가 너무 큽니다.") * ) * ), + * * @OA\Response( * response=415, * description="지원하지 않는 파일 형식", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="허용되지 않는 파일 형식입니다.") * ) @@ -100,23 +119,30 @@ public function upload(Request $request) * description="파일 목록을 조회합니다.", * tags={"Files"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="page", * in="query", * description="페이지 번호", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Parameter( * name="size", * in="query", * description="페이지 크기", + * * @OA\Schema(type="integer", example=10) * ), + * * @OA\Response( * response=200, * description="파일 목록 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="파일 목록조회"), * @OA\Property( @@ -128,8 +154,10 @@ public function upload(Request $request) * @OA\Property( * property="data", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="filename", type="string", example="document.pdf"), * @OA\Property(property="path", type="string", example="/uploads/tenant/1/document.pdf"), @@ -156,32 +184,42 @@ public function list(Request $request) * description="파일을 삭제합니다.", * tags={"Files"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property( * property="file_ids", * type="array", + * * @OA\Items(type="integer"), * example={1, 2, 3} * ) * ) * ), + * * @OA\Response( * response=200, * description="파일 삭제 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="파일 삭제") * ) * ), + * * @OA\Response( * response=404, * description="파일을 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="파일을 찾을 수 없습니다.") * ) @@ -202,18 +240,23 @@ public function delete(Request $request) * description="특정 파일의 정보를 조회합니다.", * tags={"Files"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="file_id", * in="query", * required=true, * description="파일 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="파일 정보 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="파일 정보 조회"), * @OA\Property( @@ -228,11 +271,14 @@ public function delete(Request $request) * ) * ) * ), + * * @OA\Response( * response=404, * description="파일을 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="파일을 찾을 수 없습니다.") * ) diff --git a/app/Http/Controllers/Api/V1/MaterialController.php b/app/Http/Controllers/Api/V1/MaterialController.php index c2ba290..5cc545d 100644 --- a/app/Http/Controllers/Api/V1/MaterialController.php +++ b/app/Http/Controllers/Api/V1/MaterialController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\MaterialService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; /** * @OA\Tag( @@ -24,29 +24,38 @@ public function __construct(private MaterialService $service) {} * description="테넌트의 자재 목록을 조회합니다. (Products & Materials 통합 관리)", * tags={"Products & Materials - Materials"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="page", * in="query", * description="페이지 번호", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Parameter( * name="size", * in="query", * description="페이지 크기", + * * @OA\Schema(type="integer", example=10) * ), + * * @OA\Parameter( * name="q", * in="query", * description="검색어 (자재명, 코드)", + * * @OA\Schema(type="string", example="스틸") * ), + * * @OA\Response( * response=200, * description="자재 목록 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="조회 성공"), * @OA\Property( @@ -58,8 +67,10 @@ public function __construct(private MaterialService $service) {} * @OA\Property( * property="data", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="material_code", type="string", example="MAT001"), * @OA\Property(property="name", type="string", example="스틸파이프 10mm"), @@ -87,10 +98,13 @@ public function index(Request $request) * description="새로운 자재를 등록합니다.", * tags={"Products & Materials - Materials"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="material_code", type="string", example="MAT002"), * @OA\Property(property="name", type="string", example="알루미늄 프로파일"), * @OA\Property(property="specification", type="string", example="20x20x2mm"), @@ -98,11 +112,14 @@ public function index(Request $request) * @OA\Property(property="description", type="string", example="알루미늄 프로파일 20x20") * ) * ), + * * @OA\Response( * response=201, * description="자재 등록 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="자재가 등록되었습니다."), * @OA\Property( @@ -112,18 +129,23 @@ public function index(Request $request) * ) * ) * ), + * * @OA\Response( * response=409, * description="중복된 자재 코드", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="중복된 자재 코드입니다.") * ) * ), + * * @OA\Response( * response=422, * description="유효성 검사 실패", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -142,18 +164,23 @@ public function store(Request $request) * description="특정 자재의 상세 정보를 조회합니다.", * tags={"Products & Materials - Materials"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="자재 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="자재 상세 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="조회 성공"), * @OA\Property( @@ -171,9 +198,11 @@ public function store(Request $request) * ) * ) * ), + * * @OA\Response( * response=404, * description="자재 정보를 찾을 수 없음", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -192,27 +221,35 @@ public function show(Request $request, int $id) * description="기존 자재 정보를 수정합니다.", * tags={"Products & Materials - Materials"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="자재 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="name", type="string", example="스틸파이프 12mm"), * @OA\Property(property="specification", type="string", example="직경 12mm, 두께 2mm"), * @OA\Property(property="description", type="string", example="수정된 스틸 파이프") * ) * ), + * * @OA\Response( * response=200, * description="자재 수정 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="자재가 수정되었습니다.") * ) @@ -233,36 +270,47 @@ public function update(Request $request, int $id) * description="자재를 소프트 삭제합니다. 사용 중인 자재는 삭제할 수 없습니다.", * tags={"Products & Materials - Materials"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="자재 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="자재 삭제 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="자재가 삭제되었습니다.") * ) * ), + * * @OA\Response( * response=409, * description="사용 중인 자재는 삭제 불가", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="사용 중인 자재는 삭제할 수 없습니다.") * ) * ), + * * @OA\Response( * response=404, * description="자재 정보를 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="자재 정보를 찾을 수 없습니다.") * ) diff --git a/app/Http/Controllers/Api/V1/MenuController.php b/app/Http/Controllers/Api/V1/MenuController.php index a67d108..6695318 100644 --- a/app/Http/Controllers/Api/V1/MenuController.php +++ b/app/Http/Controllers/Api/V1/MenuController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\MenuService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class MenuController extends Controller { @@ -19,7 +19,7 @@ public function index(Request $request) public function show($id) { return ApiResponse::handle(function () use ($id) { - return MenuService::show(['id' => (int)$id]); + return MenuService::show(['id' => (int) $id]); }, '메뉴 단건 조회'); } @@ -33,7 +33,9 @@ public function store(Request $request) public function update(Request $request, $id) { return ApiResponse::handle(function () use ($request, $id) { - $params = $request->all(); $params['id'] = (int)$id; + $params = $request->all(); + $params['id'] = (int) $id; + return MenuService::update($params); }, '메뉴 수정'); } @@ -41,7 +43,7 @@ public function update(Request $request, $id) public function destroy($id) { return ApiResponse::handle(function () use ($id) { - return MenuService::destroy(['id' => (int)$id]); + return MenuService::destroy(['id' => (int) $id]); }, '메뉴 삭제'); } @@ -55,7 +57,9 @@ public function reorder(Request $request) public function toggle(Request $request, $id) { return ApiResponse::handle(function () use ($request, $id) { - $params = $request->all(); $params['id'] = (int)$id; + $params = $request->all(); + $params['id'] = (int) $id; + return MenuService::toggle($params); }, '메뉴 상태 토글'); } diff --git a/app/Http/Controllers/Api/V1/ModelSetController.php b/app/Http/Controllers/Api/V1/ModelSetController.php index af08210..3e26eca 100644 --- a/app/Http/Controllers/Api/V1/ModelSetController.php +++ b/app/Http/Controllers/Api/V1/ModelSetController.php @@ -2,12 +2,12 @@ namespace App\Http\Controllers\Api\V1; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\ModelSet\CloneModelSetRequest; use App\Http\Requests\ModelSet\CreateModelSetRequest; use App\Http\Requests\ModelSet\UpdateModelSetRequest; -use App\Http\Requests\ModelSet\CloneModelSetRequest; use App\Services\ModelSet\ModelSetService; -use App\Helpers\ApiResponse; use Illuminate\Http\Request; class ModelSetController extends Controller @@ -27,7 +27,7 @@ public function index(Request $request) $modelSets = $this->modelSetService->getModelSets($request->validated()); return ApiResponse::success([ - 'model_sets' => $modelSets + 'model_sets' => $modelSets, ], __('message.fetched')); } @@ -39,7 +39,7 @@ public function show($id) $modelSet = $this->modelSetService->getModelSetDetail($id); return ApiResponse::success([ - 'model_set' => $modelSet + 'model_set' => $modelSet, ], __('message.fetched')); } @@ -51,7 +51,7 @@ public function store(CreateModelSetRequest $request) $modelSet = $this->modelSetService->createModelSet($request->validated()); return ApiResponse::success([ - 'model_set' => $modelSet + 'model_set' => $modelSet, ], __('message.created')); } @@ -63,7 +63,7 @@ public function update(UpdateModelSetRequest $request, $id) $modelSet = $this->modelSetService->updateModelSet($id, $request->validated()); return ApiResponse::success([ - 'model_set' => $modelSet + 'model_set' => $modelSet, ], __('message.updated')); } @@ -85,7 +85,7 @@ public function clone(CloneModelSetRequest $request, $id) $newModelSet = $this->modelSetService->cloneModelSet($id, $request->validated()); return ApiResponse::success([ - 'model_set' => $newModelSet + 'model_set' => $newModelSet, ], __('message.model_set.cloned')); } @@ -97,7 +97,7 @@ public function getCategoryFields($id) $fields = $this->modelSetService->getModelSetCategoryFields($id); return ApiResponse::success([ - 'category_fields' => $fields + 'category_fields' => $fields, ], __('message.fetched')); } @@ -109,7 +109,7 @@ public function getBomTemplates($id) $templates = $this->modelSetService->getModelSetBomTemplates($id); return ApiResponse::success([ - 'bom_templates' => $templates + 'bom_templates' => $templates, ], __('message.fetched')); } @@ -121,7 +121,7 @@ public function getEstimateParameters($id, Request $request) $parameters = $this->modelSetService->getEstimateParameters($id, $request->all()); return ApiResponse::success([ - 'parameters' => $parameters + 'parameters' => $parameters, ], __('message.fetched')); } @@ -134,4 +134,4 @@ public function calculateBom($id, Request $request) return ApiResponse::success($result, __('message.calculated')); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/PricingController.php b/app/Http/Controllers/Api/V1/PricingController.php index f4f7a5e..94791b8 100644 --- a/app/Http/Controllers/Api/V1/PricingController.php +++ b/app/Http/Controllers/Api/V1/PricingController.php @@ -93,4 +93,4 @@ public function destroy(int $id) return ['data' => null, 'message' => __('message.deleted')]; }); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/V1/ProductBomItemController.php b/app/Http/Controllers/Api/V1/ProductBomItemController.php index ddb1107..50eca59 100644 --- a/app/Http/Controllers/Api/V1/ProductBomItemController.php +++ b/app/Http/Controllers/Api/V1/ProductBomItemController.php @@ -40,6 +40,7 @@ public function destroy(int $id, int $item) { return ApiResponse::handle(function () use ($id, $item) { $this->service->destroy($id, $item); + return 'success'; }, 'BOM 항목 삭제'); } @@ -49,6 +50,7 @@ public function reorder(int $id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { $this->service->reorder($id, $request->input('items', [])); + return 'success'; }, 'BOM 정렬 변경'); } @@ -81,7 +83,6 @@ public function replace(Request $request, int $id) }, __('message.bom.creat')); } - /** 특정 제품 BOM에서 사용 중인 카테고리 목록 */ public function listCategories(int $id) { @@ -94,14 +95,13 @@ public function listCategories(int $id) public function suggestCategories(Request $request) { return ApiResponse::handle(function () use ($request) { - $q = $request->query('q'); - $limit = (int)($request->query('limit', 20)); + $q = $request->query('q'); + $limit = (int) ($request->query('limit', 20)); + return $this->service->listCategoriesForTenant($q, $limit); }, __('message.bom.fetch')); } - - /** Bom Tree */ public function tree(Request $request, int $id) { diff --git a/app/Http/Controllers/Api/V1/ProductController.php b/app/Http/Controllers/Api/V1/ProductController.php index 2c393d5..8f7e510 100644 --- a/app/Http/Controllers/Api/V1/ProductController.php +++ b/app/Http/Controllers/Api/V1/ProductController.php @@ -2,11 +2,10 @@ namespace App\Http\Controllers\Api\V1; - -use Illuminate\Http\Request; -use App\Http\Controllers\Controller; use App\Helpers\ApiResponse; +use App\Http\Controllers\Controller; use App\Services\ProductService; +use Illuminate\Http\Request; class ProductController extends Controller { @@ -56,6 +55,7 @@ public function destroy(int $id) { return ApiResponse::handle(function () use ($id) { $this->service->destroy($id); + return 'success'; }, '제품 삭제'); } @@ -76,4 +76,3 @@ public function toggle(int $id) }, '제품 활성 토글'); } } - diff --git a/app/Http/Controllers/Api/V1/RoleController.php b/app/Http/Controllers/Api/V1/RoleController.php index 94d29ee..27ad9d6 100644 --- a/app/Http/Controllers/Api/V1/RoleController.php +++ b/app/Http/Controllers/Api/V1/RoleController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\Authz\RoleService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class RoleController extends Controller { @@ -26,21 +26,21 @@ public function store(Request $request) public function show($id) { return ApiResponse::handle(function () use ($id) { - return RoleService::show((int)$id); + return RoleService::show((int) $id); }, '역할 상세 조회'); } public function update(Request $request, $id) { return ApiResponse::handle(function () use ($request, $id) { - return RoleService::update((int)$id, $request->all()); + return RoleService::update((int) $id, $request->all()); }, '역할 수정'); } public function destroy($id) { return ApiResponse::handle(function () use ($id) { - return RoleService::destroy((int)$id); + return RoleService::destroy((int) $id); }, '역할 삭제'); } } diff --git a/app/Http/Controllers/Api/V1/RolePermissionController.php b/app/Http/Controllers/Api/V1/RolePermissionController.php index b78f3ba..cea9fe6 100644 --- a/app/Http/Controllers/Api/V1/RolePermissionController.php +++ b/app/Http/Controllers/Api/V1/RolePermissionController.php @@ -2,38 +2,38 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\Authz\RolePermissionService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class RolePermissionController extends Controller { public function index($id, Request $request) { return ApiResponse::handle(function () use ($id) { - return RolePermissionService::list((int)$id); + return RolePermissionService::list((int) $id); }, '역할 퍼미션 목록 조회'); } public function grant($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return RolePermissionService::grant((int)$id, $request->all()); + return RolePermissionService::grant((int) $id, $request->all()); }, '역할 퍼미션 부여'); } public function revoke($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return RolePermissionService::revoke((int)$id, $request->all()); + return RolePermissionService::revoke((int) $id, $request->all()); }, '역할 퍼미션 회수'); } public function sync($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return RolePermissionService::sync((int)$id, $request->all()); + return RolePermissionService::sync((int) $id, $request->all()); }, '역할 퍼미션 동기화'); } } diff --git a/app/Http/Controllers/Api/V1/TenantController.php b/app/Http/Controllers/Api/V1/TenantController.php index 4410cea..8543248 100644 --- a/app/Http/Controllers/Api/V1/TenantController.php +++ b/app/Http/Controllers/Api/V1/TenantController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\TenantService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class TenantController extends Controller { @@ -50,6 +50,4 @@ public function restore(Request $request) return TenantService::restoreTenant($request->all()); }, '테넌트 복구'); } - } - diff --git a/app/Http/Controllers/Api/V1/TenantFieldSettingController.php b/app/Http/Controllers/Api/V1/TenantFieldSettingController.php index cbc5c70..b5eb550 100644 --- a/app/Http/Controllers/Api/V1/TenantFieldSettingController.php +++ b/app/Http/Controllers/Api/V1/TenantFieldSettingController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\TenantFieldSettingService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; /** * @OA\Tag( @@ -22,18 +22,23 @@ class TenantFieldSettingController extends Controller * description="전역 + 테넌트별 병합된 필드 설정 효과값을 조회합니다.", * tags={"Settings - Fields"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="필드 설정 목록 조회 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="조회 성공"), * @OA\Property( * property="data", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="field_key", type="string", example="product_name_required"), * @OA\Property(property="field_value", type="string", example="true"), * @OA\Property(property="source", type="string", example="tenant", description="global 또는 tenant") @@ -57,38 +62,50 @@ public function index(Request $request) * description="여러 필드 설정을 트랜잭션으로 일괄 저장합니다.", * tags={"Settings - Fields"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property( * property="fields", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="field_key", type="string", example="product_name_required"), * @OA\Property(property="field_value", type="string", example="true") * ) * ) * ) * ), + * * @OA\Response( * response=200, * description="대량 저장 성공", + * * @OA\JsonContent(ref="#/components/schemas/ApiResponse") * ), + * * @OA\Response( * response=400, * description="유효하지 않은 필드 타입", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="유효하지 않은 필드 타입입니다.") * ) * ), + * * @OA\Response( * response=422, * description="유효성 검사 실패", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -107,39 +124,52 @@ public function bulkUpsert(Request $request) * description="특정 필드 설정을 개별적으로 수정합니다.", * tags={"Settings - Fields"}, * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter( * name="key", * in="path", * required=true, * description="필드 키", + * * @OA\Schema(type="string", example="product_name_required") * ), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="field_value", type="string", example="false") * ) * ), + * * @OA\Response( * response=200, * description="필드 설정 수정 성공", + * * @OA\JsonContent(ref="#/components/schemas/ApiResponse") * ), + * * @OA\Response( * response=404, * description="필드 설정을 찾을 수 없음", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="해당 필드 설정을 찾을 수 없습니다.") * ) * ), + * * @OA\Response( * response=400, * description="유효하지 않은 필드 타입", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="유효하지 않은 필드 타입입니다.") * ) diff --git a/app/Http/Controllers/Api/V1/TenantOptionGroupController.php b/app/Http/Controllers/Api/V1/TenantOptionGroupController.php index fbdbdbe..c15c759 100644 --- a/app/Http/Controllers/Api/V1/TenantOptionGroupController.php +++ b/app/Http/Controllers/Api/V1/TenantOptionGroupController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\TenantOptionGroupService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class TenantOptionGroupController extends Controller { diff --git a/app/Http/Controllers/Api/V1/TenantOptionValueController.php b/app/Http/Controllers/Api/V1/TenantOptionValueController.php index 7f7f338..bce37b2 100644 --- a/app/Http/Controllers/Api/V1/TenantOptionValueController.php +++ b/app/Http/Controllers/Api/V1/TenantOptionValueController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\TenantOptionValueService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class TenantOptionValueController extends Controller { diff --git a/app/Http/Controllers/Api/V1/TenantUserProfileController.php b/app/Http/Controllers/Api/V1/TenantUserProfileController.php index 1d68e30..bae414c 100644 --- a/app/Http/Controllers/Api/V1/TenantUserProfileController.php +++ b/app/Http/Controllers/Api/V1/TenantUserProfileController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\TenantUserProfileService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class TenantUserProfileController extends Controller { diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php index f23e19d..3830ec4 100644 --- a/app/Http/Controllers/Api/V1/UserController.php +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -2,10 +2,10 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\MemberService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class UserController extends Controller { @@ -53,10 +53,10 @@ public function tenants(Request $request) public function switchTenant(Request $request) { - $tenant_id = $request->tenant_id; + $tenant_id = $request->tenant_id; + return ApiResponse::handle(function () use ($tenant_id) { return MemberService::switchMyTenant($tenant_id); }, '활성 테넌트 전환'); } } - diff --git a/app/Http/Controllers/Api/V1/UserRoleController.php b/app/Http/Controllers/Api/V1/UserRoleController.php index 047689a..afe4cb7 100644 --- a/app/Http/Controllers/Api/V1/UserRoleController.php +++ b/app/Http/Controllers/Api/V1/UserRoleController.php @@ -2,38 +2,38 @@ namespace App\Http\Controllers\Api\V1; -use Illuminate\Http\Request; +use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; use App\Services\Authz\UserRoleService; -use App\Helpers\ApiResponse; +use Illuminate\Http\Request; class UserRoleController extends Controller { public function index($id) { return ApiResponse::handle(function () use ($id) { - return UserRoleService::list((int)$id); + return UserRoleService::list((int) $id); }, '사용자의 역할 목록 조회'); } public function grant($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return UserRoleService::grant((int)$id, $request->all()); + return UserRoleService::grant((int) $id, $request->all()); }, '사용자에게 역할 부여'); } public function revoke($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return UserRoleService::revoke((int)$id, $request->all()); + return UserRoleService::revoke((int) $id, $request->all()); }, '사용자의 역할 회수'); } public function sync($id, Request $request) { return ApiResponse::handle(function () use ($id, $request) { - return UserRoleService::sync((int)$id, $request->all()); + return UserRoleService::sync((int) $id, $request->all()); }, '사용자의 역할 동기화'); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 26088eb..f848751 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -25,4 +25,4 @@ class Kernel extends HttpKernel * 개별 미들웨어 설정 */ protected $routeMiddleware = []; -} \ No newline at end of file +} diff --git a/app/Http/Middleware/ApiKeyMiddleware.php b/app/Http/Middleware/ApiKeyMiddleware.php index 09b895e..bb30c3d 100644 --- a/app/Http/Middleware/ApiKeyMiddleware.php +++ b/app/Http/Middleware/ApiKeyMiddleware.php @@ -24,7 +24,6 @@ public function handle(Request $request, Closure $next) 'headers' => $request->headers->all(), ]); - $apiKey = $request->header('X-API-KEY'); $validApiKey = false; @@ -37,13 +36,13 @@ public function handle(Request $request, Closure $next) ->exists(); } - if (!$validApiKey) { + if (! $validApiKey) { return response()->json(['message' => 'Unauthorized. Invalid or missing API key'], 401); } // Bearer 인증 (Sanctum) $user = []; - if($token = $request->bearerToken()) { + if ($token = $request->bearerToken()) { $accessToken = PersonalAccessToken::findToken($token); if ($accessToken && $accessToken->tokenable instanceof User) { $user = $accessToken->tokenable; @@ -79,9 +78,9 @@ public function handle(Request $request, Closure $next) // 현재 라우트 확인 (경로 또는 이름) $currentRoute = $request->route()->uri(); // 또는 $request->path() - if (!in_array($currentRoute, $allowWithoutAuth)) { + if (! in_array($currentRoute, $allowWithoutAuth)) { // 인증정보(api_user, tenant_id) 없으면 튕김 - if (!app()->bound('api_user')) { + if (! app()->bound('api_user')) { throw new AuthenticationException('회원정보 정보 없음'); } } diff --git a/app/Http/Middleware/CheckPermission.php b/app/Http/Middleware/CheckPermission.php index 5fc861f..5395818 100644 --- a/app/Http/Middleware/CheckPermission.php +++ b/app/Http/Middleware/CheckPermission.php @@ -2,11 +2,11 @@ namespace App\Http\Middleware; +use App\Models\Members\User as UserModel; +use App\Services\Authz\AccessService; use Closure; use Illuminate\Http\Request; use Spatie\Permission\PermissionRegistrar; -use App\Services\Authz\AccessService; -use App\Models\Members\User as UserModel; class CheckPermission { @@ -21,21 +21,21 @@ public function handle(Request $request, Closure $next) // perm 미지정 라우트 처리 정책 // TODO :: 초기 도입 단계: 통과. (정책에 따라 403으로 바꿔도 됨) - if (!$perm && !$permsAny) { + if (! $perm && ! $permsAny) { return $next($request); // return response()->json(['success'=>false,'message'=>'권한 설정 누락','data'=>null], 403); } // 컨텍스트 확보 $tenantId = (int) app('tenant_id'); - $userId = (int) app('api_user'); - if (!$tenantId || !$userId) { - return response()->json(['success'=>false,'message'=>'인증 또는 테넌트 정보가 없습니다.','data'=>null], 401); + $userId = (int) app('api_user'); + if (! $tenantId || ! $userId) { + return response()->json(['success' => false, 'message' => '인증 또는 테넌트 정보가 없습니다.', 'data' => null], 401); } $user = UserModel::find($userId); - if (!$user) { - return response()->json(['success'=>false,'message'=>'사용자 없음','data'=>null], 401); + if (! $user) { + return response()->json(['success' => false, 'message' => '사용자 없음', 'data' => null], 401); } // Spatie Teams 컨텍스트 고정 @@ -48,10 +48,11 @@ public function handle(Request $request, Closure $next) return $next($request); } } - return response()->json(['success'=>false,'message'=>'권한이 없습니다.','data'=>null], 403); + + return response()->json(['success' => false, 'message' => '권한이 없습니다.', 'data' => null], 403); } if (! AccessService::allows($user, $perm, $tenantId, 'api')) { - return response()->json(['success'=>false,'message'=>'권한이 없습니다.','data'=>null], 403); + return response()->json(['success' => false, 'message' => '권한이 없습니다.', 'data' => null], 403); } return $next($request); diff --git a/app/Http/Middleware/CheckSwaggerAuth.php b/app/Http/Middleware/CheckSwaggerAuth.php index 3dfcd2b..09e4214 100644 --- a/app/Http/Middleware/CheckSwaggerAuth.php +++ b/app/Http/Middleware/CheckSwaggerAuth.php @@ -2,10 +2,10 @@ namespace App\Http\Middleware; +use App\Models\Member; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; -use App\Models\Member; class CheckSwaggerAuth { @@ -13,19 +13,21 @@ public function handle(Request $request, Closure $next) { $token = Session::get('user_token'); - if (!$token) { + if (! $token) { // 원래 URL 저장 후 로그인 페이지로 이동 Session::put('redirect_to', $request->fullUrl()); + return redirect()->route('login'); } $user = Member::where('remember_token', $token)->first(); - if (!$user) { + if (! $user) { Session::forget('user_token'); Session::forget('user_id'); Session::put('redirect_to', $request->fullUrl()); + return redirect()->route('login'); } diff --git a/app/Http/Middleware/CorsMiddleware.php b/app/Http/Middleware/CorsMiddleware.php index 4763879..068bbcb 100644 --- a/app/Http/Middleware/CorsMiddleware.php +++ b/app/Http/Middleware/CorsMiddleware.php @@ -22,4 +22,4 @@ public function handle(Request $request, Closure $next): Response return $response; } -} \ No newline at end of file +} diff --git a/app/Http/Middleware/PermMapper.php b/app/Http/Middleware/PermMapper.php index dfca000..6c96ab7 100644 --- a/app/Http/Middleware/PermMapper.php +++ b/app/Http/Middleware/PermMapper.php @@ -9,18 +9,18 @@ class PermMapper { /** HTTP 메서드 → 액션 버킷 */ private array $actionMap = [ - 'GET' => 'view', - 'HEAD' => 'view', - 'POST' => 'create', - 'PUT' => 'update', - 'PATCH' => 'update', + 'GET' => 'view', + 'HEAD' => 'view', + 'POST' => 'create', + 'PUT' => 'update', + 'PATCH' => 'update', 'DELETE' => 'delete', ]; public function handle(Request $request, Closure $next) { $route = $request->route(); - if (!$route) { + if (! $route) { return $next($request); } @@ -33,6 +33,7 @@ public function handle(Request $request, Closure $next) $forced = $route->defaults['perm'] ?? $route->defaults['permission'] ?? null; if ($forced) { $request->attributes->set('perm', $forced); + return $next($request); } diff --git a/app/Http/Requests/Audit/AuditLogIndexRequest.php b/app/Http/Requests/Audit/AuditLogIndexRequest.php index 1fa51e3..f3bcf50 100644 --- a/app/Http/Requests/Audit/AuditLogIndexRequest.php +++ b/app/Http/Requests/Audit/AuditLogIndexRequest.php @@ -14,16 +14,16 @@ public function authorize(): bool public function rules(): array { return [ - 'page' => 'nullable|integer|min:1', - 'size' => 'nullable|integer|min:1|max:200', + 'page' => 'nullable|integer|min:1', + 'size' => 'nullable|integer|min:1|max:200', 'target_type' => 'nullable|string|max:100', - 'target_id' => 'nullable|integer|min:1', - 'action' => 'nullable|string|max:50', - 'actor_id' => 'nullable|integer|min:1', - 'from' => 'nullable|date', - 'to' => 'nullable|date|after_or_equal:from', - 'sort' => 'nullable|string|in:created_at', - 'order' => 'nullable|string|in:asc,desc', + 'target_id' => 'nullable|integer|min:1', + 'action' => 'nullable|string|max:50', + 'actor_id' => 'nullable|integer|min:1', + 'from' => 'nullable|date', + 'to' => 'nullable|date|after_or_equal:from', + 'sort' => 'nullable|string|in:created_at', + 'order' => 'nullable|string|in:asc,desc', ]; } } diff --git a/app/Http/Requests/Common/PaginateRequest.php b/app/Http/Requests/Common/PaginateRequest.php index 28e8024..343ca65 100644 --- a/app/Http/Requests/Common/PaginateRequest.php +++ b/app/Http/Requests/Common/PaginateRequest.php @@ -1,19 +1,23 @@ 'nullable|integer|min:1', - 'size' => 'nullable|integer|min:1|max:100', - 'q' => 'nullable|string|max:100', - 'sort' => 'nullable|string|in:id,code,name,created_at', + 'page' => 'nullable|integer|min:1', + 'size' => 'nullable|integer|min:1|max:100', + 'q' => 'nullable|string|max:100', + 'sort' => 'nullable|string|in:id,code,name,created_at', 'order' => 'nullable|string|in:asc,desc', ]; } @@ -21,9 +25,10 @@ public function rules(): array public function validatedOrDefaults(): array { $v = $this->validated(); - $v['page'] = $v['page'] ?? 1; - $v['size'] = $v['size'] ?? 20; + $v['page'] = $v['page'] ?? 1; + $v['size'] = $v['size'] ?? 20; $v['order'] = $v['order'] ?? 'desc'; + return $v; } } diff --git a/app/Http/Requests/Design/BomTemplate/CloneRequest.php b/app/Http/Requests/Design/BomTemplate/CloneRequest.php index acda037..d85d275 100644 --- a/app/Http/Requests/Design/BomTemplate/CloneRequest.php +++ b/app/Http/Requests/Design/BomTemplate/CloneRequest.php @@ -15,9 +15,9 @@ public function rules(): array { return [ 'target_version_id' => 'nullable|integer|min:1', - 'name' => 'nullable|string|max:100', - 'is_primary' => 'nullable|boolean', - 'notes' => 'nullable|string', + 'name' => 'nullable|string|max:100', + 'is_primary' => 'nullable|boolean', + 'notes' => 'nullable|string', ]; } } diff --git a/app/Http/Requests/Design/BomTemplate/DiffRequest.php b/app/Http/Requests/Design/BomTemplate/DiffRequest.php index b8fbfeb..4a225c4 100644 --- a/app/Http/Requests/Design/BomTemplate/DiffRequest.php +++ b/app/Http/Requests/Design/BomTemplate/DiffRequest.php @@ -22,8 +22,8 @@ public function messages(): array { return [ 'other_template_id.required' => __('validation.required', ['attribute' => 'other_template_id']), - 'other_template_id.integer' => __('validation.integer', ['attribute' => 'other_template_id']), - 'other_template_id.min' => __('validation.min.numeric', ['attribute' => 'other_template_id', 'min' => 1]), + 'other_template_id.integer' => __('validation.integer', ['attribute' => 'other_template_id']), + 'other_template_id.min' => __('validation.min.numeric', ['attribute' => 'other_template_id', 'min' => 1]), ]; } } diff --git a/app/Http/Requests/Design/BomTemplate/ReplaceItemsRequest.php b/app/Http/Requests/Design/BomTemplate/ReplaceItemsRequest.php index ec90832..77b2bbd 100644 --- a/app/Http/Requests/Design/BomTemplate/ReplaceItemsRequest.php +++ b/app/Http/Requests/Design/BomTemplate/ReplaceItemsRequest.php @@ -1,17 +1,21 @@ mapWithKeys(fn($rule, $key) => ["items.*.$key" => $rule]) + ->mapWithKeys(fn ($rule, $key) => ["items.*.$key" => $rule]) ->all(); return ['items' => 'required|array|min:1'] + $itemRules; @@ -21,8 +25,8 @@ public function messages(): array { return [ 'items.required' => __('validation.required', ['attribute' => 'items']), - 'items.array' => __('validation.array', ['attribute' => 'items']), - 'items.min' => __('validation.min.array', ['attribute' => 'items', 'min' => 1]), + 'items.array' => __('validation.array', ['attribute' => 'items']), + 'items.min' => __('validation.min.array', ['attribute' => 'items', 'min' => 1]), ]; } } diff --git a/app/Http/Requests/Design/CalculateBomRequest.php b/app/Http/Requests/Design/CalculateBomRequest.php index 87942a1..7b2288c 100644 --- a/app/Http/Requests/Design/CalculateBomRequest.php +++ b/app/Http/Requests/Design/CalculateBomRequest.php @@ -30,7 +30,7 @@ public function rules(): array 'parameters.power_source' => 'nullable|string|in:manual,electric,automatic', 'parameters.motor_type' => 'nullable|string|in:standard,low_noise,high_torque', 'parameters.material_grade' => 'nullable|string|in:standard,premium,luxury', - 'company_name' => 'nullable|string|max:100' + 'company_name' => 'nullable|string|max:100', ]; } @@ -61,7 +61,7 @@ public function messages(): array 'parameters.material_grade.string' => __('error.validation.string'), 'parameters.material_grade.in' => __('error.validation.in'), 'company_name.string' => __('error.validation.string'), - 'company_name.max' => __('error.validation.max.string') + 'company_name.max' => __('error.validation.max.string'), ]; } @@ -81,7 +81,7 @@ public function attributes(): array 'parameters.power_source' => '동력원', 'parameters.motor_type' => '모터타입', 'parameters.material_grade' => '자재등급', - 'company_name' => '업체명' + 'company_name' => '업체명', ]; } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Design/GetEstimateParametersRequest.php b/app/Http/Requests/Design/GetEstimateParametersRequest.php index 30cbf86..8f469f1 100644 --- a/app/Http/Requests/Design/GetEstimateParametersRequest.php +++ b/app/Http/Requests/Design/GetEstimateParametersRequest.php @@ -22,7 +22,7 @@ public function authorize(): bool public function rules(): array { return [ - 'company_name' => 'nullable|string|max:100' + 'company_name' => 'nullable|string|max:100', ]; } @@ -35,7 +35,7 @@ public function messages(): array { return [ 'company_name.string' => __('error.validation.string'), - 'company_name.max' => __('error.validation.max.string') + 'company_name.max' => __('error.validation.max.string'), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Design/Model/StoreRequest.php b/app/Http/Requests/Design/Model/StoreRequest.php index 37a70f6..6343081 100644 --- a/app/Http/Requests/Design/Model/StoreRequest.php +++ b/app/Http/Requests/Design/Model/StoreRequest.php @@ -1,21 +1,25 @@ 'required|string|max:100', - 'name' => 'required|string|max:200', + 'code' => 'required|string|max:100', + 'name' => 'required|string|max:200', 'category_id' => 'nullable|integer', - 'lifecycle' => 'nullable|string|max:50', + 'lifecycle' => 'nullable|string|max:50', 'description' => 'nullable|string', - 'is_active' => 'boolean', + 'is_active' => 'boolean', ]; } diff --git a/app/Http/Requests/Design/SaveCompanyFormulaRequest.php b/app/Http/Requests/Design/SaveCompanyFormulaRequest.php index ea13813..f93f329 100644 --- a/app/Http/Requests/Design/SaveCompanyFormulaRequest.php +++ b/app/Http/Requests/Design/SaveCompanyFormulaRequest.php @@ -40,7 +40,7 @@ public function rules(): array 'validation_rules.*.field' => 'nullable|string|max:50', 'validation_rules.*.rule' => 'nullable|string|max:200', 'validation_rules.*.message' => 'nullable|string|max:200', - 'description' => 'nullable|string|max:500' + 'description' => 'nullable|string|max:500', ]; } @@ -84,7 +84,7 @@ public function messages(): array 'validation_rules.*.message.string' => __('error.validation.string'), 'validation_rules.*.message.max' => __('error.validation.max.string'), 'description.string' => __('error.validation.string'), - 'description.max' => __('error.validation.max.string') + 'description.max' => __('error.validation.max.string'), ]; } @@ -114,7 +114,7 @@ public function attributes(): array 'validation_rules.*.field' => '검증 필드', 'validation_rules.*.rule' => '검증 규칙', 'validation_rules.*.message' => '검증 메시지', - 'description' => '설명' + 'description' => '설명', ]; } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Estimate/CreateEstimateRequest.php b/app/Http/Requests/Estimate/CreateEstimateRequest.php index 752328f..bbd8462 100644 --- a/app/Http/Requests/Estimate/CreateEstimateRequest.php +++ b/app/Http/Requests/Estimate/CreateEstimateRequest.php @@ -35,4 +35,4 @@ public function messages(): array 'parameters.array' => __('validation.array', ['attribute' => '견적 파라미터']), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Estimate/UpdateEstimateRequest.php b/app/Http/Requests/Estimate/UpdateEstimateRequest.php index 5320b8b..7900040 100644 --- a/app/Http/Requests/Estimate/UpdateEstimateRequest.php +++ b/app/Http/Requests/Estimate/UpdateEstimateRequest.php @@ -33,4 +33,4 @@ public function messages(): array 'status.in' => __('validation.in', ['attribute' => '상태']), ]; } -} \ No newline at end of file +} diff --git a/app/Models/Audit/AuditLog.php b/app/Models/Audit/AuditLog.php index 1bf5ec1..9859f96 100644 --- a/app/Models/Audit/AuditLog.php +++ b/app/Models/Audit/AuditLog.php @@ -11,12 +11,12 @@ class AuditLog extends Model protected $table = 'audit_logs'; protected $fillable = [ - 'tenant_id','target_type','target_id','action','before','after','actor_id','ip','ua','created_at', + 'tenant_id', 'target_type', 'target_id', 'action', 'before', 'after', 'actor_id', 'ip', 'ua', 'created_at', ]; protected $casts = [ 'before' => 'array', - 'after' => 'array', + 'after' => 'array', 'created_at' => 'datetime', ]; } diff --git a/app/Models/Boards/Board.php b/app/Models/Boards/Board.php index d56c998..6c23f42 100644 --- a/app/Models/Boards/Board.php +++ b/app/Models/Boards/Board.php @@ -2,7 +2,6 @@ namespace App\Models\Boards; - use Illuminate\Database\Eloquent\Model; /** @@ -11,15 +10,19 @@ class Board extends Model { protected $table = 'boards'; + protected $fillable = [ 'tenant_id', 'board_code', 'name', 'description', 'editor_type', - 'allow_files', 'max_file_count', 'max_file_size', 'extra_settings', 'is_active' + 'allow_files', 'max_file_count', 'max_file_size', 'extra_settings', 'is_active', ]; - public function customFields() { + public function customFields() + { return $this->hasMany(BoardSetting::class, 'board_id'); } - public function posts() { + + public function posts() + { return $this->hasMany(Post::class, 'board_id'); } } diff --git a/app/Models/Boards/BoardComment.php b/app/Models/Boards/BoardComment.php index 275d6f2..6aa5321 100644 --- a/app/Models/Boards/BoardComment.php +++ b/app/Models/Boards/BoardComment.php @@ -2,9 +2,9 @@ namespace App\Models\Boards; +use App\Models\Members\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Models\Members\User; /** * @mixin IdeHelperBoardComment @@ -14,20 +14,28 @@ class BoardComment extends Model use SoftDeletes; protected $table = 'board_comments'; + protected $fillable = [ - 'post_id', 'tenant_id', 'user_id', 'parent_id', 'content', 'ip_address', 'status' + 'post_id', 'tenant_id', 'user_id', 'parent_id', 'content', 'ip_address', 'status', ]; - public function post() { + public function post() + { return $this->belongsTo(Post::class, 'post_id'); } - public function user() { + + public function user() + { return $this->belongsTo(User::class, 'user_id'); } - public function parent() { + + public function parent() + { return $this->belongsTo(BoardComment::class, 'parent_id'); } - public function children() { + + public function children() + { return $this->hasMany(BoardComment::class, 'parent_id')->where('status', 'active'); } } diff --git a/app/Models/Boards/BoardSetting.php b/app/Models/Boards/BoardSetting.php index 0ff2976..a8681ae 100644 --- a/app/Models/Boards/BoardSetting.php +++ b/app/Models/Boards/BoardSetting.php @@ -10,11 +10,13 @@ class BoardSetting extends Model { protected $table = 'board_settings'; + protected $fillable = [ - 'board_id', 'name', 'field_key', 'field_type', 'field_meta', 'is_required', 'sort_order' + 'board_id', 'name', 'field_key', 'field_type', 'field_meta', 'is_required', 'sort_order', ]; - public function board() { + public function board() + { return $this->belongsTo(Board::class, 'board_id'); } } diff --git a/app/Models/Boards/Post.php b/app/Models/Boards/Post.php index 3f8bfbf..07778bb 100644 --- a/app/Models/Boards/Post.php +++ b/app/Models/Boards/Post.php @@ -2,9 +2,9 @@ namespace App\Models\Boards; +use App\Models\Commons\File; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Models\Commons\File; /** * @mixin IdeHelperPost @@ -14,18 +14,24 @@ class Post extends Model use SoftDeletes; protected $table = 'posts'; + protected $fillable = [ 'tenant_id', 'board_id', 'user_id', 'title', 'content', 'editor_type', - 'ip_address', 'is_notice', 'is_secret', 'views', 'status' + 'ip_address', 'is_notice', 'is_secret', 'views', 'status', ]; - public function files() { + public function files() + { return $this->morphMany(File::class, 'fileable'); } - public function comments() { + + public function comments() + { return $this->hasMany(BoardComment::class, 'post_id')->whereNull('parent_id')->where('status', 'active'); } - public function board() { + + public function board() + { return $this->belongsTo(Board::class, 'board_id'); } } diff --git a/app/Models/Boards/PostCustomFieldValue.php b/app/Models/Boards/PostCustomFieldValue.php index e01c6bc..1ee63ae 100644 --- a/app/Models/Boards/PostCustomFieldValue.php +++ b/app/Models/Boards/PostCustomFieldValue.php @@ -10,12 +10,16 @@ class PostCustomFieldValue extends Model { protected $table = 'post_custom_field_values'; + protected $fillable = ['post_id', 'field_id', 'value']; - public function post() { + public function post() + { return $this->belongsTo(Post::class, 'post_id'); } - public function field() { + + public function field() + { return $this->belongsTo(BoardSetting::class, 'field_id'); } } diff --git a/app/Models/Calculation/CalculationConfig.php b/app/Models/Calculation/CalculationConfig.php index 2185dc9..077f9b0 100644 --- a/app/Models/Calculation/CalculationConfig.php +++ b/app/Models/Calculation/CalculationConfig.php @@ -2,8 +2,8 @@ namespace App\Models\Calculation; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; class CalculationConfig extends Model { @@ -21,14 +21,14 @@ class CalculationConfig extends Model 'description', 'is_active', 'created_by', - 'updated_by' + 'updated_by', ]; protected $casts = [ 'parameters' => 'array', 'conditions' => 'array', 'validation_rules' => 'array', - 'is_active' => 'boolean' + 'is_active' => 'boolean', ]; /** @@ -99,4 +99,4 @@ public static function getLatestFormula(int $tenantId, string $companyName, stri ->latestVersion() ->first(); } -} \ No newline at end of file +} diff --git a/app/Models/Commons/Category.php b/app/Models/Commons/Category.php index e1ff8ad..aad2f61 100644 --- a/app/Models/Commons/Category.php +++ b/app/Models/Commons/Category.php @@ -9,38 +9,61 @@ class Category extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ - 'tenant_id','parent_id','code_group','code','name', + 'tenant_id', 'parent_id', 'code_group', 'code', 'name', 'profile_code', // capability_profile 연결 - 'is_active','sort_order','description', - 'created_by','updated_by','deleted_by' + 'is_active', 'sort_order', 'description', + 'created_by', 'updated_by', 'deleted_by', ]; protected $casts = [ - 'is_active' => 'boolean', + 'is_active' => 'boolean', 'sort_order' => 'integer', ]; protected $hidden = [ - 'deleted_by','deleted_at' + 'deleted_by', 'deleted_at', ]; // 계층 - public function parent() { return $this->belongsTo(self::class, 'parent_id'); } - public function children() { return $this->hasMany(self::class, 'parent_id'); } + public function parent() + { + return $this->belongsTo(self::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(self::class, 'parent_id'); + } // 카테고리의 제품들 - public function products() { return $this->hasMany(\App\Models\Products\Product::class, 'category_id'); } + public function products() + { + return $this->hasMany(\App\Models\Products\Product::class, 'category_id'); + } // 카테고리 필드 - public function categoryFields() { return $this->hasMany(CategoryField::class, 'category_id'); } + public function categoryFields() + { + return $this->hasMany(CategoryField::class, 'category_id'); + } // 태그(폴리모픽) — 이미 taggables 존재 - public function tags() { return $this->morphToMany(\App\Models\Commons\Tag::class, 'taggable'); } + public function tags() + { + return $this->morphToMany(\App\Models\Commons\Tag::class, 'taggable'); + } // 스코프 - public function scopeGroup($q, string $group) { return $q->where('code_group', $group); } - public function scopeCode($q, string $code) { return $q->where('code', $code); } + public function scopeGroup($q, string $group) + { + return $q->where('code_group', $group); + } + + public function scopeCode($q, string $code) + { + return $q->where('code', $code); + } } diff --git a/app/Models/Commons/CategoryField.php b/app/Models/Commons/CategoryField.php index baddc99..e22b388 100644 --- a/app/Models/Commons/CategoryField.php +++ b/app/Models/Commons/CategoryField.php @@ -9,21 +9,21 @@ class CategoryField extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $table = 'category_fields'; protected $fillable = [ - 'tenant_id','category_id', - 'field_key','field_name','field_type', - 'is_required','sort_order','default_value','options','description', - 'created_by','updated_by','deleted_by', + 'tenant_id', 'category_id', + 'field_key', 'field_name', 'field_type', + 'is_required', 'sort_order', 'default_value', 'options', 'description', + 'created_by', 'updated_by', 'deleted_by', ]; protected $casts = [ 'is_required' => 'boolean', - 'sort_order' => 'integer', - 'options' => 'array', + 'sort_order' => 'integer', + 'options' => 'array', ]; public function category() @@ -32,5 +32,8 @@ public function category() } // 편의 스코프 - public function scopeRequired($q) { return $q->where('is_required', 1); } + public function scopeRequired($q) + { + return $q->where('is_required', 1); + } } diff --git a/app/Models/Commons/CategoryLog.php b/app/Models/Commons/CategoryLog.php index 874aae5..ddacf0c 100644 --- a/app/Models/Commons/CategoryLog.php +++ b/app/Models/Commons/CategoryLog.php @@ -11,17 +11,18 @@ class CategoryLog extends Model use BelongsToTenant, ModelTrait; protected $table = 'category_logs'; + public $timestamps = false; // changed_at 컬럼 단일 사용 protected $fillable = [ - 'category_id','tenant_id','action','changed_by','changed_at', - 'before_json','after_json','remarks', + 'category_id', 'tenant_id', 'action', 'changed_by', 'changed_at', + 'before_json', 'after_json', 'remarks', ]; protected $casts = [ - 'changed_at' => 'datetime', + 'changed_at' => 'datetime', 'before_json' => 'array', - 'after_json' => 'array', + 'after_json' => 'array', ]; public function category() diff --git a/app/Models/Commons/CategoryTemplate.php b/app/Models/Commons/CategoryTemplate.php index 00d3655..edf6d9b 100644 --- a/app/Models/Commons/CategoryTemplate.php +++ b/app/Models/Commons/CategoryTemplate.php @@ -13,14 +13,14 @@ class CategoryTemplate extends Model protected $table = 'category_templates'; protected $fillable = [ - 'tenant_id','category_id','version_no','template_json','applied_at', - 'created_by','updated_by','deleted_by','remarks', + 'tenant_id', 'category_id', 'version_no', 'template_json', 'applied_at', + 'created_by', 'updated_by', 'deleted_by', 'remarks', ]; protected $casts = [ - 'version_no' => 'integer', + 'version_no' => 'integer', 'template_json' => 'array', - 'applied_at' => 'datetime', + 'applied_at' => 'datetime', ]; public function category() diff --git a/app/Models/Commons/Classification.php b/app/Models/Commons/Classification.php index 1d49960..205c500 100644 --- a/app/Models/Commons/Classification.php +++ b/app/Models/Commons/Classification.php @@ -9,7 +9,7 @@ class Classification extends Model { - use SoftDeletes, ModelTrait, BelongsToTenant; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ 'tenant_id', diff --git a/app/Models/Commons/File.php b/app/Models/Commons/File.php index 4115f03..800dbbb 100644 --- a/app/Models/Commons/File.php +++ b/app/Models/Commons/File.php @@ -2,9 +2,9 @@ namespace App\Models\Commons; +use App\Models\Members\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Models\Members\User; /** * @mixin IdeHelperFile diff --git a/app/Models/Commons/Menu.php b/app/Models/Commons/Menu.php index f6045e4..15d8eb8 100644 --- a/app/Models/Commons/Menu.php +++ b/app/Models/Commons/Menu.php @@ -2,18 +2,18 @@ namespace App\Models\Commons; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; +use App\Models\Scopes\TenantScope; use App\Traits\BelongsToTenant; use App\Traits\ModelTrait; -use App\Models\Scopes\TenantScope; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; /** * @mixin IdeHelperMenu */ class Menu extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ 'tenant_id', 'parent_id', 'name', 'url', 'is_active', 'sort_order', @@ -25,7 +25,7 @@ class Menu extends Model 'created_by', 'updated_by', 'deleted_by', - 'deleted_at' + 'deleted_at', ]; public function parent() diff --git a/app/Models/Commons/Tag.php b/app/Models/Commons/Tag.php index d1a6d14..53c10ad 100644 --- a/app/Models/Commons/Tag.php +++ b/app/Models/Commons/Tag.php @@ -23,7 +23,6 @@ public function tenant(): BelongsTo return $this->belongsTo(Tenant::class); } - /** * 제품(Product)와 연결 (N:M, 폴리모픽) */ @@ -47,5 +46,4 @@ public function materials(): MorphToMany { return $this->morphedByMany(Material::class, 'taggable'); } - } diff --git a/app/Models/Design/BomTemplate.php b/app/Models/Design/BomTemplate.php index b1f6c5a..d9d7ebc 100644 --- a/app/Models/Design/BomTemplate.php +++ b/app/Models/Design/BomTemplate.php @@ -12,8 +12,8 @@ class BomTemplate extends Model protected $table = 'bom_templates'; protected $fillable = [ - 'tenant_id','model_version_id','name','is_primary','notes', - 'calculation_schema','company_type','formula_version', + 'tenant_id', 'model_version_id', 'name', 'is_primary', 'notes', + 'calculation_schema', 'company_type', 'formula_version', ]; protected $casts = [ @@ -21,11 +21,13 @@ class BomTemplate extends Model 'calculation_schema' => 'array', ]; - public function modelVersion() { + public function modelVersion() + { return $this->belongsTo(ModelVersion::class, 'model_version_id'); } - public function items() { + public function items() + { return $this->hasMany(BomTemplateItem::class, 'bom_template_id'); } } diff --git a/app/Models/Design/BomTemplateItem.php b/app/Models/Design/BomTemplateItem.php index 2c9715e..7b8010a 100644 --- a/app/Models/Design/BomTemplateItem.php +++ b/app/Models/Design/BomTemplateItem.php @@ -9,8 +9,8 @@ class BomTemplateItem extends Model protected $table = 'bom_template_items'; protected $fillable = [ - 'tenant_id','bom_template_id','ref_type','ref_id','qty','waste_rate','uom_id','notes','sort_order', - 'is_calculated','calculation_formula','depends_on','calculation_config', + 'tenant_id', 'bom_template_id', 'ref_type', 'ref_id', 'qty', 'waste_rate', 'uom_id', 'notes', 'sort_order', + 'is_calculated', 'calculation_formula', 'depends_on', 'calculation_config', ]; protected $casts = [ @@ -21,7 +21,8 @@ class BomTemplateItem extends Model 'calculation_config' => 'array', ]; - public function template() { + public function template() + { return $this->belongsTo(BomTemplate::class, 'bom_template_id'); } } diff --git a/app/Models/Design/DesignModel.php b/app/Models/Design/DesignModel.php index 0a1a325..0500743 100644 --- a/app/Models/Design/DesignModel.php +++ b/app/Models/Design/DesignModel.php @@ -12,7 +12,7 @@ class DesignModel extends Model protected $table = 'models'; protected $fillable = [ - 'tenant_id','code','name','category_id','lifecycle','description','is_active', + 'tenant_id', 'code', 'name', 'category_id', 'lifecycle', 'description', 'is_active', ]; protected $casts = [ @@ -20,7 +20,8 @@ class DesignModel extends Model ]; // 관계: 모델은 여러 버전을 가짐 - public function versions() { + public function versions() + { return $this->hasMany(ModelVersion::class, 'model_id'); } } diff --git a/app/Models/Design/ModelVersion.php b/app/Models/Design/ModelVersion.php index b58ff7d..6358823 100644 --- a/app/Models/Design/ModelVersion.php +++ b/app/Models/Design/ModelVersion.php @@ -12,7 +12,7 @@ class ModelVersion extends Model protected $table = 'model_versions'; protected $fillable = [ - 'tenant_id','model_id','version_no','status','effective_from','effective_to','notes','is_active', + 'tenant_id', 'model_id', 'version_no', 'status', 'effective_from', 'effective_to', 'notes', 'is_active', ]; protected $casts = [ @@ -21,15 +21,18 @@ class ModelVersion extends Model 'effective_to' => 'datetime', ]; - public function model() { + public function model() + { return $this->belongsTo(DesignModel::class, 'model_id'); } - public function bomTemplates() { + public function bomTemplates() + { return $this->hasMany(BomTemplate::class, 'model_version_id'); } - public function scopeReleased($q) { + public function scopeReleased($q) + { return $q->where('status', 'RELEASED'); } } diff --git a/app/Models/Estimate/Estimate.php b/app/Models/Estimate/Estimate.php index 1647b2d..e41beb4 100644 --- a/app/Models/Estimate/Estimate.php +++ b/app/Models/Estimate/Estimate.php @@ -12,7 +12,7 @@ class Estimate extends Model { - use HasFactory, SoftDeletes, BelongsToTenant; + use BelongsToTenant, HasFactory, SoftDeletes; protected $fillable = [ 'tenant_id', @@ -75,7 +75,7 @@ public static function generateEstimateNo(int $tenantId): string $sequence = $lastEstimate ? (int) substr($lastEstimate->estimate_no, -3) + 1 : 1; - return $prefix . $date . str_pad($sequence, 3, '0', STR_PAD_LEFT); + return $prefix.$date.str_pad($sequence, 3, '0', STR_PAD_LEFT); } /** @@ -102,6 +102,6 @@ public function scopeApproved($query) public function scopeExpired($query) { return $query->whereNotNull('valid_until') - ->where('valid_until', '<', now()); + ->where('valid_until', '<', now()); } -} \ No newline at end of file +} diff --git a/app/Models/Estimate/EstimateItem.php b/app/Models/Estimate/EstimateItem.php index 30890b9..032fbea 100644 --- a/app/Models/Estimate/EstimateItem.php +++ b/app/Models/Estimate/EstimateItem.php @@ -10,7 +10,7 @@ class EstimateItem extends Model { - use HasFactory, SoftDeletes, BelongsToTenant; + use BelongsToTenant, HasFactory, SoftDeletes; protected $fillable = [ 'tenant_id', @@ -77,4 +77,4 @@ protected static function boot() $item->total_price = $item->calculateTotalPrice(); }); } -} \ No newline at end of file +} diff --git a/app/Models/MainRequest.php b/app/Models/MainRequest.php index 091a893..e7be4bd 100644 --- a/app/Models/MainRequest.php +++ b/app/Models/MainRequest.php @@ -42,12 +42,12 @@ public function flows(): HasMany */ public function estimates() { - //return $this->hasMany(MainRequestEstimate::class, 'main_request_id'); + // return $this->hasMany(MainRequestEstimate::class, 'main_request_id'); } public function orders() { - //return $this->hasMany(MainRequestOrder::class, 'main_request_id'); + // return $this->hasMany(MainRequestOrder::class, 'main_request_id'); } // ... 필요에 따라 파생 업무별 hasMany 관계 추가 diff --git a/app/Models/MainRequestFlow.php b/app/Models/MainRequestFlow.php index 000a2bb..75518cf 100644 --- a/app/Models/MainRequestFlow.php +++ b/app/Models/MainRequestFlow.php @@ -2,7 +2,6 @@ namespace App\Models; -use App\Models\MainRequest; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; diff --git a/app/Models/Materials/Material.php b/app/Models/Materials/Material.php index 63d267f..26f8533 100644 --- a/app/Models/Materials/Material.php +++ b/app/Models/Materials/Material.php @@ -5,9 +5,8 @@ use App\Models\Commons\File; use App\Models\Commons\Tag; use App\Models\Qualitys\Lot; -use App\Traits\ModelTrait; use App\Traits\BelongsToTenant; -use Filament\Forms\Components\Hidden; +use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -16,7 +15,7 @@ */ class Material extends Model { - use SoftDeletes, ModelTrait, BelongsToTenant; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ 'tenant_id', @@ -37,7 +36,7 @@ class Material extends Model protected $casts = [ 'attributes' => 'array', - 'options' => 'array', + 'options' => 'array', ]; protected $hidden = [ diff --git a/app/Models/Materials/MaterialReceipt.php b/app/Models/Materials/MaterialReceipt.php index 7e9c720..1abfe2d 100644 --- a/app/Models/Materials/MaterialReceipt.php +++ b/app/Models/Materials/MaterialReceipt.php @@ -15,7 +15,7 @@ class MaterialReceipt extends Model protected $fillable = [ 'material_id', 'receipt_date', 'lot_number', 'received_qty', 'unit', 'supplier_name', 'manufacturer_name', 'purchase_price_excl_vat', - 'weight_kg', 'status_code', 'is_inspection', 'inspection_date', 'remarks' + 'weight_kg', 'status_code', 'is_inspection', 'inspection_date', 'remarks', ]; // 자재 마스터 diff --git a/app/Models/Members/User.php b/app/Models/Members/User.php index 916455d..8c6b2c7 100644 --- a/app/Models/Members/User.php +++ b/app/Models/Members/User.php @@ -17,7 +17,7 @@ */ class User extends Authenticatable { - use HasApiTokens, Notifiable, SoftDeletes, ModelTrait, HasRoles; + use HasApiTokens, HasRoles, ModelTrait, Notifiable, SoftDeletes; protected $guard_name = 'api'; // ★ 중요: 권한/역할 가드 통일 @@ -33,10 +33,10 @@ class User extends Authenticatable protected $casts = [ 'email_verified_at' => 'datetime', - 'last_login_at' => 'datetime', - 'options' => 'array', - 'deleted_at' => 'datetime', - 'password' => 'hashed', // ← 이걸 쓰면 자동 해싱 + 'last_login_at' => 'datetime', + 'options' => 'array', + 'deleted_at' => 'datetime', + 'password' => 'hashed', // ← 이걸 쓰면 자동 해싱 ]; protected $hidden = [ diff --git a/app/Models/Members/UserMenuPermission.php b/app/Models/Members/UserMenuPermission.php index 3991756..cbb04f5 100644 --- a/app/Models/Members/UserMenuPermission.php +++ b/app/Models/Members/UserMenuPermission.php @@ -11,7 +11,7 @@ class UserMenuPermission extends Model { protected $fillable = [ - 'user_id', 'menu_id', 'access', 'read', 'write', 'export', 'approve' + 'user_id', 'menu_id', 'access', 'read', 'write', 'export', 'approve', ]; public function user() diff --git a/app/Models/Members/UserRole.php b/app/Models/Members/UserRole.php index df966a1..8a3c401 100644 --- a/app/Models/Members/UserRole.php +++ b/app/Models/Members/UserRole.php @@ -13,10 +13,10 @@ */ class UserRole extends Model { - use SoftDeletes, BelongsToTenant; + use BelongsToTenant, SoftDeletes; protected $fillable = [ - 'user_id', 'tenant_id', 'role_id', 'assigned_at' + 'user_id', 'tenant_id', 'role_id', 'assigned_at', ]; public function user() diff --git a/app/Models/Members/UserTenant.php b/app/Models/Members/UserTenant.php index cdd138a..d8a01e6 100644 --- a/app/Models/Members/UserTenant.php +++ b/app/Models/Members/UserTenant.php @@ -13,10 +13,10 @@ */ class UserTenant extends Model { - use SoftDeletes, ModelTrait, BelongsToTenant; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ - 'user_id', 'tenant_id', 'is_active', 'is_default', 'joined_at', 'left_at' + 'user_id', 'tenant_id', 'is_active', 'is_default', 'joined_at', 'left_at', ]; protected $guarded = [ @@ -27,11 +27,11 @@ class UserTenant extends Model ]; protected $casts = [ - 'joined_at' => 'datetime', - 'left_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', + 'joined_at' => 'datetime', + 'left_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', ]; protected $hidden = [ diff --git a/app/Models/Orders/Order.php b/app/Models/Orders/Order.php index 14cade0..25c121a 100644 --- a/app/Models/Orders/Order.php +++ b/app/Models/Orders/Order.php @@ -18,7 +18,7 @@ class Order extends Model protected $fillable = [ 'tenant_id', 'order_no', 'order_type_code', 'status_code', 'category_code', 'product_id', 'received_at', 'writer_id', 'client_id', 'client_contact', 'site_name', 'quantity', 'delivery_date', - 'delivery_method_code', 'memo' + 'delivery_method_code', 'memo', ]; // 상세(라인) diff --git a/app/Models/Orders/OrderHistory.php b/app/Models/Orders/OrderHistory.php index 209fc50..40c754e 100644 --- a/app/Models/Orders/OrderHistory.php +++ b/app/Models/Orders/OrderHistory.php @@ -13,7 +13,7 @@ class OrderHistory extends Model protected $table = 'order_histories'; protected $fillable = [ - 'tenant_id', 'order_id', 'history_type', 'content', 'created_by' + 'tenant_id', 'order_id', 'history_type', 'content', 'created_by', ]; public function order() diff --git a/app/Models/Orders/OrderItem.php b/app/Models/Orders/OrderItem.php index d370f9b..fe7672d 100644 --- a/app/Models/Orders/OrderItem.php +++ b/app/Models/Orders/OrderItem.php @@ -17,7 +17,7 @@ class OrderItem extends Model protected $fillable = [ 'tenant_id', 'order_id', 'serial_no', 'product_id', 'quantity', - 'status_code', 'design_code', 'remarks', 'attributes' + 'status_code', 'design_code', 'remarks', 'attributes', ]; // 투입 구성(자재/BOM 등) diff --git a/app/Models/Orders/OrderItemComponent.php b/app/Models/Orders/OrderItemComponent.php index 0a19807..c150156 100644 --- a/app/Models/Orders/OrderItemComponent.php +++ b/app/Models/Orders/OrderItemComponent.php @@ -17,7 +17,7 @@ class OrderItemComponent extends Model protected $fillable = [ 'tenant_id', 'order_item_id', 'component_type', 'component_id', 'quantity', - 'unit', 'price', 'remarks', 'created_by', 'updated_by' + 'unit', 'price', 'remarks', 'created_by', 'updated_by', ]; public function orderItem() diff --git a/app/Models/Orders/OrderVersion.php b/app/Models/Orders/OrderVersion.php index b832ec7..986e6f4 100644 --- a/app/Models/Orders/OrderVersion.php +++ b/app/Models/Orders/OrderVersion.php @@ -13,7 +13,7 @@ class OrderVersion extends Model protected $table = 'order_versions'; protected $fillable = [ - 'tenant_id', 'order_id', 'version_no', 'version_type', 'status_code', 'changed_fields', 'created_by', 'change_note' + 'tenant_id', 'order_id', 'version_no', 'version_type', 'status_code', 'changed_fields', 'created_by', 'change_note', ]; public function order() diff --git a/app/Models/Permissions/Permission.php b/app/Models/Permissions/Permission.php index bb062a4..70936a3 100644 --- a/app/Models/Permissions/Permission.php +++ b/app/Models/Permissions/Permission.php @@ -17,4 +17,4 @@ public function tenant() { return $this->belongsTo(Tenant::class); } -} \ No newline at end of file +} diff --git a/app/Models/Permissions/PermissionOverride.php b/app/Models/Permissions/PermissionOverride.php index c29b956..9a7e62d 100644 --- a/app/Models/Permissions/PermissionOverride.php +++ b/app/Models/Permissions/PermissionOverride.php @@ -11,15 +11,16 @@ class PermissionOverride extends Model use SoftDeletes; protected $table = 'permission_overrides'; + protected $guarded = ['id']; protected $casts = [ - 'tenant_id' => 'int', - 'model_id' => 'int', - 'permission_id' => 'int', - 'effect' => 'int', // 1=ALLOW, -1=DENY + 'tenant_id' => 'int', + 'model_id' => 'int', + 'permission_id' => 'int', + 'effect' => 'int', // 1=ALLOW, -1=DENY 'effective_from' => 'datetime', - 'effective_to' => 'datetime', + 'effective_to' => 'datetime', ]; /** diff --git a/app/Models/Permissions/Role.php b/app/Models/Permissions/Role.php index d76b521..06a4abb 100644 --- a/app/Models/Permissions/Role.php +++ b/app/Models/Permissions/Role.php @@ -13,7 +13,7 @@ class Role extends Model { protected $fillable = [ - 'tenant_id', 'name', 'description' + 'tenant_id', 'name', 'description', ]; public function menuPermissions() diff --git a/app/Models/Permissions/RoleMenuPermission.php b/app/Models/Permissions/RoleMenuPermission.php index 7c77033..8535b62 100644 --- a/app/Models/Permissions/RoleMenuPermission.php +++ b/app/Models/Permissions/RoleMenuPermission.php @@ -12,7 +12,7 @@ class RoleMenuPermission extends Model { protected $fillable = [ - 'role_id', 'menu_id', 'access', 'read', 'write', 'export', 'approve' + 'role_id', 'menu_id', 'access', 'read', 'write', 'export', 'approve', ]; public function role() diff --git a/app/Models/Products/CommonCode.php b/app/Models/Products/CommonCode.php index 3456b98..9cc1fa9 100644 --- a/app/Models/Products/CommonCode.php +++ b/app/Models/Products/CommonCode.php @@ -2,16 +2,17 @@ namespace App\Models\Products; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; use App\Traits\BelongsToTenant; use App\Traits\ModelTrait; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; + /** * @mixin IdeHelperCommonCode */ class CommonCode extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $table = 'common_codes'; @@ -24,7 +25,7 @@ class CommonCode extends Model 'attributes', 'description', 'is_active', - 'sort_order' + 'sort_order', ]; protected $casts = [ diff --git a/app/Models/Products/Part.php b/app/Models/Products/Part.php index c568c91..6b355a5 100644 --- a/app/Models/Products/Part.php +++ b/app/Models/Products/Part.php @@ -12,12 +12,16 @@ class Part extends Model { use SoftDeletes; - protected $fillable = ['tenant_id','code','name','category_id','part_type_id','unit','attributes','description','is_active']; - public function category() { + protected $fillable = ['tenant_id', 'code', 'name', 'category_id', 'part_type_id', 'unit', 'attributes', 'description', 'is_active']; + + public function category() + { return $this->belongsTo(CommonCode::class, 'category_id'); } - public function partType() { + + public function partType() + { return $this->belongsTo(CommonCode::class, 'part_type_id'); } diff --git a/app/Models/Products/Product.php b/app/Models/Products/Product.php index bee1269..a0f231c 100644 --- a/app/Models/Products/Product.php +++ b/app/Models/Products/Product.php @@ -12,22 +12,22 @@ class Product extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $fillable = [ - 'tenant_id','code','name','unit','category_id', + 'tenant_id', 'code', 'name', 'unit', 'category_id', 'product_type', // 라벨/분류용 - 'attributes','description', - 'is_sellable','is_purchasable','is_producible','is_active', - 'created_by','updated_by' + 'attributes', 'description', + 'is_sellable', 'is_purchasable', 'is_producible', 'is_active', + 'created_by', 'updated_by', ]; protected $casts = [ - 'attributes' => 'array', - 'is_sellable' => 'boolean', + 'attributes' => 'array', + 'is_sellable' => 'boolean', 'is_purchasable' => 'boolean', - 'is_producible' => 'boolean', - 'is_active' => 'boolean', + 'is_producible' => 'boolean', + 'is_active' => 'boolean', ]; protected $hidden = [ @@ -35,7 +35,10 @@ class Product extends Model ]; // 분류 - public function category() { return $this->belongsTo(Category::class, 'category_id'); } + public function category() + { + return $this->belongsTo(Category::class, 'category_id'); + } // BOM (자기참조) — 라인 모델 경유 public function componentLines() @@ -54,7 +57,7 @@ public function children() { return $this->belongsToMany( self::class, 'product_components', 'parent_product_id', 'child_product_id' - )->withPivot(['quantity','sort_order','is_default']) + )->withPivot(['quantity', 'sort_order', 'is_default']) ->withTimestamps(); } @@ -62,17 +65,39 @@ public function parents() { return $this->belongsToMany( self::class, 'product_components', 'child_product_id', 'parent_product_id' - )->withPivot(['quantity','sort_order','is_default']) + )->withPivot(['quantity', 'sort_order', 'is_default']) ->withTimestamps(); } // 파일 / 태그 (폴리모픽) - public function files() { return $this->morphMany(File::class, 'fileable'); } - public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function files() + { + return $this->morphMany(File::class, 'fileable'); + } + + public function tags() + { + return $this->morphToMany(Tag::class, 'taggable'); + } // 스코프 - public function scopeType($q, string $type) { return $q->where('product_type', $type); } - public function scopeSellable($q) { return $q->where('is_sellable', 1); } - public function scopePurchasable($q) { return $q->where('is_purchasable', 1); } - public function scopeProducible($q) { return $q->where('is_producible', 1); } + public function scopeType($q, string $type) + { + return $q->where('product_type', $type); + } + + public function scopeSellable($q) + { + return $q->where('is_sellable', 1); + } + + public function scopePurchasable($q) + { + return $q->where('is_purchasable', 1); + } + + public function scopeProducible($q) + { + return $q->where('is_producible', 1); + } } diff --git a/app/Models/Products/ProductComponent.php b/app/Models/Products/ProductComponent.php index 8c1ff90..aebf176 100644 --- a/app/Models/Products/ProductComponent.php +++ b/app/Models/Products/ProductComponent.php @@ -3,14 +3,14 @@ namespace App\Models\Products; use App\Models\Materials\Material; +use App\Traits\BelongsToTenant; +use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Traits\ModelTrait; -use App\Traits\BelongsToTenant; class ProductComponent extends Model { - use SoftDeletes, ModelTrait, BelongsToTenant; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $table = 'product_components'; @@ -28,7 +28,7 @@ class ProductComponent extends Model ]; protected $casts = [ - 'quantity' => 'decimal:6', + 'quantity' => 'decimal:6', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', @@ -57,6 +57,7 @@ public function referencedItem() } elseif ($this->ref_type === 'MATERIAL') { return $this->belongsTo(Material::class, 'ref_id'); } + return null; } diff --git a/app/Models/Qualitys/LotSale.php b/app/Models/Qualitys/LotSale.php index 4bd0e19..43a1d35 100644 --- a/app/Models/Qualitys/LotSale.php +++ b/app/Models/Qualitys/LotSale.php @@ -1,6 +1,5 @@ runningInConsole()) return; + if (app()->runningInConsole()) { + return; + } // request 헬퍼 사용 → request 인스턴스를 명시적으로 주입받아 사용해야 함 $request = app(Request::class); diff --git a/app/Models/SiteAdmin.php b/app/Models/SiteAdmin.php index 7828d70..142b647 100644 --- a/app/Models/SiteAdmin.php +++ b/app/Models/SiteAdmin.php @@ -13,10 +13,12 @@ class SiteAdmin extends Model use UppercaseAttributes; // 테이블 컬럼명 대문자 처리 protected $table = 'SITE_ADMIN'; + protected $primaryKey = 'UNO'; + public $timestamps = false; protected $fillable = [ - 'UNO', 'LEVEL'. 'COMMENT' + 'UNO', 'LEVEL'.'COMMENT', ]; } diff --git a/app/Models/Tenants/Department.php b/app/Models/Tenants/Department.php index b6a8492..f1def19 100644 --- a/app/Models/Tenants/Department.php +++ b/app/Models/Tenants/Department.php @@ -3,29 +3,31 @@ namespace App\Models\Tenants; use App\Models\Members\User; +use App\Models\Permissions\PermissionOverride; +use App\Models\Tenants\Pivots\DepartmentUser; +use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphMany; use Spatie\Permission\Traits\HasRoles; -use App\Traits\ModelTrait; -use App\Models\Tenants\Pivots\DepartmentUser; -use App\Models\Permissions\PermissionOverride; class Department extends Model { use HasRoles, ModelTrait; // 부서도 권한/역할을 가짐 protected $table = 'departments'; + protected $guarded = ['id']; + protected $casts = [ 'tenant_id' => 'int', 'parent_id' => 'int', 'is_active' => 'bool', - 'sort_order'=> 'int', + 'sort_order' => 'int', ]; protected $hidden = [ - 'deleted_by','deleted_at' + 'deleted_by', 'deleted_at', ]; // 스파티 가드명(프로젝트 설정에 맞게 조정) @@ -48,7 +50,7 @@ public function users() return $this->belongsToMany(User::class, 'department_user') ->using(DepartmentUser::class) ->withTimestamps() - ->withPivot(['tenant_id','is_primary','joined_at','left_at']); + ->withPivot(['tenant_id', 'is_primary', 'joined_at', 'left_at']); } /** 부서의 권한 오버라이드(DENY/임시허용) */ diff --git a/app/Models/Tenants/Payment.php b/app/Models/Tenants/Payment.php index ef79880..46d6ea6 100644 --- a/app/Models/Tenants/Payment.php +++ b/app/Models/Tenants/Payment.php @@ -13,14 +13,15 @@ class Payment extends Model use SoftDeletes; protected $fillable = [ - 'subscription_id', 'amount', 'payment_method', 'transaction_id', 'paid_at', 'status', 'memo' + 'subscription_id', 'amount', 'payment_method', 'transaction_id', 'paid_at', 'status', 'memo', ]; protected $dates = [ 'paid_at', ]; - public function subscription() { + public function subscription() + { return $this->belongsTo(Subscription::class); } } diff --git a/app/Models/Tenants/Pivots/DepartmentUser.php b/app/Models/Tenants/Pivots/DepartmentUser.php index 1b566a8..f872a3e 100644 --- a/app/Models/Tenants/Pivots/DepartmentUser.php +++ b/app/Models/Tenants/Pivots/DepartmentUser.php @@ -15,20 +15,21 @@ */ class DepartmentUser extends Model { - use SoftDeletes, BelongsToTenant, ModelTrait; + use BelongsToTenant, ModelTrait, SoftDeletes; protected $table = 'department_user'; + protected $fillable = [ - 'tenant_id','department_id','user_id','is_primary','joined_at','left_at', + 'tenant_id', 'department_id', 'user_id', 'is_primary', 'joined_at', 'left_at', ]; protected $casts = [ - 'tenant_id' => 'integer', + 'tenant_id' => 'integer', 'department_id' => 'integer', - 'user_id' => 'integer', - 'is_primary' => 'integer', - 'joined_at' => 'datetime', - 'left_at' => 'datetime', + 'user_id' => 'integer', + 'is_primary' => 'integer', + 'joined_at' => 'datetime', + 'left_at' => 'datetime', ]; public function department() diff --git a/app/Models/Tenants/Plan.php b/app/Models/Tenants/Plan.php index 19e8008..383da16 100644 --- a/app/Models/Tenants/Plan.php +++ b/app/Models/Tenants/Plan.php @@ -22,7 +22,8 @@ class Plan extends Model 'price' => 'float', ]; - public function subscriptions() { + public function subscriptions() + { return $this->hasMany(Subscription::class); } } diff --git a/app/Models/Tenants/Subscription.php b/app/Models/Tenants/Subscription.php index 4e7796b..8031a0c 100644 --- a/app/Models/Tenants/Subscription.php +++ b/app/Models/Tenants/Subscription.php @@ -20,15 +20,18 @@ class Subscription extends Model 'started_at', 'ended_at', ]; - public function tenant() { + public function tenant() + { return $this->belongsTo(Tenant::class); } - public function plan() { + public function plan() + { return $this->belongsTo(Plan::class); } - public function payments() { + public function payments() + { return $this->hasMany(Payment::class); } } diff --git a/app/Models/Tenants/Tenant.php b/app/Models/Tenants/Tenant.php index b2a73c5..a035dd8 100644 --- a/app/Models/Tenants/Tenant.php +++ b/app/Models/Tenants/Tenant.php @@ -16,7 +16,7 @@ */ class Tenant extends Model { - use SoftDeletes, ModelTrait; + use ModelTrait, SoftDeletes; protected $fillable = [ 'company_name', @@ -46,14 +46,14 @@ class Tenant extends Model ]; protected $casts = [ - 'trial_ends_at' => 'datetime', - 'expires_at' => 'datetime', - 'last_paid_at' => 'datetime', - 'max_users' => 'integer', - 'options' => 'array', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', + 'trial_ends_at' => 'datetime', + 'expires_at' => 'datetime', + 'last_paid_at' => 'datetime', + 'max_users' => 'integer', + 'options' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', ]; protected $hidden = [ diff --git a/app/Observers/MenuObserver.php b/app/Observers/MenuObserver.php index d36bbd3..1f2294b 100644 --- a/app/Observers/MenuObserver.php +++ b/app/Observers/MenuObserver.php @@ -11,16 +11,18 @@ class MenuObserver protected function actions(): array { // config/authz.php 에서 오버라이드 가능 (없으면 기본값) - return config('authz.menu_actions', ['view','create','update','delete','approve']); + return config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']); } protected string $guard = 'api'; public function created(Menu $menu): void { - if (!$this->shouldHandle($menu)) return; + if (! $this->shouldHandle($menu)) { + return; + } - $this->setTeam((int)$menu->tenant_id); + $this->setTeam((int) $menu->tenant_id); $this->ensurePermissions($menu); $this->forgetCache(); } @@ -33,27 +35,33 @@ public function updated(Menu $menu): void public function deleted(Menu $menu): void { - if (!$this->shouldHandle($menu)) return; + if (! $this->shouldHandle($menu)) { + return; + } - $this->setTeam((int)$menu->tenant_id); + $this->setTeam((int) $menu->tenant_id); $this->removePermissions($menu); $this->forgetCache(); } public function restored(Menu $menu): void { - if (!$this->shouldHandle($menu)) return; + if (! $this->shouldHandle($menu)) { + return; + } - $this->setTeam((int)$menu->tenant_id); + $this->setTeam((int) $menu->tenant_id); $this->ensurePermissions($menu); $this->forgetCache(); } public function forceDeleted(Menu $menu): void { - if (!$this->shouldHandle($menu)) return; + if (! $this->shouldHandle($menu)) { + return; + } - $this->setTeam((int)$menu->tenant_id); + $this->setTeam((int) $menu->tenant_id); $this->removePermissions($menu); $this->forgetCache(); } @@ -61,7 +69,7 @@ public function forceDeleted(Menu $menu): void /** teams 사용 시 tenant_id 없는 공용 메뉴는 스킵하는 게 안전 */ protected function shouldHandle(Menu $menu): bool { - return !is_null($menu->tenant_id); + return ! is_null($menu->tenant_id); } protected function setTeam(int $tenantId): void @@ -73,16 +81,16 @@ protected function ensurePermissions(Menu $menu): void { foreach ($this->actions() as $act) { Permission::firstOrCreate([ - 'tenant_id' => (int)$menu->tenant_id, + 'tenant_id' => (int) $menu->tenant_id, 'guard_name' => $this->guard, - 'name' => "menu:{$menu->id}.{$act}", + 'name' => "menu:{$menu->id}.{$act}", ]); } } protected function removePermissions(Menu $menu): void { - Permission::where('tenant_id', (int)$menu->tenant_id) + Permission::where('tenant_id', (int) $menu->tenant_id) ->where('guard_name', $this->guard) ->where(function ($q) use ($menu) { foreach ($this->actions() as $act) { diff --git a/app/Observers/TenantObserver.php b/app/Observers/TenantObserver.php index 798c243..3b1b5a3 100644 --- a/app/Observers/TenantObserver.php +++ b/app/Observers/TenantObserver.php @@ -9,6 +9,6 @@ class TenantObserver { public function created(Tenant $tenant): void { - app(TenantBootstrapper::class)->bootstrap((int)$tenant->id, 'STANDARD'); + app(TenantBootstrapper::class)->bootstrap((int) $tenant->id, 'STANDARD'); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fa4461e..d430274 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,13 +2,13 @@ namespace App\Providers; +use App\Models\Commons\Menu; use App\Models\Tenants\Tenant; +use App\Observers\MenuObserver; use App\Observers\TenantObserver; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\DB; -use App\Models\Commons\Menu; -use App\Observers\MenuObserver; +use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { @@ -32,13 +32,14 @@ public function register(): void public function boot(): void { // DB::enableQueryLog(); - Builder::macro('debug', function($debug = null) { + Builder::macro('debug', function ($debug = null) { if (is_null($debug) && app()->environment('local')) { $debug = true; } if ($debug) { \DB::enableQueryLog(); } + return $this; }); diff --git a/app/Providers/MigrationServiceProvider.php b/app/Providers/MigrationServiceProvider.php index a0aba60..5bb51e4 100644 --- a/app/Providers/MigrationServiceProvider.php +++ b/app/Providers/MigrationServiceProvider.php @@ -2,10 +2,10 @@ namespace App\Providers; -use Illuminate\Support\ServiceProvider; use Illuminate\Database\Events\MigrationsEnded; -use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\ServiceProvider; class MigrationServiceProvider extends ServiceProvider { @@ -36,7 +36,7 @@ private function updateRelationshipsAfterMigration(): void \Log::info('✅ 마이그레이션 후 논리적 관계 문서 업데이트 완료'); } catch (\Exception $e) { - \Log::error('❌ 논리적 관계 문서 업데이트 실패: ' . $e->getMessage()); + \Log::error('❌ 논리적 관계 문서 업데이트 실패: '.$e->getMessage()); } } @@ -48,7 +48,7 @@ private function autoCommitRelationships(): void 🤖 Generated with [Claude Code](https://claude.ai/code) -Co-Authored-By: Claude "' +Co-Authored-By: Claude "', ]; foreach ($commands as $command) { @@ -59,4 +59,4 @@ private function autoCommitRelationships(): void } } } -} \ No newline at end of file +} diff --git a/app/Repositories/MainRequestRepository.php b/app/Repositories/MainRequestRepository.php index 1920afb..baed28d 100644 --- a/app/Repositories/MainRequestRepository.php +++ b/app/Repositories/MainRequestRepository.php @@ -1,4 +1,5 @@ $mainRequestId, - 'flowable_type' => $flowableType, - 'flowable_id' => $flowableId, - 'status_code' => $status, - 'action' => $action, - 'actor_id' => $actorId, - 'content' => $content, + 'flowable_type' => $flowableType, + 'flowable_id' => $flowableId, + 'status_code' => $status, + 'action' => $action, + 'actor_id' => $actorId, + 'content' => $content, ]); } } diff --git a/app/Services/AdminPermissionService.php b/app/Services/AdminPermissionService.php index a7c56c8..182e0c7 100644 --- a/app/Services/AdminPermissionService.php +++ b/app/Services/AdminPermissionService.php @@ -13,12 +13,16 @@ public static function getPermissionsByToken(string $userToken): array $user = User::where('USER_TOKEN', $userToken) ->first(); - if (!$user) return []; + if (! $user) { + return []; + } $admin = SiteAdmin::where('UNO', $user->USER_NO) ->first(); - if (!$admin) return []; + if (! $admin) { + return []; + } $permissionCodes = DB::table('SITE_ADMIN_USER_ROLE AS ur') ->join('SITE_ADMIN_ROLE_PERMISSION AS rp', 'ur.ROLE_ID', '=', 'rp.ROLE_ID') @@ -33,6 +37,7 @@ public static function getPermissionsByToken(string $userToken): array public static function hasPermission(string $userToken, string $code): bool { $permissions = self::getPermissionsByToken($userToken); + return in_array($code, $permissions); } } diff --git a/app/Services/AdminService.php b/app/Services/AdminService.php index ac5df64..7289d9f 100644 --- a/app/Services/AdminService.php +++ b/app/Services/AdminService.php @@ -2,15 +2,13 @@ namespace App\Services; +use App\Models\Members\User; +use App\Models\Members\UserTenant; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rule; -use App\Models\Members\User; -use App\Models\Members\UserTenant; - - class AdminService { /** @@ -21,24 +19,24 @@ class AdminService public static function getTenants(array $params = []) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '성 테넌트가 없습니다.', 'code' => 400]; } - $page = isset($params['page']) ? (int)$params['page'] : 1; - $size = isset($params['size']) ? (int)$params['size'] : 10; - $keyword = $params['q'] ?? null; - $active = $params['is_active'] ?? null; // 0/1 - $sortBy = $params['sort_by'] ?? 'users.id'; - $sortDir = strtolower($params['sort_dir'] ?? 'desc') === 'asc' ? 'asc' : 'desc'; + $page = isset($params['page']) ? (int) $params['page'] : 1; + $size = isset($params['size']) ? (int) $params['size'] : 10; + $keyword = $params['q'] ?? null; + $active = $params['is_active'] ?? null; // 0/1 + $sortBy = $params['sort_by'] ?? 'users.id'; + $sortDir = strtolower($params['sort_dir'] ?? 'desc') === 'asc' ? 'asc' : 'desc'; $q = UserTenant::query() ->with(['user:id,name,email,phone']) ->where('tenant_id', $tenantId); if ($keyword) { - $q->whereHas('user', function($sub) use ($keyword) { - $sub->where(function($w) use ($keyword) { + $q->whereHas('user', function ($sub) use ($keyword) { + $sub->where(function ($w) use ($keyword) { $w->where('name', 'like', "%{$keyword}%") ->orWhere('email', 'like', "%{$keyword}%") ->orWhere('phone', 'like', "%{$keyword}%"); @@ -47,7 +45,7 @@ public static function getTenants(array $params = []) } if ($active !== null && $active !== '') { - $q->where('is_active', (int)$active); + $q->where('is_active', (int) $active); } // 조인 정렬용 @@ -77,19 +75,19 @@ public static function getTenants(array $params = []) public static function store(array $params = []) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } // 신규 회원 생성 + 역할 부여 지원 $v = Validator::make($params, [ - 'user_id' => 'required|string|max:255|unique:users,user_id', - 'name' => 'required|string|max:255', - 'email' => 'required|email|max:100|unique:users,email', - 'phone' => 'nullable|string|max:30', + 'user_id' => 'required|string|max:255|unique:users,user_id', + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:100|unique:users,email', + 'phone' => 'nullable|string|max:30', 'password' => 'required|string|min:8|max:64', - 'roles' => 'nullable|array', - 'roles.*' => 'string|max:100', // 각각의 역할 이름 + 'roles' => 'nullable|array', + 'roles.*' => 'string|max:100', // 각각의 역할 이름 ]); if ($v->fails()) { @@ -101,24 +99,24 @@ public static function store(array $params = []) return DB::transaction(function () use ($payload, $tenantId) { // 신규 사용자 생성 $user = User::create([ - 'user_id' => $payload['user_id'], - 'name' => $payload['name'], - 'email' => $payload['email'], - 'phone' => $payload['phone'] ?? null, + 'user_id' => $payload['user_id'], + 'name' => $payload['name'], + 'email' => $payload['email'], + 'phone' => $payload['phone'] ?? null, 'password' => $payload['password'], // 캐스트가 알아서 해싱 ]); // 현재 테넌트에 활성 연결 UserTenant::create([ - 'user_id' => $user->id, - 'tenant_id' => $tenantId, - 'is_active' => 1, + 'user_id' => $user->id, + 'tenant_id' => $tenantId, + 'is_active' => 1, 'is_default' => 0, - 'joined_at' => now(), + 'joined_at' => now(), ]); // 역할 부여 (Spatie Permission teams 모드 가정) - if (!empty($payload['roles']) && method_exists($user, 'assignRole')) { + if (! empty($payload['roles']) && method_exists($user, 'assignRole')) { $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; app()->instance('permission.team_id', $tenantId); @@ -132,7 +130,7 @@ public static function store(array $params = []) } return [ - 'user' => $user->only(['id','user_id','name','email','phone']), + 'user' => $user->only(['id', 'user_id', 'name', 'email', 'phone']), ]; }); } @@ -144,17 +142,17 @@ public static function store(array $params = []) public static function show(int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $user = User::whereHas('userTenants')->find($userNo); - if (!$user) { + if (! $user) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } @@ -166,38 +164,38 @@ public static function show(int $userNo) * - 회원 기본정보(user_id, name, email, phone, password) 변경 * - 역할(roles) 변경 및 삭제 처리 */ - public static function update(array $params = [], int $userNo) + public static function update(array $params, int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } // 1) 유저 존재/테넌트 소속 확인 $user = User::find($userNo); - if (!$user) { + if (! $user) { return ['error' => '해당 회원을 찾을 수 없습니다.', 'code' => 404]; } $linked = UserTenant::where('tenant_id', $tenantId) ->where('user_id', $userNo) ->exists(); - if (!$linked) { + if (! $linked) { return ['error' => '이 테넌트에 소속된 회원이 아닙니다.', 'code' => 403]; } // 2) 프로필 + roles만 수정 $v = Validator::make($params, [ - 'user_id' => ['nullable','string','max:255', Rule::unique('users','user_id')->ignore($userNo)], - 'name' => 'nullable|string|max:255', - 'email' => ['nullable','email','max:100', Rule::unique('users','email')->ignore($userNo)], - 'phone' => 'nullable|string|max:30', + 'user_id' => ['nullable', 'string', 'max:255', Rule::unique('users', 'user_id')->ignore($userNo)], + 'name' => 'nullable|string|max:255', + 'email' => ['nullable', 'email', 'max:100', Rule::unique('users', 'email')->ignore($userNo)], + 'phone' => 'nullable|string|max:30', 'password' => 'nullable|string|min:8|max:64', - 'roles' => 'nullable|array', - 'roles.*' => 'string|max:100', + 'roles' => 'nullable|array', + 'roles.*' => 'string|max:100', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; @@ -205,10 +203,10 @@ public static function update(array $params = [], int $userNo) $payload = $v->validated(); // 아무 필드도 없으면 방어 - $updatableKeys = ['user_id','name','email','phone','password']; + $updatableKeys = ['user_id', 'name', 'email', 'phone', 'password']; $hasProfileInput = (bool) array_intersect(array_keys($payload), $updatableKeys); - $hasRolesInput = array_key_exists('roles', $payload); - if (!$hasProfileInput && !$hasRolesInput) { + $hasRolesInput = array_key_exists('roles', $payload); + if (! $hasProfileInput && ! $hasRolesInput) { return ['error' => '수정할 항목이 없습니다.', 'code' => 422]; } @@ -229,7 +227,7 @@ public static function update(array $params = [], int $userNo) } } - if (!empty($updateData)) { + if (! empty($updateData)) { $user->fill($updateData); $user->save(); } @@ -248,7 +246,7 @@ public static function update(array $params = [], int $userNo) } return [ - 'user' => $user->only(['id','user_id','name','email','phone']), + 'user' => $user->only(['id', 'user_id', 'name', 'email', 'phone']), 'roles' => method_exists($user, 'getRoleNames') ? $user->getRoleNames() : [], ]; }); @@ -261,18 +259,18 @@ public static function update(array $params = [], int $userNo) public static function destroy(int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } - $ut = UserTenant::where('user_id',$userNo) + $ut = UserTenant::where('user_id', $userNo) ->where('tenant_id', $tenantId) ->first(); - if (!$ut) { + if (! $ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } @@ -289,10 +287,10 @@ public static function destroy(int $userNo) public static function restore(int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } @@ -301,7 +299,7 @@ public static function restore(int $userNo) ->where('user_id', $userNo) ->first(); - if (!$ut) { + if (! $ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } @@ -320,10 +318,10 @@ public static function restore(int $userNo) public static function toggle(int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } @@ -331,7 +329,7 @@ public static function toggle(int $userNo) ->where('user_id', $userNo) ->first(); - if (!$ut) { + if (! $ut) { return ['error' => '해당 사용자를 찾을 수 없습니다.', 'code' => 404]; } @@ -348,12 +346,12 @@ public static function toggle(int $userNo) public static function attach(array $params = []) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ - 'user_id' => 'required|integer|exists:users,id', + 'user_id' => 'required|integer|exists:users,id', 'role_name' => 'required|string|max:100', ]); if ($v->fails()) { @@ -361,7 +359,7 @@ public static function attach(array $params = []) } $user = User::find($params['user_id']); - if (!method_exists($user, 'assignRole')) { + if (! method_exists($user, 'assignRole')) { // Spatie 미사용 환경 방어 return ['error' => '역할 시스템이 활성화되어 있지 않습니다.', 'code' => 501]; } @@ -387,12 +385,12 @@ public static function attach(array $params = []) public static function detach(array $params = []) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ - 'user_id' => 'required|integer|exists:users,id', + 'user_id' => 'required|integer|exists:users,id', 'role_name' => 'required|string|max:100', ]); if ($v->fails()) { @@ -400,7 +398,7 @@ public static function detach(array $params = []) } $user = User::find($params['user_id']); - if (!method_exists($user, 'removeRole')) { + if (! method_exists($user, 'removeRole')) { return ['error' => '역할 시스템이 활성화되어 있지 않습니다.', 'code' => 501]; } @@ -422,18 +420,18 @@ public static function detach(array $params = []) * - 새 임시 비밀번호를 설정(응답으로 직접 노출 X 권장) * - 여기서는 옵션에 따라 노출/미노출 선택 가능하도록 구현 */ - public static function reset(array $params = [],int $userNo) + public static function reset(array $params, int $userNo) { $tenantId = app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userNo) { + if (! $userNo) { return ['error' => '회원 정보가 없습니다.', 'code' => 422]; } $v = Validator::make($params, [ - 'new_password' => 'nullable|string|min:8|max:64', + 'new_password' => 'nullable|string|min:8|max:64', 'return_password' => 'nullable|in:0,1', // 1이면 응답에 임시 비번 포함(개발용) ]); if ($v->fails()) { @@ -442,7 +440,7 @@ public static function reset(array $params = [],int $userNo) $payload = $v->validated(); $user = User::find($userNo); - if (!$user) { + if (! $user) { return ['error' => '유저를 찾을 수 없습니다.', 'code' => 404]; } @@ -454,7 +452,7 @@ public static function reset(array $params = [],int $userNo) // if (method_exists($user, 'tokens')) { $user->tokens()->delete(); } $resp = ['status' => 'ok']; - if (!empty($payload['return_password'])) { + if (! empty($payload['return_password'])) { // 운영에선 반환하지 말고 메일/문자 발송을 권장 $resp['temp_password'] = $new; } diff --git a/app/Services/Audit/AuditLogService.php b/app/Services/Audit/AuditLogService.php index 583f8f4..3e00c40 100644 --- a/app/Services/Audit/AuditLogService.php +++ b/app/Services/Audit/AuditLogService.php @@ -12,29 +12,29 @@ public function paginate(array $filters): LengthAwarePaginator { $tenantId = $this->tenantId(); - $page = (int)($filters['page'] ?? 1); - $size = (int)($filters['size'] ?? 20); - $sort = $filters['sort'] ?? 'created_at'; + $page = (int) ($filters['page'] ?? 1); + $size = (int) ($filters['size'] ?? 20); + $sort = $filters['sort'] ?? 'created_at'; $order = $filters['order'] ?? 'desc'; $q = AuditLog::query()->where('tenant_id', $tenantId); - if (!empty($filters['target_type'])) { + if (! empty($filters['target_type'])) { $q->where('target_type', $filters['target_type']); } - if (!empty($filters['target_id'])) { - $q->where('target_id', (int)$filters['target_id']); + if (! empty($filters['target_id'])) { + $q->where('target_id', (int) $filters['target_id']); } - if (!empty($filters['action'])) { + if (! empty($filters['action'])) { $q->where('action', $filters['action']); } - if (!empty($filters['actor_id'])) { - $q->where('actor_id', (int)$filters['actor_id']); + if (! empty($filters['actor_id'])) { + $q->where('actor_id', (int) $filters['actor_id']); } - if (!empty($filters['from'])) { + if (! empty($filters['from'])) { $q->where('created_at', '>=', $filters['from']); } - if (!empty($filters['to'])) { + if (! empty($filters['to'])) { $q->where('created_at', '<=', $filters['to']); } diff --git a/app/Services/Audit/AuditLogger.php b/app/Services/Audit/AuditLogger.php index e83f439..f5c1640 100644 --- a/app/Services/Audit/AuditLogger.php +++ b/app/Services/Audit/AuditLogger.php @@ -24,16 +24,16 @@ public function log( $ua = $request?->userAgent(); AuditLog::create([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'target_type' => $targetType, - 'target_id' => $targetId, - 'action' => $action, - 'before' => $before, - 'after' => $after, - 'actor_id' => $actorId, - 'ip' => $ip, - 'ua' => $ua, - 'created_at' => now(), + 'target_id' => $targetId, + 'action' => $action, + 'before' => $before, + 'after' => $after, + 'actor_id' => $actorId, + 'ip' => $ip, + 'ua' => $ua, + 'created_at' => now(), ]); } catch (\Throwable $e) { // 감사 로그 실패는 업무 흐름을 방해하지 않음 diff --git a/app/Services/Authz/AccessService.php b/app/Services/Authz/AccessService.php index b4f5b3f..9f813fb 100644 --- a/app/Services/Authz/AccessService.php +++ b/app/Services/Authz/AccessService.php @@ -3,17 +3,18 @@ namespace App\Services\Authz; use App\Models\Members\User; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; final class AccessService { public static function allows(User $user, string $permission, int $tenantId, ?string $guardName = null): bool { $guard = $guardName ?? config('auth.defaults.guard', 'api'); // ★ 기본 가드 - $ver = Cache::get("access:version:$tenantId", 1); // ★ 버전 토큰 + $ver = Cache::get("access:version:$tenantId", 1); // ★ 버전 토큰 $key = "access:$tenantId:{$user->id}:$guard:$permission:v{$ver}"; // ★ 키 강화 + return Cache::remember($key, now()->addSeconds(20), function () use ($user, $permission, $tenantId, $guard) { // 1) 개인 DENY diff --git a/app/Services/Authz/RolePermissionService.php b/app/Services/Authz/RolePermissionService.php index 8b142bc..f8ef631 100644 --- a/app/Services/Authz/RolePermissionService.php +++ b/app/Services/Authz/RolePermissionService.php @@ -4,8 +4,8 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; -use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; class RolePermissionService @@ -36,17 +36,19 @@ protected static function resolvePermissionNames(int $tenantId, array $params): { $names = []; - if (!empty($params['permission_names']) && is_array($params['permission_names'])) { + if (! empty($params['permission_names']) && is_array($params['permission_names'])) { // 문자열 배열만 추림 foreach ($params['permission_names'] as $n) { - if (is_string($n) && $n !== '') $names[] = trim($n); + if (is_string($n) && $n !== '') { + $names[] = trim($n); + } } } - if (!empty($params['menus']) && is_array($params['menus']) && - !empty($params['actions']) && is_array($params['actions'])) { + if (! empty($params['menus']) && is_array($params['menus']) && + ! empty($params['actions']) && is_array($params['actions'])) { - $allowed = config('authz.menu_actions', ['view','create','update','delete','approve']); + $allowed = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']); $acts = array_values(array_unique(array_filter(array_map('trim', $params['actions'])))); $acts = array_intersect($acts, $allowed); @@ -65,9 +67,9 @@ protected static function resolvePermissionNames(int $tenantId, array $params): // 존재하지 않는 Permission은 생성(tenant+guard 포함) foreach ($names as $permName) { Permission::firstOrCreate([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'guard_name' => self::$guard, - 'name' => $permName, + 'name' => $permName, ]); } @@ -80,7 +82,7 @@ public static function list(int $roleId) $tenantId = (int) app('tenant_id'); $role = self::loadRoleOrError($roleId, $tenantId); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } @@ -90,7 +92,7 @@ public static function list(int $roleId) ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard) ->orderBy('name') - ->get(['id','tenant_id','name','guard_name','created_at','updated_at']); + ->get(['id', 'tenant_id', 'name', 'guard_name', 'created_at', 'updated_at']); return $perms; } @@ -101,7 +103,7 @@ public static function grant(int $roleId, array $params = []) $tenantId = (int) app('tenant_id'); $role = self::loadRoleOrError($roleId, $tenantId); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } @@ -109,11 +111,11 @@ public static function grant(int $roleId, array $params = []) $v = Validator::make($params, [ 'permission_names' => 'sometimes|array', 'permission_names.*' => 'string|min:1', - 'menus' => 'sometimes|array', + 'menus' => 'sometimes|array', 'menus.*' => 'integer|min:1', 'actions' => 'sometimes|array', 'actions.*' => [ - 'string', Rule::in(config('authz.menu_actions', ['view','create','update','delete','approve'])), + 'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])), ], ]); if ($v->fails()) { @@ -142,18 +144,18 @@ public static function revoke(int $roleId, array $params = []) $tenantId = (int) app('tenant_id'); $role = self::loadRoleOrError($roleId, $tenantId); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ 'permission_names' => 'sometimes|array', 'permission_names.*' => 'string|min:1', - 'menus' => 'sometimes|array', + 'menus' => 'sometimes|array', 'menus.*' => 'integer|min:1', 'actions' => 'sometimes|array', 'actions.*' => [ - 'string', Rule::in(config('authz.menu_actions', ['view','create','update','delete','approve'])), + 'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])), ], ]); if ($v->fails()) { @@ -181,18 +183,18 @@ public static function sync(int $roleId, array $params = []) $tenantId = (int) app('tenant_id'); $role = self::loadRoleOrError($roleId, $tenantId); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ 'permission_names' => 'sometimes|array', 'permission_names.*' => 'string|min:1', - 'menus' => 'sometimes|array', + 'menus' => 'sometimes|array', 'menus.*' => 'integer|min:1', 'actions' => 'sometimes|array', 'actions.*' => [ - 'string', Rule::in(config('authz.menu_actions', ['view','create','update','delete','approve'])), + 'string', Rule::in(config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve'])), ], ]); if ($v->fails()) { diff --git a/app/Services/Authz/RoleService.php b/app/Services/Authz/RoleService.php index 4782bee..1c1b267 100644 --- a/app/Services/Authz/RoleService.php +++ b/app/Services/Authz/RoleService.php @@ -16,9 +16,9 @@ class RoleService public static function index(array $params = []) { $tenantId = (int) app('tenant_id'); - $page = (int)($params['page'] ?? 1); - $size = (int)($params['size'] ?? 10); - $q = trim((string)($params['q'] ?? '')); + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 10); + $q = trim((string) ($params['q'] ?? '')); $query = Role::query() ->where('tenant_id', $tenantId) @@ -45,7 +45,7 @@ public static function store(array $params = []) $v = Validator::make($params, [ 'name' => [ 'required', 'string', 'max:100', - Rule::unique('roles', 'name')->where(fn($q) => $q + Rule::unique('roles', 'name')->where(fn ($q) => $q ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard)), ], @@ -60,9 +60,9 @@ public static function store(array $params = []) app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId); $role = Role::create([ - 'tenant_id' => $tenantId, - 'guard_name' => self::$guard, - 'name' => $v->validated()['name'], + 'tenant_id' => $tenantId, + 'guard_name' => self::$guard, + 'name' => $v->validated()['name'], 'description' => $params['description'] ?? null, ]); @@ -74,11 +74,11 @@ public static function show(int $id) { $tenantId = (int) app('tenant_id'); - $role = Role::where('tenant_id',$tenantId) + $role = Role::where('tenant_id', $tenantId) ->where('guard_name', self::$guard) ->find($id); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } @@ -94,15 +94,15 @@ public static function update(int $id, array $params = []) ->where('guard_name', self::$guard) ->find($id); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ 'name' => [ - 'sometimes','string','max:100', - Rule::unique('roles','name') - ->where(fn($q)=>$q->where('tenant_id',$tenantId)->where('guard_name', self::$guard)) + 'sometimes', 'string', 'max:100', + Rule::unique('roles', 'name') + ->where(fn ($q) => $q->where('tenant_id', $tenantId)->where('guard_name', self::$guard)) ->ignore($role->id), ], 'description' => 'sometimes|nullable|string|max:255', @@ -126,7 +126,7 @@ public static function destroy(int $id) ->where('guard_name', self::$guard) ->find($id); - if (!$role) { + if (! $role) { return ['error' => '역할을 찾을 수 없습니다.', 'code' => 404]; } diff --git a/app/Services/Authz/UserRoleService.php b/app/Services/Authz/UserRoleService.php index 545bfa6..2098d4c 100644 --- a/app/Services/Authz/UserRoleService.php +++ b/app/Services/Authz/UserRoleService.php @@ -29,16 +29,18 @@ protected static function resolveRoleNames(int $tenantId, array $params): array $names = []; // A) role_names[] 직접 - if (!empty($params['role_names']) && is_array($params['role_names'])) { + if (! empty($params['role_names']) && is_array($params['role_names'])) { foreach ($params['role_names'] as $n) { - if (is_string($n) && $n !== '') $names[] = trim($n); + if (is_string($n) && $n !== '') { + $names[] = trim($n); + } } } // B) role_ids[] → 이름으로 변환 - if (!empty($params['role_ids']) && is_array($params['role_ids'])) { + if (! empty($params['role_ids']) && is_array($params['role_ids'])) { $ids = array_values(array_unique(array_map('intval', $params['role_ids']))); - if (!empty($ids)) { + if (! empty($ids)) { $rows = Role::query() ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard) @@ -53,7 +55,7 @@ protected static function resolveRoleNames(int $tenantId, array $params): array $names = array_values(array_unique(array_filter($names))); // 존재 확인(필요시 에러 처리 확장 가능) - if (!empty($names)) { + if (! empty($names)) { $count = Role::query() ->where('tenant_id', $tenantId) ->where('guard_name', self::$guard) @@ -71,7 +73,7 @@ public static function list(int $userId) $tenantId = (int) app('tenant_id'); $user = self::loadUserOrError($userId); - if (!$user) { + if (! $user) { return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404]; } @@ -81,7 +83,7 @@ public static function list(int $userId) $builder = $user->roles() ->where('roles.tenant_id', $tenantId) ->where('roles.guard_name', self::$guard) - ->select(['roles.id','roles.tenant_id','roles.name','roles.description','roles.guard_name','roles.created_at','roles.updated_at']) + ->select(['roles.id', 'roles.tenant_id', 'roles.name', 'roles.description', 'roles.guard_name', 'roles.created_at', 'roles.updated_at']) ->orderBy('roles.id', 'desc'); return $builder->get(); @@ -93,15 +95,15 @@ public static function grant(int $userId, array $params = []) $tenantId = (int) app('tenant_id'); $user = self::loadUserOrError($userId); - if (!$user) { + if (! $user) { return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; @@ -118,6 +120,7 @@ public static function grant(int $userId, array $params = []) } $user->assignRole($names); // teams 컨텍스트 적용됨 + return 'success'; } @@ -127,15 +130,15 @@ public static function revoke(int $userId, array $params = []) $tenantId = (int) app('tenant_id'); $user = self::loadUserOrError($userId); - if (!$user) { + if (! $user) { return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; @@ -152,6 +155,7 @@ public static function revoke(int $userId, array $params = []) } $user->removeRole($names); // 배열 허용 + return 'success'; } @@ -161,15 +165,15 @@ public static function sync(int $userId, array $params = []) $tenantId = (int) app('tenant_id'); $user = self::loadUserOrError($userId); - if (!$user) { + if (! $user) { return ['error' => '사용자를 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'role_names' => 'sometimes|array', + 'role_names' => 'sometimes|array', 'role_names.*' => 'string|min:1', - 'role_ids' => 'sometimes|array', - 'role_ids.*' => 'integer|min:1', + 'role_ids' => 'sometimes|array', + 'role_ids.*' => 'integer|min:1', ]); if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; @@ -187,6 +191,7 @@ public static function sync(int $userId, array $params = []) } $user->syncRoles($names); + return 'success'; } } diff --git a/app/Services/Calculation/CalculationEngine.php b/app/Services/Calculation/CalculationEngine.php index 86aa5cf..ddec129 100644 --- a/app/Services/Calculation/CalculationEngine.php +++ b/app/Services/Calculation/CalculationEngine.php @@ -2,15 +2,15 @@ namespace App\Services\Calculation; -use App\Models\Design\BomTemplate; -use App\Models\Design\BomTemplateItem; use App\Models\Calculation\CalculationConfig; +use App\Models\Design\BomTemplate; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; class CalculationEngine { protected FormulaParser $parser; + protected ParameterValidator $validator; public function __construct(FormulaParser $parser, ParameterValidator $validator) @@ -21,9 +21,10 @@ public function __construct(FormulaParser $parser, ParameterValidator $validator /** * BOM 계산 실행 - * @param int $bomTemplateId BOM 템플릿 ID - * @param array $parameters 입력 파라미터 - * @param string|null $companyName 업체명 (null시 기본값 사용) + * + * @param int $bomTemplateId BOM 템플릿 ID + * @param array $parameters 입력 파라미터 + * @param string|null $companyName 업체명 (null시 기본값 사용) * @return array 계산 결과 */ public function calculateBOM(int $bomTemplateId, array $parameters, ?string $companyName = null): array @@ -48,32 +49,33 @@ public function calculateBOM(int $bomTemplateId, array $parameters, ?string $com 'id' => $bomTemplate->id, 'name' => $bomTemplate->name, 'company_type' => $bomTemplate->company_type, - 'formula_version' => $bomTemplate->formula_version + 'formula_version' => $bomTemplate->formula_version, ], 'input_parameters' => $parameters, 'calculated_values' => $calculatedValues, 'bom_items' => $bomItems, - 'calculation_timestamp' => now() + 'calculation_timestamp' => now(), ]; } catch (\Exception $e) { Log::error('BOM 계산 실패', [ 'bom_template_id' => $bomTemplateId, 'parameters' => $parameters, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), - 'bom_template_id' => $bomTemplateId + 'bom_template_id' => $bomTemplateId, ]; } } /** * 견적시 필요한 파라미터 스키마 추출 - * @param int $bomTemplateId BOM 템플릿 ID + * + * @param int $bomTemplateId BOM 템플릿 ID * @return array 파라미터 스키마 */ public function getRequiredParameters(int $bomTemplateId): array @@ -81,7 +83,7 @@ public function getRequiredParameters(int $bomTemplateId): array $bomTemplate = BomTemplate::findOrFail($bomTemplateId); $schema = $bomTemplate->calculation_schema; - if (!$schema) { + if (! $schema) { return $this->getDefaultParameterSchema($bomTemplate); } @@ -107,7 +109,7 @@ protected function calculateIntermediateValues(BomTemplate $bomTemplate, array $ $calculated = array_merge($calculated, $this->parser->execute($sizeFormula->formula_expression, [ 'W0' => $W0, 'H0' => $H0, - 'product_type' => $productType + 'product_type' => $productType, ])); } else { // 기본 공식 (경동기업 기준) @@ -148,7 +150,7 @@ protected function calculateBomItems(Collection $bomItems, array $calculatedValu $results = []; foreach ($bomItems as $item) { - if (!$item->is_calculated) { + if (! $item->is_calculated) { // 고정 수량 아이템 $results[] = [ 'item_id' => $item->id, @@ -157,8 +159,9 @@ protected function calculateBomItems(Collection $bomItems, array $calculatedValu 'original_qty' => $item->qty, 'calculated_qty' => $item->qty, 'is_calculated' => false, - 'calculation_formula' => null + 'calculation_formula' => null, ]; + continue; } @@ -175,13 +178,13 @@ protected function calculateBomItems(Collection $bomItems, array $calculatedValu 'calculated_qty' => $calculatedQty['result'] ?? $item->qty, 'is_calculated' => true, 'calculation_formula' => $item->calculation_formula, - 'depends_on' => $item->depends_on + 'depends_on' => $item->depends_on, ]; } catch (\Exception $e) { Log::warning('BOM 아이템 계산 실패', [ 'item_id' => $item->id, 'formula' => $item->calculation_formula, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); // 계산 실패시 원래 수량 사용 @@ -192,7 +195,7 @@ protected function calculateBomItems(Collection $bomItems, array $calculatedValu 'original_qty' => $item->qty, 'calculated_qty' => $item->qty, 'is_calculated' => false, - 'calculation_error' => $e->getMessage() + 'calculation_error' => $e->getMessage(), ]; } } @@ -206,7 +209,9 @@ protected function calculateBomItems(Collection $bomItems, array $calculatedValu protected function validateParameters(BomTemplate $bomTemplate, array $parameters): void { $schema = $bomTemplate->calculation_schema; - if (!$schema) return; + if (! $schema) { + return; + } $this->validator->validate($schema, $parameters); } @@ -236,7 +241,7 @@ protected function getDefaultParameterSchema(BomTemplate $bomTemplate): array 'type' => 'integer', 'required' => true, 'min' => 500, - 'max' => 15000 + 'max' => 15000, ], [ 'key' => 'H0', @@ -244,25 +249,25 @@ protected function getDefaultParameterSchema(BomTemplate $bomTemplate): array 'type' => 'integer', 'required' => true, 'min' => 500, - 'max' => 5000 + 'max' => 5000, ], [ 'key' => 'product_type', 'label' => '제품타입', 'type' => 'select', 'options' => ['screen' => '스크린', 'steel' => '철재'], - 'required' => true + 'required' => true, ], [ 'key' => 'installation_type', 'label' => '설치방식', 'type' => 'select', 'options' => ['wall' => '벽면형', 'side' => '측면형', 'mixed' => '혼합형'], - 'required' => false - ] + 'required' => false, + ], ], 'company_type' => $bomTemplate->company_type ?: 'default', - 'formula_version' => $bomTemplate->formula_version ?: 'v1.0' + 'formula_version' => $bomTemplate->formula_version ?: 'v1.0', ]; } -} \ No newline at end of file +} diff --git a/app/Services/Calculation/FormulaParser.php b/app/Services/Calculation/FormulaParser.php index a9f71af..9fd0a23 100644 --- a/app/Services/Calculation/FormulaParser.php +++ b/app/Services/Calculation/FormulaParser.php @@ -8,8 +8,9 @@ class FormulaParser { /** * 계산식 실행 - * @param string $formula 계산식 표현식 - * @param array $variables 변수 값들 + * + * @param string $formula 계산식 표현식 + * @param array $variables 변수 값들 * @return array|float 계산 결과 */ public function execute(string $formula, array $variables): array|float @@ -28,7 +29,7 @@ public function execute(string $formula, array $variables): array|float Log::error('계산식 실행 실패', [ 'formula' => $formula, 'variables' => $variables, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); throw new \RuntimeException("계산식 실행 실패: {$e->getMessage()}"); @@ -67,7 +68,7 @@ protected function executePreDefinedFunction(string $formula, array $variables): if ($formula === 'kyungdong_screen_size') { return [ 'W1' => ($variables['W0'] ?? 0) + 160, - 'H1' => ($variables['H0'] ?? 0) + 350 + 'H1' => ($variables['H0'] ?? 0) + 350, ]; } @@ -75,7 +76,7 @@ protected function executePreDefinedFunction(string $formula, array $variables): if ($formula === 'kyungdong_steel_size') { return [ 'W1' => ($variables['W0'] ?? 0) + 110, - 'H1' => ($variables['H0'] ?? 0) + 350 + 'H1' => ($variables['H0'] ?? 0) + 350, ]; } @@ -88,17 +89,26 @@ protected function executePreDefinedFunction(string $formula, array $variables): return [ 'area' => $area, - 'weight' => ($area * 2) + ($W0 / 1000 * 14.17) + 'weight' => ($area * 2) + ($W0 / 1000 * 14.17), ]; } // 브라켓 수량 계산 if ($formula === 'bracket_quantity') { $W1 = $variables['W1'] ?? 0; - if ($W1 <= 3000) return ['result' => 2]; - if ($W1 <= 6000) return ['result' => 3]; - if ($W1 <= 9000) return ['result' => 4]; - if ($W1 <= 12000) return ['result' => 5]; + if ($W1 <= 3000) { + return ['result' => 2]; + } + if ($W1 <= 6000) { + return ['result' => 3]; + } + if ($W1 <= 9000) { + return ['result' => 4]; + } + if ($W1 <= 12000) { + return ['result' => 5]; + } + return ['result' => 5]; // 최대값 } @@ -112,10 +122,19 @@ protected function executePreDefinedFunction(string $formula, array $variables): $W1 = $variables['W1'] ?? 0; $qty = $variables['qty'] ?? 1; - if ($W1 <= 3000) return ['result' => 1 * $qty]; - if ($W1 <= 6000) return ['result' => 2 * $qty]; - if ($W1 <= 9000) return ['result' => 3 * $qty]; - if ($W1 <= 12000) return ['result' => 4 * $qty]; + if ($W1 <= 3000) { + return ['result' => 1 * $qty]; + } + if ($W1 <= 6000) { + return ['result' => 2 * $qty]; + } + if ($W1 <= 9000) { + return ['result' => 3 * $qty]; + } + if ($W1 <= 12000) { + return ['result' => 4 * $qty]; + } + return ['result' => 4 * $qty]; } @@ -123,8 +142,13 @@ protected function executePreDefinedFunction(string $formula, array $variables): if ($formula === 'shaft_size_determination') { $W1 = $variables['W1'] ?? 0; - if ($W1 <= 6000) return ['result' => 4]; // 4인치 - if ($W1 <= 8200) return ['result' => 5]; // 5인치 + if ($W1 <= 6000) { + return ['result' => 4]; + } // 4인치 + if ($W1 <= 8200) { + return ['result' => 5]; + } // 5인치 + return ['result' => 0]; // 미정의 } @@ -135,23 +159,53 @@ protected function executePreDefinedFunction(string $formula, array $variables): // 샤프트별 중량 매트릭스 if ($shaftSize == 4) { - if ($weight <= 150) return ['result' => '150K']; - if ($weight <= 300) return ['result' => '300K']; - if ($weight <= 400) return ['result' => '400K']; + if ($weight <= 150) { + return ['result' => '150K']; + } + if ($weight <= 300) { + return ['result' => '300K']; + } + if ($weight <= 400) { + return ['result' => '400K']; + } } elseif ($shaftSize == 5) { - if ($weight <= 123) return ['result' => '150K']; - if ($weight <= 246) return ['result' => '300K']; - if ($weight <= 327) return ['result' => '400K']; - if ($weight <= 500) return ['result' => '500K']; - if ($weight <= 600) return ['result' => '600K']; + if ($weight <= 123) { + return ['result' => '150K']; + } + if ($weight <= 246) { + return ['result' => '300K']; + } + if ($weight <= 327) { + return ['result' => '400K']; + } + if ($weight <= 500) { + return ['result' => '500K']; + } + if ($weight <= 600) { + return ['result' => '600K']; + } } elseif ($shaftSize == 6) { - if ($weight <= 104) return ['result' => '150K']; - if ($weight <= 208) return ['result' => '300K']; - if ($weight <= 300) return ['result' => '400K']; - if ($weight <= 424) return ['result' => '500K']; - if ($weight <= 508) return ['result' => '600K']; - if ($weight <= 800) return ['result' => '800K']; - if ($weight <= 1000) return ['result' => '1000K']; + if ($weight <= 104) { + return ['result' => '150K']; + } + if ($weight <= 208) { + return ['result' => '300K']; + } + if ($weight <= 300) { + return ['result' => '400K']; + } + if ($weight <= 424) { + return ['result' => '500K']; + } + if ($weight <= 508) { + return ['result' => '600K']; + } + if ($weight <= 800) { + return ['result' => '800K']; + } + if ($weight <= 1000) { + return ['result' => '1000K']; + } } return ['result' => '미정의']; @@ -168,11 +222,11 @@ protected function executeSimpleMath(string $formula, array $variables): float // 변수 치환 $expression = $formula; foreach ($variables as $key => $value) { - $expression = str_replace($key, (string)$value, $expression); + $expression = str_replace($key, (string) $value, $expression); } // 안전한 수학 표현식 검증 - if (!$this->isSafeMathExpression($expression)) { + if (! $this->isSafeMathExpression($expression)) { throw new \InvalidArgumentException("안전하지 않은 수학 표현식: {$expression}"); } @@ -197,9 +251,9 @@ protected function executeConditionalExpression(string $formula, array $variable // 조건 평가 if ($this->evaluateCondition($condition, $variables)) { - return is_numeric($trueValue) ? (float)$trueValue : $this->execute($trueValue, $variables)['result']; + return is_numeric($trueValue) ? (float) $trueValue : $this->execute($trueValue, $variables)['result']; } else { - return is_numeric($falseValue) ? (float)$falseValue : $this->execute($falseValue, $variables)['result']; + return is_numeric($falseValue) ? (float) $falseValue : $this->execute($falseValue, $variables)['result']; } } @@ -214,11 +268,11 @@ protected function evaluateCondition(string $condition, array $variables): bool // 변수 치환 $expression = $condition; foreach ($variables as $key => $value) { - $expression = str_replace($key, (string)$value, $expression); + $expression = str_replace($key, (string) $value, $expression); } // 안전한 조건식 검증 - if (!$this->isSafeConditionExpression($expression)) { + if (! $this->isSafeConditionExpression($expression)) { throw new \InvalidArgumentException("안전하지 않은 조건식: {$expression}"); } @@ -304,7 +358,7 @@ protected function calculateMotorBracketSize(array $variables): array 'bracket_size' => $bracketSize, 'motor_capacity' => $motorCapacity, 'calculated_weight' => $weight, - 'shaft_inch' => $inch + 'shaft_inch' => $inch, ]; } @@ -321,7 +375,7 @@ protected function isPreDefinedFunction(string $formula): bool 'motor_bracket_size', 'round_bar_quantity', 'shaft_size_determination', - 'motor_capacity_determination' + 'motor_capacity_determination', ]; return in_array($formula, $predefinedFunctions); @@ -368,6 +422,7 @@ protected function isSafeConditionExpression(string $expression): bool { // 허용된 연산자: ==, !=, <, >, <=, >=, &&, || $allowedPattern = '/^[0-9+\-*\/().\s<>=!&|]+$/'; + return preg_match($allowedPattern, $expression); } -} \ No newline at end of file +} diff --git a/app/Services/Calculation/ParameterValidator.php b/app/Services/Calculation/ParameterValidator.php index a7743fc..9d90c1a 100644 --- a/app/Services/Calculation/ParameterValidator.php +++ b/app/Services/Calculation/ParameterValidator.php @@ -9,8 +9,10 @@ class ParameterValidator { /** * 파라미터 검증 - * @param array $schema 파라미터 스키마 - * @param array $parameters 검증할 파라미터 + * + * @param array $schema 파라미터 스키마 + * @param array $parameters 검증할 파라미터 + * * @throws ValidationException */ public function validate(array $schema, array $parameters): void @@ -27,14 +29,15 @@ public function validate(array $schema, array $parameters): void /** * 검증 규칙 생성 - * @param array $schema 파라미터 스키마 + * + * @param array $schema 파라미터 스키마 * @return array 라라벨 검증 규칙 */ protected function buildValidationRules(array $schema): array { $rules = []; - if (!isset($schema['required_parameters'])) { + if (! isset($schema['required_parameters'])) { return $rules; } @@ -65,7 +68,7 @@ protected function buildValidationRules(array $schema): array case 'select': if (isset($param['options'])) { $validOptions = is_array($param['options']) ? array_keys($param['options']) : $param['options']; - $paramRules[] = 'in:' . implode(',', $validOptions); + $paramRules[] = 'in:'.implode(',', $validOptions); } break; default: @@ -74,15 +77,15 @@ protected function buildValidationRules(array $schema): array // 최소값/최대값 if (isset($param['min'])) { - $paramRules[] = 'min:' . $param['min']; + $paramRules[] = 'min:'.$param['min']; } if (isset($param['max'])) { - $paramRules[] = 'max:' . $param['max']; + $paramRules[] = 'max:'.$param['max']; } // 정규표현식 if (isset($param['pattern'])) { - $paramRules[] = 'regex:' . $param['pattern']; + $paramRules[] = 'regex:'.$param['pattern']; } $rules[$key] = implode('|', $paramRules); @@ -93,14 +96,15 @@ protected function buildValidationRules(array $schema): array /** * 검증 메시지 생성 - * @param array $schema 파라미터 스키마 + * + * @param array $schema 파라미터 스키마 * @return array 검증 메시지 */ protected function buildValidationMessages(array $schema): array { $messages = []; - if (!isset($schema['required_parameters'])) { + if (! isset($schema['required_parameters'])) { return $messages; } @@ -123,7 +127,7 @@ protected function buildValidationMessages(array $schema): array if (isset($param['options'])) { $validOptions = is_array($param['options']) ? array_values($param['options']) : $param['options']; - $messages["{$key}.in"] = "{$label}은(는) 다음 중 하나여야 합니다: " . implode(', ', $validOptions); + $messages["{$key}.in"] = "{$label}은(는) 다음 중 하나여야 합니다: ".implode(', ', $validOptions); } if (isset($param['pattern'])) { @@ -136,25 +140,28 @@ protected function buildValidationMessages(array $schema): array /** * 범위 검증 - * @param array $parameters 파라미터 - * @param array $ranges 범위 정의 + * + * @param array $parameters 파라미터 + * @param array $ranges 범위 정의 */ public function validateRanges(array $parameters, array $ranges): void { foreach ($ranges as $key => $range) { - if (!isset($parameters[$key])) continue; + if (! isset($parameters[$key])) { + continue; + } $value = $parameters[$key]; if (isset($range['min']) && $value < $range['min']) { throw new ValidationException(validator([], []), [ - $key => ["{$key}는 최소 {$range['min']} 이상이어야 합니다."] + $key => ["{$key}는 최소 {$range['min']} 이상이어야 합니다."], ]); } if (isset($range['max']) && $value > $range['max']) { throw new ValidationException(validator([], []), [ - $key => ["{$key}는 최대 {$range['max']} 이하여야 합니다."] + $key => ["{$key}는 최대 {$range['max']} 이하여야 합니다."], ]); } } @@ -162,8 +169,9 @@ public function validateRanges(array $parameters, array $ranges): void /** * 업체별 특수 검증 - * @param array $parameters 파라미터 - * @param string $companyType 업체 타입 + * + * @param array $parameters 파라미터 + * @param string $companyType 업체 타입 */ public function validateCompanySpecific(array $parameters, string $companyType): void { @@ -192,7 +200,7 @@ protected function validateKyungdongRules(array $parameters): void // 면적이 너무 크면 경고 if ($area > 50) { throw new ValidationException(validator([], []), [ - 'area' => ['면적이 50㎡를 초과합니다. 특수 산출식이 필요할 수 있습니다.'] + 'area' => ['면적이 50㎡를 초과합니다. 특수 산출식이 필요할 수 있습니다.'], ]); } } @@ -201,7 +209,7 @@ protected function validateKyungdongRules(array $parameters): void if ($parameters['product_type'] === 'screen' && isset($parameters['W0'])) { if ($parameters['W0'] > 12000) { throw new ValidationException(validator([], []), [ - 'W0' => ['스크린 제품은 가로 12,000mm를 초과할 수 없습니다.'] + 'W0' => ['스크린 제품은 가로 12,000mm를 초과할 수 없습니다.'], ]); } } @@ -215,4 +223,4 @@ protected function validateSamsungRules(array $parameters): void // 삼성물산 특수 규칙 (예시) // 실제로는 해당 업체의 요구사항에 따라 구현 } -} \ No newline at end of file +} diff --git a/app/Services/CategoryFieldService.php b/app/Services/CategoryFieldService.php index a21b5b9..047d26a 100644 --- a/app/Services/CategoryFieldService.php +++ b/app/Services/CategoryFieldService.php @@ -2,11 +2,11 @@ namespace App\Services; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Validator; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use App\Models\Commons\CategoryField; // 가정: Eloquent 모델 경로 use App\Models\Commons\Category; +use App\Models\Commons\CategoryField; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; // 가정: Eloquent 모델 경로 +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class CategoryFieldService extends Service { @@ -14,7 +14,7 @@ public function index(int $categoryId, array $params) { $tenantId = $this->tenantId(); - $size = (int)($params['size'] ?? 20); + $size = (int) ($params['size'] ?? 20); $sort = $params['sort'] ?? 'sort_order'; $order = strtolower($params['order'] ?? 'asc') === 'desc' ? 'desc' : 'asc'; @@ -28,19 +28,19 @@ public function index(int $categoryId, array $params) public function store(int $categoryId, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $this->assertCategoryExists($tenantId, $categoryId); $v = Validator::make($data, [ - 'field_key' => 'required|string|max:30|alpha_dash', - 'field_name' => 'required|string|max:100', - 'field_type' => 'required|string|max:20', - 'is_required' => 'nullable|in:Y,N', - 'sort_order' => 'nullable|integer|min:0', + 'field_key' => 'required|string|max:30|alpha_dash', + 'field_name' => 'required|string|max:100', + 'field_type' => 'required|string|max:20', + 'is_required' => 'nullable|in:Y,N', + 'sort_order' => 'nullable|integer|min:0', 'default_value' => 'nullable|string|max:100', - 'options' => 'nullable|json', - 'description' => 'nullable|string|max:255', + 'options' => 'nullable|json', + 'description' => 'nullable|string|max:255', ]); $payload = $v->validate(); @@ -54,11 +54,11 @@ public function store(int $categoryId, array $data) throw new BadRequestHttpException(__('error.duplicate_key')); // ko/error.php에 매핑 } - $payload['tenant_id'] = $tenantId; + $payload['tenant_id'] = $tenantId; $payload['category_id'] = $categoryId; $payload['is_required'] = $payload['is_required'] ?? 'N'; - $payload['sort_order'] = $payload['sort_order'] ?? 0; - $payload['created_by'] = $userId; + $payload['sort_order'] = $payload['sort_order'] ?? 0; + $payload['created_by'] = $userId; return CategoryField::create($payload); } @@ -71,34 +71,35 @@ public function show(int $fieldId) ->where('tenant_id', $tenantId) ->find($fieldId); - if (!$field) { + if (! $field) { throw new BadRequestHttpException(__('error.not_found')); } + return $field; } public function update(int $fieldId, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $field = CategoryField::query() ->where('tenant_id', $tenantId) ->find($fieldId); - if (!$field) { + if (! $field) { throw new BadRequestHttpException(__('error.not_found')); } $v = Validator::make($data, [ - 'field_key' => 'sometimes|string|max:30|alpha_dash', - 'field_name' => 'sometimes|string|max:100', - 'field_type' => 'sometimes|string|max:20', - 'is_required' => 'sometimes|in:Y,N', - 'sort_order' => 'sometimes|integer|min:0', + 'field_key' => 'sometimes|string|max:30|alpha_dash', + 'field_name' => 'sometimes|string|max:100', + 'field_type' => 'sometimes|string|max:20', + 'is_required' => 'sometimes|in:Y,N', + 'sort_order' => 'sometimes|integer|min:0', 'default_value' => 'nullable|string|max:100', - 'options' => 'nullable|json', - 'description' => 'nullable|string|max:255', + 'options' => 'nullable|json', + 'description' => 'nullable|string|max:255', ]); $payload = $v->validate(); @@ -115,6 +116,7 @@ public function update(int $fieldId, array $data) $payload['updated_by'] = $userId; $field->update($payload); + return $field->refresh(); } @@ -125,7 +127,7 @@ public function destroy(int $fieldId): void ->where('tenant_id', $tenantId) ->find($fieldId); - if (!$field) { + if (! $field) { throw new BadRequestHttpException(__('error.not_found')); } $field->delete(); @@ -137,18 +139,20 @@ public function reorder(int $categoryId, array $items): void $this->assertCategoryExists($tenantId, $categoryId); $rows = $items['items'] ?? $items; // 둘 다 허용 - if (!is_array($rows)) { + if (! is_array($rows)) { throw new BadRequestHttpException(__('error.invalid_payload')); } DB::transaction(function () use ($tenantId, $categoryId, $rows) { foreach ($rows as $row) { - if (!isset($row['id'], $row['sort_order'])) continue; + if (! isset($row['id'], $row['sort_order'])) { + continue; + } CategoryField::query() ->where('tenant_id', $tenantId) ->where('category_id', $categoryId) ->where('id', $row['id']) - ->update(['sort_order' => (int)$row['sort_order']]); + ->update(['sort_order' => (int) $row['sort_order']]); } }); } @@ -156,10 +160,10 @@ public function reorder(int $categoryId, array $items): void public function bulkUpsert(int $categoryId, array $items): array { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $this->assertCategoryExists($tenantId, $categoryId); - if (!is_array($items) || empty($items)) { + if (! is_array($items) || empty($items)) { throw new BadRequestHttpException(__('error.empty_items')); } @@ -168,24 +172,24 @@ public function bulkUpsert(int $categoryId, array $items): array DB::transaction(function () use ($tenantId, $userId, $categoryId, $items, &$result) { foreach ($items as $it) { $v = Validator::make($it, [ - 'id' => 'nullable|integer', - 'field_key' => 'sometimes|required_without:id|string|max:30|alpha_dash', - 'field_name' => 'required|string|max:100', - 'field_type' => 'required|string|max:20', - 'is_required' => 'nullable|in:Y,N', - 'sort_order' => 'nullable|integer|min:0', + 'id' => 'nullable|integer', + 'field_key' => 'sometimes|required_without:id|string|max:30|alpha_dash', + 'field_name' => 'required|string|max:100', + 'field_type' => 'required|string|max:20', + 'is_required' => 'nullable|in:Y,N', + 'sort_order' => 'nullable|integer|min:0', 'default_value' => 'nullable|string|max:100', - 'options' => 'nullable|json', - 'description' => 'nullable|string|max:255', + 'options' => 'nullable|json', + 'description' => 'nullable|string|max:255', ]); $payload = $v->validate(); - if (!empty($payload['id'])) { + if (! empty($payload['id'])) { $model = CategoryField::query() ->where('tenant_id', $tenantId) ->where('category_id', $categoryId) ->find($payload['id']); - if (!$model) { + if (! $model) { throw new BadRequestHttpException(__('error.not_found')); } @@ -218,11 +222,11 @@ public function bulkUpsert(int $categoryId, array $items): array throw new BadRequestHttpException(__('error.duplicate_key')); } - $payload['tenant_id'] = $tenantId; + $payload['tenant_id'] = $tenantId; $payload['category_id'] = $categoryId; $payload['is_required'] = $payload['is_required'] ?? 'N'; - $payload['sort_order'] = $payload['sort_order'] ?? 0; - $payload['created_by'] = $userId; + $payload['sort_order'] = $payload['sort_order'] ?? 0; + $payload['created_by'] = $userId; CategoryField::create($payload); $result['created']++; @@ -239,7 +243,7 @@ private function assertCategoryExists(int $tenantId, int $categoryId): void ->where('tenant_id', $tenantId) ->where('id', $categoryId) ->exists(); - if (!$exists) { + if (! $exists) { throw new BadRequestHttpException(__('error.category_not_found')); } } diff --git a/app/Services/CategoryLogService.php b/app/Services/CategoryLogService.php index dc34efc..64863a9 100644 --- a/app/Services/CategoryLogService.php +++ b/app/Services/CategoryLogService.php @@ -2,8 +2,8 @@ namespace App\Services; -use Illuminate\Support\Carbon; use App\Models\Commons\CategoryLog; +use Illuminate\Support\Carbon; class CategoryLogService extends Service { @@ -11,19 +11,25 @@ public function index(int $categoryId, array $params) { $tenantId = $this->tenantId(); - $size = (int)($params['size'] ?? 20); + $size = (int) ($params['size'] ?? 20); $action = $params['action'] ?? null; // insert|update|delete - $from = $params['from'] ?? null; // Y-m-d - $to = $params['to'] ?? null; + $from = $params['from'] ?? null; // Y-m-d + $to = $params['to'] ?? null; $q = CategoryLog::query() ->where('tenant_id', $tenantId) ->where('category_id', $categoryId) ->orderByDesc('changed_at'); - if ($action) $q->where('action', $action); - if ($from) $q->whereDate('changed_at', '>=', Carbon::parse($from)->toDateString()); - if ($to) $q->whereDate('changed_at', '<=', Carbon::parse($to)->toDateString()); + if ($action) { + $q->where('action', $action); + } + if ($from) { + $q->whereDate('changed_at', '>=', Carbon::parse($from)->toDateString()); + } + if ($to) { + $q->whereDate('changed_at', '<=', Carbon::parse($to)->toDateString()); + } return $q->paginate($size); } @@ -36,7 +42,10 @@ public function show(int $logId) ->where('tenant_id', $tenantId) ->find($logId); - if (!$log) throw new BadRequestHttpException(__('error.not_found')); + if (! $log) { + throw new BadRequestHttpException(__('error.not_found')); + } + return $log; } } diff --git a/app/Services/CategoryService.php b/app/Services/CategoryService.php index 41c7c9f..46d045c 100644 --- a/app/Services/CategoryService.php +++ b/app/Services/CategoryService.php @@ -14,10 +14,10 @@ public function index(array $params) { $tenantId = $this->tenantId(); - $page = (int)($params['page'] ?? 1); - $size = (int)($params['size'] ?? 20); - $q = trim((string)($params['q'] ?? '')); - $pid = $params['parent_id'] ?? null; + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); + $pid = $params['parent_id'] ?? null; $onlyActive = $params['only_active'] ?? null; $query = Category::query()->where('tenant_id', $tenantId); @@ -29,10 +29,10 @@ public function index(array $params) }); } if ($pid !== null) { - $query->where('parent_id', (int)$pid); + $query->where('parent_id', (int) $pid); } if ($onlyActive !== null) { - $query->where('is_active', (int)!!$onlyActive); + $query->where('is_active', (int) (bool) $onlyActive); } $query->orderBy('parent_id')->orderBy('sort_order')->orderBy('id'); @@ -45,7 +45,10 @@ public function show(int $id) { $tenantId = $this->tenantId(); $cat = Category::where('tenant_id', $tenantId)->find($id); - if (!$cat) throw new NotFoundHttpException(__('error.not_found')); + if (! $cat) { + throw new NotFoundHttpException(__('error.not_found')); + } + return $cat; } @@ -53,22 +56,24 @@ public function show(int $id) public function store(array $params) { $tenantId = $this->tenantId(); - $uid = $this->apiUserId(); + $uid = $this->apiUserId(); $v = Validator::make($params, [ - 'parent_id' => 'nullable|integer|min:1', - 'code' => 'nullable|string|max:50', - 'name' => 'required|string|max:100', - 'description'=> 'nullable|string|max:255', - 'is_active' => 'nullable|boolean', + 'parent_id' => 'nullable|integer|min:1', + 'code' => 'nullable|string|max:50', + 'name' => 'required|string|max:100', + 'description' => 'nullable|string|max:255', + 'is_active' => 'nullable|boolean', 'sort_order' => 'nullable|integer|min:0', ]); - if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } $data = $v->validated(); $data['tenant_id'] = $tenantId; $data['created_by'] = $uid; - $data['is_active'] = (int)($data['is_active'] ?? 1); + $data['is_active'] = (int) ($data['is_active'] ?? 1); $data['sort_order'] = $data['sort_order'] ?? 0; return Category::create($data); @@ -78,25 +83,30 @@ public function store(array $params) public function update(int $id, array $params) { $tenantId = $this->tenantId(); - $uid = $this->apiUserId(); + $uid = $this->apiUserId(); $cat = Category::where('tenant_id', $tenantId)->find($id); - if (!$cat) throw new NotFoundHttpException(__('error.not_found')); + if (! $cat) { + throw new NotFoundHttpException(__('error.not_found')); + } $v = Validator::make($params, [ - 'parent_id' => 'nullable|integer|min:1', - 'code' => 'nullable|string|max:50', - 'name' => 'sometimes|required|string|max:100', - 'description'=> 'nullable|string|max:255', - 'is_active' => 'nullable|boolean', + 'parent_id' => 'nullable|integer|min:1', + 'code' => 'nullable|string|max:50', + 'name' => 'sometimes|required|string|max:100', + 'description' => 'nullable|string|max:255', + 'is_active' => 'nullable|boolean', 'sort_order' => 'nullable|integer|min:0', ]); - if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } $payload = $v->validated(); $payload['updated_by'] = $uid; $cat->update($payload); + return $cat->refresh(); } @@ -104,14 +114,18 @@ public function update(int $id, array $params) public function destroy(int $id) { $tenantId = $this->tenantId(); - $uid = $this->apiUserId(); + $uid = $this->apiUserId(); $cat = Category::where('tenant_id', $tenantId)->find($id); - if (!$cat) throw new NotFoundHttpException(__('error.not_found')); + if (! $cat) { + throw new NotFoundHttpException(__('error.not_found')); + } // (옵션) 하위 존재 검사 $hasChild = Category::where('tenant_id', $tenantId)->where('parent_id', $id)->exists(); - if ($hasChild) throw new BadRequestHttpException(__('error.child_exists')); + if ($hasChild) { + throw new BadRequestHttpException(__('error.child_exists')); + } $cat->deleted_by = $uid; $cat->save(); @@ -125,10 +139,13 @@ public function toggle(int $id) { $tenantId = $this->tenantId(); $cat = Category::where('tenant_id', $tenantId)->find($id); - if (!$cat) throw new NotFoundHttpException(__('error.not_found')); + if (! $cat) { + throw new NotFoundHttpException(__('error.not_found')); + } $cat->is_active = $cat->is_active ? 0 : 1; $cat->save(); + return $cat->refresh(); } @@ -137,16 +154,21 @@ public function move(int $id, array $params) { $tenantId = $this->tenantId(); $v = Validator::make($params, [ - 'parent_id' => 'nullable|integer|min:1', + 'parent_id' => 'nullable|integer|min:1', 'sort_order' => 'nullable|integer|min:0', ]); - if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } $cat = Category::where('tenant_id', $tenantId)->find($id); - if (!$cat) throw new NotFoundHttpException(__('error.not_found')); + if (! $cat) { + throw new NotFoundHttpException(__('error.not_found')); + } $payload = $v->validated(); $cat->update($payload); + return $cat->refresh(); } @@ -155,15 +177,18 @@ public function reorder(array $params) { $tenantId = $this->tenantId(); $items = $params['items'] ?? null; - if (!is_array($items) || empty($items)) { + if (! is_array($items) || empty($items)) { throw new BadRequestHttpException(__('validation.required', ['attribute' => 'items'])); } foreach ($items as $row) { - if (!isset($row['id'])) continue; + if (! isset($row['id'])) { + continue; + } Category::where('tenant_id', $tenantId) - ->where('id', (int)$row['id']) - ->update(['sort_order' => (int)($row['sort_order'] ?? 0)]); + ->where('id', (int) $row['id']) + ->update(['sort_order' => (int) ($row['sort_order'] ?? 0)]); } + return 'success'; } @@ -171,26 +196,29 @@ public function reorder(array $params) public function tree(array $params) { $tenantId = $this->tenantId(); - $onlyActive = (bool)($params['only_active'] ?? false); + $onlyActive = (bool) ($params['only_active'] ?? false); $q = Category::where('tenant_id', $tenantId) - ->when($onlyActive, fn($qq) => $qq->where('is_active', 1)) + ->when($onlyActive, fn ($qq) => $qq->where('is_active', 1)) ->orderBy('parent_id')->orderBy('sort_order')->orderBy('id') - ->get(['id','parent_id','code','name','is_active','sort_order']); + ->get(['id', 'parent_id', 'code', 'name', 'is_active', 'sort_order']); $byParent = []; - foreach ($q as $c) { $byParent[$c->parent_id ?? 0][] = $c; } + foreach ($q as $c) { + $byParent[$c->parent_id ?? 0][] = $c; + } - $build = function($pid) use (&$build, &$byParent) { + $build = function ($pid) use (&$build, &$byParent) { $nodes = $byParent[$pid] ?? []; - return array_map(function($n) use ($build) { + + return array_map(function ($n) use ($build) { return [ - 'id' => $n->id, - 'code' => $n->code, - 'name' => $n->name, - 'is_active' => (int)$n->is_active, - 'sort_order' => (int)$n->sort_order, - 'children' => $build($n->id), + 'id' => $n->id, + 'code' => $n->code, + 'name' => $n->name, + 'is_active' => (int) $n->is_active, + 'sort_order' => (int) $n->sort_order, + 'children' => $build($n->id), ]; }, $nodes); }; diff --git a/app/Services/CategoryTemplateService.php b/app/Services/CategoryTemplateService.php index ff21392..f700677 100644 --- a/app/Services/CategoryTemplateService.php +++ b/app/Services/CategoryTemplateService.php @@ -2,19 +2,18 @@ namespace App\Services; +use App\Models\Commons\Category; +use App\Models\Commons\CategoryTemplate; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use App\Models\Commons\CategoryTemplate; -use App\Models\Commons\Category; -use App\Models\Commons\CategoryField; class CategoryTemplateService extends Service { public function index(int $categoryId, array $params) { $tenantId = $this->tenantId(); - $size = (int)($params['size'] ?? 20); + $size = (int) ($params['size'] ?? 20); return CategoryTemplate::query() ->where('tenant_id', $tenantId) @@ -26,14 +25,14 @@ public function index(int $categoryId, array $params) public function store(int $categoryId, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $this->assertCategoryExists($tenantId, $categoryId); $v = Validator::make($data, [ - 'version_no' => 'required|integer|min:1', + 'version_no' => 'required|integer|min:1', 'template_json' => 'required|json', - 'applied_at' => 'required|date', - 'remarks' => 'nullable|string|max:255', + 'applied_at' => 'required|date', + 'remarks' => 'nullable|string|max:255', ]); $payload = $v->validate(); @@ -46,9 +45,9 @@ public function store(int $categoryId, array $data) throw new BadRequestHttpException(__('error.duplicate_key')); // version_no 중복 } - $payload['tenant_id'] = $tenantId; + $payload['tenant_id'] = $tenantId; $payload['category_id'] = $categoryId; - $payload['created_by'] = $userId; + $payload['created_by'] = $userId; return CategoryTemplate::create($payload); } @@ -61,30 +60,36 @@ public function show(int $tplId) ->where('tenant_id', $tenantId) ->find($tplId); - if (!$tpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $tpl) { + throw new BadRequestHttpException(__('error.not_found')); + } + return $tpl; } public function update(int $tplId, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $tpl = CategoryTemplate::query() ->where('tenant_id', $tenantId) ->find($tplId); - if (!$tpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $tpl) { + throw new BadRequestHttpException(__('error.not_found')); + } $v = Validator::make($data, [ 'template_json' => 'nullable|json', - 'applied_at' => 'nullable|date', - 'remarks' => 'nullable|string|max:255', + 'applied_at' => 'nullable|date', + 'remarks' => 'nullable|string|max:255', ]); $payload = $v->validate(); $payload['updated_by'] = $userId; $tpl->update($payload); + return $tpl->refresh(); } @@ -96,7 +101,9 @@ public function destroy(int $tplId): void ->where('tenant_id', $tenantId) ->find($tplId); - if (!$tpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $tpl) { + throw new BadRequestHttpException(__('error.not_found')); + } $tpl->delete(); } @@ -108,16 +115,18 @@ public function destroy(int $tplId): void public function apply(int $categoryId, int $tplId): void { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $tpl = CategoryTemplate::query() ->where('tenant_id', $tenantId) ->where('category_id', $categoryId) ->find($tplId); - if (!$tpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $tpl) { + throw new BadRequestHttpException(__('error.not_found')); + } - DB::transaction(function () use ($tenantId, $userId, $categoryId, $tpl) { + DB::transaction(function () { // 1) categories 테이블에 활성 버전 반영(컬럼이 있다면) // Category::where('tenant_id', $tenantId)->where('id', $categoryId)->update([ // 'active_template_version' => $tpl->version_no, @@ -143,12 +152,15 @@ public function preview(int $categoryId, int $tplId): array ->where('category_id', $categoryId) ->find($tplId); - if (!$tpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $tpl) { + throw new BadRequestHttpException(__('error.not_found')); + } $json = json_decode($tpl->template_json, true); - if (!is_array($json)) { + if (! is_array($json)) { throw new BadRequestHttpException(__('error.invalid_payload')); } + // 프론트 렌더링 편의 구조로 가공 가능 return $json; } @@ -165,7 +177,9 @@ public function diff(int $categoryId, int $a, int $b): array ->where('tenant_id', $tenantId)->where('category_id', $categoryId) ->where('version_no', $b)->first(); - if (!$aTpl || !$bTpl) throw new BadRequestHttpException(__('error.not_found')); + if (! $aTpl || ! $bTpl) { + throw new BadRequestHttpException(__('error.not_found')); + } $aj = json_decode($aTpl->template_json, true) ?: []; $bj = json_decode($bTpl->template_json, true) ?: []; @@ -175,7 +189,7 @@ public function diff(int $categoryId, int $a, int $b): array $bKeys = collect($bj['fields'] ?? [])->pluck('key')->all(); return [ - 'added' => array_values(array_diff($bKeys, $aKeys)), + 'added' => array_values(array_diff($bKeys, $aKeys)), 'removed' => array_values(array_diff($aKeys, $bKeys)), // 변경(diff in detail)은 정책에 맞게 확장 ]; @@ -187,7 +201,7 @@ private function assertCategoryExists(int $tenantId, int $categoryId): void ->where('tenant_id', $tenantId) ->where('id', $categoryId) ->exists(); - if (!$exists) { + if (! $exists) { throw new BadRequestHttpException(__('error.category_not_found')); } } diff --git a/app/Services/ClassificationService.php b/app/Services/ClassificationService.php index 0baa77e..d70b97a 100644 --- a/app/Services/ClassificationService.php +++ b/app/Services/ClassificationService.php @@ -14,11 +14,11 @@ public function index(array $params) { $tenantId = $this->tenantId(); - $page = (int)($params['page'] ?? 1); - $size = (int)($params['size'] ?? 20); - $q = trim((string)($params['q'] ?? '')); + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); $group = $params['group'] ?? null; - $only = $params['only_active'] ?? null; + $only = $params['only_active'] ?? null; $query = Classification::query()->where('tenant_id', $tenantId); @@ -28,11 +28,11 @@ public function index(array $params) ->orWhere('code', 'like', "%{$q}%"); }); } - if (!is_null($group) && $group !== '') { + if (! is_null($group) && $group !== '') { $query->where('group', $group); } - if (!is_null($only)) { - $query->where('is_active', (int)!!$only); + if (! is_null($only)) { + $query->where('is_active', (int) (bool) $only); } $query->orderBy('group')->orderBy('code')->orderBy('id'); @@ -45,7 +45,10 @@ public function show(int $id) { $tenantId = $this->tenantId(); $row = Classification::where('tenant_id', $tenantId)->find($id); - if (!$row) throw new NotFoundHttpException(__('error.not_found')); + if (! $row) { + throw new NotFoundHttpException(__('error.not_found')); + } + return $row; } @@ -53,26 +56,30 @@ public function show(int $id) public function store(array $params) { $tenantId = $this->tenantId(); - $uid = $this->apiUserId(); + $uid = $this->apiUserId(); $v = Validator::make($params, [ - 'group' => 'required|string|max:50', - 'code' => 'required|string|max:50', - 'name' => 'required|string|max:100', - 'is_active' => 'nullable|boolean', + 'group' => 'required|string|max:50', + 'code' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'is_active' => 'nullable|boolean', ]); - if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } // 그룹 내 코드 중복 체크(tenant 기준) $data = $v->validated(); $exists = Classification::where('tenant_id', $tenantId) ->where('group', $data['group']) - ->where('code', $data['code']) + ->where('code', $data['code']) ->exists(); - if ($exists) throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + if ($exists) { + throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + } $data['tenant_id'] = $tenantId; - $data['is_active'] = (int)($data['is_active'] ?? 1); + $data['is_active'] = (int) ($data['is_active'] ?? 1); return Classification::create($data); } @@ -83,29 +90,36 @@ public function update(int $id, array $params) $tenantId = $this->tenantId(); $row = Classification::where('tenant_id', $tenantId)->find($id); - if (!$row) throw new NotFoundHttpException(__('error.not_found')); + if (! $row) { + throw new NotFoundHttpException(__('error.not_found')); + } $v = Validator::make($params, [ - 'group' => 'sometimes|required|string|max:50', - 'code' => 'sometimes|required|string|max:50', - 'name' => 'sometimes|required|string|max:100', - 'is_active' => 'nullable|boolean', + 'group' => 'sometimes|required|string|max:50', + 'code' => 'sometimes|required|string|max:50', + 'name' => 'sometimes|required|string|max:100', + 'is_active' => 'nullable|boolean', ]); - if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } $payload = $v->validated(); // group 또는 code 변경 시 유니크 검사 $newGroup = $payload['group'] ?? $row->group; - $newCode = $payload['code'] ?? $row->code; + $newCode = $payload['code'] ?? $row->code; $dupe = Classification::where('tenant_id', $tenantId) ->where('group', $newGroup) - ->where('code', $newCode) + ->where('code', $newCode) ->where('id', '!=', $row->id) ->exists(); - if ($dupe) throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + if ($dupe) { + throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + } $row->update($payload); + return $row->refresh(); } @@ -115,9 +129,12 @@ public function destroy(int $id) $tenantId = $this->tenantId(); $row = Classification::where('tenant_id', $tenantId)->find($id); - if (!$row) throw new NotFoundHttpException(__('error.not_found')); + if (! $row) { + throw new NotFoundHttpException(__('error.not_found')); + } $row->delete(); + return 'success'; } } diff --git a/app/Services/ClientGroupService.php b/app/Services/ClientGroupService.php index 779cd5f..8d900ca 100644 --- a/app/Services/ClientGroupService.php +++ b/app/Services/ClientGroupService.php @@ -189,4 +189,4 @@ public function toggle(int $id) return $group->refresh(); } -} \ No newline at end of file +} diff --git a/app/Services/ClientService.php b/app/Services/ClientService.php index fb61ed5..56aed5e 100644 --- a/app/Services/ClientService.php +++ b/app/Services/ClientService.php @@ -14,9 +14,9 @@ public function index(array $params) { $tenantId = $this->tenantId(); - $page = (int)($params['page'] ?? 1); - $size = (int)($params['size'] ?? 20); - $q = trim((string)($params['q'] ?? '')); + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); $onlyActive = $params['only_active'] ?? null; $query = Client::query()->where('tenant_id', $tenantId); @@ -43,9 +43,10 @@ public function show(int $id) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); - if (!$client) { + if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } + return $client; } @@ -92,7 +93,7 @@ public function update(int $id, array $params) $uid = $this->apiUserId(); $client = Client::where('tenant_id', $tenantId)->find($id); - if (!$client) { + if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } @@ -123,6 +124,7 @@ public function update(int $id, array $params) } $client->update($payload); + return $client->refresh(); } @@ -132,7 +134,7 @@ public function destroy(int $id) $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); - if (!$client) { + if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } @@ -142,6 +144,7 @@ public function destroy(int $id) } $client->delete(); + return 'success'; } @@ -150,12 +153,13 @@ public function toggle(int $id) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); - if (!$client) { + if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } $client->is_active = $client->is_active === 'Y' ? 'N' : 'Y'; $client->save(); + return $client->refresh(); } } diff --git a/app/Services/DepartmentService.php b/app/Services/DepartmentService.php index 6fc6afa..c3c9c83 100644 --- a/app/Services/DepartmentService.php +++ b/app/Services/DepartmentService.php @@ -20,6 +20,7 @@ protected function v(array $params, array $rules) if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } + return $v->validated(); } @@ -27,30 +28,34 @@ protected function v(array $params, array $rules) public function index(array $params) { $p = $this->v($params, [ - 'q' => 'nullable|string|max:100', + 'q' => 'nullable|string|max:100', 'is_active' => 'nullable|in:0,1', - 'page' => 'nullable|integer|min:1', - 'per_page' => 'nullable|integer|min:1|max:200', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:200', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $q = Department::query(); if (isset($p['is_active'])) { - $q->where('is_active', (int)$p['is_active']); + $q->where('is_active', (int) $p['is_active']); } - if (!empty($p['q'])) { + if (! empty($p['q'])) { $q->where(function ($w) use ($p) { - $w->where('name', 'like', '%' . $p['q'] . '%') - ->orWhere('code', 'like', '%' . $p['q'] . '%'); + $w->where('name', 'like', '%'.$p['q'].'%') + ->orWhere('code', 'like', '%'.$p['q'].'%'); }); } $q->orderBy('sort_order')->orderBy('name'); $perPage = $p['per_page'] ?? 20; - $page = $p['page'] ?? null; + $page = $p['page'] ?? null; return $q->paginate($perPage, ['*'], 'page', $page); } @@ -62,30 +67,36 @@ public function store(array $params) $userId = $this->apiUserId(); $p = $this->v($params, [ - 'code' => 'nullable|string|max:50', - 'name' => 'required|string|max:100', + 'code' => 'nullable|string|max:50', + 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', - 'is_active' => 'nullable|in:0,1', - 'sort_order' => 'nullable|integer', - 'created_by' => 'nullable|integer|min:1', + 'is_active' => 'nullable|in:0,1', + 'sort_order' => 'nullable|integer', + 'created_by' => 'nullable|integer|min:1', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } - if (!empty($p['code'])) { + if (! empty($p['code'])) { $exists = Department::query()->where('code', $p['code'])->exists(); - if ($exists) return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409]; + if ($exists) { + return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409]; + } } $dept = Department::create([ - 'tenant_id' => $tenantId, - 'code' => $p['code'] ?? null, - 'name' => $p['name'], + 'tenant_id' => $tenantId, + 'code' => $p['code'] ?? null, + 'name' => $p['name'], 'description' => $p['description'] ?? null, - 'is_active' => isset($p['is_active']) ? (int)$p['is_active'] : 1, - 'sort_order' => $p['sort_order'] ?? 0, - 'created_by' => $userId ?? null, - 'updated_by' => $userId ?? null, + 'is_active' => isset($p['is_active']) ? (int) $p['is_active'] : 1, + 'sort_order' => $p['sort_order'] ?? 0, + 'created_by' => $userId ?? null, + 'updated_by' => $userId ?? null, ]); return $dept->fresh(); @@ -94,49 +105,62 @@ public function store(array $params) /** 단건 */ public function show(int $id, array $params = []) { - if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; + if (! $id) { + return ['error' => 'id가 필요합니다.', 'code' => 400]; + } $res = Department::query()->find($id); - if (!$res) { + if (! $res) { return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; } + return $res; } /** 수정 */ public function update(int $id, array $params) { - if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; + if (! $id) { + return ['error' => 'id가 필요합니다.', 'code' => 400]; + } $p = $this->v($params, [ - 'code' => 'nullable|string|max:50', - 'name' => 'nullable|string|max:100', + 'code' => 'nullable|string|max:50', + 'name' => 'nullable|string|max:100', 'description' => 'nullable|string|max:255', - 'is_active' => 'nullable|in:0,1', - 'sort_order' => 'nullable|integer', - 'updated_by' => 'nullable|integer|min:1', + 'is_active' => 'nullable|in:0,1', + 'sort_order' => 'nullable|integer', + 'updated_by' => 'nullable|integer|min:1', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $dept = Department::query()->find($id); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } - if (array_key_exists('code', $p) && !is_null($p['code'])) { + if (array_key_exists('code', $p) && ! is_null($p['code'])) { $exists = Department::query() ->where('code', $p['code']) ->where('id', '!=', $id) ->exists(); - if ($exists) return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409]; + if ($exists) { + return ['error' => '이미 존재하는 부서 코드입니다.', 'code' => 409]; + } } $dept->fill([ - 'code' => array_key_exists('code', $p) ? $p['code'] : $dept->code, - 'name' => $p['name'] ?? $dept->name, + 'code' => array_key_exists('code', $p) ? $p['code'] : $dept->code, + 'name' => $p['name'] ?? $dept->name, 'description' => $p['description'] ?? $dept->description, - 'is_active' => isset($p['is_active']) ? (int)$p['is_active'] : $dept->is_active, - 'sort_order' => $p['sort_order'] ?? $dept->sort_order, - 'updated_by' => $p['updated_by'] ?? $dept->updated_by, + 'is_active' => isset($p['is_active']) ? (int) $p['is_active'] : $dept->is_active, + 'sort_order' => $p['sort_order'] ?? $dept->sort_order, + 'updated_by' => $p['updated_by'] ?? $dept->updated_by, ])->save(); return $dept->fresh(); @@ -145,18 +169,26 @@ public function update(int $id, array $params) /** 삭제(soft) */ public function destroy(int $id, array $params) { - if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; + if (! $id) { + return ['error' => 'id가 필요합니다.', 'code' => 400]; + } $p = $this->v($params, [ 'deleted_by' => 'nullable|integer|min:1', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $dept = Department::query()->find($id); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } - if (!empty($p['deleted_by'])) { + if (! empty($p['deleted_by'])) { $dept->deleted_by = $p['deleted_by']; $dept->save(); } @@ -169,20 +201,26 @@ public function destroy(int $id, array $params) public function listUsers(int $deptId, array $params) { $p = $this->v($params, [ - 'page' => 'nullable|integer|min:1', - 'per_page' => 'nullable|integer|min:1|max:200', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:200', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $dept = Department::query()->find($deptId); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } $builder = $dept->departmentUsers()->with('user') ->orderByDesc('is_primary')->orderBy('id'); $perPage = $p['per_page'] ?? 20; - $page = $p['page'] ?? null; + $page = $p['page'] ?? null; return $builder->paginate($perPage, ['*'], 'page', $page); } @@ -191,15 +229,21 @@ public function listUsers(int $deptId, array $params) public function attachUser(int $deptId, array $params) { $p = $this->v($params, [ - 'user_id' => 'required|integer|min:1', + 'user_id' => 'required|integer|min:1', 'is_primary' => 'nullable|in:0,1', - 'joined_at' => 'nullable|date', + 'joined_at' => 'nullable|date', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $dept = Department::query()->find($deptId); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } $result = DB::transaction(function () use ($dept, $p) { $tenantId = $this->tenantId(); @@ -213,7 +257,7 @@ public function attachUser(int $deptId, array $params) return ['error' => '이미 배정된 사용자입니다.', 'code' => 409]; } - if (!empty($p['is_primary']) && (int)$p['is_primary'] === 1) { + if (! empty($p['is_primary']) && (int) $p['is_primary'] === 1) { DepartmentUser::whereNull('deleted_at') ->where('user_id', $p['user_id']) ->update(['is_primary' => 0]); @@ -221,10 +265,10 @@ public function attachUser(int $deptId, array $params) $payload = [ 'department_id' => $dept->id, - 'tenant_id' => $tenantId, - 'user_id' => $p['user_id'], - 'is_primary' => isset($p['is_primary']) ? (int)$p['is_primary'] : 0, - 'joined_at' => !empty($p['joined_at']) ? Carbon::parse($p['joined_at']) : now(), + 'tenant_id' => $tenantId, + 'user_id' => $p['user_id'], + 'is_primary' => isset($p['is_primary']) ? (int) $p['is_primary'] : 0, + 'joined_at' => ! empty($p['joined_at']) ? Carbon::parse($p['joined_at']) : now(), ]; if ($du) { @@ -238,7 +282,10 @@ public function attachUser(int $deptId, array $params) return ['department_id' => $dept->id, 'user_id' => $p['user_id']]; }); - if ($result instanceof JsonResponse) return $result; + if ($result instanceof JsonResponse) { + return $result; + } + return $result; } @@ -250,12 +297,14 @@ public function detachUser(int $deptId, int $userId, array $params) ->where('user_id', $userId) ->first(); - if (!$du) return ['error' => '배정된 사용자를 찾을 수 없습니다.', 'code' => 404]; + if (! $du) { + return ['error' => '배정된 사용자를 찾을 수 없습니다.', 'code' => 404]; + } $du->delete(); return [ - 'user_id' => $userId, + 'user_id' => $userId, 'deleted_at' => now()->toDateTimeString(), ]; } @@ -266,8 +315,12 @@ public function setPrimary(int $deptId, int $userId, array $params) $p = $this->v($params, [ 'is_primary' => 'required|in:0,1', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } $result = DB::transaction(function () use ($deptId, $userId, $p) { $du = DepartmentUser::whereNull('deleted_at') @@ -275,23 +328,26 @@ public function setPrimary(int $deptId, int $userId, array $params) ->where('user_id', $userId) ->first(); - if (!$du) { + if (! $du) { return ['error' => '배정된 사용자를 찾을 수 없습니다.', 'code' => 404]; } - if ((int)$p['is_primary'] === 1) { + if ((int) $p['is_primary'] === 1) { DepartmentUser::whereNull('deleted_at') ->where('user_id', $userId) ->update(['is_primary' => 0]); } - $du->is_primary = (int)$p['is_primary']; + $du->is_primary = (int) $p['is_primary']; $du->save(); return ['user_id' => $userId, 'department_id' => $deptId, 'is_primary' => $du->is_primary]; }); - if ($result instanceof JsonResponse) return $result; + if ($result instanceof JsonResponse) { + return $result; + } + return $result; } @@ -300,19 +356,25 @@ public function listPermissions(int $deptId, array $params) { // 1) 파라미터 검증 $p = $this->v($params, [ - 'menu_id' => 'nullable|integer|min:1', - 'is_allowed' => 'nullable|in:0,1', - 'page' => 'nullable|integer|min:1', - 'per_page' => 'nullable|integer|min:1|max:200', + 'menu_id' => 'nullable|integer|min:1', + 'is_allowed' => 'nullable|in:0,1', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:200', ]); - if ($p instanceof JsonResponse) return $p; - if (isset($p['error'])) return $p; + if ($p instanceof JsonResponse) { + return $p; + } + if (isset($p['error'])) { + return $p; + } // 2) 부서 확인 $dept = Department::query()->find($deptId); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } - $tenantId = (int)$dept->tenant_id; + $tenantId = (int) $dept->tenant_id; $modelType = Department::class; // 3) ALLOW/DENY 서브쿼리 준비 (컬럼 형태 통일) @@ -328,8 +390,8 @@ public function listPermissions(int $deptId, array $params) ]) ->where([ 'model_type' => $modelType, - 'model_id' => $deptId, - 'tenant_id' => $tenantId, + 'model_id' => $deptId, + 'tenant_id' => $tenantId, ]); $denySub = DB::table('permission_overrides') @@ -343,9 +405,9 @@ public function listPermissions(int $deptId, array $params) 'updated_at as override_updated_at', ]) ->where([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'model_type' => $modelType, - 'model_id' => $deptId, + 'model_id' => $deptId, ]) ->where('effect', -1); @@ -369,11 +431,11 @@ public function listPermissions(int $deptId, array $params) // 5) 필터 if (isset($p['is_allowed'])) { - $q->where('u.is_allowed', (int)$p['is_allowed']); + $q->where('u.is_allowed', (int) $p['is_allowed']); } if (isset($p['menu_id'])) { // 권한코드가 menu.{id}.xxx 형태라는 전제 - $q->where('permissions.name', 'like', 'menu.' . (int)$p['menu_id'] . '.%'); + $q->where('permissions.name', 'like', 'menu.'.(int) $p['menu_id'].'.%'); } // 6) 정렬(ALLOW 우선, 그 다음 permission_id 오름차순) @@ -381,7 +443,7 @@ public function listPermissions(int $deptId, array $params) // 7) 페이지네이션 $perPage = $p['per_page'] ?? 20; - $page = $p['page'] ?? null; + $page = $p['page'] ?? null; return $q->paginate($perPage, ['*'], 'page', $page); } @@ -391,88 +453,93 @@ public function upsertPermissions(int $deptId, array $payload): array { // 단일이면 items로 감싸기 $items = isset($payload['permission_id']) - ? [ $payload ] + ? [$payload] : ($payload['items'] ?? $payload); - $v = Validator::make(['items'=>$items], [ + $v = Validator::make(['items' => $items], [ 'items' => 'required|array|max:1000', 'items.*.permission_id' => 'required|integer|min:1', - 'items.*.is_allowed' => 'nullable|in:0,1', // 생략 시 1(허용) + 'items.*.is_allowed' => 'nullable|in:0,1', // 생략 시 1(허용) ]); - if ($v->fails()) return ['error'=>$v->errors()->first(),'code'=>422]; + if ($v->fails()) { + return ['error' => $v->errors()->first(), 'code' => 422]; + } $dept = Department::query()->find($deptId); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } - $tenantId = (int)$dept->tenant_id; + $tenantId = (int) $dept->tenant_id; $modelType = Department::class; - $ok = 0; $failed = []; + $ok = 0; + $failed = []; DB::transaction(function () use ($items, $tenantId, $deptId, $modelType, &$ok, &$failed) { foreach ($items as $i => $r) { try { - $permissionId = (int)$r['permission_id']; - $isAllowed = array_key_exists('is_allowed', $r) ? (int)$r['is_allowed'] : 1; + $permissionId = (int) $r['permission_id']; + $isAllowed = array_key_exists('is_allowed', $r) ? (int) $r['is_allowed'] : 1; if ($isAllowed === 1) { // ALLOW: Spatie 표준에 넣고, 동일 권한의 DENY 제거 $exists = DB::table('model_has_permissions')->where([ 'permission_id' => $permissionId, - 'model_type' => $modelType, - 'model_id' => $deptId, - 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, + 'tenant_id' => $tenantId, ])->exists(); - if (!$exists) { + if (! $exists) { DB::table('model_has_permissions')->insert([ 'permission_id' => $permissionId, - 'model_type' => $modelType, - 'model_id' => $deptId, - 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, + 'tenant_id' => $tenantId, ]); } DB::table('permission_overrides')->where([ - 'tenant_id' => $tenantId, - 'model_type' => $modelType, - 'model_id' => $deptId, + 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, 'permission_id' => $permissionId, - 'effect' => -1, + 'effect' => -1, ])->delete(); } else { // DENY: overrides(effect=-1) upsert, 그리고 ALLOW 제거 $exists = DB::table('permission_overrides')->where([ - 'tenant_id' => $tenantId, - 'model_type' => $modelType, - 'model_id' => $deptId, + 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, 'permission_id' => $permissionId, ])->exists(); if ($exists) { DB::table('permission_overrides')->where([ - 'tenant_id' => $tenantId, - 'model_type' => $modelType, - 'model_id' => $deptId, + 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, 'permission_id' => $permissionId, ])->update(['effect' => -1, 'updated_at' => now()]); } else { DB::table('permission_overrides')->insert([ - 'tenant_id' => $tenantId, - 'model_type' => $modelType, - 'model_id' => $deptId, + 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, 'permission_id' => $permissionId, - 'effect' => -1, - 'created_at' => now(), - 'updated_at' => now(), + 'effect' => -1, + 'created_at' => now(), + 'updated_at' => now(), ]); } DB::table('model_has_permissions')->where([ 'permission_id' => $permissionId, - 'model_type' => $modelType, - 'model_id' => $deptId, - 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, + 'tenant_id' => $tenantId, ])->delete(); } @@ -488,9 +555,9 @@ public function upsertPermissions(int $deptId, array $payload): array }); return [ - 'processed' => count($items), - 'succeeded' => $ok, - 'failed' => $failed + 'processed' => count($items), + 'succeeded' => $ok, + 'failed' => $failed, ]; } @@ -498,41 +565,46 @@ public function upsertPermissions(int $deptId, array $payload): array public function revokePermissions(int $deptId, array $payload): array { $items = isset($payload['permission_id']) - ? [ $payload ] + ? [$payload] : ($payload['items'] ?? $payload); - $v = Validator::make(['items'=>$items], [ + $v = Validator::make(['items' => $items], [ 'items' => 'required|array|max:1000', 'items.*.permission_id' => 'required|integer|min:1', ]); - if ($v->fails()) return ['error'=>$v->errors()->first(),'code'=>422]; + if ($v->fails()) { + return ['error' => $v->errors()->first(), 'code' => 422]; + } $dept = Department::query()->find($deptId); - if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + if (! $dept) { + return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; + } - $tenantId = (int)$dept->tenant_id; + $tenantId = (int) $dept->tenant_id; $modelType = Department::class; - $ok = 0; $failed = []; + $ok = 0; + $failed = []; DB::transaction(function () use ($items, $tenantId, $deptId, $modelType, &$ok, &$failed) { foreach ($items as $i => $r) { try { - $permissionId = (int)$r['permission_id']; + $permissionId = (int) $r['permission_id']; // ALLOW 제거 DB::table('model_has_permissions')->where([ 'permission_id' => $permissionId, - 'model_type' => $modelType, - 'model_id' => $deptId, - 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, + 'tenant_id' => $tenantId, ])->delete(); // DENY/임시허용 오버라이드 제거 DB::table('permission_overrides')->where([ - 'tenant_id' => $tenantId, - 'model_type' => $modelType, - 'model_id' => $deptId, + 'tenant_id' => $tenantId, + 'model_type' => $modelType, + 'model_id' => $deptId, 'permission_id' => $permissionId, ])->delete(); @@ -550,7 +622,7 @@ public function revokePermissions(int $deptId, array $payload): array return [ 'processed' => count($items), 'succeeded' => $ok, - 'failed' => $failed + 'failed' => $failed, ]; } } diff --git a/app/Services/Design/BomCalculationService.php b/app/Services/Design/BomCalculationService.php index f2001c4..3ba56f3 100644 --- a/app/Services/Design/BomCalculationService.php +++ b/app/Services/Design/BomCalculationService.php @@ -2,10 +2,10 @@ namespace App\Services\Design; -use App\Services\Service; -use App\Services\Calculation\CalculationEngine; -use App\Models\Design\BomTemplate; use App\Models\Calculation\CalculationConfig; +use App\Models\Design\BomTemplate; +use App\Services\Calculation\CalculationEngine; +use App\Services\Service; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -20,9 +20,9 @@ public function __construct(CalculationEngine $calculationEngine) /** * 견적 파라미터 조회 - * @param int $modelId 모델 ID - * @param string|null $companyName 업체명 - * @return array + * + * @param int $modelId 모델 ID + * @param string|null $companyName 업체명 */ public function getEstimateParameters(int $modelId, ?string $companyName = null): array { @@ -38,7 +38,7 @@ public function getEstimateParameters(int $modelId, ?string $companyName = null) ->where('is_primary', true) ->first(); - if (!$bomTemplate) { + if (! $bomTemplate) { throw new \Exception("모델 ID {$modelId}에 대한 BOM 템플릿을 찾을 수 없습니다."); } @@ -58,22 +58,22 @@ public function getEstimateParameters(int $modelId, ?string $companyName = null) 'id' => $bomTemplate->modelVersion->model->id, 'name' => $bomTemplate->modelVersion->model->name ?? '모델명 없음', 'version' => $bomTemplate->modelVersion->version_no ?? 'v1.0', - 'bom_template_id' => $bomTemplate->id + 'bom_template_id' => $bomTemplate->id, ], 'company_info' => [ 'company_type' => $bomTemplate->company_type, 'formula_version' => $bomTemplate->formula_version, - 'requested_company' => $companyName + 'requested_company' => $companyName, ], 'parameters' => $parameters, - 'calculation_preview' => true + 'calculation_preview' => true, ]; } catch (\Exception $e) { Log::error('견적 파라미터 조회 실패', [ 'model_id' => $modelId, 'company_name' => $companyName, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); throw $e; @@ -82,10 +82,10 @@ public function getEstimateParameters(int $modelId, ?string $companyName = null) /** * BOM 계산 실행 - * @param int $bomTemplateId BOM 템플릿 ID - * @param array $parameters 입력 파라미터 - * @param string|null $companyName 업체명 - * @return array + * + * @param int $bomTemplateId BOM 템플릿 ID + * @param array $parameters 입력 파라미터 + * @param string|null $companyName 업체명 */ public function calculateBomEstimate(int $bomTemplateId, array $parameters, ?string $companyName = null): array { @@ -112,7 +112,7 @@ public function calculateBomEstimate(int $bomTemplateId, array $parameters, ?str return [ 'success' => $result['success'], 'data' => $result['success'] ? $result : null, - 'error' => $result['success'] ? null : $result['error'] + 'error' => $result['success'] ? null : $result['error'], ]; } catch (\Exception $e) { @@ -121,21 +121,21 @@ public function calculateBomEstimate(int $bomTemplateId, array $parameters, ?str 'bom_template_id' => $bomTemplateId, 'parameters' => $parameters, 'company_name' => $companyName, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); return [ 'success' => false, 'data' => null, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]; } } /** * 업체별 산출식 목록 조회 - * @param string $companyName 업체명 - * @return array + * + * @param string $companyName 업체명 */ public function getCompanyFormulas(string $companyName): array { @@ -150,18 +150,18 @@ public function getCompanyFormulas(string $companyName): array 'version' => $formula['version'], 'description' => $formula['description'], 'parameters' => $formula['parameters'], - 'updated_at' => $formula['updated_at'] + 'updated_at' => $formula['updated_at'], ]; - }, $formulas) + }, $formulas), ]; } /** * 업체별 산출식 등록/수정 - * @param string $companyName 업체명 - * @param string $formulaType 산출식 타입 - * @param array $formulaData 산출식 데이터 - * @return array + * + * @param string $companyName 업체명 + * @param string $formulaType 산출식 타입 + * @param array $formulaData 산출식 데이터 */ public function saveCompanyFormula(string $companyName, string $formulaType, array $formulaData): array { @@ -191,7 +191,7 @@ public function saveCompanyFormula(string $companyName, string $formulaType, arr 'validation_rules' => $formulaData['validation_rules'] ?? null, 'description' => $formulaData['description'] ?? null, 'is_active' => true, - 'created_by' => $this->apiUserId() + 'created_by' => $this->apiUserId(), ]); DB::commit(); @@ -202,7 +202,7 @@ public function saveCompanyFormula(string $companyName, string $formulaType, arr 'formula_type' => $formula->formula_type, 'version' => $formula->version, 'is_active' => $formula->is_active, - 'created_at' => $formula->created_at + 'created_at' => $formula->created_at, ]; } catch (\Exception $e) { @@ -210,7 +210,7 @@ public function saveCompanyFormula(string $companyName, string $formulaType, arr Log::error('업체별 산출식 저장 실패', [ 'company_name' => $companyName, 'formula_type' => $formulaType, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); throw $e; @@ -245,7 +245,7 @@ protected function logCalculationHistory(int $bomTemplateId, array $parameters, 'parameters' => $parameters, 'success' => $result['success'], 'calculated_items_count' => $result['success'] ? count($result['bom_items']) : 0, - 'calculated_by' => $this->apiUserId() + 'calculated_by' => $this->apiUserId(), ]); } @@ -255,11 +255,12 @@ protected function logCalculationHistory(int $bomTemplateId, array $parameters, protected function incrementVersion(string $currentVersion): string { if (preg_match('/^v(\d+)\.(\d+)$/', $currentVersion, $matches)) { - $major = (int)$matches[1]; - $minor = (int)$matches[2]; - return "v{$major}." . ($minor + 1); + $major = (int) $matches[1]; + $minor = (int) $matches[2]; + + return "v{$major}.".($minor + 1); } return 'v1.0'; } -} \ No newline at end of file +} diff --git a/app/Services/Design/BomTemplateService.php b/app/Services/Design/BomTemplateService.php index 9b632ab..c51d839 100644 --- a/app/Services/Design/BomTemplateService.php +++ b/app/Services/Design/BomTemplateService.php @@ -41,7 +41,7 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool ->where('tenant_id', $tenantId) ->find($modelVersionId); - if (!$mv) { + if (! $mv) { throw new NotFoundHttpException(__('error.not_found')); } @@ -54,13 +54,13 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool $action = 'created'; $before = null; - if (!$tpl) { + if (! $tpl) { $tpl = BomTemplate::create([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'model_version_id' => $mv->id, - 'name' => $name, - 'is_primary' => $isPrimary, - 'notes' => $notes, + 'name' => $name, + 'is_primary' => $isPrimary, + 'notes' => $notes, ]); } else { $action = 'updated'; @@ -98,7 +98,7 @@ public function updateTemplate(int $templateId, array $data): BomTemplate ->where('tenant_id', $tenantId) ->find($templateId); - if (!$tpl) { + if (! $tpl) { throw new NotFoundHttpException(__('error.not_found')); } @@ -147,7 +147,7 @@ public function deleteTemplate(int $templateId): void ->where('tenant_id', $tenantId) ->find($templateId); - if (!$tpl) { + if (! $tpl) { throw new NotFoundHttpException(__('error.not_found')); } @@ -172,11 +172,11 @@ public function show(int $templateId, bool $withItems = false): BomTemplate $q = BomTemplate::query()->where('tenant_id', $tenantId); if ($withItems) { - $q->with(['items' => fn($w) => $w->orderBy('sort_order')]); + $q->with(['items' => fn ($w) => $w->orderBy('sort_order')]); } $tpl = $q->find($templateId); - if (!$tpl) { + if (! $tpl) { throw new NotFoundHttpException(__('error.not_found')); } @@ -192,15 +192,15 @@ public function replaceItems(int $templateId, array $items): void ->where('tenant_id', $tenantId) ->find($templateId); - if (!$tpl) { + if (! $tpl) { throw new NotFoundHttpException(__('error.not_found')); } // 1차 검증 foreach ($items as $i => $row) { $refType = strtoupper((string) Arr::get($row, 'ref_type')); - $qty = (float) Arr::get($row, 'qty', 0); - if (!in_array($refType, ['MATERIAL','PRODUCT'], true)) { + $qty = (float) Arr::get($row, 'qty', 0); + if (! in_array($refType, ['MATERIAL', 'PRODUCT'], true)) { throw ValidationException::withMessages(["items.$i.ref_type" => __('error.validation_failed')]); } if ($qty <= 0) { @@ -215,14 +215,14 @@ public function replaceItems(int $templateId, array $items): void ->where('bom_template_id', $tpl->id) ->orderBy('sort_order') ->get() - ->map(fn($i) => [ + ->map(fn ($i) => [ 'ref_type' => strtoupper($i->ref_type), - 'ref_id' => (int)$i->ref_id, - 'qty' => (float)$i->qty, - 'waste_rate' => (float)$i->waste_rate, + 'ref_id' => (int) $i->ref_id, + 'qty' => (float) $i->qty, + 'waste_rate' => (float) $i->waste_rate, 'uom_id' => $i->uom_id, 'notes' => $i->notes, - 'sort_order' => (int)$i->sort_order, + 'sort_order' => (int) $i->sort_order, ])->all(); BomTemplateItem::query() @@ -234,21 +234,21 @@ public function replaceItems(int $templateId, array $items): void $payloads = []; foreach ($items as $row) { $payloads[] = [ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'bom_template_id' => $tpl->id, - 'ref_type' => strtoupper($row['ref_type']), - 'ref_id' => (int) $row['ref_id'], - 'qty' => (string) ($row['qty'] ?? 1), - 'waste_rate' => (string) ($row['waste_rate'] ?? 0), - 'uom_id' => $row['uom_id'] ?? null, - 'notes' => $row['notes'] ?? null, - 'sort_order' => (int) ($row['sort_order'] ?? 0), - 'created_at' => $now, - 'updated_at' => $now, + 'ref_type' => strtoupper($row['ref_type']), + 'ref_id' => (int) $row['ref_id'], + 'qty' => (string) ($row['qty'] ?? 1), + 'waste_rate' => (string) ($row['waste_rate'] ?? 0), + 'uom_id' => $row['uom_id'] ?? null, + 'notes' => $row['notes'] ?? null, + 'sort_order' => (int) ($row['sort_order'] ?? 0), + 'created_at' => $now, + 'updated_at' => $now, ]; } - if (!empty($payloads)) { + if (! empty($payloads)) { BomTemplateItem::insert($payloads); } @@ -283,7 +283,7 @@ public function listItems(int $templateId) ->where('tenant_id', $tenantId) ->find($templateId); - if (!$tpl) { + if (! $tpl) { throw new NotFoundHttpException(__('error.not_found')); } @@ -302,7 +302,7 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array $left = BomTemplate::query()->where('tenant_id', $tenantId)->find($leftTemplateId); $right = BomTemplate::query()->where('tenant_id', $tenantId)->find($rightTemplateId); - if (!$left || !$right) { + if (! $left || ! $right) { throw new NotFoundHttpException(__('error.not_found')); } @@ -310,30 +310,30 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array ->where('tenant_id', $tenantId) ->where('bom_template_id', $left->id) ->get() - ->map(fn($i) => [ - 'key' => strtoupper($i->ref_type) . ':' . (int)$i->ref_id, - 'ref_type' => strtoupper($i->ref_type), - 'ref_id' => (int)$i->ref_id, - 'qty' => (float)$i->qty, - 'waste_rate' => (float)$i->waste_rate, - 'uom_id' => $i->uom_id ? (int)$i->uom_id : null, - 'notes' => $i->notes, - 'sort_order' => (int)$i->sort_order, + ->map(fn ($i) => [ + 'key' => strtoupper($i->ref_type).':'.(int) $i->ref_id, + 'ref_type' => strtoupper($i->ref_type), + 'ref_id' => (int) $i->ref_id, + 'qty' => (float) $i->qty, + 'waste_rate' => (float) $i->waste_rate, + 'uom_id' => $i->uom_id ? (int) $i->uom_id : null, + 'notes' => $i->notes, + 'sort_order' => (int) $i->sort_order, ])->keyBy('key'); $rightItems = BomTemplateItem::query() ->where('tenant_id', $tenantId) ->where('bom_template_id', $right->id) ->get() - ->map(fn($i) => [ - 'key' => strtoupper($i->ref_type) . ':' . (int)$i->ref_id, - 'ref_type' => strtoupper($i->ref_type), - 'ref_id' => (int)$i->ref_id, - 'qty' => (float)$i->qty, - 'waste_rate' => (float)$i->waste_rate, - 'uom_id' => $i->uom_id ? (int)$i->uom_id : null, - 'notes' => $i->notes, - 'sort_order' => (int)$i->sort_order, + ->map(fn ($i) => [ + 'key' => strtoupper($i->ref_type).':'.(int) $i->ref_id, + 'ref_type' => strtoupper($i->ref_type), + 'ref_id' => (int) $i->ref_id, + 'qty' => (float) $i->qty, + 'waste_rate' => (float) $i->waste_rate, + 'uom_id' => $i->uom_id ? (int) $i->uom_id : null, + 'notes' => $i->notes, + 'sort_order' => (int) $i->sort_order, ])->keyBy('key'); $added = []; @@ -341,40 +341,40 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array $changed = []; foreach ($rightItems as $key => $ri) { - if (!$leftItems->has($key)) { + if (! $leftItems->has($key)) { $added[] = Arr::except($ri, ['key']); } else { $li = $leftItems[$key]; $diffs = []; - foreach (['qty','waste_rate','uom_id','notes','sort_order'] as $fld) { + foreach (['qty', 'waste_rate', 'uom_id', 'notes', 'sort_order'] as $fld) { if (($li[$fld] ?? null) !== ($ri[$fld] ?? null)) { $diffs[$fld] = ['before' => $li[$fld] ?? null, 'after' => $ri[$fld] ?? null]; } } - if (!empty($diffs)) { + if (! empty($diffs)) { $changed[] = [ 'ref_type' => $ri['ref_type'], - 'ref_id' => $ri['ref_id'], - 'changes' => $diffs, + 'ref_id' => $ri['ref_id'], + 'changes' => $diffs, ]; } } } foreach ($leftItems as $key => $li) { - if (!$rightItems->has($key)) { + if (! $rightItems->has($key)) { $removed[] = Arr::except($li, ['key']); } } $result = [ - 'left_template_id' => $left->id, + 'left_template_id' => $left->id, 'right_template_id' => $right->id, 'summary' => [ - 'added' => count($added), + 'added' => count($added), 'removed' => count($removed), 'changed' => count($changed), ], - 'added' => $added, + 'added' => $added, 'removed' => $removed, 'changed' => $changed, ]; @@ -399,7 +399,7 @@ public function cloneTemplate(int $templateId, ?int $targetVersionId = null, ?st $tenantId = $this->tenantId(); $src = BomTemplate::query()->where('tenant_id', $tenantId)->find($templateId); - if (!$src) { + if (! $src) { throw new NotFoundHttpException(__('error.not_found')); } @@ -409,29 +409,29 @@ public function cloneTemplate(int $templateId, ?int $targetVersionId = null, ?st ->where('tenant_id', $tenantId) ->find($targetVersionId); - if (!$mv) { + if (! $mv) { throw new NotFoundHttpException(__('error.not_found')); } // 이름 결정(중복 회피) - $baseName = $name ?: ($src->name . ' Copy'); + $baseName = $name ?: ($src->name.' Copy'); $newName = $baseName; $i = 2; while (BomTemplate::query() ->where('model_version_id', $mv->id) ->where('name', $newName) ->exists()) { - $newName = $baseName . ' ' . $i; + $newName = $baseName.' '.$i; $i++; } return DB::transaction(function () use ($tenantId, $src, $mv, $newName, $isPrimary, $notes) { $dest = BomTemplate::create([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'model_version_id' => $mv->id, - 'name' => $newName, - 'is_primary' => $isPrimary, - 'notes' => $notes ?? $src->notes, + 'name' => $newName, + 'is_primary' => $isPrimary, + 'notes' => $notes ?? $src->notes, ]); $now = now(); @@ -439,21 +439,21 @@ public function cloneTemplate(int $templateId, ?int $targetVersionId = null, ?st ->where('tenant_id', $tenantId) ->where('bom_template_id', $src->id) ->get() - ->map(fn($i) => [ - 'tenant_id' => $tenantId, + ->map(fn ($i) => [ + 'tenant_id' => $tenantId, 'bom_template_id' => $dest->id, - 'ref_type' => strtoupper($i->ref_type), - 'ref_id' => (int)$i->ref_id, - 'qty' => (string)$i->qty, - 'waste_rate' => (string)$i->waste_rate, - 'uom_id' => $i->uom_id, - 'notes' => $i->notes, - 'sort_order' => (int)$i->sort_order, - 'created_at' => $now, - 'updated_at' => $now, + 'ref_type' => strtoupper($i->ref_type), + 'ref_id' => (int) $i->ref_id, + 'qty' => (string) $i->qty, + 'waste_rate' => (string) $i->waste_rate, + 'uom_id' => $i->uom_id, + 'notes' => $i->notes, + 'sort_order' => (int) $i->sort_order, + 'created_at' => $now, + 'updated_at' => $now, ])->all(); - if (!empty($items)) { + if (! empty($items)) { BomTemplateItem::insert($items); } @@ -487,7 +487,7 @@ public function validateForRelease(int $modelVersionId): void ->where('tenant_id', $tenantId) ->find($modelVersionId); - if (!$mv) { + if (! $mv) { throw new NotFoundHttpException(__('error.not_found')); } @@ -498,7 +498,7 @@ public function validateForRelease(int $modelVersionId): void ->where('is_primary', true) ->first(); - if (!$primary) { + if (! $primary) { throw ValidationException::withMessages(['template' => __('error.validation_failed')]); } @@ -515,17 +515,17 @@ public function validateForRelease(int $modelVersionId): void // 중복 키 및 값 검증 $seen = []; foreach ($items as $idx => $it) { - $key = strtoupper($it->ref_type) . ':' . (int)$it->ref_id; + $key = strtoupper($it->ref_type).':'.(int) $it->ref_id; if (isset($seen[$key])) { throw ValidationException::withMessages(["items.$idx" => __('error.duplicate')]); } $seen[$key] = true; // 수량/로스율 - if ((float)$it->qty <= 0) { + if ((float) $it->qty <= 0) { throw ValidationException::withMessages(["items.$idx.qty" => __('error.validation_failed')]); } - if ((float)$it->waste_rate < 0) { + if ((float) $it->waste_rate < 0) { throw ValidationException::withMessages(["items.$idx.waste_rate" => __('error.validation_failed')]); } @@ -536,7 +536,7 @@ public function validateForRelease(int $modelVersionId): void ->where('id', $it->ref_id) ->whereNull('deleted_at') ->exists(); - if (!$exists) { + if (! $exists) { throw ValidationException::withMessages(["items.$idx.ref_id" => __('error.not_found')]); } } elseif (strtoupper($it->ref_type) === 'PRODUCT') { @@ -545,7 +545,7 @@ public function validateForRelease(int $modelVersionId): void ->where('id', $it->ref_id) ->whereNull('deleted_at') ->exists(); - if (!$exists) { + if (! $exists) { throw ValidationException::withMessages(["items.$idx.ref_id" => __('error.not_found')]); } } else { diff --git a/app/Services/Design/ModelService.php b/app/Services/Design/ModelService.php index 27b4e8c..eee0037 100644 --- a/app/Services/Design/ModelService.php +++ b/app/Services/Design/ModelService.php @@ -45,6 +45,7 @@ public function create(array $data): DesignModel return DB::transaction(function () use ($tenantId, $data) { $payload = array_merge($data, ['tenant_id' => $tenantId]); + return DesignModel::create($payload); }); } @@ -55,10 +56,10 @@ public function find(int $id): DesignModel $tenantId = $this->tenantId(); $model = DesignModel::query() ->where('tenant_id', $tenantId) - ->with(['versions' => fn($q) => $q->orderBy('version_no')]) + ->with(['versions' => fn ($q) => $q->orderBy('version_no')]) ->find($id); - if (!$model) { + if (! $model) { throw new NotFoundHttpException(__('error.not_found')); } @@ -74,7 +75,7 @@ public function update(int $id, array $data): DesignModel ->where('tenant_id', $tenantId) ->find($id); - if (!$model) { + if (! $model) { throw new NotFoundHttpException(__('error.not_found')); } @@ -103,7 +104,7 @@ public function delete(int $id): void ->where('tenant_id', $tenantId) ->find($id); - if (!$model) { + if (! $model) { throw new NotFoundHttpException(__('error.not_found')); } diff --git a/app/Services/Design/ModelVersionService.php b/app/Services/Design/ModelVersionService.php index 363598f..e306903 100644 --- a/app/Services/Design/ModelVersionService.php +++ b/app/Services/Design/ModelVersionService.php @@ -20,7 +20,7 @@ public function listByModel(int $modelId) ->where('tenant_id', $tenantId) ->find($modelId); - if (!$model) { + if (! $model) { throw new NotFoundHttpException(__('error.not_found')); } @@ -40,7 +40,7 @@ public function createDraft(int $modelId, array $extra = []): ModelVersion ->where('tenant_id', $tenantId) ->find($modelId); - if (!$model) { + if (! $model) { throw new NotFoundHttpException(__('error.not_found')); } @@ -51,7 +51,7 @@ public function createDraft(int $modelId, array $extra = []): ModelVersion $max = ModelVersion::query() ->where('model_id', $model->id) ->max('version_no'); - $versionNo = (int)($max ?? 0) + 1; + $versionNo = (int) ($max ?? 0) + 1; } else { $exists = ModelVersion::query() ->where('model_id', $model->id) @@ -63,14 +63,14 @@ public function createDraft(int $modelId, array $extra = []): ModelVersion } return ModelVersion::create([ - 'tenant_id' => $tenantId, - 'model_id' => $model->id, - 'version_no' => $versionNo, - 'status' => 'DRAFT', - 'is_active' => true, - 'notes' => $extra['notes'] ?? null, + 'tenant_id' => $tenantId, + 'model_id' => $model->id, + 'version_no' => $versionNo, + 'status' => 'DRAFT', + 'is_active' => true, + 'notes' => $extra['notes'] ?? null, 'effective_from' => $extra['effective_from'] ?? null, - 'effective_to' => $extra['effective_to'] ?? null, + 'effective_to' => $extra['effective_to'] ?? null, ]); }); } @@ -84,7 +84,7 @@ public function release(int $versionId): ModelVersion ->where('tenant_id', $tenantId) ->find($versionId); - if (!$mv) { + if (! $mv) { throw new NotFoundHttpException(__('error.not_found')); } diff --git a/app/Services/EstimateService.php b/app/Services/EstimateService.php index 7921802..0c2dd19 100644 --- a/app/Services/EstimateService.php +++ b/app/Services/EstimateService.php @@ -1,4 +1,5 @@ getClientMimeType(); $fileSize = $file->getSize(); - //$tempFile = $file->getPathname(); + // $tempFile = $file->getPathname(); - $folder = config('custom.data_path') . substr($t_id, 0, 6) . "/"; - $targetPath = $folder . $randomName; + $folder = config('custom.data_path').substr($t_id, 0, 6).'/'; + $targetPath = $folder.$randomName; - if (!is_dir($folder)) { + if (! is_dir($folder)) { mkdir($folder, 0755, true); } try { - //move_uploaded_file($tempFile, $targetPath); + // move_uploaded_file($tempFile, $targetPath); $file->move($folder, $randomName); chmod($targetPath, 0644); @@ -58,17 +58,17 @@ public static function uploadFiles($request) $originalName = $file->getClientOriginalName(); // 예: "파일명.jpg" $ext = pathinfo($originalName, PATHINFO_EXTENSION); // 확장자만 추출: "jpg" $userInputName = $request['fileName'][$key] ?? null; - $fileName = $userInputName ? $userInputName . '.' . $ext : $originalName; + $fileName = $userInputName ? $userInputName.'.'.$ext : $originalName; $randomName = bin2hex(random_bytes(16)); $fileType = $file->getClientMimeType(); $fileSize = $file->getSize(); $t_id_type = $request['fileType'][$key] ?? '00'; - $folder = config('custom.data_path') . substr($t_id, 0, 6) . "/"; - $targetPath = $folder . $randomName; + $folder = config('custom.data_path').substr($t_id, 0, 6).'/'; + $targetPath = $folder.$randomName; - if (!is_dir($folder)) { + if (! is_dir($folder)) { mkdir($folder, 0755, true); } @@ -76,7 +76,7 @@ public static function uploadFiles($request) $file->move($folder, $randomName); chmod($targetPath, 0644); - if (!empty($file_no)) { + if (! empty($file_no)) { self::deleteFiles(['f_id' => $file_no]); } // 신규 파일 등록 @@ -95,12 +95,12 @@ public static function uploadFiles($request) } } - }else if($file_no) { + } elseif ($file_no) { // 기존파일인데 업로드 파일이 없을경우 이름과 타입만 변경 if ($request['fileName'][1]) { $ext = pathinfo(DB::table('SITE_FILES')->where('F_NO', $file_no)->value('F_NAME'), PATHINFO_EXTENSION); - $data['F_NAME'] = $request['fileName'][1] . '.' . $ext; + $data['F_NAME'] = $request['fileName'][1].'.'.$ext; } if ($request['fileType'][1]) { @@ -109,6 +109,7 @@ public static function uploadFiles($request) DB::table('SITE_FILES')->where('F_NO', $file_no)->update($data); } + return self::getFiles($request); } @@ -128,7 +129,7 @@ public static function getFiles(array $params): array } if ($idx) { $query->whereIn('SF.T_ID', explode(',', $idx)); - }else if ($com_no) { + } elseif ($com_no) { $query->where('SF.T_ID', $com_no); } if ($tType) { @@ -148,7 +149,7 @@ public static function deleteFiles(array $params): string if ($table && $t_id) { $query->where('TABLE', $table)->where('T_ID', $t_id); - } else if ($f_id) { + } elseif ($f_id) { $query->whereIn('F_NO', explode(',', $f_id)); } else { return 'Error'; @@ -160,7 +161,7 @@ public static function deleteFiles(array $params): string return 'Success'; } foreach ($files as $file) { - $filePath = config('custom.data_path') . substr($file->T_ID, 0, 6) . "/" . $file->R_NAME; + $filePath = config('custom.data_path').substr($file->T_ID, 0, 6).'/'.$file->R_NAME; if (file_exists($filePath)) { unlink($filePath); } @@ -178,10 +179,12 @@ public static function findFile(array $params): ?array $query = DB::table('SITE_FILES as SF') ->select('F_NAME', 'T_ID', 'F_TYPE', DB::raw("IFNULL((SELECT COM_NAME FROM COMPANY_INFO CI WHERE SF.T_ID = CI.COM_NO AND SF.TABLE = 'COMPANY_INFO' LIMIT 1), '') as COM_NAME")); - if($F_NO) $query->where('F_NO', $F_NO); - else $query->where('R_NAME', $fileName)->first(); + if ($F_NO) { + $query->where('F_NO', $F_NO); + } else { + $query->where('R_NAME', $fileName)->first(); + } return $query; } } - diff --git a/app/Services/MaterialService.php b/app/Services/MaterialService.php index aef2309..1e98463 100644 --- a/app/Services/MaterialService.php +++ b/app/Services/MaterialService.php @@ -2,8 +2,8 @@ namespace App\Services; -use Illuminate\Support\Facades\Validator; use App\Models\Materials\Material; +use Illuminate\Support\Facades\Validator; class MaterialService extends Service { @@ -14,6 +14,7 @@ protected function v(array $input, array $rules) if ($v->fails()) { return ['error' => $v->errors()->first(), 'code' => 422]; } + return $v->validated(); } @@ -23,21 +24,23 @@ public function getMaterials(array $params) $tenantId = $this->tenantId(); $p = $this->v($params, [ - 'q' => 'nullable|string|max:100', + 'q' => 'nullable|string|max:100', 'category' => 'nullable|integer|min:1', - 'page' => 'nullable|integer|min:1', + 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); - if (isset($p['error'])) return $p; + if (isset($p['error'])) { + return $p; + } $q = Material::query() ->where('tenant_id', $tenantId); // SoftDeletes가 있으면 기본적으로 deleted_at IS NULL - if (!empty($p['category'])) { - $q->where('category_id', (int)$p['category']); + if (! empty($p['category'])) { + $q->where('category_id', (int) $p['category']); } - if (!empty($p['q'])) { - $kw = '%' . $p['q'] . '%'; + if (! empty($p['q'])) { + $kw = '%'.$p['q'].'%'; $q->where(function ($w) use ($kw) { $w->where('item_name', 'like', $kw) ->orWhere('name', 'like', $kw) @@ -49,7 +52,7 @@ public function getMaterials(array $params) $q->orderBy('id'); $perPage = $p['per_page'] ?? 20; - $page = $p['page'] ?? null; + $page = $p['page'] ?? null; return $q->paginate($perPage, ['*'], 'page', $page); } @@ -64,11 +67,13 @@ public function getMaterial(int $id) ->where('tenant_id', $tenantId) ->find($id); - if (!$row) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + if (! $row) { + return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + } // 모델에서 casts가 없을 수 있으니 안전하게 배열화 $row->attributes = is_array($row->attributes) ? $row->attributes : ($row->attributes ? json_decode($row->attributes, true) : null); - $row->options = is_array($row->options) ? $row->options : ($row->options ? json_decode($row->options, true) : null); + $row->options = is_array($row->options) ? $row->options : ($row->options ? json_decode($row->options, true) : null); return $row; } @@ -77,44 +82,46 @@ public function getMaterial(int $id) public function setMaterial(array $params) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $p = $this->v($params, [ - 'category_id' => 'nullable|integer|min:1', - 'name' => 'required|string|max:100', - 'unit' => 'required|string|max:10', + 'category_id' => 'nullable|integer|min:1', + 'name' => 'required|string|max:100', + 'unit' => 'required|string|max:10', 'is_inspection' => 'nullable|in:Y,N', - 'search_tag' => 'nullable|string', - 'remarks' => 'nullable|string', - 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map - 'options' => 'nullable|array', // [{label,value,unit}] 또는 map + 'search_tag' => 'nullable|string', + 'remarks' => 'nullable|string', + 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map + 'options' => 'nullable|array', // [{label,value,unit}] 또는 map 'material_code' => 'nullable|string|max:50', 'specification' => 'nullable|string|max:100', ]); - if (isset($p['error'])) return $p; + if (isset($p['error'])) { + return $p; + } // 기존 normalizeAttributes 사용(그대로), options는 새 normalizeOptions 사용 $attributes = $this->normalizeAttributes($p['attributes'] ?? null); - $options = $this->normalizeOptions($p['options'] ?? null); + $options = $this->normalizeOptions($p['options'] ?? null); $itemName = $this->buildItemName($p['name'], $attributes); $specText = $p['specification'] ?? $this->buildSpecText($attributes); - $m = new Material(); - $m->tenant_id = $tenantId; - $m->category_id = $p['category_id'] ?? null; - $m->name = $p['name']; - $m->item_name = $itemName; + $m = new Material; + $m->tenant_id = $tenantId; + $m->category_id = $p['category_id'] ?? null; + $m->name = $p['name']; + $m->item_name = $itemName; $m->specification = $specText; $m->material_code = $p['material_code'] ?? null; - $m->unit = $p['unit']; + $m->unit = $p['unit']; $m->is_inspection = $p['is_inspection'] ?? 'N'; - $m->search_tag = $p['search_tag'] ?? null; - $m->remarks = $p['remarks'] ?? null; - $m->attributes = $attributes ?? null; - $m->options = $options ?? null; - $m->created_by = $userId ?? 0; - $m->updated_by = $userId ?? null; + $m->search_tag = $p['search_tag'] ?? null; + $m->remarks = $p['remarks'] ?? null; + $m->attributes = $attributes ?? null; + $m->options = $options ?? null; + $m->created_by = $userId ?? 0; + $m->updated_by = $userId ?? null; $m->save(); return $this->getMaterial($m->id); @@ -124,55 +131,63 @@ public function setMaterial(array $params) public function updateMaterial(int $id, array $params = []) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); /** @var Material|null $exists */ $exists = Material::query()->where('tenant_id', $tenantId)->find($id); - if (!$exists) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + if (! $exists) { + return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + } $p = $this->v($params, [ - 'category_id' => 'nullable|integer|min:1', - 'name' => 'nullable|string|max:100', - 'unit' => 'nullable|string|max:10', + 'category_id' => 'nullable|integer|min:1', + 'name' => 'nullable|string|max:100', + 'unit' => 'nullable|string|max:10', 'is_inspection' => 'nullable|in:Y,N', - 'search_tag' => 'nullable|string', - 'remarks' => 'nullable|string', - 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map - 'options' => 'nullable|array', // [{label,value,unit}] 또는 map + 'search_tag' => 'nullable|string', + 'remarks' => 'nullable|string', + 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map + 'options' => 'nullable|array', // [{label,value,unit}] 또는 map 'material_code' => 'nullable|string|max:50', 'specification' => 'nullable|string|max:100', ]); - if (isset($p['error'])) return $p; + if (isset($p['error'])) { + return $p; + } $currentAttrs = is_array($exists->attributes) ? $exists->attributes : ($exists->attributes ? json_decode($exists->attributes, true) : null); - $currentOpts = is_array($exists->options) ? $exists->options + $currentOpts = is_array($exists->options) ? $exists->options : ($exists->options ? json_decode($exists->options, true) : null); // 변경 점만 정규화 $attrs = array_key_exists('attributes', $p) ? $this->normalizeAttributes($p['attributes']) : $currentAttrs; - $opts = array_key_exists('options', $p) + $opts = array_key_exists('options', $p) ? $this->normalizeOptions($p['options']) : $currentOpts; - $baseName = array_key_exists('name', $p) ? ($p['name'] ?? $exists->name) : $exists->name; + $baseName = array_key_exists('name', $p) ? ($p['name'] ?? $exists->name) : $exists->name; - $exists->category_id = $p['category_id'] ?? $exists->category_id; - $exists->name = $baseName; - $exists->item_name = $this->buildItemName($baseName, $attrs); + $exists->category_id = $p['category_id'] ?? $exists->category_id; + $exists->name = $baseName; + $exists->item_name = $this->buildItemName($baseName, $attrs); $exists->specification = array_key_exists('specification', $p) ? ($p['specification'] ?? null) : ($exists->specification ?: $this->buildSpecText($attrs)); $exists->material_code = $p['material_code'] ?? $exists->material_code; - $exists->unit = $p['unit'] ?? $exists->unit; + $exists->unit = $p['unit'] ?? $exists->unit; $exists->is_inspection = $p['is_inspection'] ?? $exists->is_inspection; - $exists->search_tag = $p['search_tag'] ?? $exists->search_tag; - $exists->remarks = $p['remarks'] ?? $exists->remarks; + $exists->search_tag = $p['search_tag'] ?? $exists->search_tag; + $exists->remarks = $p['remarks'] ?? $exists->remarks; - if (array_key_exists('attributes', $p)) $exists->attributes = $attrs; - if (array_key_exists('options', $p)) $exists->options = $opts; + if (array_key_exists('attributes', $p)) { + $exists->attributes = $attrs; + } + if (array_key_exists('options', $p)) { + $exists->options = $opts; + } $exists->updated_by = $userId ?? $exists->updated_by; $exists->save(); @@ -190,7 +205,9 @@ public function destroyMaterial(int $id) ->where('tenant_id', $tenantId) ->find($id); - if (!$row) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + if (! $row) { + return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + } $row->delete(); @@ -208,47 +225,61 @@ public function destroyMaterial(int $id) → specification: "두께 10T, 길이 150CM" ------------------------- */ - private function normalizeAttributes(null|array $attrs): ?array + private function normalizeAttributes(?array $attrs): ?array { - if (!$attrs) return null; + if (! $attrs) { + return null; + } $out = []; foreach ($attrs as $a) { - if (!is_array($a)) continue; - $label = trim((string)($a['label'] ?? '')); - $value = trim((string)($a['value'] ?? '')); - $unit = trim((string)($a['unit'] ?? '')); - if ($label === '' && $value === '' && $unit === '') continue; + if (! is_array($a)) { + continue; + } + $label = trim((string) ($a['label'] ?? '')); + $value = trim((string) ($a['value'] ?? '')); + $unit = trim((string) ($a['unit'] ?? '')); + if ($label === '' && $value === '' && $unit === '') { + continue; + } $out[] = ['label' => $label, 'value' => $value, 'unit' => $unit]; } + return $out ?: null; } private function buildItemName(string $name, ?array $attrs): string { - if (!$attrs || count($attrs) === 0) return $name; + if (! $attrs || count($attrs) === 0) { + return $name; + } $parts = []; foreach ($attrs as $a) { - $value = (string)($a['value'] ?? ''); - $unit = (string)($a['unit'] ?? ''); - $chunk = trim($value . $unit); - if ($chunk !== '') $parts[] = $chunk; + $value = (string) ($a['value'] ?? ''); + $unit = (string) ($a['unit'] ?? ''); + $chunk = trim($value.$unit); + if ($chunk !== '') { + $parts[] = $chunk; + } } - return trim($name . ' ' . implode(' ', $parts)); + + return trim($name.' '.implode(' ', $parts)); } private function buildSpecText(?array $attrs): ?string { - if (!$attrs || count($attrs) === 0) return null; + if (! $attrs || count($attrs) === 0) { + return null; + } $parts = []; foreach ($attrs as $a) { - $label = (string)($a['label'] ?? ''); - $value = (string)($a['value'] ?? ''); - $unit = (string)($a['unit'] ?? ''); - $valueWithUnit = trim($value . $unit); + $label = (string) ($a['label'] ?? ''); + $value = (string) ($a['value'] ?? ''); + $unit = (string) ($a['unit'] ?? ''); + $valueWithUnit = trim($value.$unit); if ($label !== '' && $valueWithUnit !== '') { $parts[] = "{$label} {$valueWithUnit}"; @@ -256,6 +287,7 @@ private function buildSpecText(?array $attrs): ?string $parts[] = $valueWithUnit; } } + return $parts ? implode(', ', $parts) : null; } @@ -264,9 +296,11 @@ private function buildSpecText(?array $attrs): ?string * - 이미 리스트(triple)면 그대로 정규화 * - 맵({"키":"값"})이면 [{label:키, value:값, unit:""}...]로 변환 */ - private function normalizeOptions(null|array $in): ?array + private function normalizeOptions(?array $in): ?array { - if (!$in) return null; + if (! $in) { + return null; + } // 연관 맵 형태인지 간단 판별 $isAssoc = array_keys($in) !== range(0, count($in) - 1); @@ -274,24 +308,32 @@ private function normalizeOptions(null|array $in): ?array if ($isAssoc) { $out = []; foreach ($in as $k => $v) { - $label = trim((string)$k); - $value = is_scalar($v) ? trim((string)$v) : json_encode($v, JSON_UNESCAPED_UNICODE); - if ($label === '' && $value === '') continue; + $label = trim((string) $k); + $value = is_scalar($v) ? trim((string) $v) : json_encode($v, JSON_UNESCAPED_UNICODE); + if ($label === '' && $value === '') { + continue; + } $out[] = ['label' => $label, 'value' => $value, 'unit' => '']; } + return $out ?: null; } // 리스트(triple) 정규화 $out = []; foreach ($in as $a) { - if (!is_array($a)) continue; - $label = trim((string)($a['label'] ?? '')); - $value = trim((string)($a['value'] ?? '')); - $unit = trim((string)($a['unit'] ?? '')); - if ($label === '' && $value === '' && $unit === '') continue; + if (! is_array($a)) { + continue; + } + $label = trim((string) ($a['label'] ?? '')); + $value = trim((string) ($a['value'] ?? '')); + $unit = trim((string) ($a['unit'] ?? '')); + if ($label === '' && $value === '' && $unit === '') { + continue; + } $out[] = ['label' => $label, 'value' => $value, 'unit' => $unit]; } + return $out ?: null; } } diff --git a/app/Services/MemberService.php b/app/Services/MemberService.php index 7b6a1b2..7443972 100644 --- a/app/Services/MemberService.php +++ b/app/Services/MemberService.php @@ -2,14 +2,13 @@ namespace App\Services; -use Illuminate\Support\Facades\Hash; use App\Models\Members\User; -use App\Models\Tenants\Tenant; use App\Models\Members\UserTenant; +use App\Models\Tenants\Tenant; +use Illuminate\Support\Facades\Hash; class MemberService { - /** * 회원 조회(리스트) */ @@ -19,7 +18,7 @@ public static function getMembers($request) $pageNo = $request->page ?? 1; $pageSize = $request->size ?? 10; - $query = User::whereHas('userTenants', function($q) { + $query = User::whereHas('userTenants', function ($q) { $q->active(); })->debug(); $query = $query->paginate($pageSize, ['*'], 'page', $pageNo); @@ -28,19 +27,18 @@ public static function getMembers($request) } - /** * 단일 회원 조회 */ public static function getMember(int $userNo) { - $query = User::whereHas('userTenants', function($q) { + $query = User::whereHas('userTenants', function ($q) { $q->active(); })->where('id', $userNo); + return $query->first(); } - /** * 내정보 확인 */ @@ -51,7 +49,7 @@ public static function getMyInfo() $data['user'] = $user; $tenantId = app('tenant_id'); - if($tenantId){ + if ($tenantId) { $tenant = Tenant::find($tenantId); $data['tenant'] = $tenant; } @@ -67,10 +65,9 @@ public static function getMyUpdate($request) $apiUser = app('api_user'); - // 요청으로 받은 수정 데이터 유효성 검사 $validatedData = $request->validate([ - 'name' => 'sometimes|string|max:255', + 'name' => 'sometimes|string|max:255', 'phone' => 'sometimes|string|max:20', 'email' => 'sometimes|email|max:100', 'options' => 'nullable|json', @@ -79,7 +76,7 @@ public static function getMyUpdate($request) $user = User::find($apiUser); - if (!$user) { + if (! $user) { return ['error' => 'User not found.', 'code' => 404]; } @@ -100,7 +97,7 @@ public static function setMyPassword($request) // 유효성 검사 (확인 비밀번호는 선택) $validated = $request->validate([ 'current_password' => 'required|string', - 'new_password' => 'required|string|min:8|max:64', + 'new_password' => 'required|string|min:8|max:64', ]); // 선택적으로 확인 비밀번호가 온 경우 체크 @@ -111,12 +108,12 @@ public static function setMyPassword($request) // 유저 조회 $user = User::find($apiUserId); - if (!$user) { + if (! $user) { return ['error' => '유저를 찾을 수 없음', 'code' => 404]; } // 현재 비밀번호 확인 - if (!Hash::check($validated['current_password'], $user->password)) { + if (! Hash::check($validated['current_password'], $user->password)) { return ['error' => '현재 비밀번호가 일치하지 않습니다.', 'code' => 400]; } @@ -148,13 +145,12 @@ public static function getMyTenants() 'tenants.id', 'tenants.company_name', 'user_tenants.is_active', - 'user_tenants.is_default' + 'user_tenants.is_default', ]); return $data; } - /** * 나의 테넌트 전환 */ @@ -175,7 +171,7 @@ public static function switchMyTenant(int $tenantId) ->where('tenant_id', $tenantId) ->update(['is_default' => 1]); - if (!$updated) { + if (! $updated) { return ['error' => '해당 테넌트를 찾을 수 없습니다.', 'code' => 404]; } diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php index 23de1b5..66ee0f8 100644 --- a/app/Services/MenuService.php +++ b/app/Services/MenuService.php @@ -17,6 +17,7 @@ protected static function tenantId(): ?int protected static function actorId(): ?int { $user = app('api_user'); // 컨테이너에 주입된 인증 사용자(객체 or 배열) + return is_object($user) ? ($user->id ?? null) : ($user['id'] ?? null); } @@ -29,9 +30,15 @@ public static function index(array $params) $q = Menu::query()->withShared($tenantId); - if (array_key_exists('parent_id', $params)) $q->where('parent_id', $params['parent_id']); - if (array_key_exists('is_active', $params)) $q->where('is_active', (int)$params['is_active']); - if (array_key_exists('hidden', $params)) $q->where('hidden', (int)$params['hidden']); + if (array_key_exists('parent_id', $params)) { + $q->where('parent_id', $params['parent_id']); + } + if (array_key_exists('is_active', $params)) { + $q->where('is_active', (int) $params['is_active']); + } + if (array_key_exists('hidden', $params)) { + $q->where('hidden', (int) $params['hidden']); + } $q->orderBy('parent_id')->orderBy('sort_order'); @@ -44,10 +51,10 @@ public static function index(array $params) */ public static function show(array $params) { - $id = (int)($params['id'] ?? 0); + $id = (int) ($params['id'] ?? 0); $tenantId = self::tenantId(); - if (!$id) { + if (! $id) { return ['error' => 'id가 필요합니다.', 'code' => 400]; } @@ -55,6 +62,7 @@ public static function show(array $params) if (empty($res['data'])) { return ['error' => 'Menu not found', 'code' => 404]; } + return $res; } @@ -64,18 +72,18 @@ public static function show(array $params) public static function store(array $params) { $tenantId = self::tenantId(); - $userId = self::actorId(); + $userId = self::actorId(); $v = Validator::make($params, [ - 'parent_id' => ['nullable','integer'], - 'name' => ['required','string','max:100'], - 'url' => ['nullable','string','max:255'], - 'is_active' => ['nullable','boolean'], - 'sort_order' => ['nullable','integer'], - 'hidden' => ['nullable','boolean'], - 'is_external' => ['nullable','boolean'], - 'external_url' => ['nullable','string','max:255'], - 'icon' => ['nullable','string','max:50'], + 'parent_id' => ['nullable', 'integer'], + 'name' => ['required', 'string', 'max:100'], + 'url' => ['nullable', 'string', 'max:255'], + 'is_active' => ['nullable', 'boolean'], + 'sort_order' => ['nullable', 'integer'], + 'hidden' => ['nullable', 'boolean'], + 'is_external' => ['nullable', 'boolean'], + 'external_url' => ['nullable', 'string', 'max:255'], + 'icon' => ['nullable', 'string', 'max:50'], ]); if ($v->fails()) { @@ -83,19 +91,19 @@ public static function store(array $params) } $data = $v->validated(); - $menu = new Menu(); - $menu->tenant_id = $tenantId; - $menu->parent_id = $data['parent_id'] ?? null; - $menu->name = $data['name']; - $menu->url = $data['url'] ?? null; - $menu->is_active = (int)($data['is_active'] ?? 1); - $menu->sort_order = (int)($data['sort_order'] ?? 0); - $menu->hidden = (int)($data['hidden'] ?? 0); - $menu->is_external = (int)($data['is_external'] ?? 0); + $menu = new Menu; + $menu->tenant_id = $tenantId; + $menu->parent_id = $data['parent_id'] ?? null; + $menu->name = $data['name']; + $menu->url = $data['url'] ?? null; + $menu->is_active = (int) ($data['is_active'] ?? 1); + $menu->sort_order = (int) ($data['sort_order'] ?? 0); + $menu->hidden = (int) ($data['hidden'] ?? 0); + $menu->is_external = (int) ($data['is_external'] ?? 0); $menu->external_url = $data['external_url'] ?? null; - $menu->icon = $data['icon'] ?? null; - $menu->created_by = $userId; - $menu->updated_by = $userId; + $menu->icon = $data['icon'] ?? null; + $menu->created_by = $userId; + $menu->updated_by = $userId; $menu->save(); // 생성 결과를 그대로 전달 @@ -107,24 +115,24 @@ public static function store(array $params) */ public static function update(array $params) { - $id = (int)($params['id'] ?? 0); + $id = (int) ($params['id'] ?? 0); $tenantId = self::tenantId(); - $userId = self::actorId(); + $userId = self::actorId(); - if (!$id) { + if (! $id) { return ['error' => 'id가 필요합니다.', 'code' => 400]; } $v = Validator::make($params, [ - 'parent_id' => ['nullable','integer'], - 'name' => ['nullable','string','max:100'], - 'url' => ['nullable','string','max:255'], - 'is_active' => ['nullable','boolean'], - 'sort_order' => ['nullable','integer'], - 'hidden' => ['nullable','boolean'], - 'is_external' => ['nullable','boolean'], - 'external_url' => ['nullable','string','max:255'], - 'icon' => ['nullable','string','max:50'], + 'parent_id' => ['nullable', 'integer'], + 'name' => ['nullable', 'string', 'max:100'], + 'url' => ['nullable', 'string', 'max:255'], + 'is_active' => ['nullable', 'boolean'], + 'sort_order' => ['nullable', 'integer'], + 'hidden' => ['nullable', 'boolean'], + 'is_external' => ['nullable', 'boolean'], + 'external_url' => ['nullable', 'string', 'max:255'], + 'icon' => ['nullable', 'string', 'max:50'], ]); if ($v->fails()) { @@ -133,14 +141,14 @@ public static function update(array $params) $data = $v->validated(); $menu = Menu::withShared($tenantId)->where('id', $id)->first(); - if (!$menu) { + if (! $menu) { return ['error' => 'Menu not found', 'code' => 404]; } $update = Arr::only($data, [ - 'parent_id','name','url','is_active','sort_order','hidden','is_external','external_url','icon' + 'parent_id', 'name', 'url', 'is_active', 'sort_order', 'hidden', 'is_external', 'external_url', 'icon', ]); - $update = array_filter($update, fn($v) => !is_null($v)); + $update = array_filter($update, fn ($v) => ! is_null($v)); if (empty($update)) { return ['error' => '수정할 데이터가 없습니다.', 'code' => 400]; @@ -157,16 +165,16 @@ public static function update(array $params) */ public static function destroy(array $params) { - $id = (int)($params['id'] ?? 0); + $id = (int) ($params['id'] ?? 0); $tenantId = self::tenantId(); - $userId = self::actorId(); + $userId = self::actorId(); - if (!$id) { + if (! $id) { return ['error' => 'id가 필요합니다.', 'code' => 400]; } $menu = Menu::withShared($tenantId)->where('id', $id)->first(); - if (!$menu) { + if (! $menu) { return ['error' => 'Menu not found', 'code' => 404]; } @@ -183,18 +191,20 @@ public static function destroy(array $params) */ public static function reorder(array $params) { - if (!is_array($params) || empty($params)) { + if (! is_array($params) || empty($params)) { return ['error' => '유효한 정렬 목록이 필요합니다.', 'code' => 422]; } $tenantId = self::tenantId(); DB::transaction(function () use ($params, $tenantId) { foreach ($params as $it) { - if (!isset($it['id'], $it['sort_order'])) continue; + if (! isset($it['id'], $it['sort_order'])) { + continue; + } - $menu = Menu::withShared($tenantId)->find((int)$it['id']); + $menu = Menu::withShared($tenantId)->find((int) $it['id']); if ($menu) { - $menu->sort_order = (int)$it['sort_order']; + $menu->sort_order = (int) $it['sort_order']; $menu->save(); } } @@ -208,26 +218,26 @@ public static function reorder(array $params) */ public static function toggle(array $params) { - $id = (int)($params['id'] ?? 0); + $id = (int) ($params['id'] ?? 0); $tenantId = self::tenantId(); - $userId = self::actorId(); + $userId = self::actorId(); - if (!$id) { + if (! $id) { return ['error' => 'id가 필요합니다.', 'code' => 400]; } $payload = array_filter([ - 'is_active' => array_key_exists('is_active', $params) ? (int)$params['is_active'] : null, - 'hidden' => array_key_exists('hidden', $params) ? (int)$params['hidden'] : null, - 'is_external' => array_key_exists('is_external', $params) ? (int)$params['is_external'] : null, - ], fn($v) => !is_null($v)); + 'is_active' => array_key_exists('is_active', $params) ? (int) $params['is_active'] : null, + 'hidden' => array_key_exists('hidden', $params) ? (int) $params['hidden'] : null, + 'is_external' => array_key_exists('is_external', $params) ? (int) $params['is_external'] : null, + ], fn ($v) => ! is_null($v)); if (empty($payload)) { return ['error' => '변경할 필드가 없습니다.', 'code' => 422]; } $menu = Menu::withShared($tenantId)->find($id); - if (!$menu) { + if (! $menu) { return ['error' => 'Menu not found', 'code' => 404]; } diff --git a/app/Services/ModelSet/ModelSetService.php b/app/Services/ModelSet/ModelSetService.php index 0df9d3d..3e20359 100644 --- a/app/Services/ModelSet/ModelSetService.php +++ b/app/Services/ModelSet/ModelSetService.php @@ -4,12 +4,12 @@ use App\Models\Commons\Category; use App\Models\Commons\CategoryField; +use App\Models\Design\BomTemplate; use App\Models\Design\Model; use App\Models\Design\ModelVersion; -use App\Models\Design\BomTemplate; use App\Models\Products\Product; -use App\Services\Service; use App\Services\Calculation\CalculationEngine; +use App\Services\Service; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -33,11 +33,11 @@ public function getModelSets(array $filters = []): Collection ->where('code_group', 'estimate') ->where('level', '>=', 2); // 루트 카테고리 제외 - if (!empty($filters['category_type'])) { + if (! empty($filters['category_type'])) { $query->where('code', $filters['category_type']); } - if (!empty($filters['is_active'])) { + if (! empty($filters['is_active'])) { $query->where('is_active', $filters['is_active']); } @@ -92,7 +92,7 @@ public function createModelSet(array $data): array ]); // 2. 동적 필드 생성 - if (!empty($data['fields'])) { + if (! empty($data['fields'])) { foreach ($data['fields'] as $fieldData) { CategoryField::create([ 'tenant_id' => $this->tenantId(), @@ -111,7 +111,7 @@ public function createModelSet(array $data): array } // 3. 모델 및 BOM 템플릿 생성 (선택사항) - if (!empty($data['create_model'])) { + if (! empty($data['create_model'])) { $this->createDefaultModel($category, $data['model_data'] ?? []); } @@ -278,7 +278,7 @@ public function getEstimateParameters($categoryId, array $filters = []): array ->filter(function ($field) { return in_array($field->field_key, [ 'open_width', 'open_height', 'quantity', - 'model_name', 'guide_rail_type', 'shutter_box' + 'model_name', 'guide_rail_type', 'shutter_box', ]); }) ->map(function ($field) { @@ -298,7 +298,7 @@ public function getEstimateParameters($categoryId, array $filters = []): array ->filter(function ($field) { return in_array($field->field_key, [ 'make_width', 'make_height', 'calculated_weight', - 'calculated_area', 'motor_bracket_size', 'motor_capacity' + 'calculated_area', 'motor_bracket_size', 'motor_capacity', ]); }) ->map(function ($field) { @@ -333,7 +333,7 @@ public function calculateModelSetBom($categoryId, array $parameters): array // BOM 템플릿 찾기 (기본 템플릿 사용) $bomTemplate = $this->findDefaultBomTemplate($categoryId, $parameters); - if (!$bomTemplate) { + if (! $bomTemplate) { throw new \Exception(__('error.bom_template.not_found')); } @@ -355,7 +355,7 @@ protected function getRelatedModels($categoryId): Collection return Model::with(['versions.bomTemplates']) ->where('tenant_id', $this->tenantId()) - ->where('code', 'like', $category->code . '%') + ->where('code', 'like', $category->code.'%') ->get() ->map(function ($model) { return [ @@ -400,8 +400,8 @@ protected function createDefaultModel(Category $category, array $modelData): voi { $model = Model::create([ 'tenant_id' => $this->tenantId(), - 'code' => $modelData['code'] ?? $category->code . '_MODEL', - 'name' => $modelData['name'] ?? $category->name . ' 기본 모델', + 'code' => $modelData['code'] ?? $category->code.'_MODEL', + 'name' => $modelData['name'] ?? $category->name.' 기본 모델', 'description' => $modelData['description'] ?? '', 'status' => 'DRAFT', 'created_by' => $this->apiUserId(), @@ -418,7 +418,7 @@ protected function createDefaultModel(Category $category, array $modelData): voi BomTemplate::create([ 'tenant_id' => $this->tenantId(), 'model_version_id' => $version->id, - 'name' => $category->name . ' 기본 BOM', + 'name' => $category->name.' 기본 BOM', 'company_type' => $this->getCompanyName($category), 'formula_version' => 'v1.0', 'calculation_schema' => $this->getDefaultCalculationSchema($category), @@ -485,4 +485,4 @@ protected function getDefaultCalculationSchema(Category $category): array 'formulas' => $this->getCalculationSchema($category->code), ]; } -} \ No newline at end of file +} diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 94371f4..835a4c4 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -2,10 +2,10 @@ namespace App\Services; -use Illuminate\Support\Facades\DB; -use Spatie\Permission\Models\Role as SpatieRole; use App\Models\Members\User; use App\Models\Tenants\Department; +use Illuminate\Support\Facades\DB; +use Spatie\Permission\Models\Role as SpatieRole; class PermissionService extends Service { @@ -20,29 +20,29 @@ public function getMenuMatrix(int $id, string $scope, array $params = []): array // 1) 대상 유효성 & model_type 결정 $modelType = $this->resolveModelType($id, $scope); - if (!$modelType) { - return ['success'=>false,'message'=>'대상 리소스를 찾을 수 없습니다.','data'=>null]; + if (! $modelType) { + return ['success' => false, 'message' => '대상 리소스를 찾을 수 없습니다.', 'data' => null]; } // 2) 메뉴 목록 $menus = DB::table('menus') - ->select('id','parent_id','name','url','sort_order') + ->select('id', 'parent_id', 'name', 'url', 'sort_order') ->orderBy('sort_order')->orderBy('id') ->get(); // 3) 권한 정의 (permissions.name = "menu:{menuId}.{action}") $perms = DB::table('permissions') - ->select('id','name','guard_name') + ->select('id', 'name', 'guard_name') ->where('guard_name', 'api') - ->where('name','like','menu:%') + ->where('name', 'like', 'menu:%') ->get(); $permMap = []; // [menuId][action] => ['id','guard','code'] foreach ($perms as $p) { if (preg_match('/^menu:(\d+)\.([a-z_]+)$/', $p->name, $m)) { - $permMap[(int)$m[1]][$m[2]] = [ - 'id' => (int)$p->id, - 'guard'=> $p->guard_name, + $permMap[(int) $m[1]][$m[2]] = [ + 'id' => (int) $p->id, + 'guard' => $p->guard_name, 'code' => $p->name, ]; } @@ -65,30 +65,30 @@ public function getMenuMatrix(int $id, string $scope, array $params = []): array $denySet = array_fill_keys($denies, true); // 5) 트리 + 액션 상태 구성 - $actions = ['view','create','update','delete','approve']; + $actions = ['view', 'create', 'update', 'delete', 'approve']; $byId = []; foreach ($menus as $m) { $node = [ - 'menu_id' => (int)$m->id, - 'parent_id' => $m->parent_id ? (int)$m->parent_id : null, - 'name' => $m->name, - 'url' => $m->url, - 'type' => 'system', - 'children' => [], - 'actions' => [], + 'menu_id' => (int) $m->id, + 'parent_id' => $m->parent_id ? (int) $m->parent_id : null, + 'name' => $m->name, + 'url' => $m->url, + 'type' => 'system', + 'children' => [], + 'actions' => [], ]; foreach ($actions as $a) { $perm = $permMap[$m->id][$a] ?? null; if ($perm) { - $pid = $perm['id']; + $pid = $perm['id']; $state = isset($denySet[$pid]) ? 'deny' : (isset($allowSet[$pid]) ? 'allow' : 'none'); $node['actions'][$a] = [ - 'permission_id' => $pid, + 'permission_id' => $pid, 'permission_code' => $perm['code'], - 'guard_name' => $perm['guard'], - 'state' => $state, - 'is_allowed' => $state === 'allow' ? 1 : 0, + 'guard_name' => $perm['guard'], + 'state' => $state, + 'is_allowed' => $state === 'allow' ? 1 : 0, ]; } else { $node['actions'][$a] = null; @@ -110,10 +110,10 @@ public function getMenuMatrix(int $id, string $scope, array $params = []): array return [ 'success' => true, - 'message' => $this->titleByScope($scope) . ' 성공', - 'data' => [ + 'message' => $this->titleByScope($scope).' 성공', + 'data' => [ 'actions' => $actions, - 'tree' => $roots, + 'tree' => $roots, ], ]; } @@ -123,9 +123,9 @@ private function resolveModelType(int $id, string $scope): ?string { return match ($scope) { 'department' => Department::query()->find($id) ? Department::class : null, - 'role' => SpatieRole::query()->find($id) ? SpatieRole::class : null, - 'user' => User::query()->find($id) ? User::class : null, - default => null, + 'role' => SpatieRole::query()->find($id) ? SpatieRole::class : null, + 'user' => User::query()->find($id) ? User::class : null, + default => null, }; } @@ -133,9 +133,9 @@ private function titleByScope(string $scope): string { return match ($scope) { 'department' => '부서 메뉴 권한 매트릭스 조회', - 'role' => '역할 메뉴 권한 매트릭스 조회', - 'user' => '유저 메뉴 권한 매트릭스 조회', - default => '메뉴 권한 매트릭스 조회', + 'role' => '역할 메뉴 권한 매트릭스 조회', + 'user' => '유저 메뉴 권한 매트릭스 조회', + default => '메뉴 권한 매트릭스 조회', }; } } diff --git a/app/Services/ProductBomService.php b/app/Services/ProductBomService.php index 4b7c0a0..954f88f 100644 --- a/app/Services/ProductBomService.php +++ b/app/Services/ProductBomService.php @@ -6,9 +6,9 @@ use App\Models\Products\Product; use App\Models\Products\ProductComponent; use App\Services\Products\ProductComponentResolver; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; -use Illuminate\Support\Arr; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -37,44 +37,46 @@ public function index(int $parentProductId, array $params) ->get(); // 리졸브(제품/자재) - $productIds = $items->where('ref_type', 'PRODUCT')->pluck('child_product_id')->filter()->unique()->values(); + $productIds = $items->where('ref_type', 'PRODUCT')->pluck('child_product_id')->filter()->unique()->values(); $materialIds = $items->where('ref_type', 'MATERIAL')->pluck('material_id')->filter()->unique()->values(); - $products = $productIds->isNotEmpty() - ? Product::query()->where('tenant_id', $tenantId)->whereIn('id', $productIds)->get(['id','code','name','product_type','category_id'])->keyBy('id') + $products = $productIds->isNotEmpty() + ? Product::query()->where('tenant_id', $tenantId)->whereIn('id', $productIds)->get(['id', 'code', 'name', 'product_type', 'category_id'])->keyBy('id') : collect(); $materials = $materialIds->isNotEmpty() - ? Material::query()->where('tenant_id', $tenantId)->whereIn('id', $materialIds)->get(['id','material_code as code','name','unit','category_id'])->keyBy('id') + ? Material::query()->where('tenant_id', $tenantId)->whereIn('id', $materialIds)->get(['id', 'material_code as code', 'name', 'unit', 'category_id'])->keyBy('id') : collect(); return $items->map(function ($row) use ($products, $materials) { $base = [ - 'id' => (int)$row->id, - 'ref_type' => $row->ref_type, - 'quantity' => $row->quantity, - 'sort_order' => (int)$row->sort_order, - 'is_default' => (int)$row->is_default, + 'id' => (int) $row->id, + 'ref_type' => $row->ref_type, + 'quantity' => $row->quantity, + 'sort_order' => (int) $row->sort_order, + 'is_default' => (int) $row->is_default, ]; if ($row->ref_type === 'PRODUCT') { $p = $products->get($row->child_product_id); + return $base + [ - 'ref_id' => (int)$row->child_product_id, - 'code' => $p?->code, - 'name' => $p?->name, - 'product_type' => $p?->product_type, - 'category_id' => $p?->category_id, - ]; + 'ref_id' => (int) $row->child_product_id, + 'code' => $p?->code, + 'name' => $p?->name, + 'product_type' => $p?->product_type, + 'category_id' => $p?->category_id, + ]; } else { // MATERIAL $m = $materials->get($row->material_id); + return $base + [ - 'ref_id' => (int)$row->material_id, - 'code' => $m?->code, - 'name' => $m?->name, - 'unit' => $m?->unit, - 'category_id' => $m?->category_id, - ]; + 'ref_id' => (int) $row->material_id, + 'code' => $m?->code, + 'name' => $m?->name, + 'unit' => $m?->unit, + 'category_id' => $m?->category_id, + ]; } })->values(); } @@ -86,41 +88,44 @@ public function index(int $parentProductId, array $params) public function bulkUpsert(int $parentProductId, array $items): array { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $this->assertProduct($tenantId, $parentProductId); - if (!is_array($items) || empty($items)) { + if (! is_array($items) || empty($items)) { throw new BadRequestHttpException(__('error.empty_items')); } - $created = 0; $updated = 0; + $created = 0; + $updated = 0; DB::transaction(function () use ($tenantId, $userId, $parentProductId, $items, &$created, &$updated) { foreach ($items as $it) { $payload = $this->validateItem($it); // ref 확인 & 자기참조 방지 - $this->assertReference($tenantId, $parentProductId, $payload['ref_type'], (int)$payload['ref_id']); + $this->assertReference($tenantId, $parentProductId, $payload['ref_type'], (int) $payload['ref_id']); - if (!empty($it['id'])) { + if (! empty($it['id'])) { $pc = ProductComponent::query() ->where('tenant_id', $tenantId) ->where('parent_product_id', $parentProductId) - ->find((int)$it['id']); - if (!$pc) throw new BadRequestHttpException(__('error.not_found')); + ->find((int) $it['id']); + if (! $pc) { + throw new BadRequestHttpException(__('error.not_found')); + } // ref 변경 허용 시: 충돌 검사 [$childProductId, $materialId] = $this->splitRef($payload); $pc->update([ - 'ref_type' => $payload['ref_type'], + 'ref_type' => $payload['ref_type'], 'child_product_id' => $childProductId, - 'material_id' => $materialId, - 'quantity' => $payload['quantity'], - 'sort_order' => $payload['sort_order'] ?? $pc->sort_order, - 'is_default' => $payload['is_default'] ?? $pc->is_default, - 'updated_by' => $userId, + 'material_id' => $materialId, + 'quantity' => $payload['quantity'], + 'sort_order' => $payload['sort_order'] ?? $pc->sort_order, + 'is_default' => $payload['is_default'] ?? $pc->is_default, + 'updated_by' => $userId, ]); $updated++; } else { @@ -128,15 +133,15 @@ public function bulkUpsert(int $parentProductId, array $items): array [$childProductId, $materialId] = $this->splitRef($payload); ProductComponent::create([ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'parent_product_id' => $parentProductId, - 'ref_type' => $payload['ref_type'], - 'child_product_id' => $childProductId, - 'material_id' => $materialId, - 'quantity' => $payload['quantity'], - 'sort_order' => $payload['sort_order'] ?? 0, - 'is_default' => $payload['is_default'] ?? 0, - 'created_by' => $userId, + 'ref_type' => $payload['ref_type'], + 'child_product_id' => $childProductId, + 'material_id' => $materialId, + 'quantity' => $payload['quantity'], + 'sort_order' => $payload['sort_order'] ?? 0, + 'is_default' => $payload['is_default'] ?? 0, + 'created_by' => $userId, ]); $created++; } @@ -150,7 +155,7 @@ public function bulkUpsert(int $parentProductId, array $items): array public function update(int $parentProductId, int $itemId, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $this->assertProduct($tenantId, $parentProductId); $pc = ProductComponent::query() @@ -158,12 +163,14 @@ public function update(int $parentProductId, int $itemId, array $data) ->where('parent_product_id', $parentProductId) ->find($itemId); - if (!$pc) throw new BadRequestHttpException(__('error.not_found')); + if (! $pc) { + throw new BadRequestHttpException(__('error.not_found')); + } $v = Validator::make($data, [ - 'ref_type' => 'sometimes|in:PRODUCT,MATERIAL', - 'ref_id' => 'sometimes|integer', - 'quantity' => 'sometimes|numeric|min:0.0001', + 'ref_type' => 'sometimes|in:PRODUCT,MATERIAL', + 'ref_id' => 'sometimes|integer', + 'quantity' => 'sometimes|numeric|min:0.0001', 'sort_order' => 'sometimes|integer|min:0', 'is_default' => 'sometimes|in:0,1', ]); @@ -171,9 +178,9 @@ public function update(int $parentProductId, int $itemId, array $data) if (isset($payload['ref_type']) || isset($payload['ref_id'])) { $refType = $payload['ref_type'] ?? $pc->ref_type; - $refId = isset($payload['ref_id']) - ? (int)$payload['ref_id'] - : ($pc->ref_type === 'PRODUCT' ? (int)$pc->child_product_id : (int)$pc->material_id); + $refId = isset($payload['ref_id']) + ? (int) $payload['ref_id'] + : ($pc->ref_type === 'PRODUCT' ? (int) $pc->child_product_id : (int) $pc->material_id); $this->assertReference($tenantId, $parentProductId, $refType, $refId); [$childProductId, $materialId] = $this->splitRef(['ref_type' => $refType, 'ref_id' => $refId]); @@ -183,9 +190,15 @@ public function update(int $parentProductId, int $itemId, array $data) $pc->material_id = $materialId; } - if (isset($payload['quantity'])) $pc->quantity = $payload['quantity']; - if (isset($payload['sort_order'])) $pc->sort_order = $payload['sort_order']; - if (isset($payload['is_default'])) $pc->is_default = $payload['is_default']; + if (isset($payload['quantity'])) { + $pc->quantity = $payload['quantity']; + } + if (isset($payload['sort_order'])) { + $pc->sort_order = $payload['sort_order']; + } + if (isset($payload['is_default'])) { + $pc->is_default = $payload['is_default']; + } $pc->updated_by = $userId; $pc->save(); @@ -204,7 +217,9 @@ public function destroy(int $parentProductId, int $itemId): void ->where('parent_product_id', $parentProductId) ->find($itemId); - if (!$pc) throw new BadRequestHttpException(__('error.not_found')); + if (! $pc) { + throw new BadRequestHttpException(__('error.not_found')); + } $pc->delete(); } @@ -214,16 +229,20 @@ public function reorder(int $parentProductId, array $items): void $tenantId = $this->tenantId(); $this->assertProduct($tenantId, $parentProductId); - if (!is_array($items)) throw new BadRequestHttpException(__('error.invalid_payload')); + if (! is_array($items)) { + throw new BadRequestHttpException(__('error.invalid_payload')); + } DB::transaction(function () use ($tenantId, $parentProductId, $items) { foreach ($items as $row) { - if (!isset($row['id'], $row['sort_order'])) continue; + if (! isset($row['id'], $row['sort_order'])) { + continue; + } ProductComponent::query() ->where('tenant_id', $tenantId) ->where('parent_product_id', $parentProductId) - ->where('id', (int)$row['id']) - ->update(['sort_order' => (int)$row['sort_order']]); + ->where('id', (int) $row['id']) + ->update(['sort_order' => (int) $row['sort_order']]); } }); } @@ -239,16 +258,16 @@ public function summary(int $parentProductId): array ->where('parent_product_id', $parentProductId) ->get(); - $cnt = $items->count(); - $cntP = $items->where('ref_type','PRODUCT')->count(); - $cntM = $items->where('ref_type','MATERIAL')->count(); - $qtySum = (string)$items->sum('quantity'); + $cnt = $items->count(); + $cntP = $items->where('ref_type', 'PRODUCT')->count(); + $cntM = $items->where('ref_type', 'MATERIAL')->count(); + $qtySum = (string) $items->sum('quantity'); return [ - 'count' => $cnt, + 'count' => $cnt, 'count_product' => $cntP, - 'count_material'=> $cntM, - 'quantity_sum' => $qtySum, + 'count_material' => $cntM, + 'quantity_sum' => $qtySum, ]; } @@ -271,20 +290,20 @@ public function validateBom(int $parentProductId): array if ($row->quantity <= 0) { $errors[] = ['id' => $row->id, 'error' => 'INVALID_QUANTITY']; } - $key = $row->ref_type . ':' . ($row->ref_type === 'PRODUCT' ? $row->child_product_id : $row->material_id); + $key = $row->ref_type.':'.($row->ref_type === 'PRODUCT' ? $row->child_product_id : $row->material_id); if (isset($seen[$key])) { $errors[] = ['id' => $row->id, 'error' => 'DUPLICATE_ITEM']; } else { $seen[$key] = true; } // 자기참조 - if ($row->ref_type === 'PRODUCT' && (int)$row->child_product_id === (int)$parentProductId) { + if ($row->ref_type === 'PRODUCT' && (int) $row->child_product_id === (int) $parentProductId) { $errors[] = ['id' => $row->id, 'error' => 'SELF_REFERENCE']; } } return [ - 'valid' => count($errors) === 0, + 'valid' => count($errors) === 0, 'errors' => $errors, ]; } @@ -294,13 +313,14 @@ public function validateBom(int $parentProductId): array private function validateItem(array $it): array { $v = Validator::make($it, [ - 'id' => 'nullable|integer', - 'ref_type' => 'required|in:PRODUCT,MATERIAL', - 'ref_id' => 'required|integer', - 'quantity' => 'required|numeric|min:0.0001', + 'id' => 'nullable|integer', + 'ref_type' => 'required|in:PRODUCT,MATERIAL', + 'ref_id' => 'required|integer', + 'quantity' => 'required|numeric|min:0.0001', 'sort_order' => 'nullable|integer|min:0', 'is_default' => 'nullable|in:0,1', ]); + return $v->validate(); } @@ -308,15 +328,16 @@ private function splitRef(array $payload): array { // returns [child_product_id, material_id] if ($payload['ref_type'] === 'PRODUCT') { - return [(int)$payload['ref_id'], null]; + return [(int) $payload['ref_id'], null]; } - return [null, (int)$payload['ref_id']]; + + return [null, (int) $payload['ref_id']]; } private function assertProduct(int $tenantId, int $productId): void { $exists = Product::query()->where('tenant_id', $tenantId)->where('id', $productId)->exists(); - if (!$exists) { + if (! $exists) { // ko: 제품 정보를 찾을 수 없습니다. throw new NotFoundHttpException(__('error.not_found_resource', ['resource' => '제품'])); } @@ -329,10 +350,14 @@ private function assertReference(int $tenantId, int $parentProductId, string $re throw new BadRequestHttpException(__('error.invalid_payload')); // 자기참조 방지 } $ok = Product::query()->where('tenant_id', $tenantId)->where('id', $refId)->exists(); - if (!$ok) throw new BadRequestHttpException(__('error.not_found')); + if (! $ok) { + throw new BadRequestHttpException(__('error.not_found')); + } } else { $ok = Material::query()->where('tenant_id', $tenantId)->where('id', $refId)->exists(); - if (!$ok) throw new BadRequestHttpException(__('error.not_found')); + if (! $ok) { + throw new BadRequestHttpException(__('error.not_found')); + } } } @@ -349,22 +374,22 @@ public function replaceBom(int $productId, array $payload): array } $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); // 0) ====== 빈 카테고리 제거 ====== $rawCats = Arr::get($payload, 'categories', []); $normalized = []; foreach ((array) $rawCats as $cat) { - $catId = Arr::get($cat, 'id'); + $catId = Arr::get($cat, 'id'); $catName = Arr::get($cat, 'name'); $items = array_values(array_filter((array) Arr::get($cat, 'items', []), function ($it) { $type = Arr::get($it, 'ref_type'); - $id = (int) Arr::get($it, 'ref_id'); - $qty = Arr::get($it, 'quantity'); + $id = (int) Arr::get($it, 'ref_id'); + $qty = Arr::get($it, 'quantity'); - return in_array($type, ['MATERIAL','PRODUCT'], true) + return in_array($type, ['MATERIAL', 'PRODUCT'], true) && $id > 0 && is_numeric($qty); })); @@ -374,8 +399,8 @@ public function replaceBom(int $productId, array $payload): array } $normalized[] = [ - 'id' => $catId, - 'name' => $catName, + 'id' => $catId, + 'name' => $catName, 'items' => $items, ]; } @@ -387,9 +412,9 @@ public function replaceBom(int $productId, array $payload): array ->delete(); return [ - 'deleted_count' => $deleted, + 'deleted_count' => $deleted, 'inserted_count' => 0, - 'message' => '모든 BOM 항목이 비어 기존 데이터를 삭제했습니다.', + 'message' => '모든 BOM 항목이 비어 기존 데이터를 삭제했습니다.', ]; } @@ -397,14 +422,14 @@ public function replaceBom(int $productId, array $payload): array $v = Validator::make( ['categories' => $normalized], [ - 'categories' => ['required', 'array', 'min:1'], - 'categories.*.id' => ['nullable', 'integer'], - 'categories.*.name' => ['nullable', 'string', 'max:100'], - 'categories.*.items' => ['required', 'array', 'min:1'], - 'categories.*.items.*.ref_type' => ['required', 'in:MATERIAL,PRODUCT'], - 'categories.*.items.*.ref_id' => ['required', 'integer', 'min:1'], - 'categories.*.items.*.quantity' => ['required', 'numeric', 'min:0'], - 'categories.*.items.*.sort_order' => ['nullable', 'integer', 'min:0'], + 'categories' => ['required', 'array', 'min:1'], + 'categories.*.id' => ['nullable', 'integer'], + 'categories.*.name' => ['nullable', 'string', 'max:100'], + 'categories.*.items' => ['required', 'array', 'min:1'], + 'categories.*.items.*.ref_type' => ['required', 'in:MATERIAL,PRODUCT'], + 'categories.*.items.*.ref_id' => ['required', 'integer', 'min:1'], + 'categories.*.items.*.quantity' => ['required', 'numeric', 'min:0'], + 'categories.*.items.*.sort_order' => ['nullable', 'integer', 'min:0'], ] ); if ($v->fails()) { @@ -413,25 +438,25 @@ public function replaceBom(int $productId, array $payload): array // 2) ====== 플랫 레코드 생성 (note 제거) ====== $rows = []; - $now = now(); + $now = now(); foreach ($normalized as $cat) { - $catId = Arr::get($cat, 'id'); + $catId = Arr::get($cat, 'id'); $catName = Arr::get($cat, 'name'); foreach ($cat['items'] as $idx => $item) { $rows[] = [ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'parent_product_id' => $productId, - 'category_id' => $catId, - 'category_name' => $catName, - 'ref_type' => $item['ref_type'], - 'ref_id' => (int) $item['ref_id'], - 'quantity' => (string) $item['quantity'], - 'sort_order' => isset($item['sort_order']) ? (int)$item['sort_order'] : $idx, - 'created_by' => $userId, - 'updated_by' => $userId, - 'created_at' => $now, - 'updated_at' => $now, + 'category_id' => $catId, + 'category_name' => $catName, + 'ref_type' => $item['ref_type'], + 'ref_id' => (int) $item['ref_id'], + 'quantity' => (string) $item['quantity'], + 'sort_order' => isset($item['sort_order']) ? (int) $item['sort_order'] : $idx, + 'created_by' => $userId, + 'updated_by' => $userId, + 'created_at' => $now, + 'updated_at' => $now, ]; } } @@ -444,23 +469,24 @@ public function replaceBom(int $productId, array $payload): array $inserted = 0; foreach (array_chunk($rows, 500) as $chunk) { - $ok =ProductComponent::insert($chunk); + $ok = ProductComponent::insert($chunk); $inserted += $ok ? count($chunk) : 0; } return [ - 'deleted_count' => $deleted, + 'deleted_count' => $deleted, 'inserted_count' => $inserted, - 'message' => 'BOM 저장 성공', + 'message' => 'BOM 저장 성공', ]; }); } - /** 제품별: 현재 BOM에 쓰인 카테고리 */ public function listCategoriesForProduct(int $productId): array { - if ($productId <= 0) throw new BadRequestHttpException(__('error.bad_request')); + if ($productId <= 0) { + throw new BadRequestHttpException(__('error.bad_request')); + } $tenantId = $this->tenantId(); diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index b048928..dafef05 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -2,15 +2,14 @@ namespace App\Services; +use App\Models\Commons\Category; use App\Models\Products\CommonCode; use App\Models\Products\Product; -use App\Models\Commons\Category; use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class ProductService extends Service { - /** * 카테고리 트리 전체 조회 (parent_id = null 기준) */ @@ -33,11 +32,11 @@ protected function fetchCategoryTree(?int $parentId = null) $tenantId = $this->tenantId(); // Base Service에서 상속받은 메서드 $query = Category::query() - ->when($tenantId, fn($q) => $q->where('tenant_id', $tenantId)) + ->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId)) ->when( is_null($parentId), - fn($q) => $q->whereNull('parent_id'), - fn($q) => $q->where('parent_id', $parentId) + fn ($q) => $q->whereNull('parent_id'), + fn ($q) => $q->where('parent_id', $parentId) ) ->where('is_active', 1) ->orderBy('sort_order'); @@ -57,7 +56,8 @@ protected function fetchCategoryTree(?int $parentId = null) */ public static function getCategoryFlat($group = 'category') { - $query = CommonCode::where('code_group',$group)->whereNull('parent_id'); + $query = CommonCode::where('code_group', $group)->whereNull('parent_id'); + return $query->get(); } @@ -66,11 +66,11 @@ public function index(array $params) { $tenantId = $this->tenantId(); - $size = (int)($params['size'] ?? 20); - $q = trim((string)($params['q'] ?? '')); - $categoryId = $params['category_id'] ?? null; + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); + $categoryId = $params['category_id'] ?? null; $productType = $params['product_type'] ?? null; // PRODUCT|PART|SUBASSEMBLY... - $active = $params['active'] ?? null; // 1/0 + $active = $params['active'] ?? null; // 1/0 $query = Product::query() ->with('category:id,name') // 필요한 컬럼만 가져오기 @@ -83,9 +83,15 @@ public function index(array $params) ->orWhere('description', 'like', "%{$q}%"); }); } - if ($categoryId) $query->where('category_id', (int)$categoryId); - if ($productType) $query->where('product_type', $productType); - if ($active !== null && $active !== '') $query->where('is_active', (int)$active); + if ($categoryId) { + $query->where('category_id', (int) $categoryId); + } + if ($productType) { + $query->where('product_type', $productType); + } + if ($active !== null && $active !== '') { + $query->where('is_active', (int) $active); + } $paginator = $query->orderBy('id')->paginate($size); @@ -96,6 +102,7 @@ public function index(array $params) $arr['created_at'] = $item->created_at ? $item->created_at->format('Y-m-d') : null; + return $arr; }) ); @@ -107,19 +114,19 @@ public function index(array $params) public function store(array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $v = Validator::make($data, [ - 'code' => 'required|string|max:30', - 'name' => 'required|string|max:100', - 'category_id' => 'required|integer', - 'product_type' => 'required|string|max:30', - 'attributes' => 'nullable|array', - 'description' => 'nullable|string|max:255', - 'is_sellable' => 'nullable|in:0,1', + 'code' => 'required|string|max:30', + 'name' => 'required|string|max:100', + 'category_id' => 'required|integer', + 'product_type' => 'required|string|max:30', + 'attributes' => 'nullable|array', + 'description' => 'nullable|string|max:255', + 'is_sellable' => 'nullable|in:0,1', 'is_purchasable' => 'nullable|in:0,1', - 'is_producible' => 'nullable|in:0,1', - 'is_active' => 'nullable|in:0,1', + 'is_producible' => 'nullable|in:0,1', + 'is_active' => 'nullable|in:0,1', ]); $payload = $v->validate(); @@ -128,14 +135,16 @@ public function store(array $data) ->where('tenant_id', $tenantId) ->where('code', $payload['code']) ->exists(); - if ($dup) throw new BadRequestHttpException(__('error.duplicate_key')); + if ($dup) { + throw new BadRequestHttpException(__('error.duplicate_key')); + } - $payload['tenant_id'] = $tenantId; - $payload['created_by'] = $userId; - $payload['is_sellable'] = $payload['is_sellable'] ?? 1; + $payload['tenant_id'] = $tenantId; + $payload['created_by'] = $userId; + $payload['is_sellable'] = $payload['is_sellable'] ?? 1; $payload['is_purchasable'] = $payload['is_purchasable'] ?? 0; - $payload['is_producible'] = $payload['is_producible'] ?? 1; - $payload['is_active'] = $payload['is_active'] ?? 1; + $payload['is_producible'] = $payload['is_producible'] ?? 1; + $payload['is_active'] = $payload['is_active'] ?? 1; // attributes array → json 저장 (Eloquent casts가 array면 그대로 가능) return Product::create($payload); @@ -146,7 +155,10 @@ public function show(int $id) { $tenantId = $this->tenantId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); - if (!$p) throw new BadRequestHttpException(__('error.not_found')); + if (! $p) { + throw new BadRequestHttpException(__('error.not_found')); + } + return $p; } @@ -154,22 +166,24 @@ public function show(int $id) public function update(int $id, array $data) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); - if (!$p) throw new BadRequestHttpException(__('error.not_found')); + if (! $p) { + throw new BadRequestHttpException(__('error.not_found')); + } $v = Validator::make($data, [ - 'code' => 'sometimes|string|max:30', - 'name' => 'sometimes|string|max:100', - 'category_id' => 'sometimes|integer', - 'product_type' => 'sometimes|string|max:30', - 'attributes' => 'nullable|array', - 'description' => 'nullable|string|max:255', - 'is_sellable' => 'nullable|in:0,1', + 'code' => 'sometimes|string|max:30', + 'name' => 'sometimes|string|max:100', + 'category_id' => 'sometimes|integer', + 'product_type' => 'sometimes|string|max:30', + 'attributes' => 'nullable|array', + 'description' => 'nullable|string|max:255', + 'is_sellable' => 'nullable|in:0,1', 'is_purchasable' => 'nullable|in:0,1', - 'is_producible' => 'nullable|in:0,1', - 'is_active' => 'nullable|in:0,1', + 'is_producible' => 'nullable|in:0,1', + 'is_active' => 'nullable|in:0,1', ]); $payload = $v->validate(); @@ -178,11 +192,14 @@ public function update(int $id, array $data) ->where('tenant_id', $tenantId) ->where('code', $payload['code']) ->exists(); - if ($dup) throw new BadRequestHttpException(__('error.duplicate_key')); + if ($dup) { + throw new BadRequestHttpException(__('error.duplicate_key')); + } } $payload['updated_by'] = $userId; $p->update($payload); + return $p->refresh(); } @@ -191,7 +208,9 @@ public function destroy(int $id): void { $tenantId = $this->tenantId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); - if (!$p) throw new BadRequestHttpException(__('error.not_found')); + if (! $p) { + throw new BadRequestHttpException(__('error.not_found')); + } $p->delete(); } @@ -199,8 +218,8 @@ public function destroy(int $id): void public function search(array $params) { $tenantId = $this->tenantId(); - $q = trim((string)($params['q'] ?? '')); - $lim = (int)($params['limit'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); + $lim = (int) ($params['limit'] ?? 20); $qr = Product::query()->where('tenant_id', $tenantId); if ($q !== '') { @@ -209,24 +228,25 @@ public function search(array $params) ->orWhere('code', 'like', "%{$q}%"); }); } - return $qr->orderBy('name')->limit($lim)->get(['id','code','name','product_type','category_id','is_active']); + + return $qr->orderBy('name')->limit($lim)->get(['id', 'code', 'name', 'product_type', 'category_id', 'is_active']); } // 활성 토글 public function toggle(int $id) { $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); + $userId = $this->apiUserId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); - if (!$p) throw new BadRequestHttpException(__('error.not_found')); + if (! $p) { + throw new BadRequestHttpException(__('error.not_found')); + } $p->is_active = $p->is_active ? 0 : 1; $p->updated_by = $userId; $p->save(); - return ['id' => $p->id, 'is_active' => (int)$p->is_active]; + return ['id' => $p->id, 'is_active' => (int) $p->is_active]; } - - } diff --git a/app/Services/Products/ProductComponentResolver.php b/app/Services/Products/ProductComponentResolver.php index e3a46f6..5104dee 100644 --- a/app/Services/Products/ProductComponentResolver.php +++ b/app/Services/Products/ProductComponentResolver.php @@ -6,7 +6,6 @@ use App\Models\Products\ProductComponent; use App\Services\Service; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; class ProductComponentResolver extends Service @@ -22,9 +21,12 @@ public function __construct( protected function productLikeTypes(): array { $all = config('products.product_like_types', []); - $byTenant = Arr::get($all, (string)$this->tenantId, null); + $byTenant = Arr::get($all, (string) $this->tenantId, null); + + if (is_array($byTenant) && $byTenant) { + return $byTenant; + } - if (is_array($byTenant) && $byTenant) return $byTenant; return Arr::get($all, '*', ['PRODUCT']); } @@ -41,11 +43,11 @@ protected function getLinesForParent(int $parentId): array ->where('parent_product_id', $parentId) ->orderBy('sort_order')->orderBy('id') ->get([ - 'id','tenant_id','parent_product_id', - 'category_id','category_name', - 'ref_type','ref_id','quantity','sort_order', + 'id', 'tenant_id', 'parent_product_id', + 'category_id', 'category_name', + 'ref_type', 'ref_id', 'quantity', 'sort_order', ]) - ->map(fn($r) => $r->getAttributes()) // ✅ 핵심 수정 + ->map(fn ($r) => $r->getAttributes()) // ✅ 핵심 수정 ->all(); return $memo[$parentId] = $rows; @@ -57,22 +59,23 @@ protected function resolveNodeInfo(string $refType, int $refId): array if ($refType === 'PRODUCT') { $p = Product::query() ->where('tenant_id', $this->tenantId) - ->find($refId, ['id','code','name','product_type','category_id']); - if (!$p) { + ->find($refId, ['id', 'code', 'name', 'product_type', 'category_id']); + if (! $p) { return [ - 'id' => $refId, - 'code' => null, - 'name' => null, + 'id' => $refId, + 'code' => null, + 'name' => null, 'product_type' => null, - 'category_id' => null, + 'category_id' => null, ]; } + return [ - 'id' => (int) $p->id, - 'code' => $p->code, - 'name' => $p->name, + 'id' => (int) $p->id, + 'code' => $p->code, + 'name' => $p->name, 'product_type' => $p->product_type, - 'category_id' => $p->category_id, + 'category_id' => $p->category_id, ]; } @@ -92,12 +95,12 @@ protected function resolveNodeInfo(string $refType, int $refId): array 'category_id', ]); - if (!$m) { + if (! $m) { return [ - 'id' => (int) $refId, - 'code' => null, - 'name' => null, - 'unit' => null, + 'id' => (int) $refId, + 'code' => null, + 'name' => null, + 'unit' => null, 'category_id' => null, ]; } @@ -106,18 +109,18 @@ protected function resolveNodeInfo(string $refType, int $refId): array $displayName = $m->item_name ?: $m->name; return [ - 'id' => (int) $m->id, - 'code' => $m->material_code, // 표준 코드 필드 - 'name' => $displayName, // 사용자에게 보일 이름 - 'unit' => $m->unit, - 'spec' => $m->specification, // 있으면 프론트에서 활용 가능 + 'id' => (int) $m->id, + 'code' => $m->material_code, // 표준 코드 필드 + 'name' => $displayName, // 사용자에게 보일 이름 + 'unit' => $m->unit, + 'spec' => $m->specification, // 있으면 프론트에서 활용 가능 'category_id' => $m->category_id, ]; } // 알 수 없는 타입 폴백 return [ - 'id' => $refId, + 'id' => $refId, 'code' => null, 'name' => null, ]; @@ -126,31 +129,31 @@ protected function resolveNodeInfo(string $refType, int $refId): array /** * 단일 제품을 루트로 트리를 생성 (재귀 / 사이클 방지 / 깊이 제한) * - * @param int $productId 루트 제품 ID - * @param int|null $maxDepth 최대 깊이(루트=0). null 이면 config default + * @param int $productId 루트 제품 ID + * @param int|null $maxDepth 최대 깊이(루트=0). null 이면 config default * @return array 트리 구조 */ public function resolveTree(int $productId, ?int $maxDepth = null): array { - $maxDepth = $maxDepth ?? (int)config('products.default_tree_depth', 10); + $maxDepth = $maxDepth ?? (int) config('products.default_tree_depth', 10); $root = Product::query() ->where('tenant_id', $this->tenantId) - ->findOrFail($productId, ['id','code','name','product_type','category_id']); + ->findOrFail($productId, ['id', 'code', 'name', 'product_type', 'category_id']); $visited = []; // 사이클 방지용 (product id 기준) $node = [ - 'type' => 'PRODUCT', - 'id' => $root->id, - 'code' => $root->code, - 'name' => $root->name, + 'type' => 'PRODUCT', + 'id' => $root->id, + 'code' => $root->code, + 'name' => $root->name, 'product_type' => $root->product_type, - 'category_id' => $root->category_id, - 'quantity' => 1, // 루트는 수량 1로 간주 - 'category' => null, // 루트는 임의 - 'children' => [], - 'depth' => 0, + 'category_id' => $root->category_id, + 'quantity' => 1, // 루트는 수량 1로 간주 + 'category' => null, // 루트는 임의 + 'children' => [], + 'depth' => 0, ]; $node['children'] = $this->resolveChildren($root->id, 0, $maxDepth, $visited); @@ -160,29 +163,30 @@ public function resolveTree(int $productId, ?int $maxDepth = null): array /** * 하위 노드(들) 재귀 확장 - * @param int $parentId - * @param int $depth - * @param int $maxDepth - * @param array $visited product-id 기준 사이클 방지 - * @return array + * + * @param array $visited product-id 기준 사이클 방지 */ protected function resolveChildren(int $parentId, int $depth, int $maxDepth, array &$visited): array { // 깊이 제한 - if ($depth >= $maxDepth) return []; + if ($depth >= $maxDepth) { + return []; + } $lines = $this->getLinesForParent($parentId); - if (!$lines) return []; + if (! $lines) { + return []; + } $productLike = $this->productLikeTypes(); $children = []; foreach ($lines as $line) { - $refType = (string)$line['ref_type']; - $refId = (int)$line['ref_id']; - $qty = (float)$line['quantity']; + $refType = (string) $line['ref_type']; + $refId = (int) $line['ref_id']; + $qty = (float) $line['quantity']; - if (!$refType || $refId <= 0) { + if (! $refType || $refId <= 0) { // 로그 남기고 스킵 // logger()->warning('Invalid component line', ['line' => $line]); continue; @@ -191,26 +195,26 @@ protected function resolveChildren(int $parentId, int $depth, int $maxDepth, arr $info = $this->resolveNodeInfo($refType, $refId); $child = [ - 'type' => $refType, - 'id' => $info['id'] ?? $refId, - 'code' => $info['code'] ?? null, - 'name' => $info['name'] ?? null, - 'product_type'=> $info['product_type'] ?? null, + 'type' => $refType, + 'id' => $info['id'] ?? $refId, + 'code' => $info['code'] ?? null, + 'name' => $info['name'] ?? null, + 'product_type' => $info['product_type'] ?? null, 'category_id' => $info['category_id'] ?? null, - 'quantity' => $qty, - 'category' => [ - 'id' => $line['category_id'], + 'quantity' => $qty, + 'category' => [ + 'id' => $line['category_id'], 'name' => $line['category_name'], ], - 'sort_order' => (int)$line['sort_order'], - 'children' => [], - 'depth' => $depth + 1, + 'sort_order' => (int) $line['sort_order'], + 'children' => [], + 'depth' => $depth + 1, ]; // 제품처럼 자식이 달릴 수 있는 타입이면 재귀 if (in_array($refType, $productLike, true)) { // 사이클 방지: 같은 product id 재방문 금지 - $pid = (int)$child['id']; + $pid = (int) $child['id']; if ($pid > 0) { if (isset($visited[$pid])) { $child['cycle'] = true; // 표식만 남기고 children 안탐 diff --git a/app/Services/Service.php b/app/Services/Service.php index b51f3b2..b84192b 100644 --- a/app/Services/Service.php +++ b/app/Services/Service.php @@ -1,4 +1,5 @@ tenantIdOrNull(); - if (!$id) { + if (! $id) { // ko/error.php 의 'tenant_id' 키 사용 throw new BadRequestHttpException(__('error.tenant_id')); } + return $id; } @@ -28,10 +31,11 @@ protected function tenantId(): int protected function apiUserId(): int { $uid = app('api_user'); - if (!$uid) { + if (! $uid) { // Handler에서 AuthenticationException은 401로 처리 중 throw new AuthenticationException(__('auth.unauthenticated')); } + return (int) $uid; } } diff --git a/app/Services/TenantBootstrap/RecipeRegistry.php b/app/Services/TenantBootstrap/RecipeRegistry.php index e6dffa2..a007213 100644 --- a/app/Services/TenantBootstrap/RecipeRegistry.php +++ b/app/Services/TenantBootstrap/RecipeRegistry.php @@ -2,7 +2,6 @@ namespace App\Services\TenantBootstrap; -use App\Services\TenantBootstrap\Contracts\TenantBootstrapStep; use App\Services\TenantBootstrap\Steps\CapabilityProfilesStep; use App\Services\TenantBootstrap\Steps\CategoriesStep; use App\Services\TenantBootstrap\Steps\MenusStep; @@ -17,14 +16,14 @@ public function steps(string $recipe = 'STANDARD'): array { return match ($recipe) { 'LITE' => [ - new CapabilityProfilesStep(), - new CategoriesStep(), + new CapabilityProfilesStep, + new CategoriesStep, ], default => [ // STANDARD - new CapabilityProfilesStep(), - new CategoriesStep(), - new MenusStep(), - new SettingsStep(), + new CapabilityProfilesStep, + new CategoriesStep, + new MenusStep, + new SettingsStep, ], }; } diff --git a/app/Services/TenantBootstrap/Steps/CapabilityProfilesStep.php b/app/Services/TenantBootstrap/Steps/CapabilityProfilesStep.php index e08cb0f..8b080fe 100644 --- a/app/Services/TenantBootstrap/Steps/CapabilityProfilesStep.php +++ b/app/Services/TenantBootstrap/Steps/CapabilityProfilesStep.php @@ -7,24 +7,27 @@ class CapabilityProfilesStep implements TenantBootstrapStep { - public function key(): string { return 'capability_profiles'; } + public function key(): string + { + return 'capability_profiles'; + } public function run(int $tenantId): void { $profiles = [ - ['FINISHED_GOOD','완제품', ['is_sellable'=>1,'is_purchasable'=>0,'is_producible'=>1,'is_stock_managed'=>1]], - ['SUB_ASSEMBLY','서브어셈블리', ['is_sellable'=>0,'is_purchasable'=>0,'is_producible'=>1,'is_stock_managed'=>1]], - ['PURCHASED_PART','구매부품', ['is_sellable'=>0,'is_purchasable'=>1,'is_producible'=>0,'is_stock_managed'=>1]], - ['PHANTOM','팬텀(가상)', ['is_sellable'=>0,'is_purchasable'=>0,'is_producible'=>1,'is_stock_managed'=>0]], + ['FINISHED_GOOD', '완제품', ['is_sellable' => 1, 'is_purchasable' => 0, 'is_producible' => 1, 'is_stock_managed' => 1]], + ['SUB_ASSEMBLY', '서브어셈블리', ['is_sellable' => 0, 'is_purchasable' => 0, 'is_producible' => 1, 'is_stock_managed' => 1]], + ['PURCHASED_PART', '구매부품', ['is_sellable' => 0, 'is_purchasable' => 1, 'is_producible' => 0, 'is_stock_managed' => 1]], + ['PHANTOM', '팬텀(가상)', ['is_sellable' => 0, 'is_purchasable' => 0, 'is_producible' => 1, 'is_stock_managed' => 0]], ]; foreach ($profiles as [$code,$name,$attrs]) { DB::table('common_codes')->updateOrInsert( - ['tenant_id'=>$tenantId,'code_group'=>'capability_profile','code'=>$code], + ['tenant_id' => $tenantId, 'code_group' => 'capability_profile', 'code' => $code], [ - 'name'=>$name, - 'attributes'=>json_encode($attrs, JSON_UNESCAPED_UNICODE), - 'is_active'=>1, 'sort_order'=>0, - 'description'=>'기본 프로필' + 'name' => $name, + 'attributes' => json_encode($attrs, JSON_UNESCAPED_UNICODE), + 'is_active' => 1, 'sort_order' => 0, + 'description' => '기본 프로필', ] ); } diff --git a/app/Services/TenantBootstrap/Steps/CategoriesStep.php b/app/Services/TenantBootstrap/Steps/CategoriesStep.php index b8bdc8c..7b0586c 100644 --- a/app/Services/TenantBootstrap/Steps/CategoriesStep.php +++ b/app/Services/TenantBootstrap/Steps/CategoriesStep.php @@ -7,26 +7,29 @@ class CategoriesStep implements TenantBootstrapStep { - public function key(): string { return 'categories_seed'; } + public function key(): string + { + return 'categories_seed'; + } public function run(int $tenantId): void { $exists = DB::table('categories')->where([ - 'tenant_id'=>$tenantId, 'code_group'=>'product', 'code'=>'DEFAULT' + 'tenant_id' => $tenantId, 'code_group' => 'product', 'code' => 'DEFAULT', ])->exists(); - if (!$exists) { + if (! $exists) { DB::table('categories')->insert([ - 'tenant_id'=>$tenantId, - 'parent_id'=>null, - 'code_group'=>'product', - 'code'=>'DEFAULT', - 'name'=>'기본 카테고리', - 'profile_code'=>'FINISHED_GOOD', - 'is_active'=>1, - 'sort_order'=>1, - 'created_at'=>now(), - 'updated_at'=>now(), + 'tenant_id' => $tenantId, + 'parent_id' => null, + 'code_group' => 'product', + 'code' => 'DEFAULT', + 'name' => '기본 카테고리', + 'profile_code' => 'FINISHED_GOOD', + 'is_active' => 1, + 'sort_order' => 1, + 'created_at' => now(), + 'updated_at' => now(), ]); } } diff --git a/app/Services/TenantBootstrap/Steps/SettingsStep.php b/app/Services/TenantBootstrap/Steps/SettingsStep.php index 95f8097..3a2a307 100644 --- a/app/Services/TenantBootstrap/Steps/SettingsStep.php +++ b/app/Services/TenantBootstrap/Steps/SettingsStep.php @@ -7,11 +7,16 @@ class SettingsStep implements TenantBootstrapStep { - public function key(): string { return 'settings_seed'; } + public function key(): string + { + return 'settings_seed'; + } public function run(int $tenantId): void { - if (!DB::getSchemaBuilder()->hasTable('settings')) return; + if (! DB::getSchemaBuilder()->hasTable('settings')) { + return; + } $pairs = [ ['general.company_name', '회사명 미설정'], @@ -20,8 +25,8 @@ public function run(int $tenantId): void ]; foreach ($pairs as [$key,$val]) { DB::table('settings')->updateOrInsert( - ['tenant_id'=>$tenantId, 'key'=>$key], - ['value'=>$val, 'updated_at'=>now(), 'created_at'=>now()] + ['tenant_id' => $tenantId, 'key' => $key], + ['value' => $val, 'updated_at' => now(), 'created_at' => now()] ); } } diff --git a/app/Services/TenantBootstrap/Support/TenantBootstrapLogger.php b/app/Services/TenantBootstrap/Support/TenantBootstrapLogger.php index 1cc7857..85a2a63 100644 --- a/app/Services/TenantBootstrap/Support/TenantBootstrapLogger.php +++ b/app/Services/TenantBootstrap/Support/TenantBootstrapLogger.php @@ -6,11 +6,18 @@ class TenantBootstrapLogger { private array $messages = []; - public function info(string $msg, array $ctx = []): void { - $this->messages[] = ['level'=>'info', 'msg'=>$msg, 'ctx'=>$ctx, 'ts'=>now()->toDateTimeString()]; + public function info(string $msg, array $ctx = []): void + { + $this->messages[] = ['level' => 'info', 'msg' => $msg, 'ctx' => $ctx, 'ts' => now()->toDateTimeString()]; } - public function error(string $msg, array $ctx = []): void { - $this->messages[] = ['level'=>'error', 'msg'=>$msg, 'ctx'=>$ctx, 'ts'=>now()->toDateTimeString()]; + + public function error(string $msg, array $ctx = []): void + { + $this->messages[] = ['level' => 'error', 'msg' => $msg, 'ctx' => $ctx, 'ts' => now()->toDateTimeString()]; + } + + public function dump(): array + { + return $this->messages; } - public function dump(): array { return $this->messages; } } diff --git a/app/Services/TenantBootstrapper.php b/app/Services/TenantBootstrapper.php index c461d3d..5de6395 100644 --- a/app/Services/TenantBootstrapper.php +++ b/app/Services/TenantBootstrapper.php @@ -12,42 +12,42 @@ public function __construct(private RecipeRegistry $registry) {} public function bootstrap(int $tenantId, string $recipe = 'STANDARD'): void { - $logger = new TenantBootstrapLogger(); - $steps = $this->registry->steps($recipe); - $ver = $this->registry->version($recipe); + $logger = new TenantBootstrapLogger; + $steps = $this->registry->steps($recipe); + $ver = $this->registry->version($recipe); DB::transaction(function () use ($tenantId, $recipe, $steps, $ver, $logger) { $runId = DB::table('tenant_bootstrap_runs')->insertGetId([ - 'tenant_id'=>$tenantId, 'recipe'=>$recipe, 'recipe_version'=>$ver, - 'status'=>'RUNNING', 'steps'=>json_encode([]), 'log'=>json_encode([]), - 'created_at'=>now(), 'updated_at'=>now(), + 'tenant_id' => $tenantId, 'recipe' => $recipe, 'recipe_version' => $ver, + 'status' => 'RUNNING', 'steps' => json_encode([]), 'log' => json_encode([]), + 'created_at' => now(), 'updated_at' => now(), ]); $done = []; try { foreach ($steps as $step) { /** @var \App\Services\TenantBootstrap\Contracts\TenantBootstrapStep $step */ - $logger->info('start step', ['key'=>$step->key()]); + $logger->info('start step', ['key' => $step->key()]); $step->run($tenantId); $done[] = $step->key(); - $logger->info('end step', ['key'=>$step->key()]); - DB::table('tenant_bootstrap_runs')->where('id',$runId)->update([ - 'steps'=>json_encode($done), - 'log'=>json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), - 'updated_at'=>now(), + $logger->info('end step', ['key' => $step->key()]); + DB::table('tenant_bootstrap_runs')->where('id', $runId)->update([ + 'steps' => json_encode($done), + 'log' => json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), + 'updated_at' => now(), ]); } - DB::table('tenant_bootstrap_runs')->where('id',$runId)->update([ - 'status'=>'SUCCESS', - 'log'=>json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), - 'updated_at'=>now(), + DB::table('tenant_bootstrap_runs')->where('id', $runId)->update([ + 'status' => 'SUCCESS', + 'log' => json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), + 'updated_at' => now(), ]); } catch (\Throwable $e) { - $logger->error('step failed', ['exception'=>$e->getMessage()]); - DB::table('tenant_bootstrap_runs')->where('id',$runId)->update([ - 'status'=>'FAILED', - 'log'=>json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), - 'updated_at'=>now(), + $logger->error('step failed', ['exception' => $e->getMessage()]); + DB::table('tenant_bootstrap_runs')->where('id', $runId)->update([ + 'status' => 'FAILED', + 'log' => json_encode($logger->dump(), JSON_UNESCAPED_UNICODE), + 'updated_at' => now(), ]); throw $e; } diff --git a/app/Services/TenantFieldSettingService.php b/app/Services/TenantFieldSettingService.php index 92b4637..8beacb5 100644 --- a/app/Services/TenantFieldSettingService.php +++ b/app/Services/TenantFieldSettingService.php @@ -2,13 +2,12 @@ namespace App\Services; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Validator; -use Illuminate\Validation\Rule; - use App\Models\Tenants\SettingFieldDef; use App\Models\Tenants\TenantFieldSetting; use App\Models\Tenants\TenantOptionGroup; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; class TenantFieldSettingService { @@ -21,23 +20,24 @@ protected static function tenantId(): ?int /** 효과값 1개 빌드: 전역정의 + 테넌트설정 merge */ protected static function buildEffectiveRow(SettingFieldDef $def, ?TenantFieldSetting $s): array { - $enabled = $s ? (bool)$s->enabled : (bool)$def->is_core; + $enabled = $s ? (bool) $s->enabled : (bool) $def->is_core; + return [ - 'field_key' => $def->field_key, - 'label' => $def->label, - 'data_type' => $def->data_type, - 'input_type' => $def->input_type, - 'option_source' => $def->option_source, - 'option_payload' => $def->option_payload, - 'storage_area' => $def->storage_area, - 'storage_key' => $def->storage_key, - 'is_core' => (bool)$def->is_core, + 'field_key' => $def->field_key, + 'label' => $def->label, + 'data_type' => $def->data_type, + 'input_type' => $def->input_type, + 'option_source' => $def->option_source, + 'option_payload' => $def->option_payload, + 'storage_area' => $def->storage_area, + 'storage_key' => $def->storage_key, + 'is_core' => (bool) $def->is_core, // tenant overlay - 'enabled' => $enabled, - 'required' => $s ? (bool)$s->required : false, - 'sort_order' => $s ? (int)$s->sort_order : 0, + 'enabled' => $enabled, + 'required' => $s ? (bool) $s->required : false, + 'sort_order' => $s ? (int) $s->sort_order : 0, 'option_group_id' => $s ? $s->option_group_id : null, - 'code_group' => $s ? $s->code_group : null, + 'code_group' => $s ? $s->code_group : null, ]; } @@ -50,7 +50,7 @@ protected static function buildEffectiveRow(SettingFieldDef $def, ?TenantFieldSe public static function index(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } @@ -74,8 +74,7 @@ public static function index(array $params = []) $rows[] = $effective; } - usort($rows, fn ($a, $b) => - ($a['sort_order'] <=> $b['sort_order']) ?: strcmp($a['field_key'], $b['field_key']) + usort($rows, fn ($a, $b) => ($a['sort_order'] <=> $b['sort_order']) ?: strcmp($a['field_key'], $b['field_key']) ); return array_values($rows); @@ -89,18 +88,18 @@ public static function index(array $params = []) public static function bulkUpsert(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ - 'items' => ['required','array','min:1'], - 'items.*.field_key' => ['required','string','max:64', Rule::exists('setting_field_defs','field_key')], - 'items.*.enabled' => ['nullable','boolean'], - 'items.*.required' => ['nullable','boolean'], - 'items.*.sort_order' => ['nullable','integer'], - 'items.*.option_group_id' => ['nullable','integer'], - 'items.*.code_group' => ['nullable','string','max:50'], + 'items' => ['required', 'array', 'min:1'], + 'items.*.field_key' => ['required', 'string', 'max:64', Rule::exists('setting_field_defs', 'field_key')], + 'items.*.enabled' => ['nullable', 'boolean'], + 'items.*.required' => ['nullable', 'boolean'], + 'items.*.sort_order' => ['nullable', 'integer'], + 'items.*.option_group_id' => ['nullable', 'integer'], + 'items.*.code_group' => ['nullable', 'string', 'max:50'], ]); if ($v->fails()) { @@ -113,11 +112,11 @@ public static function bulkUpsert(array $params = []) DB::transaction(function () use ($payload, $tenantId) { foreach ($payload['items'] as $row) { // option_group_id 유효성(해당 테넌트 소유인지) - if (!empty($row['option_group_id'])) { + if (! empty($row['option_group_id'])) { $exists = TenantOptionGroup::where('tenant_id', $tenantId) ->where('id', $row['option_group_id']) ->exists(); - if (!$exists) { + if (! $exists) { throw new \RuntimeException('option_group_id is invalid for this tenant'); } } @@ -125,11 +124,11 @@ public static function bulkUpsert(array $params = []) TenantFieldSetting::updateOrCreate( ['tenant_id' => $tenantId, 'field_key' => $row['field_key']], [ - 'enabled' => isset($row['enabled']) ? (int)$row['enabled'] : 0, - 'required' => isset($row['required']) ? (int)$row['required'] : 0, - 'sort_order' => isset($row['sort_order']) ? (int)$row['sort_order'] : 0, + 'enabled' => isset($row['enabled']) ? (int) $row['enabled'] : 0, + 'required' => isset($row['required']) ? (int) $row['required'] : 0, + 'sort_order' => isset($row['sort_order']) ? (int) $row['sort_order'] : 0, 'option_group_id' => $row['option_group_id'] ?? null, - 'code_group' => $row['code_group'] ?? null, + 'code_group' => $row['code_group'] ?? null, ] ); } @@ -150,22 +149,22 @@ public static function bulkUpsert(array $params = []) public static function updateOne(string $fieldKey, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } // 전역 key 존재 확인 $def = SettingFieldDef::where('field_key', $fieldKey)->first(); - if (!$def) { + if (! $def) { return ['error' => 'field_key not found', 'code' => 404]; } $v = Validator::make($params, [ - 'enabled' => ['nullable','boolean'], - 'required' => ['nullable','boolean'], - 'sort_order' => ['nullable','integer'], - 'option_group_id' => ['nullable','integer'], - 'code_group' => ['nullable','string','max:50'], + 'enabled' => ['nullable', 'boolean'], + 'required' => ['nullable', 'boolean'], + 'sort_order' => ['nullable', 'integer'], + 'option_group_id' => ['nullable', 'integer'], + 'code_group' => ['nullable', 'string', 'max:50'], ]); if ($v->fails()) { @@ -174,11 +173,11 @@ public static function updateOne(string $fieldKey, array $params = []) $data = $v->validated(); // option_group_id 테넌트 소유 검증 - if (!empty($data['option_group_id'])) { + if (! empty($data['option_group_id'])) { $ok = TenantOptionGroup::where('tenant_id', $tenantId) ->where('id', $data['option_group_id']) ->exists(); - if (!$ok) { + if (! $ok) { return ['error' => 'option_group_id is invalid for this tenant', 'code' => 422]; } } @@ -190,7 +189,7 @@ public static function updateOne(string $fieldKey, array $params = []) ]); // 제공된 키만 반영 - foreach (['enabled','required','sort_order','option_group_id','code_group'] as $k) { + foreach (['enabled', 'required', 'sort_order', 'option_group_id', 'code_group'] as $k) { if (array_key_exists($k, $data)) { $item->{$k} = $data[$k]; } diff --git a/app/Services/TenantOptionGroupService.php b/app/Services/TenantOptionGroupService.php index 31f212b..4dc10a1 100644 --- a/app/Services/TenantOptionGroupService.php +++ b/app/Services/TenantOptionGroupService.php @@ -2,12 +2,11 @@ namespace App\Services; -use Illuminate\Support\Facades\Validator; -use Illuminate\Validation\Rule; - +use App\Models\Tenants\TenantFieldSetting; use App\Models\Tenants\TenantOptionGroup; use App\Models\Tenants\TenantOptionValue; -use App\Models\Tenants\TenantFieldSetting; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; class TenantOptionGroupService { @@ -24,16 +23,16 @@ protected static function tenantId(): ?int public static function index(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - $per = (int)($params['per_page'] ?? $params['size'] ?? 20); - $page = isset($params['page']) ? (int)$params['page'] : null; + $per = (int) ($params['per_page'] ?? $params['size'] ?? 20); + $page = isset($params['page']) ? (int) $params['page'] : null; $q = TenantOptionGroup::where('tenant_id', $tenantId)->orderBy('group_key'); - if (!empty($params['q'])) { + if (! empty($params['q'])) { $kw = $params['q']; $q->where(function ($w) use ($kw) { $w->where('group_key', 'like', "%{$kw}%") @@ -52,18 +51,18 @@ public static function index(array $params = []) public static function store(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $v = Validator::make($params, [ 'group_key' => [ - 'required','string','max:64','alpha_dash', + 'required', 'string', 'max:64', 'alpha_dash', Rule::unique('tenant_option_groups', 'group_key') ->where(fn ($q) => $q->where('tenant_id', $tenantId)), ], - 'name' => ['required','string','max:100'], - 'description' => ['nullable','string','max:255'], + 'name' => ['required', 'string', 'max:100'], + 'description' => ['nullable', 'string', 'max:255'], ]); if ($v->fails()) { @@ -84,15 +83,15 @@ public static function store(array $params = []) public static function show(int $id) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$id) { + if (! $id) { return ['error' => 'id가 올바르지 않습니다.', 'code' => 422]; } $item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } @@ -105,27 +104,27 @@ public static function show(int $id) public static function update(int $id, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$id) { + if (! $id) { return ['error' => 'id가 올바르지 않습니다.', 'code' => 422]; } $item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ 'group_key' => [ - 'sometimes','string','max:64','alpha_dash', + 'sometimes', 'string', 'max:64', 'alpha_dash', Rule::unique('tenant_option_groups', 'group_key') ->where(fn ($q) => $q->where('tenant_id', $tenantId)) ->ignore($item->id), ], - 'name' => ['sometimes','string','max:100'], - 'description' => ['nullable','string','max:255'], + 'name' => ['sometimes', 'string', 'max:100'], + 'description' => ['nullable', 'string', 'max:255'], ]); if ($v->fails()) { @@ -151,15 +150,15 @@ public static function update(int $id, array $params = []) public static function destroy(int $id) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$id) { + if (! $id) { return ['error' => 'id가 올바르지 않습니다.', 'code' => 422]; } $item = TenantOptionGroup::where('tenant_id', $tenantId)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } diff --git a/app/Services/TenantOptionValueService.php b/app/Services/TenantOptionValueService.php index df2073a..e7c81a2 100644 --- a/app/Services/TenantOptionValueService.php +++ b/app/Services/TenantOptionValueService.php @@ -2,13 +2,12 @@ namespace App\Services; +use App\Models\Tenants\TenantOptionGroup; +use App\Models\Tenants\TenantOptionValue; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; -use App\Models\Tenants\TenantOptionGroup; -use App\Models\Tenants\TenantOptionValue; - class TenantOptionValueService { /** 활성 테넌트 ID */ @@ -30,12 +29,12 @@ protected static function loadGroup(int $tenantId, int $groupId): ?TenantOptionG public static function index(int $groupId, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } @@ -43,7 +42,7 @@ public static function index(int $groupId, array $params = []) ->orderBy('sort_order') ->orderBy('value_label'); - if (!empty($params['active_only'])) { + if (! empty($params['active_only'])) { $q->where('is_active', 1); } @@ -58,24 +57,24 @@ public static function index(int $groupId, array $params = []) public static function store(int $groupId, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'value_key' => [ - 'required','string','max:64','alpha_dash', + 'value_key' => [ + 'required', 'string', 'max:64', 'alpha_dash', Rule::unique('tenant_option_values', 'value_key') ->where(fn ($q) => $q->where('group_id', $group->id)), ], - 'value_label' => ['required','string','max:100'], - 'sort_order' => ['nullable','integer'], - 'is_active' => ['nullable','boolean'], + 'value_label' => ['required', 'string', 'max:100'], + 'sort_order' => ['nullable', 'integer'], + 'is_active' => ['nullable', 'boolean'], ]); if ($v->fails()) { @@ -95,17 +94,17 @@ public static function store(int $groupId, array $params = []) public static function show(int $groupId, int $id) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $item = TenantOptionValue::where('group_id', $group->id)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 값을 찾을 수 없습니다.', 'code' => 404]; } @@ -118,30 +117,30 @@ public static function show(int $groupId, int $id) public static function update(int $groupId, int $id, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $item = TenantOptionValue::where('group_id', $group->id)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 값을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'value_key' => [ - 'sometimes','string','max:64','alpha_dash', + 'value_key' => [ + 'sometimes', 'string', 'max:64', 'alpha_dash', Rule::unique('tenant_option_values', 'value_key') ->where(fn ($q) => $q->where('group_id', $group->id)) ->ignore($item->id), ], - 'value_label' => ['sometimes','string','max:100'], - 'sort_order' => ['nullable','integer'], - 'is_active' => ['nullable','boolean'], + 'value_label' => ['sometimes', 'string', 'max:100'], + 'sort_order' => ['nullable', 'integer'], + 'is_active' => ['nullable', 'boolean'], ]); if ($v->fails()) { @@ -165,17 +164,17 @@ public static function update(int $groupId, int $id, array $params = []) public static function destroy(int $groupId, int $id) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $item = TenantOptionValue::where('group_id', $group->id)->find($id); - if (!$item) { + if (! $item) { return ['error' => '옵션 값을 찾을 수 없습니다.', 'code' => 404]; } @@ -192,19 +191,19 @@ public static function destroy(int $groupId, int $id) public static function reorder(int $groupId, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $group = self::loadGroup($tenantId, $groupId); - if (!$group) { + if (! $group) { return ['error' => '옵션 그룹을 찾을 수 없습니다.', 'code' => 404]; } $v = Validator::make($params, [ - 'items' => ['required','array','min:1'], - 'items.*.id' => ['required','integer'], - 'items.*.sort_order' => ['required','integer'], + 'items' => ['required', 'array', 'min:1'], + 'items.*.id' => ['required', 'integer'], + 'items.*.sort_order' => ['required', 'integer'], ]); if ($v->fails()) { diff --git a/app/Services/TenantService.php b/app/Services/TenantService.php index dc79ab1..f5488df 100644 --- a/app/Services/TenantService.php +++ b/app/Services/TenantService.php @@ -2,16 +2,16 @@ namespace App\Services; -use Illuminate\Support\Facades\Validator; -use App\Models\Tenants\Tenant; use App\Models\Members\UserTenant; +use App\Models\Tenants\Tenant; +use Illuminate\Support\Facades\Validator; class TenantService { /** * 한글 자음 배열 */ - private const INITIALS = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']; + private const INITIALS = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']; /** * 대소문자를 구분하지 않는 36진수 문자열로 변경 @@ -21,9 +21,6 @@ class TenantService /** * 한글 업체명에서 초성 약어를 추출합니다. * 외부 라이브러리 없이 자체적으로 구현했습니다. - * - * @param string $tenantName - * @return string */ private function getInitials(string $tenantName): string { @@ -42,19 +39,17 @@ private function getInitials(string $tenantName): string // 한글이 아닌 문자는 무시합니다. } - $koreanInitials = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']; - $englishInitials = ['G','KK','N','D','TT','R','M','B','BB','S','SS','O','J','JJ','CH','K','T','P','H']; + $koreanInitials = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']; + $englishInitials = ['G', 'KK', 'N', 'D', 'TT', 'R', 'M', 'B', 'BB', 'S', 'SS', 'O', 'J', 'JJ', 'CH', 'K', 'T', 'P', 'H']; $initials = strtr($initials, array_combine($koreanInitials, $englishInitials)); $initials = str_replace(' ', '', $initials); + return strtoupper($initials); } /** * 10진수 숫자를 4자리 36진수 문자열로 변환합니다. - * - * @param int $number - * @return string */ private function toBase36(int $number): string { @@ -64,7 +59,7 @@ private function toBase36(int $number): string // **수정된 부분: 4자리 고정** for ($i = 0; $i < 4; $i++) { $remainder = $number % $base; - $result = $this->base36Chars[$remainder] . $result; + $result = $this->base36Chars[$remainder].$result; $number = floor($number / $base); } @@ -73,9 +68,6 @@ private function toBase36(int $number): string /** * 36진수 문자열을 10진수 숫자로 변환합니다. - * - * @param string $base36String - * @return int */ private function fromBase36(string $base36String): int { @@ -99,9 +91,6 @@ private function fromBase36(string $base36String): int /** * 한글 업체명 기반으로 순환형 테넌트 코드를 생성합니다. - * - * @param string $tenantName - * @return string */ public function generateTenantCode(string $tenantName): string { @@ -138,41 +127,40 @@ public function generateTenantCode(string $tenantName): string $formattedSequence = $this->toBase36($nextSequence); // 5. 초성 약어와 순번을 조합하여 최종 코드 생성 - $code = $initials . $formattedSequence; + $code = $initials.$formattedSequence; return $code; } - /** * 테넌트 목록 조회 (페이징) * - * @param array $params [page, size, search 등] + * @param array $params [page, size, search 등] */ public static function getTenants(array $params = []) { $tenantId = $params['tenant_id'] ?? app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { // 현재 사용자 기본 테넌트 조회 $apiUser = app('api_user'); $userTenant = UserTenant::where('user_id', $apiUser) ->where('is_default', 1) ->first(); - if (!$userTenant) { + if (! $userTenant) { return ['error' => '활성(기본) 테넌트를 찾을 수 없습니다.', 'code' => 404]; } $tenantId = $userTenant->tenant_id; } - $pageNo = isset($params['page']) ? (int)$params['page'] : 1; - $pageSize = isset($params['size']) ? (int)$params['size'] : 10; + $pageNo = isset($params['page']) ? (int) $params['page'] : 1; + $pageSize = isset($params['size']) ? (int) $params['size'] : 10; $query = Tenant::query(); // (옵션) 간단 검색 예시: 회사명/코드 - if (!empty($params['q'])) { + if (! empty($params['q'])) { $q = trim($params['q']); $query->where(function ($qq) use ($q) { $qq->where('company_name', 'like', "%{$q}%") @@ -182,8 +170,8 @@ public static function getTenants(array $params = []) } // (옵션) 정렬 - if (!empty($params['sort']) && in_array($params['sort'], ['company_name','code','created_at','updated_at'])) { - $dir = (!empty($params['dir']) && in_array(strtolower($params['dir']), ['asc','desc'])) ? $params['dir'] : 'desc'; + if (! empty($params['sort']) && in_array($params['sort'], ['company_name', 'code', 'created_at', 'updated_at'])) { + $dir = (! empty($params['dir']) && in_array(strtolower($params['dir']), ['asc', 'desc'])) ? $params['dir'] : 'desc'; $query->orderBy($params['sort'], $dir); } else { $query->orderByDesc('id'); @@ -199,21 +187,21 @@ public static function getTenants(array $params = []) * - params.tenant_id 가 있으면 해당 테넌트 * - 없으면 현재 사용자 기본(is_default=1) 테넌트 * - * @param array $params [tenant_id] + * @param array $params [tenant_id] */ public static function getTenant(array $params = []) { $tenantId = $params['tenant_id'] ?? app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { // 현재 사용자 기본 테넌트 조회 $apiUser = app('api_user'); $userTenant = UserTenant::where('user_id', $apiUser) ->where('is_default', 1) ->first(); - if (!$userTenant) { + if (! $userTenant) { return ['error' => '활성(기본) 테넌트를 찾을 수 없습니다.', 'code' => 404]; } $tenantId = $userTenant->tenant_id; @@ -221,7 +209,7 @@ public static function getTenant(array $params = []) // 필요한 컬럼만 선택 (원하면 조정) $query = Tenant::query() - ->select('id','company_name','code','email','phone','address','business_num','corp_reg_no','ceo_name','homepage','fax','logo','admin_memo','options','created_at','updated_at') + ->select('id', 'company_name', 'code', 'email', 'phone', 'address', 'business_num', 'corp_reg_no', 'ceo_name', 'homepage', 'fax', 'logo', 'admin_memo', 'options', 'created_at', 'updated_at') ->where('id', $tenantId); return $query->first(); @@ -229,28 +217,25 @@ public static function getTenant(array $params = []) /** * 테넌트 등록 - * - * @param array $params */ public static function storeTenants(array $params = []) { $validator = Validator::make($params, [ 'company_name' => 'required|string|max:255', - 'email' => 'nullable|email|max:100', - 'phone' => 'nullable|string|max:30', - 'address' => 'nullable|string|max:255', + 'email' => 'nullable|email|max:100', + 'phone' => 'nullable|string|max:30', + 'address' => 'nullable|string|max:255', 'business_num' => 'nullable|string|max:30', - 'corp_reg_no' => 'nullable|string|max:30', - 'ceo_name' => 'nullable|string|max:100', - 'homepage' => 'nullable|string|max:255', - 'fax' => 'nullable|string|max:50', - 'logo' => 'nullable|string|max:255', - 'admin_memo' => 'nullable|string', - 'options' => 'nullable', // JSON 문자열 저장이라면 'nullable|json' + 'corp_reg_no' => 'nullable|string|max:30', + 'ceo_name' => 'nullable|string|max:100', + 'homepage' => 'nullable|string|max:255', + 'fax' => 'nullable|string|max:50', + 'logo' => 'nullable|string|max:255', + 'admin_memo' => 'nullable|string', + 'options' => 'nullable', // JSON 문자열 저장이라면 'nullable|json' ]); - if ($validator->fails()) { return ['error' => $validator->errors()->first(), 'code' => 400]; } @@ -275,15 +260,14 @@ public static function storeTenants(array $params = []) ->where('is_default', 1) ->update(['is_default' => 0]); - // 성성된 테넌트를 나의 테넌트로 셋팅 $apiUser = app('api_user'); UserTenant::create([ - 'user_id' => $apiUser, - 'tenant_id' => $tenant->id, - 'is_active' => 0, + 'user_id' => $apiUser, + 'tenant_id' => $tenant->id, + 'is_active' => 0, 'is_default' => 1, - 'joined_at' => now(), + 'joined_at' => now(), ]); // 생성된 리소스를 그대로 반환 (목록 카드용 요약 원하면 컬럼 제한) @@ -292,32 +276,30 @@ public static function storeTenants(array $params = []) /** * 테넌트 수정 - * - * @param array $params */ public static function updateTenant(array $params = []) { $validator = Validator::make($params, [ 'company_name' => 'sometimes|string|max:255', - 'email' => 'sometimes|nullable|email|max:100', - 'phone' => 'sometimes|nullable|string|max:30', - 'address' => 'sometimes|nullable|string|max:255', + 'email' => 'sometimes|nullable|email|max:100', + 'phone' => 'sometimes|nullable|string|max:30', + 'address' => 'sometimes|nullable|string|max:255', 'business_num' => 'sometimes|nullable|string|max:30', - 'corp_reg_no' => 'sometimes|nullable|string|max:30', - 'ceo_name' => 'sometimes|nullable|string|max:100', - 'homepage' => 'sometimes|nullable|string|max:255', - 'fax' => 'sometimes|nullable|string|max:50', - 'logo' => 'sometimes|nullable|string|max:255', - 'admin_memo' => 'sometimes|nullable|string', - 'options' => 'sometimes|nullable', // JSON 문자열이면 'sometimes|nullable|json' + 'corp_reg_no' => 'sometimes|nullable|string|max:30', + 'ceo_name' => 'sometimes|nullable|string|max:100', + 'homepage' => 'sometimes|nullable|string|max:255', + 'fax' => 'sometimes|nullable|string|max:50', + 'logo' => 'sometimes|nullable|string|max:255', + 'admin_memo' => 'sometimes|nullable|string', + 'options' => 'sometimes|nullable', // JSON 문자열이면 'sometimes|nullable|json' ]); if ($validator->fails()) { return ['error' => $validator->errors()->first(), 'code' => 400]; } - $payload = $validator->validated(); + $payload = $validator->validated(); $tenantId = app('tenant_id') ?? null; unset($payload['tenant_id']); @@ -326,7 +308,7 @@ public static function updateTenant(array $params = []) } $tenant = Tenant::find($tenantId); - if (!$tenant) { + if (! $tenant) { return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404]; } @@ -338,17 +320,17 @@ public static function updateTenant(array $params = []) /** * 테넌트 삭제(탈퇴) — 소프트 삭제 가정 * - * @param int $tenant_id + * @param int $tenant_id */ public static function destroyTenant(array $params = []) { $tenantId = $params['tenant_id'] ?? app('tenant_id'); - if (!$tenantId) { + if (! $tenantId) { return ['error' => 'tenant_id가 필요합니다.', 'code' => 400]; } $tenant = Tenant::find($tenantId); - if (!$tenant) { + if (! $tenant) { return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404]; } @@ -360,7 +342,7 @@ public static function destroyTenant(array $params = []) /** * 테넌트 복구 (소프트 삭제된 레코드 대상) * - * @param array $params [tenant_id:int] + * @param array $params [tenant_id:int] */ public static function restoreTenant(array $params = []) { @@ -368,7 +350,7 @@ public static function restoreTenant(array $params = []) // 소프트 삭제 포함 조회 $tenant = Tenant::withTrashed()->find($tenantId); - if (!$tenant) { + if (! $tenant) { return ['error' => '테넌트를 찾을 수 없습니다.', 'code' => 404]; } diff --git a/app/Services/TenantUserProfileService.php b/app/Services/TenantUserProfileService.php index 88c5189..3c2025c 100644 --- a/app/Services/TenantUserProfileService.php +++ b/app/Services/TenantUserProfileService.php @@ -2,10 +2,10 @@ namespace App\Services; -use Illuminate\Support\Facades\DB; use App\Models\Tenants\SettingFieldDef; use App\Models\Tenants\TenantFieldSetting; use App\Models\Tenants\TenantUserProfile; +use Illuminate\Support\Facades\DB; class TenantUserProfileService { @@ -18,16 +18,19 @@ protected static function tenantId(): ?int /** 효과값 필드(Enabled) 맵: field_key => [def, setting] */ protected static function effectiveFieldMap(int $tenantId): array { - $defs = SettingFieldDef::all()->keyBy('field_key'); + $defs = SettingFieldDef::all()->keyBy('field_key'); $settings = TenantFieldSetting::where('tenant_id', $tenantId)->get()->keyBy('field_key'); $map = []; foreach ($defs as $key => $def) { $s = $settings->get($key); - $enabled = $s ? (bool)$s->enabled : (bool)$def->is_core; - if (!$enabled) continue; + $enabled = $s ? (bool) $s->enabled : (bool) $def->is_core; + if (! $enabled) { + continue; + } $map[$key] = ['def' => $def, 'setting' => $s]; } + return $map; } @@ -38,15 +41,15 @@ protected static function effectiveFieldMap(int $tenantId): array public static function index(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - $per = (int)($params['per_page'] ?? $params['size'] ?? 20); - $q = TenantUserProfile::where('tenant_id', $tenantId) + $per = (int) ($params['per_page'] ?? $params['size'] ?? 20); + $q = TenantUserProfile::where('tenant_id', $tenantId) ->with(['user:id,name,email']); - if (!empty($params['q'])) { + if (! empty($params['q'])) { $kw = $params['q']; $q->where(function ($w) use ($kw) { $w->where('display_name', 'like', "%{$kw}%") @@ -54,7 +57,7 @@ public static function index(array $params = []) }); } - $page = isset($params['page']) ? (int)$params['page'] : null; + $page = isset($params['page']) ? (int) $params['page'] : null; $data = $q->orderByDesc('id')->paginate($per, ['*'], 'page', $page); return $data; @@ -66,10 +69,10 @@ public static function index(array $params = []) public static function show(int $userId) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userId) { + if (! $userId) { return ['error' => 'userId가 올바르지 않습니다.', 'code' => 422]; } @@ -78,7 +81,7 @@ public static function show(int $userId) ->with(['user:id,name,email']) ->first(); - if (!$item) { + if (! $item) { return ['error' => '프로필을 찾을 수 없습니다.', 'code' => 404]; } @@ -92,27 +95,29 @@ public static function show(int $userId) public static function update(int $userId, array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } - if (!$userId) { + if (! $userId) { return ['error' => 'userId가 올바르지 않습니다.', 'code' => 422]; } // (선택) 간단 유효성: 최소 1개 키 존재 - if (empty($params) || !is_array($params)) { + if (empty($params) || ! is_array($params)) { return ['error' => '수정할 항목이 없습니다.', 'code' => 422]; } - $fields = self::effectiveFieldMap($tenantId); + $fields = self::effectiveFieldMap($tenantId); $profile = TenantUserProfile::firstOrCreate(['tenant_id' => $tenantId, 'user_id' => $userId]); try { DB::transaction(function () use ($fields, $profile, $params) { foreach ($fields as $key => $meta) { - if (!array_key_exists($key, $params)) continue; // 입력이 없으면 스킵 + if (! array_key_exists($key, $params)) { + continue; + } // 입력이 없으면 스킵 - $def = $meta['def']; + $def = $meta['def']; $value = $params[$key]; switch ($def->storage_area) { @@ -124,7 +129,7 @@ public static function update(int $userId, array $params = []) case 'tenant_profile_json': $jsonKey = $def->storage_key ?: $key; - $extra = is_array($profile->json_extra) ? $profile->json_extra : (array) json_decode((string)$profile->json_extra, true); + $extra = is_array($profile->json_extra) ? $profile->json_extra : (array) json_decode((string) $profile->json_extra, true); $extra[$jsonKey] = $value; $profile->json_extra = $extra; break; @@ -155,11 +160,11 @@ public static function update(int $userId, array $params = []) public static function me(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $userId = auth()->id(); - if (!$userId) { + if (! $userId) { return ['error' => '인증 정보가 없습니다.', 'code' => 401]; } @@ -179,13 +184,14 @@ public static function me(array $params = []) public static function updateMe(array $params = []) { $tenantId = self::tenantId(); - if (!$tenantId) { + if (! $tenantId) { return ['error' => '활성 테넌트가 없습니다.', 'code' => 400]; } $userId = auth()->id(); - if (!$userId) { + if (! $userId) { return ['error' => '인증 정보가 없습니다.', 'code' => 401]; } + return self::update($userId, $params); } } diff --git a/app/Support/Validation/BomItemRules.php b/app/Support/Validation/BomItemRules.php index 42d31d3..aeadca3 100644 --- a/app/Support/Validation/BomItemRules.php +++ b/app/Support/Validation/BomItemRules.php @@ -1,4 +1,5 @@ 'required|string|in:MATERIAL,PRODUCT', - 'ref_id' => 'required|integer|min:1', - 'qty' => 'required|numeric|gt:0', + 'ref_type' => 'required|string|in:MATERIAL,PRODUCT', + 'ref_id' => 'required|integer|min:1', + 'qty' => 'required|numeric|gt:0', 'waste_rate' => 'nullable|numeric|min:0', - 'uom_id' => 'nullable|integer|min:1', - 'notes' => 'nullable|string|max:255', + 'uom_id' => 'nullable|integer|min:1', + 'notes' => 'nullable|string|max:255', 'sort_order' => 'nullable|integer', - 'category_id'=> 'nullable|integer', // 선택: 카테고리 분류 사용 시 + 'category_id' => 'nullable|integer', // 선택: 카테고리 분류 사용 시 ]; } } diff --git a/app/Swagger/v1/AdminApi.php b/app/Swagger/v1/AdminApi.php index 2b80334..f1d0f56 100644 --- a/app/Swagger/v1/AdminApi.php +++ b/app/Swagger/v1/AdminApi.php @@ -14,19 +14,24 @@ class AdminApi * summary="사용자 목록", * description="필터/검색/페이지네이션으로 사용자 목록을 조회합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="q", in="query", description="이름/이메일 검색어", @OA\Schema(type="string")), * @OA\Parameter(name="tenant_id", in="query", description="특정 테넌트로 필터", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="role", in="query", description="역할 코드", @OA\Schema(type="string", example="manager")), * @OA\Parameter(name="is_active", in="query", description="활성여부", @OA\Schema(type="boolean", example=1)), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", @@ -47,6 +52,7 @@ class AdminApi * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -64,11 +70,14 @@ public function index() {} * summary="사용자 생성", * description="새 사용자를 생성합니다. (초기 비밀번호/역할 포함 가능)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", * required={"name","email","password"}, + * * @OA\Property(property="user_id", type="string", example="test001"), * @OA\Property(property="name", type="string", example="김관리"), * @OA\Property(property="email", type="string", example="admin@kdcorp.co.kr"), @@ -77,16 +86,20 @@ public function index() {} * @OA\Property(property="roles", type="array", @OA\Items(type="string"), example={"manager"}) * ) * ), + * * @OA\Response( * response=201, * description="생성됨", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/User")) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -104,15 +117,20 @@ public function store() {} * summary="사용자 단건 조회", * description="ID 기준 사용자 상세", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={@OA\Schema(ref="#/components/schemas/ApiResponse")}, + * * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/User")) * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -129,17 +147,22 @@ public function show() {} * summary="사용자 수정", * description="이름/연락처/역할/활성여부 등 변경", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="name", type="string", example="김관리"), * @OA\Property(property="phone", type="string", example="010-3333-4444"), * @OA\Property(property="is_active", type="integer", example=1), * @OA\Property(property="roles", type="array", @OA\Items(type="string"), example={"manager","staff"}) * ) * ), + * * @OA\Response(response=200, description="수정 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -158,18 +181,23 @@ public function update() {} * summary="활성/비활성 전환", * description="지정된 사용자의 is_active 상태를 변경합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="사용자 고유 ID", + * * @OA\Schema(type="integer") * ), + * * @OA\Response( * response=200, * description="변경 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="테넌트 사용자 활성/비활성 성공"), * @OA\Property( @@ -179,6 +207,7 @@ public function update() {} * ) * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -196,16 +225,21 @@ public function toggleStatus() {} * summary="사용자 삭제(소프트 삭제)", * description="deleted_at / deleted_by 기록", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=204, * description="삭제성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="string", example="Success") * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -222,16 +256,21 @@ public function destroy() {} * summary="삭제 복구", * description="소프트 삭제 복구", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=204, * description="삭제 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="string", example="Success") * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -248,10 +287,14 @@ public function restore() {} * summary="역할 부여", * description="사용자에게 역할 추가", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, + * * @OA\JsonContent(type="object", * required={"roles"}, + * * @OA\Property(property="roles", type="array", @OA\Items(type="string"), example={"manager"}) * ) * ), @@ -259,12 +302,15 @@ public function restore() {} * @OA\Response( * response=204, * description="부여 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -283,17 +329,22 @@ public function attachRoles() {} * summary="역할 해제", * description="사용자에서 특정 역할 제거", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="role", in="path", required=true, @OA\Schema(type="string", example="manager")), + * * @OA\Response( * response=204, * description="부여 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -313,26 +364,34 @@ public function detachRole() {} * - 관리자 권한 확인은 미들웨어/가드에서 처리됩니다. * - 기본적으로 응답에 비밀번호를 노출하지 않으며, return_password=1일 때만 임시 비밀번호를 반환합니다(운영 환경에서는 노출 비권장).", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="사용자 고유 ID", + * * @OA\Schema(type="integer") * ), + * * @OA\RequestBody( * required=false, + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="new_password", type="string", minLength=8, maxLength=64, example="Temp!1234", description="지정 시 해당 값으로 비밀번호 초기화, 미지정 시 서버에서 랜덤 생성"), * @OA\Property(property="return_password", type="integer", enum={0,1}, example=0, description="1이면 응답에 임시 비밀번호 포함(개발/테스트용)") * ) * ), + * * @OA\Response( * response=200, * description="초기화 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="테넌트 사용자 비밀번호 초기화 성공"), * @OA\Property( @@ -343,6 +402,7 @@ public function detachRole() {} * ) * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/AuditLogApi.php b/app/Swagger/v1/AuditLogApi.php index ecea023..4321133 100644 --- a/app/Swagger/v1/AuditLogApi.php +++ b/app/Swagger/v1/AuditLogApi.php @@ -16,6 +16,7 @@ class AuditLogApi * tags={"Design Audit"}, * summary="List audit logs", * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", minimum=1)), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", minimum=1, maximum=200)), * @OA\Parameter(name="target_type", in="query", @OA\Schema(type="string")), @@ -24,11 +25,14 @@ class AuditLogApi * @OA\Parameter(name="actor_id", in="query", @OA\Schema(type="integer")), * @OA\Parameter(name="from", in="query", @OA\Schema(type="string", format="date-time")), * @OA\Parameter(name="to", in="query", @OA\Schema(type="string", format="date-time")), + * * @OA\Response( * response=200, * description="List", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean"), * @OA\Property(property="message", type="string"), * @OA\Property(property="data", type="object", diff --git a/app/Swagger/v1/AuthApi.php b/app/Swagger/v1/AuthApi.php index 9134120..41b00d5 100644 --- a/app/Swagger/v1/AuthApi.php +++ b/app/Swagger/v1/AuthApi.php @@ -17,6 +17,7 @@ * schema="SignupRequest", * type="object", * required={"user_id","name","email","password"}, + * * @OA\Property(property="user_id", type="string", maxLength=255, example="userId", description="로그인 ID (고유)"), * @OA\Property(property="name", type="string", maxLength=255, example="Kent"), * @OA\Property(property="email", type="string", maxLength=100, example="codebridge@gmail.com"), @@ -27,6 +28,7 @@ * @OA\Schema( * schema="SignupResponseData", * type="object", + * * @OA\Property( * property="user", * ref="#/components/schemas/Member" @@ -38,13 +40,13 @@ * type="object", * description="회원 요약 정보(회원가입 응답용)", * required={"id","user_id","name","email","phone"}, + * * @OA\Property(property="id", type="integer", example=6), * @OA\Property(property="user_id", type="string", example="userId"), * @OA\Property(property="name", type="string", example="Kent"), * @OA\Property(property="email", type="string", example="codebridge1@gmail.com"), * @OA\Property(property="phone", type="string", example="010-4820-9104") * ) - * */ class AuthApi { @@ -54,6 +56,7 @@ class AuthApi * tags={"Auth"}, * summary="API Key 인증 확인", * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Response(response=200, description="API Key 인증 성공"), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -66,19 +69,25 @@ public function debugApiKey() {} * tags={"Auth"}, * summary="로그인 (토큰 발급)", * security={{"ApiKeyAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"user_id","user_pwd"}, + * * @OA\Property(property="user_id", type="string", example="hamss"), * @OA\Property(property="user_pwd", type="string", example="StrongPass!1234") * ) * ), + * * @OA\Response( * response=200, * description="로그인 성공", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="로그인 성공"), * @OA\Property(property="data", type="object", @@ -86,6 +95,7 @@ public function debugApiKey() {} * ) * ) * ), + * * @OA\Response(response=401, description="로그인 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -97,15 +107,19 @@ public function login() {} * tags={"Auth"}, * summary="로그아웃 (Access 및 Token 무효화)", * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="로그아웃 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="로그아웃 성공"), * @OA\Property(property="data", type="null", example=null) * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -118,14 +132,19 @@ public function logout() {} * summary="회원가입", * description="신규 회원을 생성합니다. (API Key 필요)", * security={{"ApiKeyAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/SignupRequest")), + * * @OA\Response( * response=200, * description="회원가입 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", @@ -135,6 +154,7 @@ public function logout() {} * } * ) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패(API Key 누락/오류)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) diff --git a/app/Swagger/v1/BomCalculationApi.php b/app/Swagger/v1/BomCalculationApi.php index 97c030d..0756f5e 100644 --- a/app/Swagger/v1/BomCalculationApi.php +++ b/app/Swagger/v1/BomCalculationApi.php @@ -9,6 +9,7 @@ * schema="EstimateParametersResponse", * type="object", * required={"success","message","data"}, + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="견적 파라미터를 성공적으로 조회했습니다."), * @OA\Property( @@ -35,8 +36,10 @@ * @OA\Property( * property="required_parameters", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="key", type="string", example="W0"), * @OA\Property(property="label", type="string", example="오픈사이즈 가로(mm)"), * @OA\Property(property="type", type="string", example="integer"), @@ -53,6 +56,7 @@ * schema="CalculateBomRequest", * type="object", * required={"parameters"}, + * * @OA\Property( * property="parameters", * type="object", @@ -67,6 +71,7 @@ * schema="CalculateBomResponse", * type="object", * required={"success","message","data"}, + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="BOM 계산이 완료되었습니다."), * @OA\Property( @@ -98,8 +103,10 @@ * @OA\Property( * property="bom_items", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="item_id", type="integer", example=1), * @OA\Property(property="ref_type", type="string", example="MATERIAL"), * @OA\Property(property="ref_id", type="integer", example=101), @@ -116,6 +123,7 @@ * schema="CompanyFormulasResponse", * type="object", * required={"success","message","data"}, + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="업체 산출식 목록을 조회했습니다."), * @OA\Property( @@ -126,8 +134,10 @@ * @OA\Property( * property="formulas", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="type", type="string", example="manufacturing_size"), * @OA\Property(property="version", type="string", example="v2.0"), * @OA\Property(property="description", type="string", example="제작사이즈 계산식"), @@ -141,25 +151,32 @@ * schema="SaveFormulaRequest", * type="object", * required={"formula_expression","parameters"}, + * * @OA\Property(property="formula_expression", type="string", example="bracket_quantity"), * @OA\Property( * property="parameters", * type="array", + * * @OA\Items(type="string"), * example={"W1", "H1"} * ), + * * @OA\Property( * property="conditions", * type="array", + * * @OA\Items(type="string"), * example={"product_type=screen"} * ), + * * @OA\Property( * property="validation_rules", * type="array", + * * @OA\Items(type="string"), * example={"W1>0", "H1>0"} * ), + * * @OA\Property(property="description", type="string", example="브라켓 수량 계산식") * ) * @@ -167,6 +184,7 @@ * schema="FormulaTestRequest", * type="object", * required={"formula_expression","test_parameters"}, + * * @OA\Property(property="formula_expression", type="string", example="bracket_quantity"), * @OA\Property( * property="test_parameters", @@ -179,6 +197,7 @@ * schema="FormulaTestResponse", * type="object", * required={"success","message","data"}, + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="계산식 테스트가 완료되었습니다."), * @OA\Property( @@ -208,28 +227,36 @@ class BomCalculationApi * description="특정 모델의 견적 시 필요한 입력 파라미터 스키마를 조회합니다. BOM에 정의된 조건만 동적으로 추출하여 반환합니다.", * tags={"BOM Calculation"}, * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Parameter( * name="model_id", * in="path", * required=true, * description="모델 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Parameter( * name="company_name", * in="query", * required=false, * description="업체명 (선택사항)", + * * @OA\Schema(type="string", example="경동기업") * ), + * * @OA\Response( * response=200, * description="견적 파라미터 조회 성공", + * * @OA\JsonContent(ref="#/components/schemas/EstimateParametersResponse") * ), + * * @OA\Response( * response=404, * description="모델을 찾을 수 없음", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -243,26 +270,34 @@ public function getEstimateParameters() {} * description="입력된 파라미터를 기반으로 BOM 수량을 동적으로 계산합니다. 업체별 산출식을 적용하여 실시간 견적을 생성합니다.", * tags={"BOM Calculation"}, * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Parameter( * name="bom_template_id", * in="path", * required=true, * description="BOM 템플릿 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\RequestBody( * required=true, * description="계산 파라미터", + * * @OA\JsonContent(ref="#/components/schemas/CalculateBomRequest") * ), + * * @OA\Response( * response=200, * description="BOM 계산 성공", + * * @OA\JsonContent(ref="#/components/schemas/CalculateBomResponse") * ), + * * @OA\Response( * response=400, * description="잘못된 파라미터", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -276,21 +311,27 @@ public function calculateBom() {} * description="특정 업체의 등록된 산출식 목록을 조회합니다.", * tags={"BOM Calculation"}, * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Parameter( * name="company_name", * in="path", * required=true, * description="업체명", + * * @OA\Schema(type="string", example="경동기업") * ), + * * @OA\Response( * response=200, * description="업체 산출식 목록 조회 성공", + * * @OA\JsonContent(ref="#/components/schemas/CompanyFormulasResponse") * ), + * * @OA\Response( * response=404, * description="업체를 찾을 수 없음", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -304,32 +345,42 @@ public function getCompanyFormulas() {} * description="특정 업체의 산출식을 등록하거나 수정합니다. 기존 산출식이 있으면 새 버전으로 업데이트됩니다.", * tags={"BOM Calculation"}, * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\Parameter( * name="company_name", * in="path", * required=true, * description="업체명", + * * @OA\Schema(type="string", example="경동기업") * ), + * * @OA\Parameter( * name="formula_type", * in="path", * required=true, * description="산출식 타입", + * * @OA\Schema(type="string", example="manufacturing_size") * ), + * * @OA\RequestBody( * required=true, * description="산출식 데이터", + * * @OA\JsonContent(ref="#/components/schemas/SaveFormulaRequest") * ), + * * @OA\Response( * response=201, * description="업체 산출식 등록 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", @@ -344,9 +395,11 @@ public function getCompanyFormulas() {} * } * ) * ), + * * @OA\Response( * response=400, * description="잘못된 요청", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) @@ -360,22 +413,28 @@ public function saveCompanyFormula() {} * description="산출식을 실제 적용하기 전에 테스트해볼 수 있습니다.", * tags={"BOM Calculation"}, * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, * description="테스트할 계산식과 파라미터", + * * @OA\JsonContent(ref="#/components/schemas/FormulaTestRequest") * ), + * * @OA\Response( * response=200, * description="계산식 테스트 성공", + * * @OA\JsonContent(ref="#/components/schemas/FormulaTestResponse") * ), + * * @OA\Response( * response=400, * description="잘못된 계산식", + * * @OA\JsonContent(ref="#/components/schemas/ErrorResponse") * ) * ) */ public function testFormula() {} -} \ No newline at end of file +} diff --git a/app/Swagger/v1/CategoryApi.php b/app/Swagger/v1/CategoryApi.php index 55bc3a3..6ea9614 100644 --- a/app/Swagger/v1/CategoryApi.php +++ b/app/Swagger/v1/CategoryApi.php @@ -9,6 +9,7 @@ * schema="Category", * type="object", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=12), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="parent_id", type="integer", nullable=true, example=null), @@ -24,12 +25,15 @@ * @OA\Schema( * schema="CategoryPagination", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/Category") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/categories?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=3), @@ -37,7 +41,9 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -55,6 +61,7 @@ * schema="CategoryCreateRequest", * type="object", * required={"name"}, + * * @OA\Property(property="parent_id", type="integer", nullable=true), * @OA\Property(property="code", type="string", nullable=true, maxLength=50), * @OA\Property(property="name", type="string", maxLength=100), @@ -66,6 +73,7 @@ * @OA\Schema( * schema="CategoryUpdateRequest", * type="object", + * * @OA\Property(property="parent_id", type="integer", nullable=true), * @OA\Property(property="code", type="string", nullable=true, maxLength=50), * @OA\Property(property="name", type="string", maxLength=100), @@ -77,6 +85,7 @@ * @OA\Schema( * schema="CategoryTreeNode", * type="object", + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="code", type="string", nullable=true, example="ELC"), * @OA\Property(property="name", type="string", example="전자"), @@ -93,17 +102,22 @@ class CategoryApi * tags={"Category"}, * summary="카테고리 목록", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)), * @OA\Parameter(name="q", in="query", description="코드/이름 검색", @OA\Schema(type="string")), * @OA\Parameter(name="parent_id", in="query", @OA\Schema(type="integer", nullable=true)), * @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -115,13 +129,18 @@ public function index() {} * tags={"Category"}, * summary="카테고리 단건 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Category")) * }) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -133,13 +152,18 @@ public function show() {} * tags={"Category"}, * summary="카테고리 생성", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryCreateRequest")), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Category")) * }) * ), + * * @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -151,10 +175,15 @@ public function store() {} * tags={"Category"}, * summary="카테고리 수정", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Category")) * }) @@ -169,7 +198,9 @@ public function update() {} * tags={"Category"}, * summary="카테고리 삭제(soft)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -181,9 +212,13 @@ public function destroy() {} * tags={"Category"}, * summary="활성/비활성 토글", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="변경 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Category")) * }) @@ -198,14 +233,20 @@ public function toggle() {} * tags={"Category"}, * summary="부모/순서 이동", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent( * type="object", + * * @OA\Property(property="parent_id", type="integer", nullable=true), * @OA\Property(property="sort_order", type="integer", nullable=true) * )), + * * @OA\Response(response=200, description="이동 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Category")) * }) @@ -220,14 +261,17 @@ public function move() {} * tags={"Category"}, * summary="정렬순서 일괄 변경", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent( * type="object", + * * @OA\Property(property="items", type="array", @OA\Items( * type="object", * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="sort_order", type="integer", example=10) * )) * )), + * * @OA\Response(response=200, description="저장 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -239,9 +283,13 @@ public function reorder() {} * tags={"Category"}, * summary="카테고리 트리", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/CategoryTreeNode"))) * }) diff --git a/app/Swagger/v1/CategoryExtras.php b/app/Swagger/v1/CategoryExtras.php index 5c0488c..07c3be8 100644 --- a/app/Swagger/v1/CategoryExtras.php +++ b/app/Swagger/v1/CategoryExtras.php @@ -19,6 +19,7 @@ * schema="CategoryField", * type="object", * required={"id","tenant_id","category_id","field_key","field_name","field_type"}, + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="category_id", type="integer", example=7), @@ -43,12 +44,15 @@ * @OA\Schema( * schema="CategoryFieldPagination", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/CategoryField") * ), + * * @OA\Property(property="per_page", type="integer", example=20), * @OA\Property(property="total", type="integer", example=3) * ) @@ -57,6 +61,7 @@ * schema="CategoryFieldCreateRequest", * type="object", * required={"field_key","field_name","field_type"}, + * * @OA\Property(property="field_key", type="string", example="height"), * @OA\Property(property="field_name", type="string", example="높이(mm)"), * @OA\Property(property="field_type", type="string", example="number"), @@ -75,6 +80,7 @@ * @OA\Schema( * schema="CategoryFieldUpdateRequest", * type="object", + * * @OA\Property(property="field_key", type="string", example="height"), * @OA\Property(property="field_name", type="string", example="높이(mm)"), * @OA\Property(property="field_type", type="string", example="number"), @@ -93,9 +99,11 @@ * @OA\Schema( * schema="CategoryFieldReorderRequest", * type="array", + * * @OA\Items( * type="object", * required={"id","sort_order"}, + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="sort_order", type="integer", example=1) * ) @@ -105,11 +113,14 @@ * schema="CategoryFieldBulkUpsertRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="id", type="integer", nullable=true, example=null), * @OA\Property(property="field_key", type="string", example="thickness"), * @OA\Property(property="field_name", type="string", example="두께(mm)"), @@ -127,6 +138,7 @@ * schema="CategoryTemplate", * type="object", * required={"id","tenant_id","category_id","version_no","template_json","applied_at"}, + * * @OA\Property(property="id", type="integer", example=1001), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="category_id", type="integer", example=7), @@ -147,6 +159,7 @@ * schema="CategoryTemplateCreateRequest", * type="object", * required={"version_no","template_json","applied_at"}, + * * @OA\Property(property="version_no", type="integer", example=4), * @OA\Property( * property="template_json", @@ -165,6 +178,7 @@ * @OA\Schema( * schema="CategoryTemplateUpdateRequest", * type="object", + * * @OA\Property( * property="template_json", * type="object", @@ -178,6 +192,7 @@ * schema="CategoryLog", * type="object", * required={"id","tenant_id","category_id","action","changed_at"}, + * * @OA\Property(property="id", type="integer", example=501), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="category_id", type="integer", example=7), @@ -203,19 +218,24 @@ class CategoryExtras * tags={"Category-Fields"}, * summary="카테고리별 필드 목록", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), description="카테고리 ID"), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), * @OA\Parameter(name="sort", in="query", @OA\Schema(type="string", example="sort_order")), * @OA\Parameter(name="order", in="query", @OA\Schema(type="string", enum={"asc","desc"}, example="asc")), + * * @OA\Response( * response=200, * description="목록 조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryFieldPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -227,16 +247,22 @@ public function fieldsIndex() {} * tags={"Category-Fields"}, * summary="카테고리 필드 생성", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryFieldCreateRequest")), + * * @OA\Response( * response=200, * description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryField")) * }) * ), + * * @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -248,15 +274,20 @@ public function fieldsStore() {} * tags={"Category-Fields"}, * summary="카테고리 필드 단건 조회", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="field", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryField")) * }) * ), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -268,12 +299,17 @@ public function fieldsShow() {} * tags={"Category-Fields"}, * summary="카테고리 필드 수정", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="field", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryFieldUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryField")) * }) @@ -288,10 +324,13 @@ public function fieldsUpdate() {} * tags={"Category-Fields"}, * summary="카테고리 필드 삭제", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="field", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="삭제 성공", + * * @OA\JsonContent(ref="#/components/schemas/ApiResponse") * ) * ) @@ -304,8 +343,11 @@ public function fieldsDestroy() {} * tags={"Category-Fields"}, * summary="카테고리 필드 정렬 저장", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryFieldReorderRequest")), + * * @OA\Response(response=200, description="저장 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -317,14 +359,20 @@ public function fieldsReorder() {} * tags={"Category-Fields"}, * summary="카테고리 필드 일괄 업서트", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryFieldBulkUpsertRequest")), + * * @OA\Response( * response=200, * description="업서트 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="created", type="integer", example=2), * @OA\Property(property="updated", type="integer", example=3) @@ -348,23 +396,30 @@ public function fieldsBulkUpsert() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 버전 목록", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, * description="목록 조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/CategoryTemplate") * ), + * * @OA\Property(property="total", type="integer", example=3) * ) * ) @@ -380,16 +435,22 @@ public function templatesIndex() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 생성(새 버전)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryTemplateCreateRequest")), + * * @OA\Response( * response=200, * description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryTemplate")) * }) * ), + * * @OA\Response(response=409, description="버전 중복", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -401,11 +462,15 @@ public function templatesStore() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 단건 조회", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="tpl", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryTemplate")) * }) @@ -420,12 +485,17 @@ public function templatesShow() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 수정", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="tpl", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CategoryTemplateUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryTemplate")) * }) @@ -440,7 +510,9 @@ public function templatesUpdate() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 삭제", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="tpl", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -452,8 +524,10 @@ public function templatesDestroy() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 적용(활성화)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="tpl", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="적용 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -465,11 +539,14 @@ public function templatesApply() {} * tags={"Category-Templates"}, * summary="카테고리 템플릿 미리보기", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="tpl", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="미리보기 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse")}) * ) * ) @@ -482,15 +559,20 @@ public function templatesPreview() {} * tags={"Category-Templates"}, * summary="두 버전 비교(diff)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="a", in="query", required=true, @OA\Schema(type="integer"), description="비교 기준 버전"), * @OA\Parameter(name="b", in="query", required=true, @OA\Schema(type="integer"), description="비교 대상 버전"), + * * @OA\Response( * response=200, * description="비교 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="added", type="array", @OA\Items(type="string")), * @OA\Property(property="removed", type="array", @OA\Items(type="string")) @@ -514,26 +596,33 @@ public function templatesDiff() {} * tags={"Category-Logs"}, * summary="카테고리 변경 이력 목록", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="action", in="query", @OA\Schema(type="string", enum={"insert","update","delete"})), * @OA\Parameter(name="from", in="query", @OA\Schema(type="string", format="date")), * @OA\Parameter(name="to", in="query", @OA\Schema(type="string", format="date")), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, * description="목록 조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/CategoryLog") * ), + * * @OA\Property(property="total", type="integer", example=12) * ) * ) @@ -549,11 +638,15 @@ public function logsIndex() {} * tags={"Category-Logs"}, * summary="카테고리 변경 이력 단건 조회", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="log", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CategoryLog")) * }) diff --git a/app/Swagger/v1/ClassificationApi.php b/app/Swagger/v1/ClassificationApi.php index 1efbe9c..f7f1721 100644 --- a/app/Swagger/v1/ClassificationApi.php +++ b/app/Swagger/v1/ClassificationApi.php @@ -9,6 +9,7 @@ * schema="Classification", * type="object", * required={"id","group","name"}, + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="group", type="string", example="product_type"), @@ -25,12 +26,15 @@ * schema="ClassificationPagination", * type="object", * description="라라벨 LengthAwarePaginator 기본 구조", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/Classification") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/classifications?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=3), @@ -38,7 +42,9 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -56,6 +62,7 @@ * schema="ClassificationCreateRequest", * type="object", * required={"group","name"}, + * * @OA\Property(property="group", type="string", maxLength=50, example="product_type"), * @OA\Property(property="code", type="string", nullable=true, maxLength=50, example="OUTER"), * @OA\Property(property="name", type="string", maxLength=100, example="아우터"), @@ -67,6 +74,7 @@ * @OA\Schema( * schema="ClassificationUpdateRequest", * type="object", + * * @OA\Property(property="group", type="string", maxLength=50), * @OA\Property(property="code", type="string", nullable=true, maxLength=50), * @OA\Property(property="name", type="string", maxLength=100), @@ -84,19 +92,24 @@ class ClassificationApi * summary="분류 목록", * description="그룹/검색/활성여부 조건으로 분류 목록을 페이징 반환합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)), * @OA\Parameter(name="q", in="query", description="코드/이름 검색", @OA\Schema(type="string")), * @OA\Parameter(name="group", in="query", description="분류 그룹 키", @OA\Schema(type="string")), * @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClassificationPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -108,15 +121,20 @@ public function index() {} * tags={"Classification"}, * summary="분류 단건 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification")) * }) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -128,15 +146,20 @@ public function show() {} * tags={"Classification"}, * summary="분류 생성", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClassificationCreateRequest")), + * * @OA\Response( * response=200, * description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification")) * }) * ), + * * @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -148,16 +171,22 @@ public function store() {} * tags={"Classification"}, * summary="분류 수정", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClassificationUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification")) * }) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -169,7 +198,9 @@ public function update() {} * tags={"Classification"}, * summary="분류 삭제(soft)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) diff --git a/app/Swagger/v1/ClientApi.php b/app/Swagger/v1/ClientApi.php index 1690777..74a765c 100644 --- a/app/Swagger/v1/ClientApi.php +++ b/app/Swagger/v1/ClientApi.php @@ -9,6 +9,7 @@ * schema="Client", * type="object", * required={"id","client_code","name"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID"), @@ -26,12 +27,15 @@ * @OA\Schema( * schema="ClientPagination", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/Client") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/clients?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=3), @@ -39,7 +43,9 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -57,6 +63,7 @@ * schema="ClientCreateRequest", * type="object", * required={"client_code","name"}, + * * @OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID"), * @OA\Property(property="client_code", type="string", maxLength=50, example="CLIENT_001"), * @OA\Property(property="name", type="string", maxLength=100, example="거래처명"), @@ -70,6 +77,7 @@ * @OA\Schema( * schema="ClientUpdateRequest", * type="object", + * * @OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID"), * @OA\Property(property="client_code", type="string", maxLength=50), * @OA\Property(property="name", type="string", maxLength=100), @@ -88,16 +96,21 @@ class ClientApi * tags={"Client"}, * summary="거래처 목록", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)), * @OA\Parameter(name="q", in="query", description="거래처 코드/이름 검색", @OA\Schema(type="string")), * @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -109,13 +122,18 @@ public function index() {} * tags={"Client"}, * summary="거래처 단건 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Client")) * }) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -127,13 +145,18 @@ public function show() {} * tags={"Client"}, * summary="거래처 생성", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClientCreateRequest")), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Client")) * }) * ), + * * @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -145,10 +168,15 @@ public function store() {} * tags={"Client"}, * summary="거래처 수정", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClientUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Client")) * }) @@ -163,7 +191,9 @@ public function update() {} * tags={"Client"}, * summary="거래처 삭제(soft)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -175,9 +205,13 @@ public function destroy() {} * tags={"Client"}, * summary="활성/비활성 토글", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="변경 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Client")) * }) diff --git a/app/Swagger/v1/ClientGroupApi.php b/app/Swagger/v1/ClientGroupApi.php index 26d0e4c..eb748bb 100644 --- a/app/Swagger/v1/ClientGroupApi.php +++ b/app/Swagger/v1/ClientGroupApi.php @@ -9,6 +9,7 @@ * schema="ClientGroup", * type="object", * required={"id","group_code","group_name","price_rate"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="group_code", type="string", example="VIP"), @@ -22,12 +23,15 @@ * @OA\Schema( * schema="ClientGroupPagination", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/ClientGroup") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/client-groups?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=3), @@ -35,7 +39,9 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -53,6 +59,7 @@ * schema="ClientGroupCreateRequest", * type="object", * required={"group_code","group_name","price_rate"}, + * * @OA\Property(property="group_code", type="string", maxLength=30, example="VIP", description="그룹 코드"), * @OA\Property(property="group_name", type="string", maxLength=100, example="VIP 고객", description="그룹명"), * @OA\Property(property="price_rate", type="number", format="decimal", example=0.9, description="가격 배율 (1.0=기준가)"), @@ -62,6 +69,7 @@ * @OA\Schema( * schema="ClientGroupUpdateRequest", * type="object", + * * @OA\Property(property="group_code", type="string", maxLength=30), * @OA\Property(property="group_name", type="string", maxLength=100), * @OA\Property(property="price_rate", type="number", format="decimal"), @@ -76,16 +84,21 @@ class ClientGroupApi * tags={"ClientGroup"}, * summary="고객 그룹 목록", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)), * @OA\Parameter(name="q", in="query", description="그룹 코드/이름 검색", @OA\Schema(type="string")), * @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientGroupPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -97,13 +110,18 @@ public function index() {} * tags={"ClientGroup"}, * summary="고객 그룹 단건 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientGroup")) * }) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -116,13 +134,18 @@ public function show() {} * summary="고객 그룹 생성", * description="고객 그룹을 생성합니다. 같은 group_code로 이전에 삭제된 그룹이 있으면 자동으로 복원하고 새 데이터로 업데이트합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClientGroupCreateRequest")), + * * @OA\Response(response=200, description="생성 성공 (또는 삭제된 데이터 복원)", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientGroup")) * }) * ), + * * @OA\Response(response=400, description="검증 실패 또는 중복 코드", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -134,10 +157,15 @@ public function store() {} * tags={"ClientGroup"}, * summary="고객 그룹 수정", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClientGroupUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientGroup")) * }) @@ -152,7 +180,9 @@ public function update() {} * tags={"ClientGroup"}, * summary="고객 그룹 삭제(soft)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -164,9 +194,13 @@ public function destroy() {} * tags={"ClientGroup"}, * summary="활성/비활성 토글", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="변경 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClientGroup")) * }) @@ -174,4 +208,4 @@ public function destroy() {} * ) */ public function toggle() {} -} \ No newline at end of file +} diff --git a/app/Swagger/v1/CommonApi.php b/app/Swagger/v1/CommonApi.php index a2dc3f5..d35fea9 100644 --- a/app/Swagger/v1/CommonApi.php +++ b/app/Swagger/v1/CommonApi.php @@ -2,6 +2,4 @@ namespace App\Swagger\v1; - - - class CommonApi {} +class CommonApi {} diff --git a/app/Swagger/v1/CommonComponents.php b/app/Swagger/v1/CommonComponents.php index f79bc1d..1eaa9c0 100644 --- a/app/Swagger/v1/CommonComponents.php +++ b/app/Swagger/v1/CommonComponents.php @@ -11,6 +11,7 @@ * in="query", * required=false, * description="페이지 번호(1부터)", + * * @OA\Schema(type="integer", minimum=1, example=1) * ) * @@ -20,12 +21,13 @@ * in="query", * required=false, * description="페이지 당 개수(기본 20)", + * * @OA\Schema(type="integer", minimum=1, maximum=200, example=20) * ) - * * @OA\Schema( * schema="ApiResponse", * type="object", + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="요청 성공"), * @OA\Property(property="data", nullable=true) @@ -34,6 +36,7 @@ * @OA\Schema( * schema="PaginationMeta", * type="object", + * * @OA\Property(property="page", type="integer", example=1), * @OA\Property(property="size", type="integer", example=20), * @OA\Property(property="total", type="integer", example=126), @@ -44,6 +47,7 @@ * schema="User", * type="object", * required={"id","name","email"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="name", type="string", example="홍길동"), @@ -59,6 +63,7 @@ * schema="LoginRequest", * type="object", * required={"email","password"}, + * * @OA\Property(property="email", type="string", example="hong@kdcorp.co.kr"), * @OA\Property(property="password", type="string", example="secret1234!") * ) @@ -66,6 +71,7 @@ * @OA\Schema( * schema="TokenResponse", * type="object", + * * @OA\Property(property="access_token", type="string", example="eyJhbGciOi..."), * @OA\Property(property="token_type", type="string", example="Bearer"), * @OA\Property(property="expires_in", type="integer", example=3600) @@ -74,6 +80,7 @@ * @OA\Schema( * schema="UserUpdateRequest", * type="object", + * * @OA\Property(property="name", type="string", example="홍길동"), * @OA\Property(property="phone", type="string", example="010-1111-2222") * ) @@ -82,6 +89,7 @@ * schema="PasswordChangeRequest", * type="object", * required={"current_password","new_password"}, + * * @OA\Property(property="current_password", type="string", example="oldPass!234"), * @OA\Property(property="new_password", type="string", example="NewPass!234") * ) @@ -90,6 +98,7 @@ * schema="SwitchTenantRequest", * type="object", * required={"tenant_id"}, + * * @OA\Property(property="tenant_id", type="integer", example=2, description="스위치할 테넌트 ID") * ) * @@ -97,6 +106,7 @@ * schema="ErrorResponse", * type="object", * description="공통 에러 응답 포맷", + * * @OA\Property(property="success", type="boolean", example=false), * @OA\Property(property="message", type="string", example="요청 실패"), * @OA\Property( diff --git a/app/Swagger/v1/DepartmentApi.php b/app/Swagger/v1/DepartmentApi.php index a484bb7..b2660a1 100644 --- a/app/Swagger/v1/DepartmentApi.php +++ b/app/Swagger/v1/DepartmentApi.php @@ -21,6 +21,7 @@ * type="object", * description="부서 상세", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=7), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="code", type="string", nullable=true, example="OPS"), @@ -37,6 +38,7 @@ * type="object", * description="부서 요약", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=7), * @OA\Property(property="code", type="string", nullable=true, example="OPS"), * @OA\Property(property="name", type="string", example="운영팀"), @@ -47,6 +49,7 @@ * @OA\Schema( * schema="DepartmentList", * type="array", + * * @OA\Items(ref="#/components/schemas/DepartmentBrief") * ) * @@ -54,6 +57,7 @@ * schema="DepartmentCreateRequest", * type="object", * required={"name"}, + * * @OA\Property(property="code", type="string", nullable=true, example="OPS"), * @OA\Property(property="name", type="string", example="운영팀"), * @OA\Property(property="description", type="string", nullable=true, example="서비스 운영 총괄"), @@ -64,6 +68,7 @@ * @OA\Schema( * schema="DepartmentUpdateRequest", * type="object", + * * @OA\Property(property="code", type="string", nullable=true, example="OPS2"), * @OA\Property(property="name", type="string", example="운영기획팀"), * @OA\Property(property="description", type="string", nullable=true, example="운영 기획/성과 관리"), @@ -75,6 +80,7 @@ * schema="DepartmentUserAttachRequest", * type="object", * required={"user_id"}, + * * @OA\Property(property="user_id", type="integer", example=12), * @OA\Property(property="is_primary", type="integer", enum={0,1}, nullable=true, example=0), * @OA\Property(property="joined_at", type="string", format="date-time", nullable=true, example="2025-08-21 10:00:00") @@ -85,6 +91,7 @@ * type="object", * description="부서원 요약", * required={"id","name","email"}, + * * @OA\Property(property="id", type="integer", example=12), * @OA\Property(property="name", type="string", example="홍길동"), * @OA\Property(property="email", type="string", format="email", example="hong@example.com"), @@ -96,6 +103,7 @@ * schema="DepartmentPermissionUpsertSingle", * type="object", * required={"permission_id"}, + * * @OA\Property(property="permission_id", type="integer", example=25), * @OA\Property(property="is_allowed", type="integer", enum={0,1}, example=1, description="1=ALLOW, 0=DENY(차단)") * ) @@ -104,9 +112,11 @@ * schema="DepartmentPermissionUpsertMany", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items(ref="#/components/schemas/DepartmentPermissionUpsertSingle") * ) * ) @@ -115,6 +125,7 @@ * schema="DepartmentPermissionRevokeSingle", * type="object", * required={"permission_id"}, + * * @OA\Property(property="permission_id", type="integer", example=25) * ) * @@ -122,14 +133,15 @@ * schema="DepartmentPermissionRevokeMany", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items(ref="#/components/schemas/DepartmentPermissionRevokeSingle") * ) * ) */ - class DepartmentApi { /** @@ -139,12 +151,16 @@ class DepartmentApi * description="테넌트 범위 내 부서 목록을 페이징으로 반환합니다. (q로 이름/코드 검색)", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer", example=10)), * @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string", example="운영")), * @OA\Parameter(name="is_active", in="query", required=false, @OA\Schema(type="integer", enum={0,1}, example=1)), + * * @OA\Response(response=200, description="부서 목록 조회 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 목록 조회 성공"), * @OA\Property(property="data", type="object", @@ -155,6 +171,7 @@ class DepartmentApi * ) * ) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -166,14 +183,19 @@ public function index() {} * summary="부서 단건 조회", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\Response(response=200, description="부서 조회 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 조회 성공"), * @OA\Property(property="data", ref="#/components/schemas/Department") * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -185,14 +207,19 @@ public function show() {} * summary="부서 생성", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/DepartmentCreateRequest")), + * * @OA\Response(response=200, description="부서 생성 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 생성 성공"), * @OA\Property(property="data", ref="#/components/schemas/Department") * ) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -204,15 +231,21 @@ public function store() {} * summary="부서 수정", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/DepartmentUpdateRequest")), + * * @OA\Response(response=200, description="부서 수정 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 수정 성공"), * @OA\Property(property="data", ref="#/components/schemas/Department") * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -225,9 +258,13 @@ public function update() {} * summary="부서 삭제(소프트)", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\Response(response=200, description="부서 삭제 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 삭제 성공"), * @OA\Property(property="data", type="object", @@ -236,6 +273,7 @@ public function update() {} * ) * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -248,11 +286,15 @@ public function destroy() {} * description="해당 부서에 속한 사용자 목록을 페이징으로 반환합니다.", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer", example=20)), + * * @OA\Response(response=200, description="부서 사용자 목록 조회 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 사용자 목록 조회 성공"), * @OA\Property(property="data", type="object", @@ -263,6 +305,7 @@ public function destroy() {} * ) * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -274,10 +317,15 @@ public function users() {} * summary="부서 사용자 배정(단건)", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/DepartmentUserAttachRequest")), + * * @OA\Response(response=200, description="부서 사용자 배정 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 사용자 배정 성공"), * @OA\Property(property="data", type="object", @@ -286,6 +334,7 @@ public function users() {} * ) * ) * ), + * * @OA\Response(response=409, description="이미 배정됨", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) @@ -299,10 +348,14 @@ public function attachUser() {} * summary="부서 사용자 해제(단건)", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), * @OA\Parameter(name="user", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\Response(response=200, description="부서 사용자 해제 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 사용자 해제 성공"), * @OA\Property(property="data", type="object", @@ -311,6 +364,7 @@ public function attachUser() {} * ) * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -322,16 +376,23 @@ public function detachUser() {} * summary="주부서 설정/해제", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), * @OA\Parameter(name="user", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( + * * @OA\Property(property="is_primary", type="integer", enum={0,1}, example=1) * ) * ), + * * @OA\Response(response=200, description="주부서 설정 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="주부서 설정 성공"), * @OA\Property(property="data", type="object", @@ -341,6 +402,7 @@ public function detachUser() {} * ) * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -354,13 +416,17 @@ public function setPrimary() {} * description="부서에 설정된 ALLOW/DENY 목록을 조회합니다. (is_allowed=1|0, menu_id 필터 지원)", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), * @OA\Parameter(name="menu_id", in="query", required=false, @OA\Schema(type="integer", example=101)), * @OA\Parameter(name="is_allowed", in="query", required=false, @OA\Schema(type="integer", enum={0,1}, example=1)), * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer", example=20)), + * * @OA\Response(response=200, description="부서 권한 목록 조회 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 권한 목록 조회 성공"), * @OA\Property(property="data", type="object", @@ -368,7 +434,9 @@ public function setPrimary() {} * @OA\Property(property="per_page", type="integer", example=20), * @OA\Property(property="total", type="integer", example=3), * @OA\Property(property="data", type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="permission_id", type="integer", example=25), * @OA\Property(property="permission_code", type="string", example="menu.101.read"), * @OA\Property(property="is_allowed", type="integer", example=1), @@ -380,6 +448,7 @@ public function setPrimary() {} * ) * ) * ), + * * @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -392,16 +461,23 @@ public function listPermissions() {} * description="permission_id 기준으로 ALLOW(1) 또는 DENY(0) 처리합니다. 단건 또는 items 배열을 모두 지원합니다.", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent(oneOf={ + * * @OA\Schema(ref="#/components/schemas/DepartmentPermissionUpsertSingle"), * @OA\Schema(ref="#/components/schemas/DepartmentPermissionUpsertMany") * }) * ), + * * @OA\Response(response=200, description="부서 권한 적용 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 권한 적용 성공"), * @OA\Property(property="data", type="object", @@ -411,6 +487,7 @@ public function listPermissions() {} * ) * ) * ), + * * @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -424,16 +501,23 @@ public function upsertPermission() {} * description="지정 권한을 부서 매핑에서 제거합니다. 단건 또는 items 배열을 모두 지원합니다.", * tags={"Department"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=7)), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent(oneOf={ + * * @OA\Schema(ref="#/components/schemas/DepartmentPermissionRevokeSingle"), * @OA\Schema(ref="#/components/schemas/DepartmentPermissionRevokeMany") * }) * ), + * * @OA\Response(response=200, description="부서 권한 해제 성공", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="부서 권한 해제 성공"), * @OA\Property(property="data", type="object", @@ -443,6 +527,7 @@ public function upsertPermission() {} * ) * ) * ), + * * @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) diff --git a/app/Swagger/v1/DesignBomTemplateExtras.php b/app/Swagger/v1/DesignBomTemplateExtras.php index c54501c..eb97fc1 100644 --- a/app/Swagger/v1/DesignBomTemplateExtras.php +++ b/app/Swagger/v1/DesignBomTemplateExtras.php @@ -16,13 +16,17 @@ class DesignBomTemplateExtras * tags={"Design BOM"}, * summary="Diff two BOM templates", * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter(name="templateId", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="other_template_id", in="query", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="Success", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean"), * @OA\Property(property="message", type="string"), * @OA\Property(property="data", type="object", @@ -58,21 +62,28 @@ class DesignBomTemplateExtras * tags={"Design BOM"}, * summary="Clone a BOM template (deep copy)", * security={{"ApiKeyAuth": {}, "BearerAuth": {}}}, + * * @OA\Parameter(name="templateId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=false, + * * @OA\JsonContent(type="object", + * * @OA\Property(property="target_version_id", type="integer", nullable=true), * @OA\Property(property="name", type="string", nullable=true), * @OA\Property(property="is_primary", type="boolean", nullable=true), * @OA\Property(property="notes", type="string", nullable=true) * ) * ), + * * @OA\Response( * response=200, * description="Cloned", + * * @OA\JsonContent( * type="object", + * * @OA\Property(property="success", type="boolean"), * @OA\Property(property="message", type="string"), * @OA\Property(property="data", type="object", @@ -90,6 +101,7 @@ class DesignBomTemplateExtras * @OA\Schema( * schema="DesignBomItemDiffRow", * type="object", + * * @OA\Property(property="ref_type", type="string", example="MATERIAL"), * @OA\Property(property="ref_id", type="integer"), * @OA\Property(property="qty", type="number"), diff --git a/app/Swagger/v1/DesignModelApi.php b/app/Swagger/v1/DesignModelApi.php index dec54cb..719814c 100644 --- a/app/Swagger/v1/DesignModelApi.php +++ b/app/Swagger/v1/DesignModelApi.php @@ -17,6 +17,7 @@ * schema="DesignModel", * type="object", * required={"id","tenant_id","code","name"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="code", type="string", example="KSS01"), @@ -34,6 +35,7 @@ * schema="DesignModelPagination", * type="object", * description="라라벨 LengthAwarePaginator", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/DesignModel")), * @OA\Property(property="first_page_url", type="string", example="/api/v1/design/models?page=1"), @@ -58,6 +60,7 @@ * schema="ModelVersion", * type="object", * required={"id","tenant_id","model_id","version_no","status"}, + * * @OA\Property(property="id", type="integer", example=201), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="model_id", type="integer", example=101), @@ -76,6 +79,7 @@ * schema="BomTemplate", * type="object", * required={"id","tenant_id","model_version_id","name"}, + * * @OA\Property(property="id", type="integer", example=301), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="model_version_id", type="integer", example=201), @@ -92,6 +96,7 @@ * schema="BomTemplateItem", * type="object", * required={"id","tenant_id","bom_template_id","ref_type","ref_id","qty"}, + * * @OA\Property(property="id", type="integer", example=401), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="bom_template_id", type="integer", example=301), @@ -110,12 +115,15 @@ * schema="BomTemplateItemReplaceRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items( * type="object", * required={"ref_type","ref_id","qty"}, + * * @OA\Property(property="ref_type", type="string", enum={"MATERIAL","PRODUCT"}, example="MATERIAL"), * @OA\Property(property="ref_id", type="integer", example=101), * @OA\Property(property="qty", type="number", format="double", example=2), @@ -140,18 +148,23 @@ class DesignModelApi * summary="모델 목록", * description="모델(설계 상위) 목록을 페이징으로 조회합니다.", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="q", in="query", description="검색어(code/name/description like)", @OA\Schema(type="string")), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/DesignModelPagination")) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -165,10 +178,13 @@ public function index() {} * summary="모델 생성", * description="모델(설계 상위)을 생성합니다.", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( * required={"code","name"}, + * * @OA\Property(property="code", type="string", maxLength=100, example="KSS01"), * @OA\Property(property="name", type="string", maxLength=200, example="KSS 표준 모델"), * @OA\Property(property="category_id", type="integer", nullable=true, example=12), @@ -177,10 +193,13 @@ public function index() {} * @OA\Property(property="is_active", type="boolean", example=true) * ) * ), + * * @OA\Response(response=200, description="등록 성공", @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/DesignModel")) * })), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -193,11 +212,15 @@ public function store() {} * summary="모델 상세", * description="단일 모델을 조회합니다. (versions 포함 가능)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/DesignModel")) * })), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -209,10 +232,14 @@ public function show() {} * tags={"Model"}, * summary="모델 수정", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=true, + * * @OA\JsonContent( + * * @OA\Property(property="code", type="string", maxLength=100, example="KSS01"), * @OA\Property(property="name", type="string", maxLength=200, example="KSS 표준 모델(개정)"), * @OA\Property(property="category_id", type="integer", nullable=true, example=12), @@ -221,6 +248,7 @@ public function show() {} * @OA\Property(property="is_active", type="boolean", example=true) * ) * ), + * * @OA\Response(response=200, description="수정 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) , * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -233,7 +261,9 @@ public function update() {} * tags={"Model"}, * summary="모델 삭제(soft)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -252,13 +282,18 @@ class ModelVersionApi * tags={"ModelVersion"}, * summary="모델의 버전 목록", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="modelId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ModelVersion"))) * }) * ), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -271,22 +306,30 @@ public function listVersions() {} * summary="버전 DRAFT 생성", * description="version_no 미지정 시 자동 증가", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="modelId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=false, + * * @OA\JsonContent( + * * @OA\Property(property="version_no", type="integer", nullable=true, example=1), * @OA\Property(property="notes", type="string", nullable=true, example="초안 메모"), * @OA\Property(property="effective_from", type="string", format="date-time", nullable=true), * @OA\Property(property="effective_to", type="string", format="date-time", nullable=true) * ) * ), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ModelVersion")) * }) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -298,13 +341,18 @@ public function createDraft() {} * tags={"ModelVersion"}, * summary="버전 RELEASED 전환", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="versionId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="전환 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ModelVersion")) * }) * ), + * * @OA\Response(response=409, description="상태 충돌", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -323,13 +371,18 @@ class BomTemplateApi * tags={"BomTemplate"}, * summary="모델버전의 BOM 템플릿 목록", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="versionId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/BomTemplate"))) * }) * ), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -342,21 +395,29 @@ public function listByVersion() {} * summary="BOM 템플릿 upsert (name 기준)", * description="is_primary=true 지정 시 동일 모델버전의 기존 대표 템플릿은 자동 해제", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="versionId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody( * required=false, + * * @OA\JsonContent( + * * @OA\Property(property="name", type="string", maxLength=100, example="Main"), * @OA\Property(property="is_primary", type="boolean", example=true), * @OA\Property(property="notes", type="string", nullable=true, example="표준 템플릿") * ) * ), + * * @OA\Response(response=200, description="저장 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/BomTemplate")) * }) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -368,13 +429,18 @@ public function upsertTemplate() {} * tags={"BomTemplate"}, * summary="BOM 템플릿 상세 (항목 포함)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="templateId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/BomTemplate")) * }) * ), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -387,8 +453,11 @@ public function show() {} * summary="BOM 항목 일괄 치환", * description="기존 항목을 모두 삭제 후 본문 items로 재삽입", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="templateId", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomTemplateItemReplaceRequest")), + * * @OA\Response(response=200, description="저장 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) diff --git a/app/Swagger/v1/FieldProfileApi.php b/app/Swagger/v1/FieldProfileApi.php index efd5807..fb45c28 100644 --- a/app/Swagger/v1/FieldProfileApi.php +++ b/app/Swagger/v1/FieldProfileApi.php @@ -22,6 +22,7 @@ class FieldProfileApi * type="object", * description="전역정의 + 테넌트설정이 merge된 필드 효과값", * required={"field_key","label","data_type","input_type","enabled"}, + * * @OA\Property(property="field_key", type="string", example="position"), * @OA\Property(property="label", type="string", example="직급"), * @OA\Property(property="data_type", type="string", example="string"), @@ -41,6 +42,7 @@ class FieldProfileApi * @OA\Schema( * schema="FieldSettingUpdateRequest", * type="object", + * * @OA\Property(property="enabled", type="boolean", example=true), * @OA\Property(property="required", type="boolean", example=false), * @OA\Property(property="sort_order", type="integer", example=20), @@ -52,6 +54,7 @@ class FieldProfileApi * schema="FieldSettingBulkItem", * type="object", * required={"field_key"}, + * * @OA\Property(property="field_key", type="string", example="position"), * @OA\Property(property="enabled", type="boolean", example=true), * @OA\Property(property="required", type="boolean", example=false), @@ -59,13 +62,16 @@ class FieldProfileApi * @OA\Property(property="option_group_id", type="integer", nullable=true, example=5), * @OA\Property(property="code_group", type="string", nullable=true, example="employment_type") * ) + * * @OA\Schema( * schema="FieldSettingBulkRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items(ref="#/components/schemas/FieldSettingBulkItem") * ) * ) @@ -79,23 +85,28 @@ public function _fieldSchemasNoop() {} * schema="OptionGroup", * type="object", * required={"id","group_key","name"}, + * * @OA\Property(property="id", type="integer", example=3), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="group_key", type="string", example="position"), * @OA\Property(property="name", type="string", example="직급표(2025)"), * @OA\Property(property="description", type="string", nullable=true, example="본사 기준") * ) + * * @OA\Schema( * schema="OptionGroupCreateRequest", * type="object", * required={"group_key","name"}, + * * @OA\Property(property="group_key", type="string", example="job_title"), * @OA\Property(property="name", type="string", example="직책"), * @OA\Property(property="description", type="string", nullable=true, example="영업조직용") * ) + * * @OA\Schema( * schema="OptionGroupUpdateRequest", * type="object", + * * @OA\Property(property="group_key", type="string", example="job_title"), * @OA\Property(property="name", type="string", example="직책(개정)"), * @OA\Property(property="description", type="string", nullable=true, example="영업/CS 통합") @@ -105,6 +116,7 @@ public function _fieldSchemasNoop() {} * schema="OptionValue", * type="object", * required={"id","group_id","value_key","value_label"}, + * * @OA\Property(property="id", type="integer", example=10), * @OA\Property(property="group_id", type="integer", example=3), * @OA\Property(property="value_key", type="string", example="manager"), @@ -112,34 +124,42 @@ public function _fieldSchemasNoop() {} * @OA\Property(property="sort_order", type="integer", example=30), * @OA\Property(property="is_active", type="boolean", example=true) * ) + * * @OA\Schema( * schema="OptionValueCreateRequest", * type="object", * required={"value_key","value_label"}, + * * @OA\Property(property="value_key", type="string", example="director"), * @OA\Property(property="value_label", type="string", example="이사"), * @OA\Property(property="sort_order", type="integer", example=40), * @OA\Property(property="is_active", type="boolean", example=true) * ) + * * @OA\Schema( * schema="OptionValueUpdateRequest", * type="object", + * * @OA\Property(property="value_key", type="string", example="director"), * @OA\Property(property="value_label", type="string", example="이사"), * @OA\Property(property="sort_order", type="integer", example=45), * @OA\Property(property="is_active", type="boolean", example=false) * ) + * * @OA\Schema( * schema="OptionValueReorderRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", * description="정렬 대상 목록", + * * @OA\Items( * type="object", * required={"id","sort_order"}, + * * @OA\Property(property="id", type="integer", example=10), * @OA\Property(property="sort_order", type="integer", example=100) * ) @@ -156,6 +176,7 @@ public function _optionSchemasNoop() {} * type="object", * description="테넌트별 사용자 프로필", * required={"tenant_id","user_id"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="user_id", type="integer", example=55), @@ -170,6 +191,7 @@ public function _optionSchemasNoop() {} * @OA\Property(property="json_extra", type="object", nullable=true, * example={"employee_no":"A-001","entry_date":"2023-01-02","work_type":"hybrid"}) * ) + * * @OA\Schema( * schema="ProfileUpdateRequest", * type="object", @@ -185,6 +207,7 @@ public function _optionSchemasNoop() {} * schema="ProfilePagination", * type="object", * description="LengthAwarePaginator 구조", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Profile")), * @OA\Property(property="total", type="integer", example=3), @@ -205,16 +228,21 @@ public function _profileSchemasNoop() {} * description="전역 필드 정의와 테넌트 설정을 병합한, 실제 화면 노출용(Enabled) 필드 목록을 반환합니다.", * tags={"Tenant.Fields"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="array", + * * @OA\Items(ref="#/components/schemas/FieldEffective") * )) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) @@ -229,15 +257,20 @@ public function fieldsIndex() {} * description="여러 필드에 대해 enabled/required/sort_order/option_group_id/code_group 설정을 한번에 저장합니다.", * tags={"Tenant.Fields"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/FieldSettingBulkRequest")), + * * @OA\Response( * response=200, * description="저장 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", example={"updated":true})) * }) * ), + * * @OA\Response(response=400, description="유효성 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -253,19 +286,25 @@ public function fieldsBulk() {} * description="특정 field_key에 대한 테넌트 설정을 수정합니다.", * tags={"Tenant.Fields"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="key", in="path", required=true, * description="수정할 필드 키", @OA\Schema(type="string", example="position") * ), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/FieldSettingUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/FieldEffective")) * }) * ), + * * @OA\Response(response=400, description="유효성 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -282,21 +321,27 @@ public function fieldsUpdateOne() {} * description="해당 테넌트의 옵션 그룹 목록을 페이징으로 반환합니다.", * tags={"Tenant.Option Groups"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), * @OA\Parameter(name="q", in="query", required=false, description="검색어(그룹키/이름)", @OA\Schema(type="string", example="position")), + * * @OA\Response( * response=200, * description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/OptionGroup")), * @OA\Property(property="total", type="integer", example=3) * )) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -309,9 +354,13 @@ public function optGroupsIndex() {} * description="옵션 그룹을 생성합니다.", * tags={"Tenant.Option Groups"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionGroupCreateRequest")), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup")) * }) @@ -326,9 +375,13 @@ public function optGroupsStore() {} * summary="옵션 그룹 단건 조회", * tags={"Tenant.Option Groups"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup")) * }) @@ -343,10 +396,15 @@ public function optGroupsShow() {} * summary="옵션 그룹 수정", * tags={"Tenant.Option Groups"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionGroupUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionGroup")) * }) @@ -361,9 +419,13 @@ public function optGroupsUpdate() {} * summary="옵션 그룹 삭제", * tags={"Tenant.Option Groups"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\Response(response=200, description="삭제 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", nullable=true, example=null)) * }) @@ -378,10 +440,14 @@ public function optGroupsDestroy() {} * summary="옵션 값 목록", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), * @OA\Parameter(name="active_only", in="query", required=false, @OA\Schema(type="boolean", example=true)), + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/OptionValue"))) * }) @@ -396,10 +462,15 @@ public function optionValuesIndex() {} * summary="옵션 값 생성", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueCreateRequest")), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue")) * }) @@ -414,10 +485,14 @@ public function optionValuesStore() {} * summary="옵션 값 단건 조회", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)), + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue")) * }) @@ -432,11 +507,16 @@ public function optionValuesShow() {} * summary="옵션 값 수정", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/OptionValue")) * }) @@ -451,10 +531,14 @@ public function optionValuesUpdate() {} * summary="옵션 값 삭제", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=10)), + * * @OA\Response(response=200, description="삭제 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", nullable=true, example=null)) * }) @@ -469,10 +553,15 @@ public function optionValuesDestroy() {} * summary="옵션 값 정렬 순서 일괄 변경", * tags={"Tenant.Option Values"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="gid", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/OptionValueReorderRequest")), + * * @OA\Response(response=200, description="변경 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", example={"reordered":true})) * }) @@ -488,11 +577,15 @@ public function optionValuesReorder() {} * description="테넌트 내 회원 프로필 목록을 페이징으로 반환합니다.", * tags={"Tenant.Profiles"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), * @OA\Parameter(name="q", in="query", required=false, description="검색어(표기명/사번 등)", @OA\Schema(type="string", example="A-001")), + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ProfilePagination")) * }) @@ -507,9 +600,13 @@ public function profilesIndex() {} * summary="프로필 단건 조회", * tags={"Tenant.Profiles"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="userId", in="path", required=true, @OA\Schema(type="integer", example=55)), + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile")) * }) @@ -525,10 +622,15 @@ public function profilesShow() {} * description="관리자 권한으로 해당 사용자의 프로필을 수정합니다. 테넌트에서 enabled된 필드만 반영됩니다.", * tags={"Tenant.Profiles"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="userId", in="path", required=true, @OA\Schema(type="integer", example=55)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProfileUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile")) * }) @@ -543,8 +645,11 @@ public function profilesUpdate() {} * summary="내 프로필 조회", * tags={"Tenant.Profiles"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Response(response=200, description="성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile")) * }) @@ -559,9 +664,13 @@ public function profilesMe() {} * summary="내 프로필 수정", * tags={"Tenant.Profiles"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProfileUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Profile")) * }) diff --git a/app/Swagger/v1/FileApi.php b/app/Swagger/v1/FileApi.php index 89da2e9..cff078d 100644 --- a/app/Swagger/v1/FileApi.php +++ b/app/Swagger/v1/FileApi.php @@ -2,5 +2,4 @@ namespace App\Swagger\v1; - - class FileApi {} +class FileApi {} diff --git a/app/Swagger/v1/MaterialApi.php b/app/Swagger/v1/MaterialApi.php index 3eafde1..87ac721 100644 --- a/app/Swagger/v1/MaterialApi.php +++ b/app/Swagger/v1/MaterialApi.php @@ -17,6 +17,7 @@ * schema="MaterialAttribute", * type="object", * description="규격 정보 한 항목", + * * @OA\Property(property="label", type="string", example="두께"), * @OA\Property(property="value", type="string", example="10"), * @OA\Property(property="unit", type="string", example="T") @@ -27,6 +28,7 @@ * type="object", * description="자재 상세", * required={"id","tenant_id","name","item_name","unit"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="category_id", type="integer", nullable=true, example=3), @@ -42,8 +44,10 @@ * property="attributes", * type="array", * nullable=true, + * * @OA\Items(ref="#/components/schemas/MaterialAttribute") * ), + * * @OA\Property( * property="options", * type="object", @@ -61,6 +65,7 @@ * type="object", * description="자재 요약", * required={"id","name","item_name","unit"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="category_id", type="integer", nullable=true, example=3), * @OA\Property(property="name", type="string", example="철판"), @@ -71,12 +76,14 @@ * @OA\Schema( * schema="MaterialList", * type="array", + * * @OA\Items(ref="#/components/schemas/MaterialBrief") * ) * * @OA\Schema( * schema="MaterialIndexParams", * type="object", + * * @OA\Property(property="q", type="string", nullable=true, example="알루미늄"), * @OA\Property(property="category", type="integer", nullable=true, example=3), * @OA\Property(property="page", type="integer", nullable=true, example=1), @@ -87,6 +94,7 @@ * schema="MaterialCreateRequest", * type="object", * required={"name","unit"}, + * * @OA\Property(property="category_id", type="integer", nullable=true, example=3), * @OA\Property(property="name", type="string", example="철판"), * @OA\Property(property="unit", type="string", example="EA"), @@ -97,8 +105,10 @@ * property="attributes", * type="array", * nullable=true, + * * @OA\Items(ref="#/components/schemas/MaterialAttribute") * ), + * * @OA\Property( * property="options", * type="object", @@ -112,6 +122,7 @@ * @OA\Schema( * schema="MaterialUpdateRequest", * type="object", + * * @OA\Property(property="category_id", type="integer", nullable=true, example=3), * @OA\Property(property="name", type="string", nullable=true, example="철판(개선)"), * @OA\Property(property="unit", type="string", nullable=true, example="KG"), @@ -122,8 +133,10 @@ * property="attributes", * type="array", * nullable=true, + * * @OA\Items(ref="#/components/schemas/MaterialAttribute") * ), + * * @OA\Property( * property="options", * type="object", @@ -134,7 +147,6 @@ * @OA\Property(property="specification", type="string", nullable=true, example="두께 12T, 길이 180CM") * ) */ - class MaterialApi { /** @@ -144,18 +156,23 @@ class MaterialApi * description="자재 목록을 페이징으로 반환합니다. (q로 코드/이름/태그 검색, category로 분류 필터)", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string"), description="검색어(이름/코드/태그 등)"), * @OA\Parameter(name="category", in="query", required=false, @OA\Schema(type="integer"), description="카테고리 ID"), * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer", example=20)), + * * @OA\Response( * response=200, description="자재 목록 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property( * property="data", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property(property="per_page", type="integer", example=20), * @OA\Property(property="total", type="integer", example=2), @@ -175,16 +192,21 @@ public function index() {} * description="자재를 등록합니다. item_name/specification은 name+attributes를 기반으로 서버에서 자동 생성됩니다.", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialCreateRequest")), + * * @OA\Response( * response=200, description="자재 등록 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) * } * ) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -196,16 +218,21 @@ public function store() {} * summary="자재 단건 조회", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * * @OA\Response( * response=200, description="자재 단건 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) * } * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -218,17 +245,23 @@ public function show() {} * description="자재를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")), + * * @OA\Response( * response=200, description="자재 수정 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) * } * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -242,17 +275,23 @@ public function updatePut() {} * description="자재의 일부 필드를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")), + * * @OA\Response( * response=200, description="자재 수정 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) * } * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -266,7 +305,9 @@ public function updatePatch() {} * description="자재를 소프트 삭제합니다.", * tags={"Material"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * * @OA\Response(response=200, description="자재 삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) diff --git a/app/Swagger/v1/MenuApi.php b/app/Swagger/v1/MenuApi.php index 4376ef7..5ef5252 100644 --- a/app/Swagger/v1/MenuApi.php +++ b/app/Swagger/v1/MenuApi.php @@ -12,6 +12,7 @@ * type="object", * description="메뉴 상세", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=12), * @OA\Property(property="tenant_id", type="integer", nullable=true, example=1, description="null=공용"), * @OA\Property(property="parent_id", type="integer", nullable=true, example=null), @@ -34,6 +35,7 @@ * type="object", * description="메뉴 요약", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=12), * @OA\Property(property="parent_id", type="integer", nullable=true, example=null), * @OA\Property(property="name", type="string", example="메뉴 관리"), @@ -50,6 +52,7 @@ * @OA\Schema( * schema="MenuList", * type="array", + * * @OA\Items(ref="#/components/schemas/MenuBrief") * ) * @@ -57,6 +60,7 @@ * schema="MenuCreateRequest", * type="object", * required={"name"}, + * * @OA\Property(property="parent_id", type="integer", nullable=true, example=null, description="상위 메뉴 ID"), * @OA\Property(property="name", type="string", example="새 메뉴", description="메뉴명"), * @OA\Property(property="slug", type="string", nullable=true, example="menu.new", description="권한 키로도 활용"), @@ -72,6 +76,7 @@ * @OA\Schema( * schema="MenuUpdateRequest", * type="object", + * * @OA\Property(property="parent_id", type="integer", nullable=true, example=null), * @OA\Property(property="name", type="string", example="메뉴명 변경"), * @OA\Property(property="slug", type="string", nullable=true, example="menu.changed"), @@ -93,12 +98,16 @@ class MenuApi * description="테넌트 범위 내(또는 공용) 메뉴 목록을 반환합니다. parent_id/is_active/hidden 필터 지원.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="parent_id", in="query", required=false, @OA\Schema(type="integer", example=0)), * @OA\Parameter(name="is_active", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="hidden", in="query", required=false, @OA\Schema(type="integer", example=0)), + * * @OA\Response(response=200, description="목록 조회 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse"), @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/MenuList"))}) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) @@ -113,10 +122,14 @@ public function index() {} * description="ID로 메뉴 상세 정보를 조회합니다.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\Response(response=200, description="단건 조회 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse"), @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Menu"))}) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -132,13 +145,19 @@ public function show() {} * description="새로운 메뉴를 등록합니다.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MenuCreateRequest")), + * * @OA\Response(response=200, description="등록 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse"), @OA\Schema(@OA\Property(property="data", type="object", @OA\Property(property="id", type="integer", example=12)))}) * ), + * * @OA\Response(response=400, description="파라미터 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * * @OA\ResponsE(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -151,11 +170,16 @@ public function store() {} * description="기존 메뉴 정보를 수정합니다(부분 수정).", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MenuUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse"), @OA\Schema(@OA\Property(property="data", type="object", @OA\Property(property="id", type="integer", example=12)))}) * ), + * * @OA\Response(response=400, description="파라미터 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -172,10 +196,14 @@ public function update() {} * description="지정한 메뉴를 소프트 삭제합니다.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\Response(response=200, description="삭제 성공", + * * @OA\JsonContent(allOf={@OA\Schema(ref="#/components/schemas/ApiResponse"), @OA\Schema(@OA\Property(property="data", type="object", @OA\Property(property="id", type="integer", example=12), @OA\Property(property="deleted", type="boolean", example=true)))}) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -191,13 +219,17 @@ public function destroy() {} * description="여러 메뉴의 sort_order를 일괄 변경합니다.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent( * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="id", type="integer", example=12), * @OA\Property(property="sort_order", type="integer", example=1) * ) * )), + * * @OA\Response(response=200, description="정렬 변경 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=400, description="파라미터 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -214,12 +246,16 @@ public function reorder() {} * description="is_active / hidden / is_external 중 하나 이상을 토글합니다.", * tags={"Menu"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=12)), + * * @OA\RequestBody(required=true, @OA\JsonContent(type="object", + * * @OA\Property(property="is_active", type="boolean", nullable=true, example=true), * @OA\Property(property="hidden", type="boolean", nullable=true, example=false), * @OA\Property(property="is_external", type="boolean", nullable=true, example=false) * )), + * * @OA\Response(response=200, description="토글 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=400, description="파라미터 오류", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/ModelApi.php b/app/Swagger/v1/ModelApi.php index bdf1f7a..d436d34 100644 --- a/app/Swagger/v1/ModelApi.php +++ b/app/Swagger/v1/ModelApi.php @@ -2,6 +2,4 @@ namespace App\Swagger\v1; - - - class ModelApi {} +class ModelApi {} diff --git a/app/Swagger/v1/PermissionApi.php b/app/Swagger/v1/PermissionApi.php index eac31ce..eb10e4c 100644 --- a/app/Swagger/v1/PermissionApi.php +++ b/app/Swagger/v1/PermissionApi.php @@ -19,6 +19,7 @@ * @OA\Schema( * schema="MenuMatrixAction", * type="object", + * * @OA\Property(property="permission_id", type="integer", example=1), * @OA\Property(property="permission_code", type="string", example="menu:16.view"), * @OA\Property(property="guard_name", type="string", example="api"), @@ -29,6 +30,7 @@ * @OA\Schema( * schema="MenuMatrixNode", * type="object", + * * @OA\Property(property="menu_id", type="integer", example=16), * @OA\Property(property="parent_id", type="integer", nullable=true, example=10), * @OA\Property(property="name", type="string", example="스크린 작업"), @@ -46,6 +48,7 @@ * @OA\Property( * property="children", * type="array", + * * @OA\Items(ref="#/components/schemas/MenuMatrixNode") * ) * ) @@ -53,19 +56,22 @@ * @OA\Schema( * schema="MenuMatrixPayload", * type="object", + * * @OA\Property( * property="actions", * type="array", + * * @OA\Items(type="string", example="view") * ), + * * @OA\Property( * property="tree", * type="array", + * * @OA\Items(ref="#/components/schemas/MenuMatrixNode") * ) * ) */ - class PermissionApi { /** @@ -75,19 +81,25 @@ class PermissionApi * description="부서 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.", * tags={"Permission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="부서 ID", @OA\Schema(type="integer", example=1)), + * * @OA\Response( * response=200, * description="부서 메뉴 권한 매트릭스 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload") * ) * } * ) * ), + * * @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -100,19 +112,25 @@ public function deptMenuMatrix() {} * description="스파티 기본 Role 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.", * tags={"Permission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="역할 ID", @OA\Schema(type="integer", example=3)), + * * @OA\Response( * response=200, * description="역할 메뉴 권한 매트릭스 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload") * ) * } * ) * ), + * * @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -125,19 +143,25 @@ public function roleMenuMatrix() {} * description="사용자 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.", * tags={"Permission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, description="사용자 ID", @OA\Schema(type="integer", example=12)), + * * @OA\Response( * response=200, * description="유저 메뉴 권한 매트릭스 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload") * ) * } * ) * ), + * * @OA\Response(response=404, description="사용자 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ diff --git a/app/Swagger/v1/PricingApi.php b/app/Swagger/v1/PricingApi.php index 59dae6a..6ba3279 100644 --- a/app/Swagger/v1/PricingApi.php +++ b/app/Swagger/v1/PricingApi.php @@ -9,6 +9,7 @@ * schema="PriceHistory", * type="object", * required={"id","item_type_code","item_id","price_type_code","price","started_at"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT", description="항목 유형"), @@ -25,12 +26,15 @@ * @OA\Schema( * schema="PriceHistoryPagination", * type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/PriceHistory") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/pricing?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=3), @@ -38,7 +42,9 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -56,6 +62,7 @@ * schema="PriceUpsertRequest", * type="object", * required={"item_type_code","item_id","price_type_code","price","started_at"}, + * * @OA\Property(property="item_type_code", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"), * @OA\Property(property="item_id", type="integer", example=10), * @OA\Property(property="price_type_code", type="string", enum={"SALE","PURCHASE"}, example="SALE"), @@ -68,6 +75,7 @@ * @OA\Schema( * schema="PriceQueryResult", * type="object", + * * @OA\Property(property="price", type="number", format="decimal", nullable=true, example=50000.00), * @OA\Property(property="price_history_id", type="integer", nullable=true, example=1), * @OA\Property(property="client_group_id", type="integer", nullable=true, example=1), @@ -78,10 +86,13 @@ * schema="BulkPriceQueryRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="item_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"), * @OA\Property(property="item_id", type="integer", example=10) * ) @@ -93,10 +104,13 @@ * @OA\Schema( * schema="BulkPriceQueryResult", * type="object", + * * @OA\Property( * property="prices", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="item_type", type="string", example="PRODUCT"), * @OA\Property(property="item_id", type="integer", example=10), * @OA\Property(property="price", type="number", nullable=true, example=50000.00), @@ -115,18 +129,23 @@ class PricingApi * tags={"Pricing"}, * summary="가격 이력 목록", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="item_type_code", in="query", @OA\Schema(type="string", enum={"PRODUCT","MATERIAL"})), * @OA\Parameter(name="item_id", in="query", @OA\Schema(type="integer")), * @OA\Parameter(name="price_type_code", in="query", @OA\Schema(type="string", enum={"SALE","PURCHASE"})), * @OA\Parameter(name="client_group_id", in="query", @OA\Schema(type="integer")), * @OA\Parameter(name="date", in="query", description="특정 날짜 기준 유효한 가격", @OA\Schema(type="string", format="date")), * @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=15)), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PriceHistoryPagination")) * }) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -139,12 +158,16 @@ public function index() {} * summary="단일 항목 가격 조회", * description="특정 제품/자재의 현재 유효한 가격 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="item_type", in="query", required=true, @OA\Schema(type="string", enum={"PRODUCT","MATERIAL"})), * @OA\Parameter(name="item_id", in="query", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="client_id", in="query", @OA\Schema(type="integer"), description="고객 ID (고객 그룹별 가격 적용)"), * @OA\Parameter(name="date", in="query", @OA\Schema(type="string", format="date"), description="기준일 (미지정시 오늘)"), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PriceQueryResult")) * }) @@ -160,9 +183,13 @@ public function show() {} * summary="여러 항목 일괄 가격 조회", * description="여러 제품/자재의 가격을 한 번에 조회", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BulkPriceQueryRequest")), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/BulkPriceQueryResult")) * }) @@ -178,13 +205,18 @@ public function bulk() {} * summary="가격 등록/수정", * description="가격 이력 등록 (동일 조건 존재 시 업데이트)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PriceUpsertRequest")), + * * @OA\Response(response=200, description="저장 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PriceHistory")) * }) * ), + * * @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -196,9 +228,11 @@ public function upsert() {} * tags={"Pricing"}, * summary="가격 이력 삭제(soft)", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ public function destroy() {} -} \ No newline at end of file +} diff --git a/app/Swagger/v1/ProductApi.php b/app/Swagger/v1/ProductApi.php index 7f2bded..b632a2a 100644 --- a/app/Swagger/v1/ProductApi.php +++ b/app/Swagger/v1/ProductApi.php @@ -12,10 +12,12 @@ * ========= 공용 스키마(이 파일에서 사용하는 것만 정의) ========= * * 트리 노드 스키마 (재귀) + * * @OA\Schema( * schema="BomTreeNode", * type="object", * description="BOM 트리 한 노드(제품/자재 공통)", + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="type", type="string", example="PRODUCT", description="PRODUCT / MATERIAL 등"), * @OA\Property(property="code", type="string", example="PRD-001"), @@ -34,24 +36,29 @@ * property="children", * type="array", * description="자식 노드 목록(없으면 빈 배열)", + * * @OA\Items(ref="#/components/schemas/BomTreeNode") * ) * ) * * BOM 카테고리 사용/추천 항목 + * * @OA\Schema( * schema="BomCategoryStat", * type="object", + * * @OA\Property(property="category_id", type="integer", nullable=true, example=1), * @OA\Property(property="category_name", type="string", example="기본"), * @OA\Property(property="count", type="integer", example=5, description="해당 카테고리를 사용하는 BOM 항목 수(빈도)") * ) * * BOM 전체 교체 저장용 스키마 + * * @OA\Schema( * schema="BomReplaceItem", * type="object", * required={"ref_type","ref_id","quantity"}, + * * @OA\Property(property="ref_type", type="string", enum={"MATERIAL","PRODUCT"}, example="MATERIAL", description="참조 타입"), * @OA\Property(property="ref_id", type="integer", example=201, description="참조 ID (materials.id 또는 products.id)"), * @OA\Property(property="quantity", type="number", format="float", example=2, description="수량(0 이상, 소수 허용)"), @@ -62,11 +69,13 @@ * schema="BomReplaceCategory", * type="object", * required={"items"}, + * * @OA\Property(property="id", type="integer", nullable=true, example=1, description="프론트 임시 카테고리 ID(선택)"), * @OA\Property(property="name", type="string", nullable=true, example="기본", description="프론트 카테고리명(선택)"), * @OA\Property( * property="items", * type="array", + * * @OA\Items(ref="#/components/schemas/BomReplaceItem") * ) * ) @@ -75,9 +84,11 @@ * schema="BomReplaceRequest", * type="object", * required={"categories"}, + * * @OA\Property( * property="categories", * type="array", + * * @OA\Items(ref="#/components/schemas/BomReplaceCategory") * ), * example={ @@ -104,6 +115,7 @@ * @OA\Schema( * schema="BomReplaceResult", * type="object", + * * @OA\Property(property="deleted_count", type="integer", example=5, description="삭제된 기존 항목 수"), * @OA\Property(property="inserted_count", type="integer", example=7, description="신규로 삽입된 항목 수") * ) @@ -112,28 +124,35 @@ class ProductApi { /** * 카테고리 목록 조회 (기존) + * * @OA\Get( * path="/api/v1/product/category", * summary="제품 카테고리 목록 조회", * description="제품 카테고리(최상위: parent_id = null) 리스트를 반환합니다.", * tags={"Products"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="카테고리 목록 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/ProductCategory") * ) * ) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -142,21 +161,26 @@ public function productCategoryIndex() {} /** * 제품 목록/검색 + * * @OA\Get( * path="/api/v1/products", * tags={"Products"}, * summary="제품 목록/검색", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), * @OA\Parameter(name="q", in="query", @OA\Schema(type="string"), description="코드/이름/설명 검색"), * @OA\Parameter(name="category_id", in="query", @OA\Schema(type="integer")), * @OA\Parameter(name="product_type", in="query", @OA\Schema(type="string", example="PRODUCT")), * @OA\Parameter(name="active", in="query", @OA\Schema(type="integer", enum={0,1})), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ProductPagination")) * }) @@ -167,16 +191,21 @@ public function productsIndex() {} /** * 제품 생성 + * * @OA\Post( * path="/api/v1/products", * tags={"Products"}, * summary="제품 생성", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProductCreateRequest")), + * * @OA\Response( * response=200, * description="생성 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Product")) * }) @@ -187,20 +216,26 @@ public function productsStore() {} /** * 제품 단건 + * * @OA\Get( * path="/api/v1/products/{id}", * tags={"Products"}, * summary="제품 단건 조회", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Product")) * }) * ), + * * @OA\Response(response=404, description="없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) */ @@ -208,17 +243,23 @@ public function productsShow() {} /** * 제품 수정 + * * @OA\Patch( * path="/api/v1/products/{id}", * tags={"Products"}, * summary="제품 수정", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ProductUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Product")) * }) @@ -229,12 +270,15 @@ public function productsUpdate() {} /** * 제품 삭제(soft) + * * @OA\Delete( * path="/api/v1/products/{id}", * tags={"Products"}, * summary="제품 삭제(soft)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -242,23 +286,31 @@ public function productsDestroy() {} /** * 제품 간편 검색(모달/드롭다운) + * * @OA\Get( * path="/api/v1/products/search", * tags={"Products"}, * summary="제품 간편 검색", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="q", in="query", @OA\Schema(type="string")), * @OA\Parameter(name="limit", in="query", @OA\Schema(type="integer"), description="기본 20"), + * * @OA\Response( * response=200, * description="검색 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="code", type="string", example="PRD-001"), * @OA\Property(property="name", type="string", example="스크린 모듈 KS001"), @@ -276,18 +328,24 @@ public function productsSearch() {} /** * 제품 활성/비활성 토글 + * * @OA\Post( * path="/api/v1/products/{id}/toggle", * tags={"Products"}, * summary="제품 활성/비활성 토글", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="토글 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="is_active", type="integer", example=0) @@ -302,18 +360,24 @@ public function productsToggle() {} /** * BOM 항목 목록 + * * @OA\Get( * path="/api/v1/products/{id}/bom/items", * tags={"Products-BOM"}, * summary="BOM 항목 목록(제품+자재 병합)", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/BomItem")) * ) * }) @@ -324,19 +388,26 @@ public function bomItemsIndex() {} /** * BOM 대량 업서트 + * * @OA\Post( * path="/api/v1/products/{id}/bom/items/bulk", * tags={"Products-BOM"}, * summary="BOM 항목 대량 업서트", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomItemBulkUpsertRequest")), + * * @OA\Response( * response=200, * description="업서트 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="created", type="integer", example=2), * @OA\Property(property="updated", type="integer", example=3) @@ -350,18 +421,24 @@ public function bomItemsBulk() {} /** * BOM 단건 수정 + * * @OA\Patch( * path="/api/v1/products/{id}/bom/items/{item}", * tags={"Products-BOM"}, * summary="BOM 항목 단건 수정", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="item", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomItemUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/BomItem")) * }) @@ -372,13 +449,16 @@ public function bomItemsUpdate() {} /** * BOM 단건 삭제 + * * @OA\Delete( * path="/api/v1/products/{id}/bom/items/{item}", * tags={"Products-BOM"}, * summary="BOM 항목 단건 삭제", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Parameter(name="item", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -386,13 +466,17 @@ public function bomItemsDestroy() {} /** * BOM 정렬 변경 + * * @OA\Post( * path="/api/v1/products/{id}/bom/items/reorder", * tags={"Products-BOM"}, * summary="BOM 정렬 변경", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomReorderRequest")), + * * @OA\Response(response=200, description="저장 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")) * ) */ @@ -400,18 +484,24 @@ public function bomItemsReorder() {} /** * BOM 요약(건수/합계 등) + * * @OA\Get( * path="/api/v1/products/{id}/bom/summary", * tags={"Products-BOM"}, * summary="BOM 요약", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="요약 성공", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="count", type="integer", example=5), * @OA\Property(property="count_product", type="integer", example=2), @@ -427,22 +517,30 @@ public function bomSummary() {} /** * BOM 유효성 검사 + * * @OA\Get( * path="/api/v1/products/{id}/bom/validate", * tags={"Products-BOM"}, * summary="BOM 유효성 검사", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * * @OA\Response( * response=200, * description="검증 결과", + * * @OA\JsonContent(allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", type="object", * @OA\Property(property="valid", type="boolean", example=false), * @OA\Property(property="errors", type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="error", type="string", example="DUPLICATE_ITEM") * ) @@ -458,33 +556,42 @@ public function bomValidate() {} /** * BOM 전체 교체 저장 * 프론트에서 보낸 현재 BOM 상태로 기존 구성을 모두 삭제 후 재등록합니다. + * * @OA\Post( * path="/api/v1/products/{id}/bom", * tags={"Products-BOM"}, * summary="BOM 구성 저장(구성 전체 교체)", * description="기존 BOM을 모두 삭제하고, 전달된 categories/items 기준으로 다시 저장합니다.", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="상위(모델) 제품 ID", + * * @OA\Schema(type="integer", example=123) * ), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BomReplaceRequest")), + * * @OA\Response( * response=200, * description="저장 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="message", type="string", example="BOM 항목이 저장되었습니다."), * @OA\Property(property="data", ref="#/components/schemas/BomReplaceResult") * ) * } * ) * ), + * * @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -497,29 +604,37 @@ public function bomReplace() {} /** * 제품별 사용 중인 BOM 카테고리 목록 + * * @OA\Get( * path="/api/v1/products/{id}/bom/categories", * tags={"Products-BOM"}, * summary="해당 제품에서 사용 중인 BOM 카테고리 목록", * description="product_components 테이블에서 해당 제품(parent_product_id)의 카테고리(id/name)를 집계하여 반환합니다.", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="id", * in="path", * required=true, * description="상위(모델) 제품 ID", + * * @OA\Schema(type="integer", example=123) * ), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/BomCategoryStat") * ) * ) @@ -534,6 +649,7 @@ public function bomReplace() {} * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -542,36 +658,46 @@ public function bomCategoriesForProduct() {} /** * 테넌트 전역 카테고리 추천(히스토리 기반) + * * @OA\Get( * path="/api/v1/products/bom/categories", * tags={"Products-BOM"}, * summary="자주 사용된 BOM 카테고리 추천", * description="테넌트 전체 product_components 데이터를 집계해 카테고리 사용 빈도가 높은 순으로 반환합니다. q로 부분 검색 가능합니다.", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="q", * in="query", * required=false, * description="카테고리명 부분 검색", + * * @OA\Schema(type="string", example="기") * ), + * * @OA\Parameter( * name="limit", * in="query", * required=false, * description="최대 항목 수(기본 20)", + * * @OA\Schema(type="integer", example=20) * ), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/BomCategoryStat") * ) * ) @@ -587,6 +713,7 @@ public function bomCategoriesForProduct() {} * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) * ) @@ -595,27 +722,34 @@ public function bomCategoriesSuggest() {} /** * 제품 BOM 트리 조회 + * * @OA\Get( * path="/api/v1/products/{id}/bom/tree", * tags={"Products-BOM"}, * summary="제품 BOM 트리 조회(재귀)", * description="특정 제품의 하위 구성(제품/자재)을 재귀적으로 트리 형태로 반환합니다. depth로 최대 깊이를 제한합니다(기본 10).", * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", minimum=1), description="제품 ID"), * @OA\Parameter( * name="depth", * in="query", * required=false, * description="재귀 깊이(루트=0). 기본 10", + * * @OA\Schema(type="integer", minimum=0, maximum=50, default=10, example=5) * ), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="object", @@ -635,6 +769,7 @@ public function bomCategoriesSuggest() {} * } * ) * ), + * * @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="대상 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/ProductExtraSchemas.php b/app/Swagger/v1/ProductExtraSchemas.php index db1d3b6..eb5b142 100644 --- a/app/Swagger/v1/ProductExtraSchemas.php +++ b/app/Swagger/v1/ProductExtraSchemas.php @@ -9,10 +9,12 @@ class ProductExtraSchemas { /** * 제품 카테고리 스키마 + * * @OA\Schema( * schema="ProductCategory", * type="object", * required={"id","code_group","code","name","is_active","sort_order"}, + * * @OA\Property(property="id", type="integer", example=4), * @OA\Property(property="code_group", type="string", example="category"), * @OA\Property(property="code", type="string", example="BP"), @@ -33,10 +35,12 @@ public function _category() {} /** * 제품 관련 스키마들 + * * @OA\Schema( * schema="Product", * type="object", * required={"id","tenant_id","code","name","category_id","product_type","is_active"}, + * * @OA\Property(property="id", type="integer", example=101), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="code", type="string", example="PRD-001"), @@ -57,12 +61,15 @@ public function _category() {} * schema="ProductPagination", * type="object", * description="라라벨 LengthAwarePaginator 구조", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/Product") * ), + * * @OA\Property(property="per_page", type="integer", example=20), * @OA\Property(property="total", type="integer", example=123) * ) @@ -71,6 +78,7 @@ public function _category() {} * schema="ProductCreateRequest", * type="object", * required={"code","name","category_id","product_type"}, + * * @OA\Property(property="code", type="string", example="PRD-001"), * @OA\Property(property="name", type="string", example="스크린 모듈 KS001"), * @OA\Property(property="category_id", type="integer", example=7), @@ -86,6 +94,7 @@ public function _category() {} * @OA\Schema( * schema="ProductUpdateRequest", * type="object", + * * @OA\Property(property="code", type="string", example="PRD-001"), * @OA\Property(property="name", type="string", example="스크린 모듈 KS001"), * @OA\Property(property="category_id", type="integer", example=7), @@ -102,10 +111,12 @@ public function _product() {} /** * BOM 스키마들 + * * @OA\Schema( * schema="BomItem", * type="object", * required={"id","ref_type","ref_id","quantity","sort_order"}, + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"), * @OA\Property(property="ref_id", type="integer", example=3), @@ -121,11 +132,14 @@ public function _product() {} * schema="BomItemBulkUpsertRequest", * type="object", * required={"items"}, + * * @OA\Property( * property="items", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="id", type="integer", nullable=true, example=null), * @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="MATERIAL"), * @OA\Property(property="ref_id", type="integer", example=5), @@ -139,6 +153,7 @@ public function _product() {} * @OA\Schema( * schema="BomItemUpdateRequest", * type="object", + * * @OA\Property(property="ref_type", type="string", enum={"PRODUCT","MATERIAL"}, example="PRODUCT"), * @OA\Property(property="ref_id", type="integer", example=9), * @OA\Property(property="quantity", type="number", format="float", example=1.5000), @@ -149,9 +164,11 @@ public function _product() {} * @OA\Schema( * schema="BomReorderRequest", * type="array", + * * @OA\Items( * type="object", * required={"id","sort_order"}, + * * @OA\Property(property="id", type="integer", example=11), * @OA\Property(property="sort_order", type="integer", example=1) * ) diff --git a/app/Swagger/v1/RoleApi.php b/app/Swagger/v1/RoleApi.php index 3eb2e6f..8c7700d 100644 --- a/app/Swagger/v1/RoleApi.php +++ b/app/Swagger/v1/RoleApi.php @@ -12,6 +12,7 @@ * type="object", * description="역할 상세", * required={"id","name","guard_name"}, + * * @OA\Property(property="id", type="integer", example=3), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="name", type="string", example="menu-manager"), @@ -26,6 +27,7 @@ * type="object", * description="역할 요약", * required={"id","name"}, + * * @OA\Property(property="id", type="integer", example=3), * @OA\Property(property="name", type="string", example="readonly"), * @OA\Property(property="description", type="string", nullable=true, example="읽기 전용"), @@ -35,6 +37,7 @@ * @OA\Schema( * schema="RoleList", * type="array", + * * @OA\Items(ref="#/components/schemas/RoleBrief") * ) * @@ -42,6 +45,7 @@ * schema="RoleCreateRequest", * type="object", * required={"name"}, + * * @OA\Property(property="name", type="string", example="menu-manager", description="역할명(테넌트+가드 내 고유)"), * @OA\Property(property="description", type="string", nullable=true, example="메뉴 관리 역할") * ) @@ -49,6 +53,7 @@ * @OA\Schema( * schema="RoleUpdateRequest", * type="object", + * * @OA\Property(property="name", type="string", example="menu-admin"), * @OA\Property(property="description", type="string", nullable=true, example="설명 변경") * ) @@ -62,14 +67,19 @@ class RoleApi * description="테넌트 범위 내 역할 목록을 페이징으로 반환합니다. (q로 이름/설명 검색)", * tags={"Role"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="size", in="query", required=false, @OA\Schema(type="integer", example=10)), * @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string", example="read")), + * * @OA\Response(response=200, description="목록 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", type="object", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property(property="per_page", type="integer", example=10), * @OA\Property(property="total", type="integer", example=2), @@ -78,6 +88,7 @@ class RoleApi * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) @@ -92,15 +103,20 @@ public function index() {} * description="새로운 역할을 생성합니다.", * tags={"Role"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RoleCreateRequest")), + * * @OA\Response(response=200, description="생성 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Role")) * } * ) * ), + * * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -116,15 +132,20 @@ public function store() {} * description="ID로 역할 상세를 조회합니다.", * tags={"Role"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\Response(response=200, description="단건 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Role")) * } * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -140,16 +161,22 @@ public function show() {} * description="기존 역할 정보를 부분 수정합니다.", * tags={"Role"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RoleUpdateRequest")), + * * @OA\Response(response=200, description="수정 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Role")) * } * ) * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -166,10 +193,14 @@ public function update() {} * description="지정한 역할을 삭제합니다.", * tags={"Role"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=3)), + * * @OA\Response(response=200, description="삭제 성공", + * * @OA\JsonContent(ref="#/components/schemas/ApiResponse") * ), + * * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/RolePermissionApi.php b/app/Swagger/v1/RolePermissionApi.php index e7c99fc..8f83b01 100644 --- a/app/Swagger/v1/RolePermissionApi.php +++ b/app/Swagger/v1/RolePermissionApi.php @@ -15,6 +15,7 @@ * type="object", * description="퍼미션 요약", * required={"id","name","guard_name"}, + * * @OA\Property(property="id", type="integer", example=15), * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="name", type="string", example="menu:101.view"), @@ -26,6 +27,7 @@ * @OA\Schema( * schema="PermissionList", * type="array", + * * @OA\Items(ref="#/components/schemas/PermissionBrief") * ) * @@ -37,11 +39,14 @@ * @OA\Schema( * description="방법 A: 퍼미션 이름 배열", * required={"permission_names"}, + * * @OA\Property(property="permission_names", type="array", @OA\Items(type="string"), example={"menu:101.view","menu:101.create"}) * ), + * * @OA\Schema( * description="방법 B: 메뉴+액션 조합", * required={"menus","actions"}, + * * @OA\Property(property="menus", type="array", @OA\Items(type="integer"), example={101,102}), * @OA\Property(property="actions", type="array", @OA\Items(type="string"), example={"view","create","update","delete"}) * ) @@ -56,11 +61,14 @@ * @OA\Schema( * description="방법 A: 퍼미션 이름 배열", * required={"permission_names"}, + * * @OA\Property(property="permission_names", type="array", @OA\Items(type="string"), example={"menu:101.view","menu:101.create"}) * ), + * * @OA\Schema( * description="방법 B: 메뉴+액션 조합", * required={"menus","actions"}, + * * @OA\Property(property="menus", type="array", @OA\Items(type="integer"), example={101}), * @OA\Property(property="actions", type="array", @OA\Items(type="string"), example={"create"}) * ) @@ -75,11 +83,14 @@ * @OA\Schema( * description="방법 A: 퍼미션 이름 배열", * required={"permission_names"}, + * * @OA\Property(property="permission_names", type="array", @OA\Items(type="string"), example={"menu:101.view","menu:101.update"}) * ), + * * @OA\Schema( * description="방법 B: 메뉴+액션 조합", * required={"menus","actions"}, + * * @OA\Property(property="menus", type="array", @OA\Items(type="integer"), example={101,102}), * @OA\Property(property="actions", type="array", @OA\Items(type="string"), example={"view","update"}) * ) @@ -95,15 +106,20 @@ class RolePermissionApi * description="해당 역할에 현재 부여된 퍼미션 목록을 반환합니다.", * tags={"RolePermission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=3), + * * @OA\Response(response=200, description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PermissionList")) * } * ) * ), + * * @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -119,8 +135,11 @@ public function list() {} * description="퍼미션 이름 배열 또는 메뉴ID+액션 조합으로 역할에 권한을 부여합니다.", * tags={"RolePermission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=3), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RolePermissionGrantRequest")), + * * @OA\Response(response=200, description="부여 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="역할/퍼미션 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -138,8 +157,11 @@ public function grant() {} * description="퍼미션 이름 배열 또는 메뉴ID+액션 조합으로 권한을 회수합니다.", * tags={"RolePermission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=3), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RolePermissionRevokeRequest")), + * * @OA\Response(response=200, description="회수 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="역할/퍼미션 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -157,8 +179,11 @@ public function revoke() {} * description="전달된 목록으로 역할의 권한을 완전히 교체합니다.", * tags={"RolePermission"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=3), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/RolePermissionSyncRequest")), + * * @OA\Response(response=200, description="동기화 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="역할/퍼미션 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/SAMInfo.php b/app/Swagger/v1/SAMInfo.php index 3115db1..e28936e 100644 --- a/app/Swagger/v1/SAMInfo.php +++ b/app/Swagger/v1/SAMInfo.php @@ -7,6 +7,7 @@ * version="1.0.0", * title="SAM API Documentation", * description="===============================
[공통 에러 응답 포맷]
400: 필수 파라미터 누락
401: 인증 실패
403: 권한 없음
404: 존재하지 않는 URI 또는 데이터
405: 허용되지 않는 메서드
500: 서버 에러

모든 에러 응답 예시:
{
  "success": false,
  "message": "에러 메시지",
  "data": null
}
===============================", + * * @OA\Contact( * email="shine1324@gmail.com" * ) @@ -23,7 +24,6 @@ * in="header", * name="X-API-KEY" * ) - * * @OA\SecurityScheme( * securityScheme="BearerAuth", * type="http", diff --git a/app/Swagger/v1/TenantApi.php b/app/Swagger/v1/TenantApi.php index 467de9f..5825758 100644 --- a/app/Swagger/v1/TenantApi.php +++ b/app/Swagger/v1/TenantApi.php @@ -14,6 +14,7 @@ * type="object", * description="테넌트 상세 정보", * required={"id","company_name"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="company_name", type="string", example="(주)경동기업"), * @OA\Property(property="code", type="string", example="KDCOM"), @@ -37,12 +38,15 @@ * schema="TenantPagination", * type="object", * description="라라벨 LengthAwarePaginator 기본 구조", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/TenantBrief") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/tenants/list?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=1), @@ -50,8 +54,10 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -69,6 +75,7 @@ * schema="TenantCreateRequest", * type="object", * required={"company_name"}, + * * @OA\Property(property="company_name", type="string", example="(주)신규기업", description="회사명"), * @OA\Property(property="email", type="string", nullable=true, example="newcompany@example.com", description="대표 이메일"), * @OA\Property(property="phone", type="string", nullable=true, example="01012345678", description="대표 연락처"), @@ -80,6 +87,7 @@ * @OA\Schema( * schema="TenantUpdateRequest", * type="object", + * * @OA\Property(property="tenant_id", type="integer", example=1, description="수정 대상 테넌트 ID"), * @OA\Property(property="company_name", type="string", example="(주)신규기업", description="회사명"), * @OA\Property(property="email", type="string", example="newcompany@example.com", description="대표 이메일"), @@ -98,18 +106,23 @@ class TenantApi * description="등록된 모든 테넌트 목록을 페이징 형태로 반환합니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, * description="테넌트 목록 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/TenantPagination")) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=405, description="허용되지 않는 메서드", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -125,23 +138,29 @@ public function index() {} * description="활성(현재) 테넌트의 상세 정보를 조회합니다. 필요 시 쿼리로 특정 테넌트를 확장할 수 있습니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="tenant_id", * in="query", * required=false, * description="조회할 테넌트 ID (없으면 활성 테넌트)", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="테넌트 정보 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Tenant")) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -158,20 +177,26 @@ public function show() {} * description="새로운 테넌트를 등록합니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/TenantCreateRequest")), + * * @OA\Response( * response=200, * description="등록 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="message", type="string", example="등록 성공"), * @OA\Property(property="data", ref="#/components/schemas/TenantBrief") * ) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -188,20 +213,26 @@ public function store() {} * description="기존 테넌트 정보를 수정합니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/TenantUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="message", type="string", example="수정 성공"), * @OA\Property(property="data", ref="#/components/schemas/TenantBrief") * ) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -219,19 +250,24 @@ public function update() {} * description="테넌트를 삭제(또는 탈퇴 처리)합니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="삭제 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="message", type="string", example="삭제 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -249,26 +285,33 @@ public function destroy() {} * description="삭제(소프트 삭제)된 테넌트를 복구합니다.", * tags={"Tenant"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="tenant_id", * in="path", * required=true, * description="복구할 테넌트 ID", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="복구 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="message", type="string", example="복구 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/UserApi.php b/app/Swagger/v1/UserApi.php index 8c48b42..c272696 100644 --- a/app/Swagger/v1/UserApi.php +++ b/app/Swagger/v1/UserApi.php @@ -7,13 +7,13 @@ * @OA\Tag(name="User", description="사용자 본인 정보/비밀번호 변경 등") */ - /** * @OA\Schema( * schema="Member", * type="object", * description="회원 기본 정보", * required={"id","user_id","name","email"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="user_id", type="string", example="hamss"), * @OA\Property(property="phone", type="string", nullable=true, example="010-4820-9104"), @@ -33,12 +33,15 @@ * schema="MemberPagination", * type="object", * description="라라벨 LengthAwarePaginator 기본 구조", + * * @OA\Property(property="current_page", type="integer", example=1), * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/Member") * ), + * * @OA\Property(property="first_page_url", type="string", example="/api/v1/users/index?page=1"), * @OA\Property(property="from", type="integer", example=1), * @OA\Property(property="last_page", type="integer", example=1), @@ -46,8 +49,10 @@ * @OA\Property( * property="links", * type="array", + * * @OA\Items( * type="object", + * * @OA\Property(property="url", type="string", nullable=true, example=null), * @OA\Property(property="label", type="string", example="« Previous"), * @OA\Property(property="active", type="boolean", example=false) @@ -66,6 +71,7 @@ * type="object", * description="간단 테넌트 정보", * required={"id","company_name"}, + * * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="company_name", type="string", example="(주)경동기업"), * @OA\Property(property="code", type="string", example="KDCOM"), @@ -86,13 +92,13 @@ * schema="MeResponseData", * type="object", * description="내 정보 + 테넌트 정보", + * * @OA\Property(property="user", ref="#/components/schemas/Member"), * @OA\Property(property="tenant", ref="#/components/schemas/TenantBrief") * ) */ class UserApi { - /** * @OA\Get( * path="/api/v1/users/me", @@ -100,16 +106,20 @@ class UserApi * description="내 정보와 활성 테넌트 정보를 반환합니다.", * tags={"User"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Response( * response=200, * description="나의 정보 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/MeResponseData")) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패 (헤더 누락, 유효하지 않은 토큰/키 등)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=405, description="허용되지 않는 메서드", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) @@ -124,17 +134,22 @@ public function me() {} * summary="내 정보 수정", * description="이름/연락처 등 프로필 정보를 수정합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UserUpdateRequest")), + * * @OA\Response( * response=200, * description="수정 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Member")) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -152,16 +167,21 @@ public function updateMe() {} * summary="비밀번호 변경", * description="현재 비밀번호 검증 후 새 비밀번호로 변경합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PasswordChangeRequest")), + * * @OA\Response( * response=204, * description="변경 성공(콘텐츠 없음)", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -178,17 +198,23 @@ public function changePassword() {} * summary="내 테넌트 목록", * description="사용자가 소속된 테넌트 목록을 반환합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(type="object", + * * @OA\Property(property="tenant_id", type="integer", example=1), * @OA\Property(property="tenant_name", type="string", example="경동기업"), * @OA\Property(property="is_active", type="boolean", example=true) @@ -198,6 +224,7 @@ public function changePassword() {} * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -214,16 +241,21 @@ public function myTenants() {} * summary="활성 테넌트 전환", * description="현재 세션/토큰의 활성 테넌트를 전환합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/SwitchTenantRequest")), + * * @OA\Response( * response=204, * description="전환 성공(콘텐츠 없음)", + * * @OA\JsonContent( + * * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), * @OA\Property(property="data", type="object", nullable=true, example=null) * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -234,7 +266,6 @@ public function myTenants() {} */ public function switchTenant() {} - /** * @OA\Get( * path="/api/v1/users/index", @@ -242,20 +273,26 @@ public function switchTenant() {} * description="회원 목록을 페이징 형태로 반환합니다.", * tags={"User"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), + * * @OA\Response( * response=200, * description="회원 목록 조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property(property="data", ref="#/components/schemas/MemberPagination") * ) * } * ) * ), + * * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -273,23 +310,29 @@ public function index() {} * description="user_no 기준으로 회원 상세 정보를 조회합니다.", * tags={"User"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter( * name="user_no", * in="path", * required=true, * description="회원 번호 (USER_NO)", + * * @OA\Schema(type="integer", example=1) * ), + * * @OA\Response( * response=200, * description="회원 상세조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Member")) * } * ) * ), + * * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=404, description="회원 정보 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=405, description="허용되지 않는 메서드", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Swagger/v1/UserRoleApi.php b/app/Swagger/v1/UserRoleApi.php index e5dc353..e74ff24 100644 --- a/app/Swagger/v1/UserRoleApi.php +++ b/app/Swagger/v1/UserRoleApi.php @@ -17,10 +17,13 @@ * oneOf={ * @OA\Schema( * required={"role_names"}, + * * @OA\Property(property="role_names", type="array", @OA\Items(type="string"), example={"menu-manager","readonly"}) * ), + * * @OA\Schema( * required={"role_ids"}, + * * @OA\Property(property="role_ids", type="array", @OA\Items(type="integer"), example={1,2}) * ) * } @@ -33,10 +36,13 @@ * oneOf={ * @OA\Schema( * required={"role_names"}, + * * @OA\Property(property="role_names", type="array", @OA\Items(type="string"), example={"readonly"}) * ), + * * @OA\Schema( * required={"role_ids"}, + * * @OA\Property(property="role_ids", type="array", @OA\Items(type="integer"), example={2}) * ) * } @@ -49,10 +55,13 @@ * oneOf={ * @OA\Schema( * required={"role_names"}, + * * @OA\Property(property="role_names", type="array", @OA\Items(type="string"), example={"menu-manager"}) * ), + * * @OA\Schema( * required={"role_ids"}, + * * @OA\Property(property="role_ids", type="array", @OA\Items(type="integer"), example={1}) * ) * } @@ -67,23 +76,30 @@ class UserRoleApi * description="해당 사용자에게 현재 부여된 역할 목록을 반환합니다.", * tags={"UserRole"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1), + * * @OA\Response( * response=200, * description="조회 성공", + * * @OA\JsonContent( * allOf={ + * * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( + * * @OA\Property( * property="data", * type="array", + * * @OA\Items(ref="#/components/schemas/RoleBrief") * ) * ) * } * ) * ), + * * @OA\Response(response=404, description="사용자 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -99,8 +115,11 @@ public function index() {} * description="role_names 또는 role_ids로 여러 역할을 부여합니다.", * tags={"UserRole"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UserRoleGrantRequest")), + * * @OA\Response(response=200, description="부여 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="사용자/역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -118,8 +137,11 @@ public function grant() {} * description="role_names 또는 role_ids로 여러 역할을 회수합니다.", * tags={"UserRole"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UserRoleRevokeRequest")), + * * @OA\Response(response=200, description="회수 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="사용자/역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -137,8 +159,11 @@ public function revoke() {} * description="전달된 목록으로 사용자의 역할을 완전히 교체합니다.", * tags={"UserRole"}, * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer"), example=1), + * * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UserRoleSyncRequest")), + * * @OA\Response(response=200, description="동기화 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=404, description="사용자/역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/app/Traits/UppercaseAttributes.php b/app/Traits/UppercaseAttributes.php index b185aae..a3ebbca 100644 --- a/app/Traits/UppercaseAttributes.php +++ b/app/Traits/UppercaseAttributes.php @@ -7,18 +7,21 @@ trait UppercaseAttributes protected function getAttributeFromArray($key) { $upperKey = strtoupper($key); + return parent::getAttributeFromArray($upperKey); } public function __get($key) { $upperKey = strtoupper($key); + return parent::__get($upperKey); } public function __set($key, $value) { $upperKey = strtoupper($key); + return parent::__set($upperKey, $value); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 6f72ad2..4157c81 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,18 +1,15 @@ withRouting( @@ -27,7 +24,7 @@ $middleware->alias([ 'auth.apikey' => ApiKeyMiddleware::class, // 인증: apikey + basic auth 'swagger.auth' => CheckSwaggerAuth::class, - 'perm.map' => PermMapper::class, // 전처리: 라우트명 → perm 키 생성/주입 + 'perm.map' => PermMapper::class, // 전처리: 라우트명 → perm 키 생성/주입 'permission' => CheckPermission::class, // 검사: perm 키로 접근 허용/차단 ]); }) diff --git a/config/authz.php b/config/authz.php index e60f7ce..f311a20 100644 --- a/config/authz.php +++ b/config/authz.php @@ -2,5 +2,5 @@ return [ // 메뉴 생성 시 자동으로 만들 권한 액션 세트 - 'menu_actions' => ['view','create','update','delete','approve', 'export', 'manage'], + 'menu_actions' => ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'], ]; diff --git a/config/cors.php b/config/cors.php index eda71cb..3432a11 100644 --- a/config/cors.php +++ b/config/cors.php @@ -9,4 +9,4 @@ 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => false, -]; \ No newline at end of file +]; diff --git a/config/er-diagram-generator.php b/config/er-diagram-generator.php index 0f82e8f..fe96202 100644 --- a/config/er-diagram-generator.php +++ b/config/er-diagram-generator.php @@ -59,5 +59,5 @@ 'splines' => 'ortho', // 직선 연결 'overlap' => 'false', 'concentrate' => 'true', // 관계선 집중화 - ] -]; \ No newline at end of file + ], +]; diff --git a/config/products.php b/config/products.php index e16428a..bddcd20 100644 --- a/config/products.php +++ b/config/products.php @@ -1,4 +1,5 @@ [ diff --git a/database/migrations/2025_08_15_000000_create_authz_structures.php b/database/migrations/2025_08_15_000000_create_authz_structures.php index 3e35c6c..c9bfe38 100644 --- a/database/migrations/2025_08_15_000000_create_authz_structures.php +++ b/database/migrations/2025_08_15_000000_create_authz_structures.php @@ -15,23 +15,23 @@ public function up(): void */ Schema::table('menus', function (Blueprint $table) { // slug 추가 (권한 키로 활용) - 테넌트 내 유니크 - if (!Schema::hasColumn('menus', 'slug')) { + if (! Schema::hasColumn('menus', 'slug')) { $table->string('slug', 150)->nullable()->after('name')->comment('메뉴 슬러그(권한 키)'); } // Soft delete - if (!Schema::hasColumn('menus', 'deleted_at')) { + if (! Schema::hasColumn('menus', 'deleted_at')) { $table->softDeletes()->comment('소프트삭제 시각'); } // acted-by (누가 생성/수정/삭제 했는지) - if (!Schema::hasColumn('menus', 'created_by')) { + if (! Schema::hasColumn('menus', 'created_by')) { $table->unsignedBigInteger('created_by')->nullable()->after('updated_at')->comment('생성자 사용자 ID'); } - if (!Schema::hasColumn('menus', 'updated_by')) { + if (! Schema::hasColumn('menus', 'updated_by')) { $table->unsignedBigInteger('updated_by')->nullable()->after('created_by')->comment('수정자 사용자 ID'); } - if (!Schema::hasColumn('menus', 'deleted_by')) { + if (! Schema::hasColumn('menus', 'deleted_by')) { $table->unsignedBigInteger('deleted_by')->nullable()->after('updated_by')->comment('삭제자 사용자 ID'); } @@ -45,7 +45,7 @@ public function up(): void /** * 2) 부서 테이블 */ - if (!Schema::hasTable('departments')) { + if (! Schema::hasTable('departments')) { Schema::create('departments', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK: 부서 ID'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -74,7 +74,7 @@ public function up(): void * 3) 부서-사용자 매핑 * - 동일 부서-사용자 중복 매핑 방지(tenant_id 포함) */ - if (!Schema::hasTable('department_user')) { + if (! Schema::hasTable('department_user')) { Schema::create('department_user', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -101,7 +101,7 @@ public function up(): void * - ALLOW/DENY는 is_allowed로 표현 * - 필요시 메뉴 단위 범위 제한을 위해 menu_id(옵션) */ - if (!Schema::hasTable('department_permissions')) { + if (! Schema::hasTable('department_permissions')) { Schema::create('department_permissions', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -129,7 +129,7 @@ public function up(): void * 5) 사용자 퍼미션 오버라이드 (개인 단위 허용/차단) * - 개인 DENY가 최우선 → 해석 레이어에서 우선순위 처리 */ - if (!Schema::hasTable('user_permission_overrides')) { + if (! Schema::hasTable('user_permission_overrides')) { Schema::create('user_permission_overrides', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -206,7 +206,7 @@ private function indexExists(string $table, string $indexName): bool $connection = Schema::getConnection(); $schemaManager = $connection->getDoctrineSchemaManager(); $doctrineTable = $schemaManager->introspectTable( - $connection->getTablePrefix() . $table + $connection->getTablePrefix().$table ); foreach ($doctrineTable->getIndexes() as $idx) { if ($idx->getName() === $indexName) { @@ -216,6 +216,7 @@ private function indexExists(string $table, string $indexName): bool } catch (\Throwable $e) { // 무시 } + return false; } }; diff --git a/database/migrations/2025_08_15_000100_update_spatie_permission_for_teams_and_guard.php b/database/migrations/2025_08_15_000100_update_spatie_permission_for_teams_and_guard.php index 716e85a..e5d250b 100644 --- a/database/migrations/2025_08_15_000100_update_spatie_permission_for_teams_and_guard.php +++ b/database/migrations/2025_08_15_000100_update_spatie_permission_for_teams_and_guard.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -12,10 +12,10 @@ public function up(): void // ===== permissions ===== if (Schema::hasTable('permissions')) { Schema::table('permissions', function (Blueprint $table) { - if (!Schema::hasColumn('permissions', 'guard_name')) { + if (! Schema::hasColumn('permissions', 'guard_name')) { $table->string('guard_name', 50)->default('api')->after('name'); } - if (!Schema::hasColumn('permissions', 'tenant_id')) { + if (! Schema::hasColumn('permissions', 'tenant_id')) { $table->unsignedBigInteger('tenant_id')->nullable()->after('guard_name'); $table->index(['tenant_id']); } @@ -35,10 +35,10 @@ public function up(): void // ===== roles ===== if (Schema::hasTable('roles')) { Schema::table('roles', function (Blueprint $table) { - if (!Schema::hasColumn('roles', 'guard_name')) { + if (! Schema::hasColumn('roles', 'guard_name')) { $table->string('guard_name', 50)->default('api')->after('name'); } - if (!Schema::hasColumn('roles', 'tenant_id')) { + if (! Schema::hasColumn('roles', 'tenant_id')) { // 이미 있으시다 했지만 혹시 없을 경우 대비 $table->unsignedBigInteger('tenant_id')->nullable()->after('guard_name'); } @@ -61,7 +61,7 @@ public function up(): void // ===== model_has_roles ===== if (Schema::hasTable('model_has_roles')) { Schema::table('model_has_roles', function (Blueprint $table) { - if (!Schema::hasColumn('model_has_roles', 'tenant_id')) { + if (! Schema::hasColumn('model_has_roles', 'tenant_id')) { $table->unsignedBigInteger('tenant_id')->nullable()->after('model_id'); } }); @@ -88,7 +88,7 @@ public function up(): void // ===== model_has_permissions ===== if (Schema::hasTable('model_has_permissions')) { Schema::table('model_has_permissions', function (Blueprint $table) { - if (!Schema::hasColumn('model_has_permissions', 'tenant_id')) { + if (! Schema::hasColumn('model_has_permissions', 'tenant_id')) { $table->unsignedBigInteger('tenant_id')->nullable()->after('model_id'); } }); diff --git a/database/migrations/2025_08_15_000200_drop_slug_from_menus_table.php b/database/migrations/2025_08_15_000200_drop_slug_from_menus_table.php index 0063738..fa4b096 100644 --- a/database/migrations/2025_08_15_000200_drop_slug_from_menus_table.php +++ b/database/migrations/2025_08_15_000200_drop_slug_from_menus_table.php @@ -2,10 +2,11 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { // 1) 유니크 인덱스 존재 시 제거 (information_schema로 확인) @@ -32,7 +33,7 @@ public function up(): void public function down(): void { // 1) slug 컬럼 복구 - if (!Schema::hasColumn('menus', 'slug')) { + if (! Schema::hasColumn('menus', 'slug')) { Schema::table('menus', function (Blueprint $table) { $table->string('slug', 150)->nullable()->comment('메뉴 슬러그(권한 키)'); }); diff --git a/database/migrations/2025_08_19_000001_add_colomn_parent_id_departments.php b/database/migrations/2025_08_19_000001_add_colomn_parent_id_departments.php index 0547942..ee25182 100644 --- a/database/migrations/2025_08_19_000001_add_colomn_parent_id_departments.php +++ b/database/migrations/2025_08_19_000001_add_colomn_parent_id_departments.php @@ -2,15 +2,15 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { // 1) parent_id 추가 (tenant_id 뒤에 배치) - if (!Schema::hasColumn('departments', 'parent_id')) { + if (! Schema::hasColumn('departments', 'parent_id')) { Schema::table('departments', function (Blueprint $table) { $table->unsignedBigInteger('parent_id') ->nullable() @@ -46,8 +46,8 @@ public function up(): void $sql[] = "MODIFY COLUMN `deleted_by` BIGINT UNSIGNED NULL COMMENT '삭제자 사용자 ID' AFTER `{$after}`"; } - if (!empty($sql)) { - DB::statement("ALTER TABLE `departments` " . implode(", ", $sql)); + if (! empty($sql)) { + DB::statement('ALTER TABLE `departments` '.implode(', ', $sql)); } } } @@ -82,8 +82,8 @@ public function down(): void $sql[] = "MODIFY COLUMN `deleted_by` BIGINT UNSIGNED NULL COMMENT '삭제자 사용자 ID' AFTER `updated_by`"; } - if (!empty($sql)) { - DB::statement("ALTER TABLE `departments` " . implode(", ", $sql)); + if (! empty($sql)) { + DB::statement('ALTER TABLE `departments` '.implode(', ', $sql)); } } } diff --git a/database/migrations/2025_08_21_000000_unify_permissions_to_spatie_and_overrides.php b/database/migrations/2025_08_21_000000_unify_permissions_to_spatie_and_overrides.php index 91c1fe1..bd143e3 100644 --- a/database/migrations/2025_08_21_000000_unify_permissions_to_spatie_and_overrides.php +++ b/database/migrations/2025_08_21_000000_unify_permissions_to_spatie_and_overrides.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -16,7 +16,7 @@ public function up(): void { // 1) permission_overrides (DENY 최우선, 선택적 ALLOW 지원) - if (!Schema::hasTable('permission_overrides')) { + if (! Schema::hasTable('permission_overrides')) { Schema::create('permission_overrides', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -41,11 +41,11 @@ public function up(): void // 2) 혼재 테이블 드롭(존재하면) foreach ([ - 'user_menu_permissions', - 'role_menu_permissions', - 'department_permissions', - 'user_permission_overrides', - ] as $legacyTable) { + 'user_menu_permissions', + 'role_menu_permissions', + 'department_permissions', + 'user_permission_overrides', + ] as $legacyTable) { if (Schema::hasTable($legacyTable)) { Schema::drop($legacyTable); } @@ -53,7 +53,7 @@ public function up(): void // 3) 보조 인덱스(선택) : model_has_permissions (tenant_id, permission_id) if (Schema::hasTable('model_has_permissions')) { - if (!$this->mysqlIndexExists('model_has_permissions', 'idx_mhp_tenant_perm')) { + if (! $this->mysqlIndexExists('model_has_permissions', 'idx_mhp_tenant_perm')) { Schema::table('model_has_permissions', function (Blueprint $table) { $table->index(['tenant_id', 'permission_id'], 'idx_mhp_tenant_perm'); }); @@ -69,7 +69,7 @@ public function down(): void } // 2) 드롭했던 테이블 복구(원본 스키마로) - if (!Schema::hasTable('user_menu_permissions')) { + if (! Schema::hasTable('user_menu_permissions')) { Schema::create('user_menu_permissions', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK: 사용자-메뉴 권한 ID'); $table->unsignedBigInteger('user_id')->comment('FK: 사용자 ID'); @@ -87,7 +87,7 @@ public function down(): void }); } - if (!Schema::hasTable('role_menu_permissions')) { + if (! Schema::hasTable('role_menu_permissions')) { Schema::create('role_menu_permissions', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK: 역할-메뉴 권한 ID'); $table->unsignedBigInteger('role_id')->comment('FK: 역할 ID'); @@ -105,7 +105,7 @@ public function down(): void }); } - if (!Schema::hasTable('department_permissions')) { + if (! Schema::hasTable('department_permissions')) { Schema::create('department_permissions', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -123,7 +123,7 @@ public function down(): void }); } - if (!Schema::hasTable('user_permission_overrides')) { + if (! Schema::hasTable('user_permission_overrides')) { Schema::create('user_permission_overrides', function (Blueprint $table) { $table->bigIncrements('id')->comment('PK'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); diff --git a/database/migrations/2025_08_21_000100_create_materials_table.php b/database/migrations/2025_08_21_000100_create_materials_table.php index ff10f3a..7d54958 100644 --- a/database/migrations/2025_08_21_000100_create_materials_table.php +++ b/database/migrations/2025_08_21_000100_create_materials_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -11,7 +11,7 @@ public function up(): void { Schema::table('materials', function (Blueprint $table) { // category_id - if (!Schema::hasColumn('materials', 'category_id')) { + if (! Schema::hasColumn('materials', 'category_id')) { $table->unsignedBigInteger('category_id') ->nullable() ->after('tenant_id') @@ -20,7 +20,7 @@ public function up(): void } // item_name - if (!Schema::hasColumn('materials', 'item_name')) { + if (! Schema::hasColumn('materials', 'item_name')) { $table->string('item_name', 255) ->nullable() ->after('name') @@ -40,10 +40,19 @@ public function up(): void public function down(): void { // FK 제거 시도 - try { DB::statement('ALTER TABLE materials DROP FOREIGN KEY materials_category_id_foreign'); } catch (\Throwable $e) {} + try { + DB::statement('ALTER TABLE materials DROP FOREIGN KEY materials_category_id_foreign'); + } catch (\Throwable $e) { + } // 컬럼 제거 - try { DB::statement('ALTER TABLE materials DROP COLUMN category_id'); } catch (\Throwable $e) {} - try { DB::statement('ALTER TABLE materials DROP COLUMN item_name'); } catch (\Throwable $e) {} + try { + DB::statement('ALTER TABLE materials DROP COLUMN category_id'); + } catch (\Throwable $e) { + } + try { + DB::statement('ALTER TABLE materials DROP COLUMN item_name'); + } catch (\Throwable $e) { + } } }; diff --git a/database/migrations/2025_08_22_001500_finalize_categories_products_profile_and_bom.php b/database/migrations/2025_08_22_001500_finalize_categories_products_profile_and_bom.php index 85035a5..927180f 100644 --- a/database/migrations/2025_08_22_001500_finalize_categories_products_profile_and_bom.php +++ b/database/migrations/2025_08_22_001500_finalize_categories_products_profile_and_bom.php @@ -12,8 +12,10 @@ private function dropUniqueIfColumns(string $table, array $columns): void { $indexes = collect(DB::select("SHOW INDEX FROM `{$table}`"))->groupBy('Key_name'); foreach ($indexes as $name => $rows) { - $nonUnique = (int)($rows->first()->Non_unique ?? 1); - if ($nonUnique !== 0) continue; + $nonUnique = (int) ($rows->first()->Non_unique ?? 1); + if ($nonUnique !== 0) { + continue; + } $cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all(); if ($cols === $columns) { DB::statement("ALTER TABLE `{$table}` DROP INDEX `{$name}`"); @@ -48,8 +50,14 @@ public function up(): void * - profile_code 컬럼 추가(+ 인덱스) */ // A-1. 값 변환(Y/N → 1/0) 시도 (이미 숫자여도 무해) - try { DB::statement("UPDATE categories SET is_active = 1 WHERE is_active = 'Y'"); } catch (\Throwable $e) {} - try { DB::statement("UPDATE categories SET is_active = 0 WHERE is_active = 'N'"); } catch (\Throwable $e) {} + try { + DB::statement("UPDATE categories SET is_active = 1 WHERE is_active = 'Y'"); + } catch (\Throwable $e) { + } + try { + DB::statement("UPDATE categories SET is_active = 0 WHERE is_active = 'N'"); + } catch (\Throwable $e) { + } // A-2. 타입 변경 DB::statement("ALTER TABLE categories @@ -57,21 +65,24 @@ public function up(): void // A-3. 유니크 (tenant_id, code) → (tenant_id, code_group, code) $this->dropUniqueIfColumns('categories', ['tenant_id', 'code']); - $idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name'); + $idx = collect(DB::select('SHOW INDEX FROM `categories`'))->groupBy('Key_name'); $hasTarget = false; foreach ($idx as $name => $rows) { - $nonUnique = (int)($rows->first()->Non_unique ?? 1); + $nonUnique = (int) ($rows->first()->Non_unique ?? 1); $cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all(); - if ($nonUnique === 0 && $cols === ['tenant_id', 'code_group', 'code']) { $hasTarget = true; break; } + if ($nonUnique === 0 && $cols === ['tenant_id', 'code_group', 'code']) { + $hasTarget = true; + break; + } } - if (!$hasTarget) { - DB::statement("ALTER TABLE `categories` - ADD UNIQUE KEY `uq_tenant_codegroup_code` (`tenant_id`,`code_group`,`code`)"); + if (! $hasTarget) { + DB::statement('ALTER TABLE `categories` + ADD UNIQUE KEY `uq_tenant_codegroup_code` (`tenant_id`,`code_group`,`code`)'); } // A-4. profile_code 추가 (common_codes.code, code_group='capability_profile') Schema::table('categories', function (Blueprint $table) { - if (!Schema::hasColumn('categories', 'profile_code')) { + if (! Schema::hasColumn('categories', 'profile_code')) { $table->string('profile_code', 30) ->nullable() ->after('code_group') @@ -79,10 +90,10 @@ public function up(): void } }); // 인덱스 (tenant_id, profile_code) - $hasIdx = collect(DB::select("SHOW INDEX FROM `categories`")) - ->contains(fn($r) => $r->Key_name === 'idx_categories_tenant_profilecode'); - if (!$hasIdx) { - DB::statement("CREATE INDEX `idx_categories_tenant_profilecode` ON `categories` (`tenant_id`, `profile_code`)"); + $hasIdx = collect(DB::select('SHOW INDEX FROM `categories`')) + ->contains(fn ($r) => $r->Key_name === 'idx_categories_tenant_profilecode'); + if (! $hasIdx) { + DB::statement('CREATE INDEX `idx_categories_tenant_profilecode` ON `categories` (`tenant_id`, `profile_code`)'); } /** @@ -96,19 +107,19 @@ public function up(): void ->references('id')->on('categories') ->onUpdate('cascade')->onDelete('restrict'); - if (!Schema::hasColumn('products', 'product_type')) { + if (! Schema::hasColumn('products', 'product_type')) { $table->string('product_type', 30) ->default('PRODUCT') ->comment("제품유형: PRODUCT/PART/SUBASSEMBLY 등 (common_codes.code_group='product_type')") ->after('category_id'); } - if (!Schema::hasColumn('products', 'is_sellable')) { + if (! Schema::hasColumn('products', 'is_sellable')) { $table->tinyInteger('is_sellable')->default(1)->comment('판매가능(1/0)')->after('description'); } - if (!Schema::hasColumn('products', 'is_purchasable')) { + if (! Schema::hasColumn('products', 'is_purchasable')) { $table->tinyInteger('is_purchasable')->default(0)->comment('구매가능(1/0)')->after('is_sellable'); } - if (!Schema::hasColumn('products', 'is_producible')) { + if (! Schema::hasColumn('products', 'is_producible')) { $table->tinyInteger('is_producible')->default(1)->comment('제조가능(1/0)')->after('is_purchasable'); } }); @@ -117,7 +128,7 @@ public function up(): void * C) BOM 자기참조 테이블 신설 (product_components) * - parts/boms/bom_items 레거시 제거 */ - if (!Schema::hasTable('product_components')) { + if (! Schema::hasTable('product_components')) { Schema::create('product_components', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); @@ -155,7 +166,7 @@ public function down(): void if (Schema::hasTable('product_components')) { Schema::drop('product_components'); } - if (!Schema::hasTable('parts')) { + if (! Schema::hasTable('parts')) { Schema::create('parts', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id'); @@ -171,10 +182,10 @@ public function down(): void $table->unsignedBigInteger('updated_by')->nullable(); $table->timestamps(); $table->softDeletes(); - $table->unique(['tenant_id','code']); + $table->unique(['tenant_id', 'code']); }); } - if (!Schema::hasTable('boms')) { + if (! Schema::hasTable('boms')) { Schema::create('boms', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id'); @@ -191,10 +202,10 @@ public function down(): void $table->unsignedBigInteger('updated_by')->nullable(); $table->timestamps(); $table->softDeletes(); - $table->unique(['tenant_id','product_id','code']); + $table->unique(['tenant_id', 'product_id', 'code']); }); } - if (!Schema::hasTable('bom_items')) { + if (! Schema::hasTable('bom_items')) { Schema::create('bom_items', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id'); @@ -218,10 +229,18 @@ public function down(): void * - FK: categories → common_codes */ Schema::table('products', function (Blueprint $table) { - if (Schema::hasColumn('products', 'is_sellable')) $table->dropColumn('is_sellable'); - if (Schema::hasColumn('products', 'is_purchasable')) $table->dropColumn('is_purchasable'); - if (Schema::hasColumn('products', 'is_producible')) $table->dropColumn('is_producible'); - if (Schema::hasColumn('products', 'product_type')) $table->dropColumn('product_type'); + if (Schema::hasColumn('products', 'is_sellable')) { + $table->dropColumn('is_sellable'); + } + if (Schema::hasColumn('products', 'is_purchasable')) { + $table->dropColumn('is_purchasable'); + } + if (Schema::hasColumn('products', 'is_producible')) { + $table->dropColumn('is_producible'); + } + if (Schema::hasColumn('products', 'product_type')) { + $table->dropColumn('product_type'); + } }); $this->dropProductsCategoryFk(); @@ -237,10 +256,10 @@ public function down(): void * - 유니크: (tenant_id, code_group, code) → (tenant_id, code) * - is_active: CHAR('Y'/'N') 복구 + 값 재변환 */ - $hasIdx = collect(DB::select("SHOW INDEX FROM `categories`")) - ->contains(fn($r) => $r->Key_name === 'idx_categories_tenant_profilecode'); + $hasIdx = collect(DB::select('SHOW INDEX FROM `categories`')) + ->contains(fn ($r) => $r->Key_name === 'idx_categories_tenant_profilecode'); if ($hasIdx) { - DB::statement("DROP INDEX `idx_categories_tenant_profilecode` ON `categories`"); + DB::statement('DROP INDEX `idx_categories_tenant_profilecode` ON `categories`'); } Schema::table('categories', function (Blueprint $table) { if (Schema::hasColumn('categories', 'profile_code')) { @@ -248,27 +267,36 @@ public function down(): void } }); - $idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name'); + $idx = collect(DB::select('SHOW INDEX FROM `categories`'))->groupBy('Key_name'); if ($idx->has('uq_tenant_codegroup_code')) { - DB::statement("ALTER TABLE `categories` DROP INDEX `uq_tenant_codegroup_code`"); + DB::statement('ALTER TABLE `categories` DROP INDEX `uq_tenant_codegroup_code`'); } // (tenant_id, code) 유니크 복구(없을 때만) - $idx = collect(DB::select("SHOW INDEX FROM `categories`"))->groupBy('Key_name'); + $idx = collect(DB::select('SHOW INDEX FROM `categories`'))->groupBy('Key_name'); $hasOld = false; foreach ($idx as $name => $rows) { - $nonUnique = (int)($rows->first()->Non_unique ?? 1); + $nonUnique = (int) ($rows->first()->Non_unique ?? 1); $cols = collect($rows)->sortBy('Seq_in_index')->pluck('Column_name')->values()->all(); - if ($nonUnique === 0 && $cols === ['tenant_id', 'code']) { $hasOld = true; break; } + if ($nonUnique === 0 && $cols === ['tenant_id', 'code']) { + $hasOld = true; + break; + } } - if (!$hasOld) { - DB::statement("ALTER TABLE `categories` - ADD UNIQUE KEY `categories_tenant_id_code_unique` (`tenant_id`,`code`)"); + if (! $hasOld) { + DB::statement('ALTER TABLE `categories` + ADD UNIQUE KEY `categories_tenant_id_code_unique` (`tenant_id`,`code`)'); } // 타입 복구 + 값 재변환 DB::statement("ALTER TABLE categories MODIFY COLUMN is_active CHAR(1) NOT NULL DEFAULT 'Y' COMMENT '활성여부(Y/N)'"); - try { DB::statement("UPDATE categories SET is_active = 'Y' WHERE is_active = 1"); } catch (\Throwable $e) {} - try { DB::statement("UPDATE categories SET is_active = 'N' WHERE is_active = 0"); } catch (\Throwable $e) {} + try { + DB::statement("UPDATE categories SET is_active = 'Y' WHERE is_active = 1"); + } catch (\Throwable $e) { + } + try { + DB::statement("UPDATE categories SET is_active = 'N' WHERE is_active = 0"); + } catch (\Throwable $e) { + } } }; diff --git a/database/migrations/2025_08_22_001600_create_tenant_bootstrap_runs_table.php b/database/migrations/2025_08_22_001600_create_tenant_bootstrap_runs_table.php index 931fbf6..8c37603 100644 --- a/database/migrations/2025_08_22_001600_create_tenant_bootstrap_runs_table.php +++ b/database/migrations/2025_08_22_001600_create_tenant_bootstrap_runs_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('tenant_bootstrap_runs', function (Blueprint $t) { $t->bigIncrements('id'); $t->unsignedBigInteger('tenant_id'); @@ -19,7 +21,9 @@ public function up(): void { $t->index(['tenant_id', 'recipe']); }); } - public function down(): void { + + public function down(): void + { Schema::dropIfExists('tenant_bootstrap_runs'); } }; diff --git a/database/migrations/2025_08_22_002000_create_classifications_table.php b/database/migrations/2025_08_22_002000_create_classifications_table.php index f78f537..8af0e29 100644 --- a/database/migrations/2025_08_22_002000_create_classifications_table.php +++ b/database/migrations/2025_08_22_002000_create_classifications_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { diff --git a/database/migrations/2025_08_25_153000_update_product_components_add_ref_type_and_material_id.php b/database/migrations/2025_08_25_153000_update_product_components_add_ref_type_and_material_id.php index 8500333..8831f71 100644 --- a/database/migrations/2025_08_25_153000_update_product_components_add_ref_type_and_material_id.php +++ b/database/migrations/2025_08_25_153000_update_product_components_add_ref_type_and_material_id.php @@ -37,12 +37,12 @@ public function up(): void // 3) 기존 유니크 키 재정의: // (tenant_id, parent_product_id, ref_type, child_product_id, material_id, sort_order) // - 제품/자재 타입과 각각의 ID를 모두 포함하도록 변경 - DB::statement("ALTER TABLE product_components DROP INDEX uq_component_row"); - DB::statement(" + DB::statement('ALTER TABLE product_components DROP INDEX uq_component_row'); + DB::statement(' ALTER TABLE product_components ADD UNIQUE INDEX uq_component_row (tenant_id, parent_product_id, ref_type, child_product_id, material_id, sort_order) - "); + '); // 4) (선택) CHECK 제약: MySQL 8.0.16+ 에서만 유효 // ref_type=PRODUCT -> child_product_id NOT NULL AND material_id NULL @@ -69,25 +69,25 @@ public function down(): void { // CHECK 제약 삭제 (가능한 경우만) try { - DB::statement(" + DB::statement(' ALTER TABLE product_components DROP CHECK chk_ref_type_consistency - "); + '); } catch (\Throwable $e) { // 무시 } // 유니크 키를 원래대로 복구 try { - DB::statement("ALTER TABLE product_components DROP INDEX uq_component_row"); + DB::statement('ALTER TABLE product_components DROP INDEX uq_component_row'); } catch (\Throwable $e) { // 무시 } - DB::statement(" + DB::statement(' ALTER TABLE product_components ADD UNIQUE INDEX uq_component_row (tenant_id, parent_product_id, child_product_id, sort_order) - "); + '); Schema::table('product_components', function (Blueprint $table) { // FK 우선 제거 diff --git a/database/migrations/2025_08_26_222920_add_unit_to_products_table.php b/database/migrations/2025_08_26_222920_add_unit_to_products_table.php index 3989c0c..ac48569 100644 --- a/database/migrations/2025_08_26_222920_add_unit_to_products_table.php +++ b/database/migrations/2025_08_26_222920_add_unit_to_products_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::table('products', function (Blueprint $table) { diff --git a/database/migrations/2025_08_28_000100_alter_product_components_unify_ref_columns.php b/database/migrations/2025_08_28_000100_alter_product_components_unify_ref_columns.php index 7acd5e5..af0c6d9 100644 --- a/database/migrations/2025_08_28_000100_alter_product_components_unify_ref_columns.php +++ b/database/migrations/2025_08_28_000100_alter_product_components_unify_ref_columns.php @@ -5,7 +5,8 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { // 1) 기존 제약/인덱스 해제 (있으면 제거) @@ -15,7 +16,10 @@ public function up(): void 'product_components_tenant_id_parent_product_id_index', ]; foreach ($dropIndexes as $idx) { - try { DB::statement("ALTER TABLE `product_components` DROP INDEX `$idx`"); } catch (\Throwable $e) {} + try { + DB::statement("ALTER TABLE `product_components` DROP INDEX `$idx`"); + } catch (\Throwable $e) { + } } $dropFks = [ @@ -24,26 +28,32 @@ public function up(): void 'product_components_parent_product_id_foreign', ]; foreach ($dropFks as $fk) { - try { DB::statement("ALTER TABLE `product_components` DROP FOREIGN KEY `$fk`"); } catch (\Throwable $e) {} + try { + DB::statement("ALTER TABLE `product_components` DROP FOREIGN KEY `$fk`"); + } catch (\Throwable $e) { + } } // 2) 데이터 손실 허용 → TRUNCATE 로 비우고 진행 - try { DB::statement("TRUNCATE TABLE `product_components`"); } catch (\Throwable $e) {} + try { + DB::statement('TRUNCATE TABLE `product_components`'); + } catch (\Throwable $e) { + } // 3) 컬럼 추가/수정 Schema::table('product_components', function (Blueprint $table) { // 프론트 카테고리 메타(선택 저장) - if (!Schema::hasColumn('product_components', 'category_id')) { + if (! Schema::hasColumn('product_components', 'category_id')) { $table->unsignedBigInteger('category_id')->nullable()->after('parent_product_id') ->comment('프론트 카테고리 ID(선택)'); } - if (!Schema::hasColumn('product_components', 'category_name')) { + if (! Schema::hasColumn('product_components', 'category_name')) { $table->string('category_name', 100)->nullable()->after('category_id') ->comment('프론트 카테고리명(선택)'); } // 통합 참조키 ref_id 추가 - if (!Schema::hasColumn('product_components', 'ref_id')) { + if (! Schema::hasColumn('product_components', 'ref_id')) { $table->unsignedBigInteger('ref_id')->nullable()->after('ref_type') ->comment('참조 ID (materials.id 또는 products.id)'); } @@ -95,23 +105,29 @@ public function up(): void public function down(): void { // 인덱스 제거 - foreach (['idx_tenant_parent','idx_tenant_ref','idx_tenant_category','idx_tenant_sort'] as $idx) { - try { DB::statement("ALTER TABLE `product_components` DROP INDEX `$idx`"); } catch (\Throwable $e) {} + foreach (['idx_tenant_parent', 'idx_tenant_ref', 'idx_tenant_category', 'idx_tenant_sort'] as $idx) { + try { + DB::statement("ALTER TABLE `product_components` DROP INDEX `$idx`"); + } catch (\Throwable $e) { + } } // 데이터 손실 허용: TRUNCATE 후 원형에 가깝게 복원 - try { DB::statement("TRUNCATE TABLE `product_components`"); } catch (\Throwable $e) {} + try { + DB::statement('TRUNCATE TABLE `product_components`'); + } catch (\Throwable $e) { + } // 컬럼 복원 Schema::table('product_components', function (Blueprint $table) { // child_product_id, material_id, is_default 복원 - if (!Schema::hasColumn('product_components', 'child_product_id')) { + if (! Schema::hasColumn('product_components', 'child_product_id')) { $table->unsignedBigInteger('child_product_id')->nullable()->after('ref_type')->comment('하위 제품/부품 ID'); } - if (!Schema::hasColumn('product_components', 'material_id')) { + if (! Schema::hasColumn('product_components', 'material_id')) { $table->unsignedBigInteger('material_id')->nullable()->after('child_product_id')->comment('자재 ID'); } - if (!Schema::hasColumn('product_components', 'is_default')) { + if (! Schema::hasColumn('product_components', 'is_default')) { $table->tinyInteger('is_default')->default(0)->after('sort_order')->comment('기본 BOM 여부(1/0)'); } }); @@ -124,10 +140,10 @@ public function down(): void "); // quantity 정밀도 원복 - DB::statement(" + DB::statement(' ALTER TABLE `product_components` MODIFY COLUMN `quantity` DECIMAL(18,4) NOT NULL DEFAULT 1.0000 - "); + '); // ref_id 제거 if (Schema::hasColumn('product_components', 'ref_id')) { @@ -148,49 +164,55 @@ public function down(): void // 원래 인덱스/제약 복원 (FK 최소화 정책이지만 down에서는 원형 회귀) try { - DB::statement(" + DB::statement(' ALTER TABLE `product_components` ADD CONSTRAINT `uq_component_row` UNIQUE (`tenant_id`,`parent_product_id`,`ref_type`,`child_product_id`,`material_id`,`sort_order`) - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } try { - DB::statement(" + DB::statement(' ALTER TABLE `product_components` ADD CONSTRAINT `product_components_child_product_id_foreign` FOREIGN KEY (`child_product_id`) REFERENCES `products`(`id`) - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } try { - DB::statement(" + DB::statement(' ALTER TABLE `product_components` ADD CONSTRAINT `product_components_material_id_foreign` FOREIGN KEY (`material_id`) REFERENCES `materials`(`id`) ON DELETE SET NULL - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } try { - DB::statement(" + DB::statement(' ALTER TABLE `product_components` ADD CONSTRAINT `product_components_parent_product_id_foreign` FOREIGN KEY (`parent_product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } try { - DB::statement(" + DB::statement(' CREATE INDEX `product_components_tenant_id_child_product_id_index` ON `product_components`(`tenant_id`,`child_product_id`) - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } try { - DB::statement(" + DB::statement(' CREATE INDEX `product_components_tenant_id_parent_product_id_index` ON `product_components`(`tenant_id`,`parent_product_id`) - "); - } catch (\Throwable $e) {} + '); + } catch (\Throwable $e) { + } } }; diff --git a/database/migrations/2025_09_05_000001_create_models_table.php b/database/migrations/2025_09_05_000001_create_models_table.php index b56f15f..7e2acfc 100644 --- a/database/migrations/2025_09_05_000001_create_models_table.php +++ b/database/migrations/2025_09_05_000001_create_models_table.php @@ -4,9 +4,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ // 설계 상위: models - public function up(): void { + public function up(): void + { Schema::create('models', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); @@ -25,7 +27,8 @@ public function up(): void { }); } - public function down(): void { + public function down(): void + { Schema::dropIfExists('models'); } }; diff --git a/database/migrations/2025_09_05_000002_create_model_versions_table.php b/database/migrations/2025_09_05_000002_create_model_versions_table.php index dc22ae6..1bf5352 100644 --- a/database/migrations/2025_09_05_000002_create_model_versions_table.php +++ b/database/migrations/2025_09_05_000002_create_model_versions_table.php @@ -4,9 +4,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ // 설계 버전: model_versions - public function up(): void { + public function up(): void + { Schema::create('model_versions', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); @@ -26,7 +28,8 @@ public function up(): void { }); } - public function down(): void { + public function down(): void + { Schema::dropIfExists('model_versions'); } }; diff --git a/database/migrations/2025_09_05_000003_create_bom_templates_table.php b/database/migrations/2025_09_05_000003_create_bom_templates_table.php index 428f3d8..54fc009 100644 --- a/database/migrations/2025_09_05_000003_create_bom_templates_table.php +++ b/database/migrations/2025_09_05_000003_create_bom_templates_table.php @@ -4,9 +4,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ // 설계용 BOM 템플릿: bom_templates - public function up(): void { + public function up(): void + { Schema::create('bom_templates', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); @@ -23,7 +25,8 @@ public function up(): void { }); } - public function down(): void { + public function down(): void + { Schema::dropIfExists('bom_templates'); } }; diff --git a/database/migrations/2025_09_05_000004_create_bom_template_items_table.php b/database/migrations/2025_09_05_000004_create_bom_template_items_table.php index d2ee7ee..42f253c 100644 --- a/database/migrations/2025_09_05_000004_create_bom_template_items_table.php +++ b/database/migrations/2025_09_05_000004_create_bom_template_items_table.php @@ -4,9 +4,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ // 설계용 BOM 항목: bom_template_items - public function up(): void { + public function up(): void + { Schema::create('bom_template_items', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); @@ -26,7 +28,8 @@ public function up(): void { }); } - public function down(): void { + public function down(): void + { Schema::dropIfExists('bom_template_items'); } }; diff --git a/database/migrations/2025_09_11_000100_create_audit_logs_table.php b/database/migrations/2025_09_11_000100_create_audit_logs_table.php index e18e671..b2f8b80 100644 --- a/database/migrations/2025_09_11_000100_create_audit_logs_table.php +++ b/database/migrations/2025_09_11_000100_create_audit_logs_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::create('audit_logs', function (Blueprint $table) { diff --git a/database/migrations/2025_09_22_215127_add_calculation_fields_to_bom_tables.php b/database/migrations/2025_09_22_215127_add_calculation_fields_to_bom_tables.php index b4c84f8..a643069 100644 --- a/database/migrations/2025_09_22_215127_add_calculation_fields_to_bom_tables.php +++ b/database/migrations/2025_09_22_215127_add_calculation_fields_to_bom_tables.php @@ -4,13 +4,15 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * BOM 테이블에 산출식 관련 필드 추가 * - bom_templates: 견적시 필요한 파라미터 스키마 정의 * - bom_template_items: 개별 아이템의 계산식 정의 */ - public function up(): void { + public function up(): void + { // BOM 템플릿에 산출식 조건 스키마 추가 Schema::table('bom_templates', function (Blueprint $table) { $table->json('calculation_schema')->nullable()->comment('견적 파라미터 스키마 (JSON)'); @@ -30,7 +32,8 @@ public function up(): void { /** * Reverse the migrations. */ - public function down(): void { + public function down(): void + { Schema::table('bom_template_items', function (Blueprint $table) { $table->dropColumn(['is_calculated', 'calculation_formula', 'depends_on', 'calculation_config']); }); @@ -39,4 +42,4 @@ public function down(): void { $table->dropColumn(['calculation_schema', 'company_type', 'formula_version']); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_22_215217_create_calculation_configs_table.php b/database/migrations/2025_09_22_215217_create_calculation_configs_table.php index eaaa89f..8b80f02 100644 --- a/database/migrations/2025_09_22_215217_create_calculation_configs_table.php +++ b/database/migrations/2025_09_22_215217_create_calculation_configs_table.php @@ -4,12 +4,14 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * 업체별 산출식 설정 테이블 생성 * 다양한 업체(경동기업, 삼성물산 등)의 고유한 산출식을 관리 */ - public function up(): void { + public function up(): void + { Schema::create('calculation_configs', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('tenant_id')->comment('테넌트ID'); @@ -35,7 +37,8 @@ public function up(): void { /** * Reverse the migrations. */ - public function down(): void { + public function down(): void + { Schema::dropIfExists('calculation_configs'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_24_000002_create_dynamic_estimate_fields.php b/database/migrations/2025_09_24_000002_create_dynamic_estimate_fields.php index 4986600..83e7688 100644 --- a/database/migrations/2025_09_24_000002_create_dynamic_estimate_fields.php +++ b/database/migrations/2025_09_24_000002_create_dynamic_estimate_fields.php @@ -1,8 +1,6 @@ 'model_name', 'name' => '모델명', 'type' => 'select', 'required' => true, 'order' => 1, - 'options' => ['KSS01', 'KSS02', 'KSE01', 'KWE01', 'KDSS01', '스크린비인정'], - 'desc' => '스크린 제품 모델 선택 (col4)'], + 'options' => ['KSS01', 'KSS02', 'KSE01', 'KWE01', 'KDSS01', '스크린비인정'], + 'desc' => '스크린 제품 모델 선택 (col4)'], ['key' => 'sequence', 'name' => '순번', 'type' => 'text', 'required' => false, 'order' => 2, - 'desc' => '견적 항목 순번 (col1)'], + 'desc' => '견적 항목 순번 (col1)'], ['key' => 'product_category', 'name' => '대분류', 'type' => 'text', 'required' => true, 'order' => 3, - 'default' => '스크린', 'desc' => '제품 대분류 (col2)'], + 'default' => '스크린', 'desc' => '제품 대분류 (col2)'], ['key' => 'sub_category', 'name' => '중분류', 'type' => 'text', 'required' => false, 'order' => 4, - 'desc' => '제품 중분류 (col3)'], + 'desc' => '제품 중분류 (col3)'], // 사이즈 관련 ['key' => 'open_width', 'name' => '오픈사이즈 가로(mm)', 'type' => 'number', 'required' => true, 'order' => 10, - 'desc' => '개구부 가로 사이즈 W0'], + 'desc' => '개구부 가로 사이즈 W0'], ['key' => 'open_height', 'name' => '오픈사이즈 세로(mm)', 'type' => 'number', 'required' => true, 'order' => 11, - 'desc' => '개구부 세로 사이즈 H0'], + 'desc' => '개구부 세로 사이즈 H0'], ['key' => 'make_width', 'name' => '제작사이즈 가로(mm)', 'type' => 'number', 'required' => false, 'order' => 12, - 'desc' => '제작 가로 사이즈 W1 (자동계산, col10)'], + 'desc' => '제작 가로 사이즈 W1 (자동계산, col10)'], ['key' => 'make_height', 'name' => '제작사이즈 세로(mm)', 'type' => 'number', 'required' => false, 'order' => 13, - 'desc' => '제작 세로 사이즈 H1 (자동계산, col11)'], + 'desc' => '제작 세로 사이즈 H1 (자동계산, col11)'], ['key' => 'quantity', 'name' => '수량', 'type' => 'number', 'required' => true, 'order' => 14, - 'default' => '1', 'desc' => '제품 수량 (col14)'], + 'default' => '1', 'desc' => '제품 수량 (col14)'], // 부품 관련 ['key' => 'guide_rail_type', 'name' => '가이드레일 유형', 'type' => 'select', 'required' => true, 'order' => 20, - 'options' => ['벽면형', '측면형', '혼합형'], - 'desc' => '가이드레일 설치 방식 (col6)'], + 'options' => ['벽면형', '측면형', '혼합형'], + 'desc' => '가이드레일 설치 방식 (col6)'], ['key' => 'shutter_box', 'name' => '셔터박스', 'type' => 'select', 'required' => false, 'order' => 30, - 'options' => ['', '500*380', '500*350', 'custom'], - 'desc' => '셔터박스 사이즈 선택 (col36)'], + 'options' => ['', '500*380', '500*350', 'custom'], + 'desc' => '셔터박스 사이즈 선택 (col36)'], ['key' => 'shutter_box_custom', 'name' => '셔터박스 직접입력', 'type' => 'text', 'required' => false, 'order' => 31, - 'desc' => '셔터박스 직접입력 시 사이즈'], + 'desc' => '셔터박스 직접입력 시 사이즈'], ['key' => 'front_bottom', 'name' => '전면밑', 'type' => 'number', 'required' => false, 'order' => 32, - 'default' => '50', 'desc' => '전면밑 치수 (mm)'], + 'default' => '50', 'desc' => '전면밑 치수 (mm)'], ['key' => 'rail_width', 'name' => '레일폭', 'type' => 'number', 'required' => false, 'order' => 33, - 'default' => '70', 'desc' => '레일 폭 치수 (mm)'], + 'default' => '70', 'desc' => '레일 폭 치수 (mm)'], ['key' => 'box_direction', 'name' => '박스방향', 'type' => 'select', 'required' => false, 'order' => 34, - 'options' => ['양면', '밑면', '후면'], - 'default' => '양면', 'desc' => '셔터박스 설치 방향'], + 'options' => ['양면', '밑면', '후면'], + 'default' => '양면', 'desc' => '셔터박스 설치 방향'], // 모터 관련 ['key' => 'motor_bracket_size', 'name' => '모터브라켓 사이즈', 'type' => 'text', 'required' => false, 'order' => 40, - 'desc' => '중량+인치 기반 자동계산 브라켓 사이즈'], + 'desc' => '중량+인치 기반 자동계산 브라켓 사이즈'], ['key' => 'motor_capacity', 'name' => '모터 용량', 'type' => 'text', 'required' => false, 'order' => 41, - 'desc' => '계산된 모터 용량'], + 'desc' => '계산된 모터 용량'], ['key' => 'shaft_inch', 'name' => '샤프트 인치', 'type' => 'select', 'required' => false, 'order' => 42, - 'options' => ['4', '5', '6', '8'], - 'desc' => '샤프트 사이즈 (인치)'], + 'options' => ['4', '5', '6', '8'], + 'desc' => '샤프트 사이즈 (인치)'], // 마구리 관련 ['key' => 'maguri_length', 'name' => '마구리 길이', 'type' => 'number', 'required' => false, 'order' => 50, - 'desc' => '마구리 길이 치수 (col45)'], + 'desc' => '마구리 길이 치수 (col45)'], ['key' => 'maguri_wing', 'name' => '마구리 윙', 'type' => 'number', 'required' => false, 'order' => 51, - 'desc' => '마구리 윙 길이 치수'], + 'desc' => '마구리 윙 길이 치수'], // 계산 결과 ['key' => 'calculated_weight', 'name' => '계산 중량', 'type' => 'number', 'required' => false, 'order' => 60, - 'desc' => '자동 계산된 중량 (kg)'], + 'desc' => '자동 계산된 중량 (kg)'], ['key' => 'calculated_area', 'name' => '계산 면적', 'type' => 'number', 'required' => false, 'order' => 61, - 'desc' => '자동 계산된 면적 (㎡)'], + 'desc' => '자동 계산된 면적 (㎡)'], ['key' => 'unit_price', 'name' => '단가', 'type' => 'number', 'required' => false, 'order' => 70, - 'desc' => '제품 단가 (원)'], + 'desc' => '제품 단가 (원)'], ['key' => 'total_price', 'name' => '금액', 'type' => 'number', 'required' => false, 'order' => 71, - 'desc' => '총 금액 (원)'], + 'desc' => '총 금액 (원)'], ]; // 5. 철재 카테고리의 동적 필드들 $steelFields = [ // 기본 정보 ['key' => 'model_name', 'name' => '모델명', 'type' => 'select', 'required' => true, 'order' => 1, - 'options' => ['KQTS01', 'KTE01', '철재비인정'], - 'desc' => '철재 제품 모델 선택'], + 'options' => ['KQTS01', 'KTE01', '철재비인정'], + 'desc' => '철재 제품 모델 선택'], ['key' => 'sequence', 'name' => '순번', 'type' => 'text', 'required' => false, 'order' => 2, - 'desc' => '견적 항목 순번'], + 'desc' => '견적 항목 순번'], ['key' => 'product_category', 'name' => '대분류', 'type' => 'text', 'required' => true, 'order' => 3, - 'default' => '철재', 'desc' => '제품 대분류'], + 'default' => '철재', 'desc' => '제품 대분류'], // 사이즈 관련 (철재는 다른 계산식) ['key' => 'open_width', 'name' => '오픈사이즈 가로(mm)', 'type' => 'number', 'required' => true, 'order' => 10, - 'desc' => '개구부 가로 사이즈 W0'], + 'desc' => '개구부 가로 사이즈 W0'], ['key' => 'open_height', 'name' => '오픈사이즈 세로(mm)', 'type' => 'number', 'required' => true, 'order' => 11, - 'desc' => '개구부 세로 사이즈 H0'], + 'desc' => '개구부 세로 사이즈 H0'], ['key' => 'make_width', 'name' => '제작사이즈 가로(mm)', 'type' => 'number', 'required' => false, 'order' => 12, - 'desc' => '제작 가로 사이즈 W1 (W0+110)'], + 'desc' => '제작 가로 사이즈 W1 (W0+110)'], ['key' => 'make_height', 'name' => '제작사이즈 세로(mm)', 'type' => 'number', 'required' => false, 'order' => 13, - 'desc' => '제작 세로 사이즈 H1 (H0+350)'], + 'desc' => '제작 세로 사이즈 H1 (H0+350)'], ['key' => 'quantity', 'name' => '수량', 'type' => 'number', 'required' => true, 'order' => 14, - 'default' => '1', 'desc' => '제품 수량'], + 'default' => '1', 'desc' => '제품 수량'], // 철재 특화 필드 ['key' => 'slat_thickness', 'name' => '스라트 두께', 'type' => 'select', 'required' => true, 'order' => 20, - 'options' => ['0.8mm', '1.0mm', '1.2mm', '1.5mm', '2.0mm'], - 'desc' => '철재 스라트 두께'], + 'options' => ['0.8mm', '1.0mm', '1.2mm', '1.5mm', '2.0mm'], + 'desc' => '철재 스라트 두께'], ['key' => 'bending_work', 'name' => '절곡 가공', 'type' => 'checkbox', 'required' => false, 'order' => 21, - 'desc' => '절곡 가공 여부'], + 'desc' => '절곡 가공 여부'], ['key' => 'welding_work', 'name' => '용접 가공', 'type' => 'checkbox', 'required' => false, 'order' => 22, - 'desc' => '용접 가공 여부'], + 'desc' => '용접 가공 여부'], // 환봉, 각파이프 등 ['key' => 'round_bar_quantity', 'name' => '환봉 수량', 'type' => 'number', 'required' => false, 'order' => 30, - 'desc' => '환봉 필요 수량 (자동계산)'], + 'desc' => '환봉 필요 수량 (자동계산)'], ['key' => 'square_pipe', 'name' => '각파이프', 'type' => 'text', 'required' => false, 'order' => 31, - 'desc' => '각파이프 사양'], + 'desc' => '각파이프 사양'], // 모터 관련 (철재용) ['key' => 'motor_bracket_size', 'name' => '모터브라켓 사이즈', 'type' => 'text', 'required' => false, 'order' => 40, - 'desc' => '중량+인치 기반 브라켓 사이즈'], + 'desc' => '중량+인치 기반 브라켓 사이즈'], ['key' => 'shaft_inch', 'name' => '샤프트 인치', 'type' => 'select', 'required' => false, 'order' => 41, - 'options' => ['4', '5', '6', '8'], - 'desc' => '샤프트 사이즈 (인치)'], + 'options' => ['4', '5', '6', '8'], + 'desc' => '샤프트 사이즈 (인치)'], // 계산 결과 ['key' => 'calculated_weight', 'name' => '계산 중량', 'type' => 'number', 'required' => false, 'order' => 60, - 'desc' => '자동 계산된 중량 (kg)'], + 'desc' => '자동 계산된 중량 (kg)'], ['key' => 'unit_price', 'name' => '단가', 'type' => 'number', 'required' => false, 'order' => 70, - 'desc' => '제품 단가 (원)'], + 'desc' => '제품 단가 (원)'], ['key' => 'total_price', 'name' => '금액', 'type' => 'number', 'required' => false, 'order' => 71, - 'desc' => '총 금액 (원)'], + 'desc' => '총 금액 (원)'], ]; // 6. 스크린 카테고리 필드 생성 @@ -256,4 +254,4 @@ public function down(): void DB::table('category_fields')->whereIn('category_id', $categoryIds)->delete(); DB::table('categories')->whereIn('id', $categoryIds)->delete(); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_24_000003_create_estimates_table.php b/database/migrations/2025_09_24_000003_create_estimates_table.php index b33cdf0..14e3646 100644 --- a/database/migrations/2025_09_24_000003_create_estimates_table.php +++ b/database/migrations/2025_09_24_000003_create_estimates_table.php @@ -29,7 +29,7 @@ public function up(): void $table->decimal('total_amount', 15, 2)->nullable()->comment('총 견적금액'); $table->enum('status', ['DRAFT', 'SENT', 'APPROVED', 'REJECTED', 'EXPIRED']) - ->default('DRAFT')->comment('견적 상태'); + ->default('DRAFT')->comment('견적 상태'); $table->text('notes')->nullable()->comment('비고'); $table->date('valid_until')->nullable()->comment('견적 유효기간'); @@ -100,4 +100,4 @@ public function down(): void Schema::dropIfExists('estimate_items'); Schema::dropIfExists('estimates'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_24_214146_remove_non_critical_foreign_keys_phase1.php b/database/migrations/2025_09_24_214146_remove_non_critical_foreign_keys_phase1.php index 4266127..0b9e4e7 100644 --- a/database/migrations/2025_09_24_214146_remove_non_critical_foreign_keys_phase1.php +++ b/database/migrations/2025_09_24_214146_remove_non_critical_foreign_keys_phase1.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; /** * Phase 1: 중요하지 않은 FK 제약조건 제거 @@ -20,7 +20,7 @@ */ private function findForeignKeyName(string $table, string $column): ?string { - $result = DB::selectOne(" + $result = DB::selectOne(' SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() @@ -28,7 +28,7 @@ private function findForeignKeyName(string $table, string $column): ?string AND COLUMN_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1 - ", [$table, $column]); + ', [$table, $column]); return $result ? $result->CONSTRAINT_NAME : null; } @@ -66,8 +66,8 @@ public function up(): void $classificationIndexExists = DB::selectOne(" SHOW INDEX FROM classifications WHERE Column_name = 'tenant_id' "); - if (!$classificationIndexExists) { - DB::statement("CREATE INDEX idx_classifications_tenant_id ON classifications (tenant_id)"); + if (! $classificationIndexExists) { + DB::statement('CREATE INDEX idx_classifications_tenant_id ON classifications (tenant_id)'); echo "✅ Added index: classifications.tenant_id\n"; } else { echo "ℹ️ Index exists: classifications.tenant_id\n"; @@ -77,8 +77,8 @@ public function up(): void $departmentIndexExists = DB::selectOne(" SHOW INDEX FROM departments WHERE Column_name = 'parent_id' "); - if (!$departmentIndexExists) { - DB::statement("CREATE INDEX idx_departments_parent_id ON departments (parent_id)"); + if (! $departmentIndexExists) { + DB::statement('CREATE INDEX idx_departments_parent_id ON departments (parent_id)'); echo "✅ Added index: departments.parent_id\n"; } else { echo "ℹ️ Index exists: departments.parent_id\n"; @@ -106,7 +106,7 @@ public function down(): void }); echo "✅ Restored FK: departments.parent_id → departments\n"; } catch (\Throwable $e) { - echo "⚠️ Could not restore FK: departments.parent_id - " . $e->getMessage() . "\n"; + echo '⚠️ Could not restore FK: departments.parent_id - '.$e->getMessage()."\n"; } // 2. classifications.tenant_id → tenants FK 복구 @@ -121,9 +121,9 @@ public function down(): void }); echo "✅ Restored FK: classifications.tenant_id → tenants\n"; } catch (\Throwable $e) { - echo "⚠️ Could not restore FK: classifications.tenant_id - " . $e->getMessage() . "\n"; + echo '⚠️ Could not restore FK: classifications.tenant_id - '.$e->getMessage()."\n"; } echo "\n🔄 Phase 1 FK 복구 완료!\n"; } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_24_214200_remove_estimate_foreign_keys_phase2.php b/database/migrations/2025_09_24_214200_remove_estimate_foreign_keys_phase2.php index 62910c1..61b75a0 100644 --- a/database/migrations/2025_09_24_214200_remove_estimate_foreign_keys_phase2.php +++ b/database/migrations/2025_09_24_214200_remove_estimate_foreign_keys_phase2.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; /** * Phase 2: 견적 시스템 FK 제약조건 제거 @@ -22,7 +22,7 @@ */ private function findForeignKeyName(string $table, string $column): ?string { - $result = DB::selectOne(" + $result = DB::selectOne(' SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() @@ -30,7 +30,7 @@ private function findForeignKeyName(string $table, string $column): ?string AND COLUMN_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1 - ", [$table, $column]); + ', [$table, $column]); return $result ? $result->CONSTRAINT_NAME : null; } @@ -57,8 +57,9 @@ public function up(): void $estimatesExists = Schema::hasTable('estimates'); $estimateItemsExists = Schema::hasTable('estimate_items'); - if (!$estimatesExists && !$estimateItemsExists) { + if (! $estimatesExists && ! $estimateItemsExists) { echo "ℹ️ 견적 시스템 테이블이 존재하지 않습니다. 건너뜁니다.\n"; + return; } @@ -76,8 +77,8 @@ public function up(): void $indexExists = DB::selectOne(" SHOW INDEX FROM estimates WHERE Key_name = 'idx_estimates_tenant_model_set' "); - if (!$indexExists) { - DB::statement("CREATE INDEX idx_estimates_tenant_model_set ON estimates (tenant_id, model_set_id)"); + if (! $indexExists) { + DB::statement('CREATE INDEX idx_estimates_tenant_model_set ON estimates (tenant_id, model_set_id)'); echo "✅ Added performance index: estimates(tenant_id, model_set_id)\n"; } } @@ -96,8 +97,8 @@ public function up(): void $indexExists = DB::selectOne(" SHOW INDEX FROM estimate_items WHERE Key_name = 'idx_estimate_items_tenant_estimate' "); - if (!$indexExists) { - DB::statement("CREATE INDEX idx_estimate_items_tenant_estimate ON estimate_items (tenant_id, estimate_id)"); + if (! $indexExists) { + DB::statement('CREATE INDEX idx_estimate_items_tenant_estimate ON estimate_items (tenant_id, estimate_id)'); echo "✅ Added performance index: estimate_items(tenant_id, estimate_id)\n"; } } @@ -129,7 +130,7 @@ public function down(): void }); echo "✅ Restored FK: estimate_items.estimate_id → estimates\n"; } catch (\Throwable $e) { - echo "⚠️ Could not restore FK: estimate_items.estimate_id - " . $e->getMessage() . "\n"; + echo '⚠️ Could not restore FK: estimate_items.estimate_id - '.$e->getMessage()."\n"; } } @@ -144,10 +145,10 @@ public function down(): void }); echo "✅ Restored FK: estimates.model_set_id → categories\n"; } catch (\Throwable $e) { - echo "⚠️ Could not restore FK: estimates.model_set_id - " . $e->getMessage() . "\n"; + echo '⚠️ Could not restore FK: estimates.model_set_id - '.$e->getMessage()."\n"; } } echo "\n🔄 Phase 2 FK 복구 완료!\n"; } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_24_214300_remove_material_foreign_key_phase3.php b/database/migrations/2025_09_24_214300_remove_material_foreign_key_phase3.php index dc067f7..1a5348c 100644 --- a/database/migrations/2025_09_24_214300_remove_material_foreign_key_phase3.php +++ b/database/migrations/2025_09_24_214300_remove_material_foreign_key_phase3.php @@ -1,9 +1,8 @@ CONSTRAINT_NAME : null; } @@ -55,8 +54,9 @@ public function up(): void echo "📋 분석 범위: BOM 시스템의 자재 참조 관계\n\n"; // product_components 테이블 존재 여부 확인 - if (!Schema::hasTable('product_components')) { + if (! Schema::hasTable('product_components')) { echo "ℹ️ product_components 테이블이 존재하지 않습니다. 건너뜁니다.\n"; + return; } @@ -64,20 +64,22 @@ public function up(): void // 테이블 구조 확인 $columns = DB::select('DESCRIBE product_components'); - $columnNames = array_map(function($col) { return $col->Field; }, $columns); + $columnNames = array_map(function ($col) { + return $col->Field; + }, $columns); echo " 현재 테이블 구조:\n"; - echo " - ref_type 컬럼: " . (in_array('ref_type', $columnNames) ? "존재 (통합 참조 타입)" : "없음") . "\n"; - echo " - ref_id 컬럼: " . (in_array('ref_id', $columnNames) ? "존재 (통합 참조 ID)" : "없음") . "\n"; - echo " - material_id 컬럼: " . (in_array('material_id', $columnNames) ? "존재" : "없음 (예상대로)") . "\n\n"; + echo ' - ref_type 컬럼: '.(in_array('ref_type', $columnNames) ? '존재 (통합 참조 타입)' : '없음')."\n"; + echo ' - ref_id 컬럼: '.(in_array('ref_id', $columnNames) ? '존재 (통합 참조 ID)' : '없음')."\n"; + echo ' - material_id 컬럼: '.(in_array('material_id', $columnNames) ? '존재' : '없음 (예상대로)')."\n\n"; // 현재 FK 상태 확인 $parentProductFk = $this->findForeignKeyName('product_components', 'parent_product_id'); $refIdFk = $this->findForeignKeyName('product_components', 'ref_id'); echo "2️⃣ FK 제약조건 상태 확인...\n"; - echo " - parent_product_id FK: " . ($parentProductFk ? "존재 ({$parentProductFk})" : "없음") . "\n"; - echo " - ref_id FK: " . ($refIdFk ? "존재 ({$refIdFk})" : "없음 (유연한 구조)") . "\n\n"; + echo ' - parent_product_id FK: '.($parentProductFk ? "존재 ({$parentProductFk})" : '없음')."\n"; + echo ' - ref_id FK: '.($refIdFk ? "존재 ({$refIdFk})" : '없음 (유연한 구조)')."\n\n"; // 성능을 위한 인덱스 확인 echo "3️⃣ 성능 인덱스 상태 확인...\n"; @@ -88,14 +90,14 @@ public function up(): void SHOW INDEX FROM product_components WHERE Key_name LIKE '%ref_id%' OR Column_name = 'ref_id' "); - echo " - ref_type 인덱스: " . ($refTypeIndexExists ? "존재" : "없음") . "\n"; - echo " - ref_id 인덱스: " . ($refIdIndexExists ? "존재" : "없음") . "\n"; + echo ' - ref_type 인덱스: '.($refTypeIndexExists ? '존재' : '없음')."\n"; + echo ' - ref_id 인덱스: '.($refIdIndexExists ? '존재' : '없음')."\n"; // 필요시 성능 인덱스 추가 - if (!$refTypeIndexExists || !$refIdIndexExists) { + if (! $refTypeIndexExists || ! $refIdIndexExists) { echo "\n4️⃣ 성능 인덱스 추가...\n"; try { - DB::statement("CREATE INDEX idx_components_ref_type_id ON product_components (ref_type, ref_id)"); + DB::statement('CREATE INDEX idx_components_ref_type_id ON product_components (ref_type, ref_id)'); echo "✅ Added composite index: product_components(ref_type, ref_id)\n"; } catch (\Exception $e) { echo "ℹ️ Index may already exist or not needed\n"; @@ -122,7 +124,7 @@ public function down(): void if (Schema::hasTable('product_components')) { echo "1️⃣ 성능 인덱스 제거...\n"; try { - DB::statement("DROP INDEX idx_components_ref_type_id ON product_components"); + DB::statement('DROP INDEX idx_components_ref_type_id ON product_components'); echo "✅ Removed index: product_components(ref_type, ref_id)\n"; } catch (\Exception $e) { echo "ℹ️ Index may not exist or already removed\n"; @@ -132,4 +134,4 @@ public function down(): void echo "\n🔄 Phase 3 분석 롤백 완료!\n"; echo "📝 참고: 원래 ref_type/ref_id 구조가 복구되었습니다.\n"; } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_10_14_204237_change_role_column_to_varchar_in_users_table.php b/database/migrations/2025_10_14_204237_change_role_column_to_varchar_in_users_table.php index 358b2ac..de7ecec 100644 --- a/database/migrations/2025_10_14_204237_change_role_column_to_varchar_in_users_table.php +++ b/database/migrations/2025_10_14_204237_change_role_column_to_varchar_in_users_table.php @@ -1,8 +1,6 @@ 'FINISHED_GOOD', 'name' => '완제품', 'attributes' => [ - 'is_sellable' => 1, + 'is_sellable' => 1, 'is_purchasable' => 0, - 'is_producible' => 1, + 'is_producible' => 1, 'is_stock_managed' => 1, ], ], @@ -27,9 +27,9 @@ public function run(): void 'code' => 'SUB_ASSEMBLY', 'name' => '서브어셈블리', 'attributes' => [ - 'is_sellable' => 0, + 'is_sellable' => 0, 'is_purchasable' => 0, - 'is_producible' => 1, + 'is_producible' => 1, 'is_stock_managed' => 1, ], ], @@ -37,9 +37,9 @@ public function run(): void 'code' => 'PURCHASED_PART', 'name' => '구매부품', 'attributes' => [ - 'is_sellable' => 0, + 'is_sellable' => 0, 'is_purchasable' => 1, - 'is_producible' => 0, + 'is_producible' => 0, 'is_stock_managed' => 1, ], ], @@ -47,9 +47,9 @@ public function run(): void 'code' => 'PHANTOM', 'name' => '팬텀(가상)', 'attributes' => [ - 'is_sellable' => 0, + 'is_sellable' => 0, 'is_purchasable' => 0, - 'is_producible' => 1, + 'is_producible' => 1, 'is_stock_managed' => 0, ], ], @@ -58,16 +58,16 @@ public function run(): void foreach ($profiles as $p) { DB::table('common_codes')->updateOrInsert( [ - 'tenant_id' => $tenantId, + 'tenant_id' => $tenantId, 'code_group' => $group, - 'code' => $p['code'], + 'code' => $p['code'], ], [ - 'name' => $p['name'], - 'attributes' => json_encode($p['attributes']), + 'name' => $p['name'], + 'attributes' => json_encode($p['attributes']), 'description' => '기본 프로필', - 'is_active' => 1, - 'sort_order' => 0, + 'is_active' => 1, + 'sort_order' => 0, ] ); } diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php index 466e3cf..8b3d17d 100644 --- a/database/seeders/CategorySeeder.php +++ b/database/seeders/CategorySeeder.php @@ -2,22 +2,22 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; use App\Models\Commons\Category; +use Illuminate\Database\Seeder; class CategorySeeder extends Seeder { public function run(): void { Category::create([ - 'tenant_id' => 1, - 'parent_id' => null, - 'code_group' => 'product', - 'code' => 'DEFAULT', - 'name' => '기본 카테고리', - 'profile_code'=> 'FINISHED_GOOD', // capability_profile 참조 - 'is_active' => 1, - 'sort_order' => 1, + 'tenant_id' => 1, + 'parent_id' => null, + 'code_group' => 'product', + 'code' => 'DEFAULT', + 'name' => '기본 카테고리', + 'profile_code' => 'FINISHED_GOOD', // capability_profile 참조 + 'is_active' => 1, + 'sort_order' => 1, 'description' => '시스템 기본 카테고리', ]); } diff --git a/database/seeders/DemoSystemSeeder.php b/database/seeders/DemoSystemSeeder.php index acf4e40..2d16467 100644 --- a/database/seeders/DemoSystemSeeder.php +++ b/database/seeders/DemoSystemSeeder.php @@ -3,10 +3,10 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; -use Shared\Models\Members\User; -use Shared\Models\Sales\Prospect; -use Shared\Models\Sales\DemoLink; use Illuminate\Support\Facades\Hash; +use Shared\Models\Members\User; +use Shared\Models\Sales\DemoLink; +use Shared\Models\Sales\Prospect; class DemoSystemSeeder extends Seeder { diff --git a/lang/en/error.php b/lang/en/error.php index 1cf64a9..2791f85 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -11,11 +11,11 @@ return [ // 4xx Common - 'not_found' => 'The requested URI or data was not found.', // 404 - 'tenant_id' => 'No active tenant is set.', // 400 (Service::tenantId() missing) - 'unauthenticated' => 'Authentication failed.', // 401 - 'forbidden' => 'You do not have permission for this request.', // 403 - 'bad_request' => 'Invalid request.', // 400 + 'not_found' => 'The requested URI or data was not found.', // 404 + 'tenant_id' => 'No active tenant is set.', // 400 (Service::tenantId() missing) + 'unauthenticated' => 'Authentication failed.', // 401 + 'forbidden' => 'You do not have permission for this request.', // 403 + 'bad_request' => 'Invalid request.', // 400 // Validation / Parameters 'validation_failed' => 'Request data validation failed.', // 422 @@ -25,12 +25,12 @@ 'not_found_resource' => ':resource could not be found.', // 404 // Business rules - 'duplicate' => 'Duplicate data exists.', - 'conflict' => 'The request conflicts with the current state.', // 409 - 'state_invalid' => 'The current state does not allow this operation.', // 400/409 + 'duplicate' => 'Duplicate data exists.', + 'conflict' => 'The request conflicts with the current state.', // 409 + 'state_invalid' => 'The current state does not allow this operation.', // 400/409 // Server errors - 'server_error' => 'An internal server error occurred.', // 5xx + 'server_error' => 'An internal server error occurred.', // 5xx // Estimate related errors 'estimate' => [ diff --git a/lang/en/message.php b/lang/en/message.php index 08fb52d..2cdb173 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -1,4 +1,5 @@ 'Fetched successfully.', - 'created' => 'Created successfully.', - 'updated' => 'Updated successfully.', - 'deleted' => 'Deleted successfully.', - 'restored' => 'Restored successfully.', - 'toggled' => 'Status updated successfully.', + 'fetched' => 'Fetched successfully.', + 'created' => 'Created successfully.', + 'updated' => 'Updated successfully.', + 'deleted' => 'Deleted successfully.', + 'restored' => 'Restored successfully.', + 'toggled' => 'Status updated successfully.', 'bulk_upsert' => 'Bulk save completed successfully.', - 'reordered' => 'Reorder completed successfully.', - 'no_changes' => 'No changes detected.', + 'reordered' => 'Reorder completed successfully.', + 'no_changes' => 'No changes detected.', // Auth / Session - 'login_success' => 'Login successful.', - 'logout_success' => 'You have been logged out.', - 'signup_success' => 'Sign-up completed successfully.', + 'login_success' => 'Login successful.', + 'logout_success' => 'You have been logged out.', + 'signup_success' => 'Sign-up completed successfully.', // Tenant / Context 'tenant_switched' => 'Active tenant has been switched.', // Resource-specific details 'product' => [ - 'created' => 'Product has been created.', - 'updated' => 'Product has been updated.', - 'deleted' => 'Product has been deleted.', - 'toggled' => 'Product status has been updated.', + 'created' => 'Product has been created.', + 'updated' => 'Product has been updated.', + 'deleted' => 'Product has been deleted.', + 'toggled' => 'Product status has been updated.', ], 'bom' => [ - 'fetched' => 'BOM items have been fetched.', + 'fetched' => 'BOM items have been fetched.', 'bulk_upsert' => 'BOM items have been saved.', - 'reordered' => 'BOM order has been updated.', - 'fetch' => 'BOM item fetch', - 'create' => 'BOM item created', - 'update' => 'BOM item updated', - 'delete' => 'BOM item deleted', - 'restore' => 'BOM item restored', + 'reordered' => 'BOM order has been updated.', + 'fetch' => 'BOM item fetch', + 'create' => 'BOM item created', + 'update' => 'BOM item updated', + 'delete' => 'BOM item deleted', + 'restore' => 'BOM item restored', ], 'category' => [ - 'fields_saved' => 'Category fields have been saved.', - 'template_saved' => 'Category template has been saved.', + 'fields_saved' => 'Category fields have been saved.', + 'template_saved' => 'Category template has been saved.', 'template_applied' => 'Category template has been applied.', ], 'design' => [ 'template_cloned' => 'BOM template has been cloned.', - 'template_diff' => 'BOM template differences have been computed.', + 'template_diff' => 'BOM template differences have been computed.', ], 'model_set' => [ - 'cloned' => 'Model set cloned successfully.', + 'cloned' => 'Model set cloned successfully.', 'calculated' => 'BOM calculation completed.', ], 'estimate' => [ - 'cloned' => 'Estimate cloned successfully.', + 'cloned' => 'Estimate cloned successfully.', 'status_changed' => 'Estimate status updated.', ], @@ -71,25 +72,25 @@ // Settings & Configuration Management 'settings' => [ - 'fields_updated' => 'Field settings have been updated.', - 'fields_bulk_saved' => 'Field settings bulk save completed.', - 'options_saved' => 'Option group has been saved.', - 'options_reordered' => 'Option values reordered successfully.', - 'common_code_saved' => 'Common code has been saved.', + 'fields_updated' => 'Field settings have been updated.', + 'fields_bulk_saved' => 'Field settings bulk save completed.', + 'options_saved' => 'Option group has been saved.', + 'options_reordered' => 'Option values reordered successfully.', + 'common_code_saved' => 'Common code has been saved.', ], // Materials Management (Products & Materials integrated) 'materials' => [ - 'created' => 'Material has been created.', - 'updated' => 'Material has been updated.', - 'deleted' => 'Material has been deleted.', - 'fetched' => 'Materials list retrieved successfully.', + 'created' => 'Material has been created.', + 'updated' => 'Material has been updated.', + 'deleted' => 'Material has been deleted.', + 'fetched' => 'Materials list retrieved successfully.', ], // File Management 'file' => [ 'uploaded' => 'File has been uploaded.', - 'deleted' => 'File has been deleted.', - 'fetched' => 'File list retrieved successfully.', + 'deleted' => 'File has been deleted.', + 'fetched' => 'File list retrieved successfully.', ], ]; diff --git a/public/html/inc/header.php b/public/html/inc/header.php index 26d6d66..3a7ac84 100644 --- a/public/html/inc/header.php +++ b/public/html/inc/header.php @@ -24,10 +24,10 @@ 생산관리 diff --git a/public/tenant/api/login_process.php b/public/tenant/api/login_process.php index 49581da..fb6a76c 100644 --- a/public/tenant/api/login_process.php +++ b/public/tenant/api/login_process.php @@ -1,4 +1,5 @@ false, 'message' => '아이디를 입력하세요.']); exit; } @@ -21,4 +22,3 @@ echo json_encode(['success' => true]); exit; -?> diff --git a/public/tenant/api/register_process.php b/public/tenant/api/register_process.php index bc1547e..3405348 100644 --- a/public/tenant/api/register_process.php +++ b/public/tenant/api/register_process.php @@ -1,4 +1,5 @@ alert('모든 항목을 입력하세요.'); history.back();"; exit; } @@ -23,5 +24,5 @@ $_SESSION['user_id'] = 'user'; // 가입 성공 → 대시보드 이동 -header("Location: /tenant/member/dashboard.php"); +header('Location: /tenant/member/dashboard.php'); exit; diff --git a/public/tenant/approval/instances.php b/public/tenant/approval/instances.php index 9b66383..f27dd16 100644 --- a/public/tenant/approval/instances.php +++ b/public/tenant/approval/instances.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/approval/objects.php b/public/tenant/approval/objects.php index 40ba75d..7481f51 100644 --- a/public/tenant/approval/objects.php +++ b/public/tenant/approval/objects.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/approval/rules.php b/public/tenant/approval/rules.php index b0a648b..61037cd 100644 --- a/public/tenant/approval/rules.php +++ b/public/tenant/approval/rules.php @@ -1,4 +1,5 @@ -
@@ -6,7 +7,7 @@
- 결재 규칙 () + 결재 규칙 ()
@@ -130,7 +131,7 @@ }); $('#btnAddRule').on('click', function(){ - alert('규칙 추가(샘플) /api/approval/rule_add.php?object='); + alert('규칙 추가(샘플) /api/approval/rule_add.php?object='); }); // 규칙 선택 diff --git a/public/tenant/inc/config.php b/public/tenant/inc/config.php index ac16853..3e42c73 100644 --- a/public/tenant/inc/config.php +++ b/public/tenant/inc/config.php @@ -1,3 +1,3 @@ diff --git a/public/tenant/inc/navi.php b/public/tenant/inc/navi.php index 28e7900..1a5d111 100644 --- a/public/tenant/inc/navi.php +++ b/public/tenant/inc/navi.php @@ -2,110 +2,110 @@ // 섹션-서브메뉴 정의 (링크는 네가 쓰는 경로에 맞춰두었어) $NAV = [ 'item' => [ - 'title'=>'품목', - 'items'=>[ - ['라벨'=>'품목관리','href'=>'/tenant/material/list.php'], - ['라벨'=>'BOM 단품(기초 바람시)','href'=>'/tenant/product//bending.php'], - ['라벨'=>'BOM 결합 상품','href'=>'/tenant/product/bom_combined.php'], - ['라벨'=>'제품 단가','href'=>'/tenant/product/product_price.php'], - ['라벨'=>'코드 및 로트 관리','href'=>'/tenant/product/code_lot.php'], + 'title' => '품목', + 'items' => [ + ['라벨' => '품목관리', 'href' => '/tenant/material/list.php'], + ['라벨' => 'BOM 단품(기초 바람시)', 'href' => '/tenant/product//bending.php'], + ['라벨' => 'BOM 결합 상품', 'href' => '/tenant/product/bom_combined.php'], + ['라벨' => '제품 단가', 'href' => '/tenant/product/product_price.php'], + ['라벨' => '코드 및 로트 관리', 'href' => '/tenant/product/code_lot.php'], ], ], 'order' => [ - 'title'=>'수주', - 'items'=>[ - ['라벨'=>'견적 관리','href'=>'/tenant/order/estimate.php'], - ['라벨'=>'수주 관리','href'=>'/tenant/order/manage.php'], - ['라벨'=>'수주 등록/수정','href'=>'/tenant/order/edit.php'], - ['라벨'=>'상태 관리','href'=>'/tenant/order/status.php'], + 'title' => '수주', + 'items' => [ + ['라벨' => '견적 관리', 'href' => '/tenant/order/estimate.php'], + ['라벨' => '수주 관리', 'href' => '/tenant/order/manage.php'], + ['라벨' => '수주 등록/수정', 'href' => '/tenant/order/edit.php'], + ['라벨' => '상태 관리', 'href' => '/tenant/order/status.php'], ], ], 'process' => [ - 'title'=>'생산', - 'items'=>[ - ['라벨'=>'공정관리','href'=>'/tenant/process/processes.php'], - ['라벨'=>'작업관리','href'=>'/tenant/process/tasks.php'], - ['라벨'=>'작업지시 설정','href'=>'/tenant/process/process_settings.php'], - ['라벨'=>'스크린 작업','href'=>'/tenant/production/screen_work.php'], - ['라벨'=>'슬랫 작업','href'=>'/tenant/production/screen_work.php'], - ['라벨'=>'절곡 작업','href'=>'/tenant/production/screen_work.php'], + 'title' => '생산', + 'items' => [ + ['라벨' => '공정관리', 'href' => '/tenant/process/processes.php'], + ['라벨' => '작업관리', 'href' => '/tenant/process/tasks.php'], + ['라벨' => '작업지시 설정', 'href' => '/tenant/process/process_settings.php'], + ['라벨' => '스크린 작업', 'href' => '/tenant/production/screen_work.php'], + ['라벨' => '슬랫 작업', 'href' => '/tenant/production/screen_work.php'], + ['라벨' => '절곡 작업', 'href' => '/tenant/production/screen_work.php'], ], ], 'shipment' => [ - 'title'=>'출고', - 'items'=>[ - ['라벨'=>'출고 현황 조회','href'=>'/tenant/shipment/status.php'], - ['라벨'=>'월간 출고 일정','href'=>'/tenant/shipment/monthly_schedule.php'], + 'title' => '출고', + 'items' => [ + ['라벨' => '출고 현황 조회', 'href' => '/tenant/shipment/status.php'], + ['라벨' => '월간 출고 일정', 'href' => '/tenant/shipment/monthly_schedule.php'], ], ], 'quality' => [ - 'title'=>'품질', - 'items'=>[ - ['라벨'=>'제품 검사 요청','href'=>'/tenant/quality/request.php'], - ['라벨'=>'제품 검사 일정','href'=>'/tenant/quality/schedule.php'], - ['라벨'=>'품질 인정 실적 대장','href'=>'/tenant/quality/record.php'], - ['라벨'=>'인정서류 자료실','href'=>'/tenant/quality/docs.php'], + 'title' => '품질', + 'items' => [ + ['라벨' => '제품 검사 요청', 'href' => '/tenant/quality/request.php'], + ['라벨' => '제품 검사 일정', 'href' => '/tenant/quality/schedule.php'], + ['라벨' => '품질 인정 실적 대장', 'href' => '/tenant/quality/record.php'], + ['라벨' => '인정서류 자료실', 'href' => '/tenant/quality/docs.php'], ], ], 'inventory' => [ - 'title'=>'재고', - 'items'=>[ - ['라벨'=>'재고 조회','href'=>'/tenant/inventory/stock.php'], - ['라벨'=>'수입 검사 대장','href'=>'/tenant/material/inspection_list.php'], - ['라벨'=>'입고 현황','href'=>'/tenant/material/inventory_status.php'], - ['라벨'=>'부적합품 관리','href'=>'/tenant/inventory/defective.php'], + 'title' => '재고', + 'items' => [ + ['라벨' => '재고 조회', 'href' => '/tenant/inventory/stock.php'], + ['라벨' => '수입 검사 대장', 'href' => '/tenant/material/inspection_list.php'], + ['라벨' => '입고 현황', 'href' => '/tenant/material/inventory_status.php'], + ['라벨' => '부적합품 관리', 'href' => '/tenant/inventory/defective.php'], ], ], 'subscription' => [ - 'title'=>'구독', - 'items'=>[ - ['라벨'=>'구독 하기','href'=>'/tenant/tenant/subscribe.php'], - ['라벨'=>'구독 관리','href'=>'/tenant/subscription/list.php'], - ['라벨'=>'회사 조회','href'=>'/tenant/subscription/tenant_list.php'], - ['라벨'=>'상품별 회사 리스트','href'=>'/tenant/subscription/tenant_product_list.php'], + 'title' => '구독', + 'items' => [ + ['라벨' => '구독 하기', 'href' => '/tenant/tenant/subscribe.php'], + ['라벨' => '구독 관리', 'href' => '/tenant/subscription/list.php'], + ['라벨' => '회사 조회', 'href' => '/tenant/subscription/tenant_list.php'], + ['라벨' => '상품별 회사 리스트', 'href' => '/tenant/subscription/tenant_product_list.php'], ], ], 'member' => [ 'title' => '회원', 'items' => [ - ['라벨'=>'가입','href'=>'/tenant/member/register.php'], - ['라벨'=>'회사 선택','href'=>'/tenant/member/tenant_select.php'], - ['라벨'=>'내정보 수정','href'=>'/tenant/member/profile_edit.php'], - ['라벨'=>'탈퇴','href'=>'/tenant/member/withdraw.php'], - ['라벨'=>'회원가입 컬럼설정','href'=>'/tenant/settings/member_fields.php'], + ['라벨' => '가입', 'href' => '/tenant/member/register.php'], + ['라벨' => '회사 선택', 'href' => '/tenant/member/tenant_select.php'], + ['라벨' => '내정보 수정', 'href' => '/tenant/member/profile_edit.php'], + ['라벨' => '탈퇴', 'href' => '/tenant/member/withdraw.php'], + ['라벨' => '회원가입 컬럼설정', 'href' => '/tenant/settings/member_fields.php'], ], ], 'permission' => [ - 'title'=>'메뉴/권한', - 'items'=>[ - ['라벨'=>'메뉴 관리','href'=>'/tenant/permission/tenant_menu.php'], - ['라벨'=>'부서 권한 설정','href'=>'/tenant/permission/department.php'], - ['라벨'=>'역할 권한 설정','href'=>'/tenant/permission/role.php'], - ['라벨'=>'유저 권한 설정','href'=>'/tenant/permission/user.php'], - ['라벨'=>'권한 분석','href'=>'/tenant/permission/analyze.php'], + 'title' => '메뉴/권한', + 'items' => [ + ['라벨' => '메뉴 관리', 'href' => '/tenant/permission/tenant_menu.php'], + ['라벨' => '부서 권한 설정', 'href' => '/tenant/permission/department.php'], + ['라벨' => '역할 권한 설정', 'href' => '/tenant/permission/role.php'], + ['라벨' => '유저 권한 설정', 'href' => '/tenant/permission/user.php'], + ['라벨' => '권한 분석', 'href' => '/tenant/permission/analyze.php'], ], ], 'approval' => [ - 'title'=>'결재(승인)', - 'items'=>[ - ['라벨'=>'결재 대상 관리','href'=>'/tenant/approval/objects.php'], - ['라벨'=>'결재 규칙/결재선 관리','href'=>'/tenant/approval/rules.php'], - ['라벨'=>'결재권자 관리','href'=>'/tenant/approval/pool.php'], - ['라벨'=>'결재 현황 조회','href'=>'/tenant/approval/instances.php'], + 'title' => '결재(승인)', + 'items' => [ + ['라벨' => '결재 대상 관리', 'href' => '/tenant/approval/objects.php'], + ['라벨' => '결재 규칙/결재선 관리', 'href' => '/tenant/approval/rules.php'], + ['라벨' => '결재권자 관리', 'href' => '/tenant/approval/pool.php'], + ['라벨' => '결재 현황 조회', 'href' => '/tenant/approval/instances.php'], ], ], 'tenant' => [ - 'title'=>'회사', - 'items'=>[ - ['라벨'=>'회사 가입','href'=>'/tenant/tenant/join.php'], - ['라벨'=>'수정','href'=>'/tenant/tenant/edit.php'], - ['라벨'=>'탈퇴','href'=>'/tenant/tenant/withdraw.php'], - ['라벨'=>'카테고리 관리','href'=>'/tenant/category/category_list.php'], - ['라벨'=>'분류 관리','href'=>'/tenant/category/subcategory_list.php'], - ['라벨'=>'부서 관리','href'=>'/tenant/tenant/department_list.php'], - ['라벨'=>'역할 관리','href'=>'/tenant/tenant/role_list.php'], - ['라벨'=>'유저 관리','href'=>'/tenant/tenant/user_list.php'], - ['라벨'=>'옵션 관리','href'=>'/tenant/tenant/option.php'], + 'title' => '회사', + 'items' => [ + ['라벨' => '회사 가입', 'href' => '/tenant/tenant/join.php'], + ['라벨' => '수정', 'href' => '/tenant/tenant/edit.php'], + ['라벨' => '탈퇴', 'href' => '/tenant/tenant/withdraw.php'], + ['라벨' => '카테고리 관리', 'href' => '/tenant/category/category_list.php'], + ['라벨' => '분류 관리', 'href' => '/tenant/category/subcategory_list.php'], + ['라벨' => '부서 관리', 'href' => '/tenant/tenant/department_list.php'], + ['라벨' => '역할 관리', 'href' => '/tenant/tenant/role_list.php'], + ['라벨' => '유저 관리', 'href' => '/tenant/tenant/user_list.php'], + ['라벨' => '옵션 관리', 'href' => '/tenant/tenant/option.php'], ], ], ]; @@ -114,14 +114,14 @@ $NAV_ROOT = [ 'title' => '기본', 'items' => [ - ['라벨'=>'인트로','href'=>'/tenant/member/intro.php'], - ['라벨'=>'대시보드','href'=>'/tenant/member/dashboard.php'], + ['라벨' => '인트로', 'href' => '/tenant/member/intro.php'], + ['라벨' => '대시보드', 'href' => '/tenant/member/dashboard.php'], ], ]; // ③ 현재 경로/섹션 파악 -$currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); -$hasSection = isset($CURRENT_SECTION, $NAV[$CURRENT_SECTION]); +$currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +$hasSection = isset($CURRENT_SECTION, $NAV[$CURRENT_SECTION]); // ④ JS/초기 렌더에 사용할 "초기 섹션" (없으면 root 사용) $INITIAL_SECTION = $hasSection ? $CURRENT_SECTION : 'root'; @@ -144,7 +144,7 @@
@@ -187,7 +187,7 @@ class="nav-link top-link $(function(){ - const NAV = ; + const NAV = ; const INITIAL_SECTION = ''; // 'root' 또는 실제 섹션 키 const $subnav = $('#subnav'); diff --git a/public/tenant/index.php b/public/tenant/index.php index f554a00..0c54c92 100644 --- a/public/tenant/index.php +++ b/public/tenant/index.php @@ -1,13 +1,13 @@ diff --git a/public/tenant/inventory/defective.php b/public/tenant/inventory/defective.php index 50767da..6aa870b 100644 --- a/public/tenant/inventory/defective.php +++ b/public/tenant/inventory/defective.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/inventory/stock.php b/public/tenant/inventory/stock.php index 50767da..6aa870b 100644 --- a/public/tenant/inventory/stock.php +++ b/public/tenant/inventory/stock.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/material/inspection_list.php b/public/tenant/material/inspection_list.php index 50767da..6aa870b 100644 --- a/public/tenant/material/inspection_list.php +++ b/public/tenant/material/inspection_list.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/material/inventory_status.php b/public/tenant/material/inventory_status.php index ee060b5..0ef666f 100644 --- a/public/tenant/material/inventory_status.php +++ b/public/tenant/material/inventory_status.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/material/list.php b/public/tenant/material/list.php index cb87a8f..899a779 100644 --- a/public/tenant/material/list.php +++ b/public/tenant/material/list.php @@ -1,6 +1,6 @@ 리스트 -$CURRENT_SECTION='item';; +$CURRENT_SECTION = 'item'; include '../inc/header.php'; ?>
diff --git a/public/tenant/member/logout.php b/public/tenant/member/logout.php index 219c40e..3e56d8d 100644 --- a/public/tenant/member/logout.php +++ b/public/tenant/member/logout.php @@ -1,9 +1,9 @@ diff --git a/public/tenant/member/profile_edit.php b/public/tenant/member/profile_edit.php index aa2fb40..b0b32c8 100644 --- a/public/tenant/member/profile_edit.php +++ b/public/tenant/member/profile_edit.php @@ -14,11 +14,11 @@
- +
- +
diff --git a/public/tenant/member/tenant_add_process.php b/public/tenant/member/tenant_add_process.php index 2947334..891d8ad 100644 --- a/public/tenant/member/tenant_add_process.php +++ b/public/tenant/member/tenant_add_process.php @@ -1,10 +1,11 @@ alert('회사명을 입력하세요.'); history.back();"; exit; } @@ -17,6 +18,5 @@ $_SESSION['tenant_name'] = $new_tenant_name; // 대시보드로 이동 -header("Location: /tenant/member/dashboard.php"); +header('Location: /tenant/member/dashboard.php'); exit; -?> diff --git a/public/tenant/member/tenant_select.php b/public/tenant/member/tenant_select.php index 55e3a1e..4291650 100644 --- a/public/tenant/member/tenant_select.php +++ b/public/tenant/member/tenant_select.php @@ -9,7 +9,6 @@ ['id' => 103, 'name' => '네이버'], ]; - ?>
@@ -20,9 +19,9 @@
diff --git a/public/tenant/member/tenant_select_process.php b/public/tenant/member/tenant_select_process.php index 2d91cab..6f45e19 100644 --- a/public/tenant/member/tenant_select_process.php +++ b/public/tenant/member/tenant_select_process.php @@ -1,10 +1,11 @@ alert('테넌트를 선택하세요.'); history.back();"; exit; } @@ -13,6 +14,5 @@ $_SESSION['tenant_id'] = $tenant_id; // 대시보드로 이동 -header("Location: /tenant/member/dashboard.php"); +header('Location: /tenant/member/dashboard.php'); exit; -?> diff --git a/public/tenant/order/edit.php b/public/tenant/order/edit.php index 19b4211..c17572e 100644 --- a/public/tenant/order/edit.php +++ b/public/tenant/order/edit.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/order/estimate.php b/public/tenant/order/estimate.php index 375fef1..2ae7dae 100644 --- a/public/tenant/order/estimate.php +++ b/public/tenant/order/estimate.php @@ -1,5 +1,5 @@
diff --git a/public/tenant/order/estimate_form.php b/public/tenant/order/estimate_form.php index 6a599a3..6e04f22 100644 --- a/public/tenant/order/estimate_form.php +++ b/public/tenant/order/estimate_form.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/order/manage.php b/public/tenant/order/manage.php index 3d210d8..2669477 100644 --- a/public/tenant/order/manage.php +++ b/public/tenant/order/manage.php @@ -1,5 +1,5 @@
diff --git a/public/tenant/order/status.php b/public/tenant/order/status.php index 19b4211..c17572e 100644 --- a/public/tenant/order/status.php +++ b/public/tenant/order/status.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/permission/analyze.php b/public/tenant/permission/analyze.php index c2b10a4..28a0510 100644 --- a/public/tenant/permission/analyze.php +++ b/public/tenant/permission/analyze.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/permission/approver.php b/public/tenant/permission/approver.php index 6205f6e..fef1871 100644 --- a/public/tenant/permission/approver.php +++ b/public/tenant/permission/approver.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/permission/audit.php b/public/tenant/permission/audit.php index 6758cfe..da78a76 100644 --- a/public/tenant/permission/audit.php +++ b/public/tenant/permission/audit.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/permission/department.php b/public/tenant/permission/department.php index 3c8f11f..92b4041 100644 --- a/public/tenant/permission/department.php +++ b/public/tenant/permission/department.php @@ -1,6 +1,6 @@ diff --git a/public/tenant/permission/role.php b/public/tenant/permission/role.php index c87f13d..3150133 100644 --- a/public/tenant/permission/role.php +++ b/public/tenant/permission/role.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/permission/structure.php b/public/tenant/permission/structure.php index 28b8a2a..9767e50 100644 --- a/public/tenant/permission/structure.php +++ b/public/tenant/permission/structure.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/permission/tenant_menu.php b/public/tenant/permission/tenant_menu.php index 69e5a07..b49502f 100644 --- a/public/tenant/permission/tenant_menu.php +++ b/public/tenant/permission/tenant_menu.php @@ -1,4 +1,5 @@ - +
diff --git a/public/tenant/permission/user.php b/public/tenant/permission/user.php index 27d3b27..f3036b3 100644 --- a/public/tenant/permission/user.php +++ b/public/tenant/permission/user.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/product/bending.php b/public/tenant/product/bending.php index 7f76830..5f2a35c 100644 --- a/public/tenant/product/bending.php +++ b/public/tenant/product/bending.php @@ -1,5 +1,5 @@
diff --git a/public/tenant/product/bom.php b/public/tenant/product/bom.php index cdbc9a7..d4b3e9f 100644 --- a/public/tenant/product/bom.php +++ b/public/tenant/product/bom.php @@ -1,9 +1,9 @@
diff --git a/public/tenant/product/bom_combined.php b/public/tenant/product/bom_combined.php index 27cf8e7..5f3082e 100644 --- a/public/tenant/product/bom_combined.php +++ b/public/tenant/product/bom_combined.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/product/bom_editor.php b/public/tenant/product/bom_editor.php index 753bf36..245a388 100644 --- a/public/tenant/product/bom_editor.php +++ b/public/tenant/product/bom_editor.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/product/code_lot.php b/public/tenant/product/code_lot.php index 27cf8e7..5f3082e 100644 --- a/public/tenant/product/code_lot.php +++ b/public/tenant/product/code_lot.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/product/model.php b/public/tenant/product/model.php index fceecf2..56fc35d 100644 --- a/public/tenant/product/model.php +++ b/public/tenant/product/model.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/production/screen_progress.php b/public/tenant/production/screen_progress.php index 0d108e4..8e39a22 100644 --- a/public/tenant/production/screen_progress.php +++ b/public/tenant/production/screen_progress.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/production/screen_work.php b/public/tenant/production/screen_work.php index b397ab7..7f44721 100644 --- a/public/tenant/production/screen_work.php +++ b/public/tenant/production/screen_work.php @@ -1,6 +1,6 @@
diff --git a/public/tenant/production/slat_work.php b/public/tenant/production/slat_work.php index 9cde691..736f1b8 100644 --- a/public/tenant/production/slat_work.php +++ b/public/tenant/production/slat_work.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/production/work_instruction.php b/public/tenant/production/work_instruction.php index 91c4f70..da905ca 100644 --- a/public/tenant/production/work_instruction.php +++ b/public/tenant/production/work_instruction.php @@ -1,8 +1,8 @@
@@ -56,7 +56,9 @@
설정순위
diff --git a/public/tenant/quality/docs.php b/public/tenant/quality/docs.php index 11edc15..5fcb45f 100644 --- a/public/tenant/quality/docs.php +++ b/public/tenant/quality/docs.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/quality/record.php b/public/tenant/quality/record.php index 11edc15..5fcb45f 100644 --- a/public/tenant/quality/record.php +++ b/public/tenant/quality/record.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/quality/request.php b/public/tenant/quality/request.php index 11edc15..5fcb45f 100644 --- a/public/tenant/quality/request.php +++ b/public/tenant/quality/request.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/quality/schedule.php b/public/tenant/quality/schedule.php index 11edc15..5fcb45f 100644 --- a/public/tenant/quality/schedule.php +++ b/public/tenant/quality/schedule.php @@ -1,5 +1,5 @@ diff --git a/public/tenant/settings/member_fields.php b/public/tenant/settings/member_fields.php index af28c08..eec8b3d 100644 --- a/public/tenant/settings/member_fields.php +++ b/public/tenant/settings/member_fields.php @@ -7,67 +7,71 @@ // ====== 샘플 데이터 (실구현은 DB 조인) ====== // setting_field_defs $FIELD_DEFS = [ - ['field_key'=>'name','label'=>'이름','data_type'=>'string','input_type'=>'text','option_source'=>null], - ['field_key'=>'email','label'=>'이메일','data_type'=>'string','input_type'=>'text','option_source'=>null], - ['field_key'=>'phone','label'=>'전화번호','data_type'=>'string','input_type'=>'text','option_source'=>null], - ['field_key'=>'department','label'=>'부서','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'position','label'=>'직급','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'job_title','label'=>'직책','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'manager_id','label'=>'직속 상급자','data_type'=>'bigint','input_type'=>'select_user','option_source'=>null], - ['field_key'=>'hire_date','label'=>'입사일','data_type'=>'date','input_type'=>'date','option_source'=>null], - ['field_key'=>'employment_type','label'=>'고용형태','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'work_location','label'=>'근무지','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'work_type','label'=>'근무형태','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'gender','label'=>'성별','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'bank_name','label'=>'은행','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'], - ['field_key'=>'bank_account','label'=>'계좌번호','data_type'=>'string','input_type'=>'text','option_source'=>null], - ['field_key'=>'employee_no','label'=>'사번','data_type'=>'string','input_type'=>'text','option_source'=>null], - ['field_key'=>'profile_photo','label'=>'프로필 사진','data_type'=>'file','input_type'=>'file','option_source'=>null], + ['field_key' => 'name', 'label' => '이름', 'data_type' => 'string', 'input_type' => 'text', 'option_source' => null], + ['field_key' => 'email', 'label' => '이메일', 'data_type' => 'string', 'input_type' => 'text', 'option_source' => null], + ['field_key' => 'phone', 'label' => '전화번호', 'data_type' => 'string', 'input_type' => 'text', 'option_source' => null], + ['field_key' => 'department', 'label' => '부서', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'position', 'label' => '직급', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'job_title', 'label' => '직책', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'manager_id', 'label' => '직속 상급자', 'data_type' => 'bigint', 'input_type' => 'select_user', 'option_source' => null], + ['field_key' => 'hire_date', 'label' => '입사일', 'data_type' => 'date', 'input_type' => 'date', 'option_source' => null], + ['field_key' => 'employment_type', 'label' => '고용형태', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'work_location', 'label' => '근무지', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'work_type', 'label' => '근무형태', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'gender', 'label' => '성별', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'bank_name', 'label' => '은행', 'data_type' => 'string', 'input_type' => 'select', 'option_source' => 'tenant_list'], + ['field_key' => 'bank_account', 'label' => '계좌번호', 'data_type' => 'string', 'input_type' => 'text', 'option_source' => null], + ['field_key' => 'employee_no', 'label' => '사번', 'data_type' => 'string', 'input_type' => 'text', 'option_source' => null], + ['field_key' => 'profile_photo', 'label' => '프로필 사진', 'data_type' => 'file', 'input_type' => 'file', 'option_source' => null], ]; // tenant_option_groups (id, name) — select 항목에서 선택할 그룹 $OPTION_GROUPS = [ - ['id'=>101,'group_key'=>'department','name'=>'부서 그룹'], - ['id'=>102,'group_key'=>'position','name'=>'직급 그룹'], - ['id'=>103,'group_key'=>'job_title','name'=>'직책 그룹'], - ['id'=>104,'group_key'=>'employment_type','name'=>'고용형태 그룹'], - ['id'=>105,'group_key'=>'work_location','name'=>'근무지 그룹'], - ['id'=>106,'group_key'=>'work_type','name'=>'근무형태 그룹'], - ['id'=>107,'group_key'=>'gender','name'=>'성별 그룹'], - ['id'=>108,'group_key'=>'bank_name','name'=>'은행 그룹'], + ['id' => 101, 'group_key' => 'department', 'name' => '부서 그룹'], + ['id' => 102, 'group_key' => 'position', 'name' => '직급 그룹'], + ['id' => 103, 'group_key' => 'job_title', 'name' => '직책 그룹'], + ['id' => 104, 'group_key' => 'employment_type', 'name' => '고용형태 그룹'], + ['id' => 105, 'group_key' => 'work_location', 'name' => '근무지 그룹'], + ['id' => 106, 'group_key' => 'work_type', 'name' => '근무형태 그룹'], + ['id' => 107, 'group_key' => 'gender', 'name' => '성별 그룹'], + ['id' => 108, 'group_key' => 'bank_name', 'name' => '은행 그룹'], ]; // tenant_field_settings (샘플: enabled/required/sort/order/group) // key = field_key $SETTINGS = [ - 'name' => ['enabled'=>1,'required'=>1,'sort_order'=>10,'option_group_id'=>null], - 'email' => ['enabled'=>1,'required'=>1,'sort_order'=>20,'option_group_id'=>null], - 'phone' => ['enabled'=>1,'required'=>0,'sort_order'=>30,'option_group_id'=>null], - 'department' => ['enabled'=>1,'required'=>1,'sort_order'=>40,'option_group_id'=>101], - 'position' => ['enabled'=>1,'required'=>0,'sort_order'=>50,'option_group_id'=>102], - 'job_title' => ['enabled'=>0,'required'=>0,'sort_order'=>60,'option_group_id'=>103], - 'manager_id' => ['enabled'=>0,'required'=>0,'sort_order'=>70,'option_group_id'=>null], - 'hire_date' => ['enabled'=>1,'required'=>0,'sort_order'=>80,'option_group_id'=>null], - 'employment_type' => ['enabled'=>1,'required'=>0,'sort_order'=>90,'option_group_id'=>104], - 'work_location' => ['enabled'=>1,'required'=>0,'sort_order'=>100,'option_group_id'=>105], - 'work_type' => ['enabled'=>0,'required'=>0,'sort_order'=>110,'option_group_id'=>106], - 'gender' => ['enabled'=>0,'required'=>0,'sort_order'=>120,'option_group_id'=>107], - 'bank_name' => ['enabled'=>0,'required'=>0,'sort_order'=>130,'option_group_id'=>108], - 'bank_account' => ['enabled'=>0,'required'=>0,'sort_order'=>140,'option_group_id'=>null], - 'employee_no' => ['enabled'=>1,'required'=>0,'sort_order'=>150,'option_group_id'=>null], - 'profile_photo' => ['enabled'=>0,'required'=>0,'sort_order'=>160,'option_group_id'=>null], + 'name' => ['enabled' => 1, 'required' => 1, 'sort_order' => 10, 'option_group_id' => null], + 'email' => ['enabled' => 1, 'required' => 1, 'sort_order' => 20, 'option_group_id' => null], + 'phone' => ['enabled' => 1, 'required' => 0, 'sort_order' => 30, 'option_group_id' => null], + 'department' => ['enabled' => 1, 'required' => 1, 'sort_order' => 40, 'option_group_id' => 101], + 'position' => ['enabled' => 1, 'required' => 0, 'sort_order' => 50, 'option_group_id' => 102], + 'job_title' => ['enabled' => 0, 'required' => 0, 'sort_order' => 60, 'option_group_id' => 103], + 'manager_id' => ['enabled' => 0, 'required' => 0, 'sort_order' => 70, 'option_group_id' => null], + 'hire_date' => ['enabled' => 1, 'required' => 0, 'sort_order' => 80, 'option_group_id' => null], + 'employment_type' => ['enabled' => 1, 'required' => 0, 'sort_order' => 90, 'option_group_id' => 104], + 'work_location' => ['enabled' => 1, 'required' => 0, 'sort_order' => 100, 'option_group_id' => 105], + 'work_type' => ['enabled' => 0, 'required' => 0, 'sort_order' => 110, 'option_group_id' => 106], + 'gender' => ['enabled' => 0, 'required' => 0, 'sort_order' => 120, 'option_group_id' => 107], + 'bank_name' => ['enabled' => 0, 'required' => 0, 'sort_order' => 130, 'option_group_id' => 108], + 'bank_account' => ['enabled' => 0, 'required' => 0, 'sort_order' => 140, 'option_group_id' => null], + 'employee_no' => ['enabled' => 1, 'required' => 0, 'sort_order' => 150, 'option_group_id' => null], + 'profile_photo' => ['enabled' => 0, 'required' => 0, 'sort_order' => 160, 'option_group_id' => null], ]; // helper: 옵션그룹 셀렉트 HTML -function render_group_select($field_key,$sel,$groups,$def){ - if (!($def['input_type']==='select' && $def['option_source']==='tenant_list')) return ''; +function render_group_select($field_key, $sel, $groups, $def) +{ + if (! ($def['input_type'] === 'select' && $def['option_source'] === 'tenant_list')) { + return ''; + } $html = ''; + return $html; } @@ -75,22 +79,28 @@ function render_group_select($field_key,$sel,$groups,$def){ function group_name_by_id($id, $groups) { foreach ($groups as $g) { - if ((string)$g['id'] === (string)$id) return $g['name']; + if ((string) $g['id'] === (string) $id) { + return $g['name']; + } } + return null; } function render_group_label($field_key, $sel_id, $groups, $def) { // select + tenant_list 타입만 노출, 그 외는 대시 - if (!($def['input_type'] === 'select' && $def['option_source'] === 'tenant_list')) { + if (! ($def['input_type'] === 'select' && $def['option_source'] === 'tenant_list')) { return ''; } - if (!$sel_id) return '미지정'; + if (! $sel_id) { + return '미지정'; + } $name = htmlspecialchars(group_name_by_id($sel_id, $groups) ?: '알 수 없음'); + // 필요하면 관리 링크 표시 - return '' . $name . '' - . ' 관리'; + return ''.$name.'' + .' 관리'; } ?> @@ -130,20 +140,21 @@ function render_group_label($field_key, $sel_id, $groups, $def) $sb; }); - foreach($FIELD_DEFS as $def): - $key = $def['field_key']; - $set = $SETTINGS[$key] ?? ['enabled'=>0,'required'=>0,'sort_order'=>9999,'option_group_id'=>null]; - $enabled = (int)$set['enabled']; - $required = (int)$set['required']; - $order = (int)$set['sort_order']; - $groupId = $set['option_group_id']; - ?> +foreach ($FIELD_DEFS as $def) { + $key = $def['field_key']; + $set = $SETTINGS[$key] ?? ['enabled' => 0, 'required' => 0, 'sort_order' => 9999, 'option_group_id' => null]; + $enabled = (int) $set['enabled']; + $required = (int) $set['required']; + $order = (int) $set['sort_order']; + $groupId = $set['option_group_id']; + ?>
@@ -157,15 +168,15 @@ function render_group_label($field_key, $sel_id, $groups, $def)
·
- +
- > + >
- > + >
@@ -173,7 +184,7 @@ function render_group_label($field_key, $sel_id, $groups, $def) - +
@@ -194,8 +205,8 @@ function render_group_label($field_key, $sel_id, $groups, $def)