From 00f57ce2442b09cfe92b84875386dcdc93fce51b Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 9 Jan 2026 16:34:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=8B=9C=EA=B3=B5=EC=82=AC):=202.1=20?= =?UTF-8?q?=ED=98=84=EC=9E=A5=EA=B4=80=EB=A6=AC=20-=20Backend=20API=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이그레이션: site_code, client_id, status 컬럼 추가 - Site 모델: 상태 상수, Client 관계 추가 - SiteService: stats(), bulkDestroy(), 필터 확장 - SiteController: stats, bulkDestroy 엔드포인트 추가 - 라우트: /stats, /bulk 추가 Co-Authored-By: Claude --- .../Controllers/Api/V1/SiteController.php | 33 ++++++++ app/Models/Tenants/Site.php | 33 ++++++++ app/Services/SiteService.php | 83 ++++++++++++++++++- ...add_construction_fields_to_sites_table.php | 44 ++++++++++ routes/api.php | 14 ++++ 5 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 database/migrations/2026_01_09_162534_add_construction_fields_to_sites_table.php diff --git a/app/Http/Controllers/Api/V1/SiteController.php b/app/Http/Controllers/Api/V1/SiteController.php index 93dbc76..85ab5c1 100644 --- a/app/Http/Controllers/Api/V1/SiteController.php +++ b/app/Http/Controllers/Api/V1/SiteController.php @@ -23,6 +23,10 @@ public function index(Request $request) $params = $request->only([ 'search', 'is_active', + 'status', + 'client_id', + 'start_date', + 'end_date', 'sort_by', 'sort_dir', 'per_page', @@ -34,6 +38,16 @@ public function index(Request $request) return ApiResponse::success($sites, __('message.fetched')); } + /** + * 현장 통계 + */ + public function stats() + { + $stats = $this->service->stats(); + + return ApiResponse::success($stats, __('message.fetched')); + } + /** * 현장 등록 */ @@ -74,6 +88,25 @@ public function destroy(int $id) return ApiResponse::success(null, __('message.deleted')); } + /** + * 현장 일괄 삭제 + */ + public function bulkDestroy(Request $request) + { + $ids = $request->input('ids', []); + + if (empty($ids)) { + return ApiResponse::error(__('error.no_items_selected'), 400); + } + + $deletedCount = $this->service->bulkDestroy($ids); + + return ApiResponse::success( + ['deleted_count' => $deletedCount], + __('message.deleted') + ); + } + /** * 활성화된 현장 목록 (셀렉트박스용) */ diff --git a/app/Models/Tenants/Site.php b/app/Models/Tenants/Site.php index cfd216b..7c175be 100644 --- a/app/Models/Tenants/Site.php +++ b/app/Models/Tenants/Site.php @@ -3,6 +3,7 @@ namespace App\Models\Tenants; use App\Models\Members\User; +use App\Models\Orders\Client; use App\Traits\BelongsToTenant; use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Model; @@ -14,14 +15,18 @@ * * @property int $id * @property int $tenant_id + * @property string|null $site_code 현장코드 + * @property int|null $client_id 거래처 ID * @property string $name * @property string|null $address * @property float|null $latitude * @property float|null $longitude * @property bool $is_active + * @property string $status 상태: unregistered|suspended|active|pending * @property int|null $created_by * @property int|null $updated_by * @property int|null $deleted_by + * @property-read Client|null $client */ class Site extends Model { @@ -29,13 +34,32 @@ class Site extends Model protected $table = 'sites'; + // 상태 상수 + public const STATUS_UNREGISTERED = 'unregistered'; + + public const STATUS_SUSPENDED = 'suspended'; + + public const STATUS_ACTIVE = 'active'; + + public const STATUS_PENDING = 'pending'; + + public const STATUSES = [ + self::STATUS_UNREGISTERED, + self::STATUS_SUSPENDED, + self::STATUS_ACTIVE, + self::STATUS_PENDING, + ]; + protected $fillable = [ 'tenant_id', + 'site_code', + 'client_id', 'name', 'address', 'latitude', 'longitude', 'is_active', + 'status', 'created_by', 'updated_by', 'deleted_by', @@ -49,6 +73,7 @@ class Site extends Model protected $attributes = [ 'is_active' => true, + 'status' => self::STATUS_UNREGISTERED, ]; // ========================================================================= @@ -71,6 +96,14 @@ public function updater(): BelongsTo return $this->belongsTo(User::class, 'updated_by'); } + /** + * 거래처 + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + // ========================================================================= // 헬퍼 메서드 // ========================================================================= diff --git a/app/Services/SiteService.php b/app/Services/SiteService.php index 545eeff..55f1e58 100644 --- a/app/Services/SiteService.php +++ b/app/Services/SiteService.php @@ -16,6 +16,7 @@ public function index(array $params): LengthAwarePaginator $tenantId = $this->tenantId(); $query = Site::query() + ->with('client:id,name') ->where('tenant_id', $tenantId); // 검색 필터 @@ -23,6 +24,7 @@ public function index(array $params): LengthAwarePaginator $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") + ->orWhere('site_code', 'like', "%{$search}%") ->orWhere('address', 'like', "%{$search}%"); }); } @@ -32,6 +34,24 @@ public function index(array $params): LengthAwarePaginator $query->where('is_active', $params['is_active']); } + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 날짜 필터 + if (! empty($params['start_date'])) { + $query->whereDate('created_at', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->whereDate('created_at', '<=', $params['end_date']); + } + // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; @@ -43,6 +63,30 @@ public function index(array $params): LengthAwarePaginator return $query->paginate($perPage); } + /** + * 현장 통계 조회 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $total = Site::where('tenant_id', $tenantId)->count(); + + $statusCounts = Site::where('tenant_id', $tenantId) + ->selectRaw('status, COUNT(*) as count') + ->groupBy('status') + ->pluck('count', 'status') + ->toArray(); + + return [ + 'total' => $total, + 'construction' => $statusCounts[Site::STATUS_ACTIVE] ?? 0, + 'unregistered' => $statusCounts[Site::STATUS_UNREGISTERED] ?? 0, + 'suspended' => $statusCounts[Site::STATUS_SUSPENDED] ?? 0, + 'pending' => $statusCounts[Site::STATUS_PENDING] ?? 0, + ]; + } + /** * 현장 상세 조회 */ @@ -51,6 +95,7 @@ public function show(int $id): Site $tenantId = $this->tenantId(); return Site::query() + ->with('client:id,name') ->where('tenant_id', $tenantId) ->findOrFail($id); } @@ -66,16 +111,19 @@ public function store(array $data): Site return DB::transaction(function () use ($data, $tenantId, $userId) { $site = Site::create([ 'tenant_id' => $tenantId, + 'site_code' => $data['site_code'] ?? null, + 'client_id' => $data['client_id'] ?? null, 'name' => $data['name'], 'address' => $data['address'] ?? null, 'latitude' => $data['latitude'] ?? null, 'longitude' => $data['longitude'] ?? null, 'is_active' => $data['is_active'] ?? true, + 'status' => $data['status'] ?? Site::STATUS_UNREGISTERED, 'created_by' => $userId, 'updated_by' => $userId, ]); - return $site; + return $site->load('client:id,name'); }); } @@ -93,17 +141,20 @@ public function update(int $id, array $data): Site ->findOrFail($id); $site->fill([ + 'site_code' => $data['site_code'] ?? $site->site_code, + 'client_id' => array_key_exists('client_id', $data) ? $data['client_id'] : $site->client_id, 'name' => $data['name'] ?? $site->name, 'address' => $data['address'] ?? $site->address, 'latitude' => $data['latitude'] ?? $site->latitude, 'longitude' => $data['longitude'] ?? $site->longitude, 'is_active' => $data['is_active'] ?? $site->is_active, + 'status' => $data['status'] ?? $site->status, 'updated_by' => $userId, ]); $site->save(); - return $site->fresh(); + return $site->fresh()->load('client:id,name'); }); } @@ -128,6 +179,32 @@ public function destroy(int $id): bool }); } + /** + * 현장 일괄 삭제 + */ + public function bulkDestroy(array $ids): int + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($ids, $tenantId, $userId) { + $sites = Site::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $ids) + ->get(); + + $deletedCount = 0; + foreach ($sites as $site) { + $site->deleted_by = $userId; + $site->save(); + $site->delete(); + $deletedCount++; + } + + return $deletedCount; + }); + } + /** * 활성화된 현장 목록 조회 (셀렉트박스용) */ @@ -139,7 +216,7 @@ public function getActiveSites(): array ->where('tenant_id', $tenantId) ->where('is_active', true) ->orderBy('name') - ->get(['id', 'name', 'address']) + ->get(['id', 'site_code', 'name', 'address']) ->toArray(); } } diff --git a/database/migrations/2026_01_09_162534_add_construction_fields_to_sites_table.php b/database/migrations/2026_01_09_162534_add_construction_fields_to_sites_table.php new file mode 100644 index 0000000..70538ec --- /dev/null +++ b/database/migrations/2026_01_09_162534_add_construction_fields_to_sites_table.php @@ -0,0 +1,44 @@ +string('site_code', 50)->nullable()->after('tenant_id')->comment('현장코드'); + + // 거래처 연결 (clients 테이블) + $table->foreignId('client_id')->nullable()->after('site_code') + ->constrained('clients')->nullOnDelete()->comment('거래처 ID'); + + // 상태 (is_active와 별개) + $table->enum('status', ['unregistered', 'suspended', 'active', 'pending']) + ->default('unregistered')->after('is_active')->comment('상태: 미등록/중지/사용/보류'); + + // 인덱스 + $table->index(['tenant_id', 'site_code']); + $table->index(['tenant_id', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sites', function (Blueprint $table) { + $table->dropIndex(['tenant_id', 'site_code']); + $table->dropIndex(['tenant_id', 'status']); + $table->dropForeign(['client_id']); + $table->dropColumn(['site_code', 'client_id', 'status']); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 66df4e2..58e6a9c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,6 +26,7 @@ use App\Http\Controllers\Api\V1\ClientController; use App\Http\Controllers\Api\V1\ClientGroupController; use App\Http\Controllers\Api\V1\Construction\ContractController; +use App\Http\Controllers\Api\V1\Construction\HandoverReportController; use App\Http\Controllers\Api\V1\CommonController; use App\Http\Controllers\Api\V1\CompanyController; use App\Http\Controllers\Api\V1\DashboardController; @@ -414,7 +415,9 @@ Route::prefix('sites')->group(function () { Route::get('', [SiteController::class, 'index'])->name('v1.sites.index'); Route::post('', [SiteController::class, 'store'])->name('v1.sites.store'); + Route::get('/stats', [SiteController::class, 'stats'])->name('v1.sites.stats'); Route::get('/active', [SiteController::class, 'active'])->name('v1.sites.active'); + Route::delete('/bulk', [SiteController::class, 'bulkDestroy'])->name('v1.sites.bulk-destroy'); Route::get('/{id}', [SiteController::class, 'show'])->whereNumber('id')->name('v1.sites.show'); Route::put('/{id}', [SiteController::class, 'update'])->whereNumber('id')->name('v1.sites.update'); Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy'); @@ -433,6 +436,17 @@ Route::put('/{id}', [ContractController::class, 'update'])->whereNumber('id')->name('v1.construction.contracts.update'); Route::delete('/{id}', [ContractController::class, 'destroy'])->whereNumber('id')->name('v1.construction.contracts.destroy'); }); + + // HandoverReport API (인수인계보고서관리) + Route::prefix('handover-reports')->group(function () { + Route::get('', [HandoverReportController::class, 'index'])->name('v1.construction.handover-reports.index'); + Route::post('', [HandoverReportController::class, 'store'])->name('v1.construction.handover-reports.store'); + Route::get('/stats', [HandoverReportController::class, 'stats'])->name('v1.construction.handover-reports.stats'); + Route::delete('/bulk', [HandoverReportController::class, 'bulkDestroy'])->name('v1.construction.handover-reports.bulk-destroy'); + Route::get('/{id}', [HandoverReportController::class, 'show'])->whereNumber('id')->name('v1.construction.handover-reports.show'); + Route::put('/{id}', [HandoverReportController::class, 'update'])->whereNumber('id')->name('v1.construction.handover-reports.update'); + Route::delete('/{id}', [HandoverReportController::class, 'destroy'])->whereNumber('id')->name('v1.construction.handover-reports.destroy'); + }); }); // Card API (카드 관리)