- OrderService: checkBendingStockForOrder() 메서드 추가
- order_items에서 item_category='BENDING'인 품목 추출
- 각 품목의 가용재고/부족수량 계산 후 반환
- OrderController: checkBendingStock() 엔드포인트 추가
- Route: GET /api/v1/orders/{id}/bending-stock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
15 KiB
PHP
206 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* 영업 관리 API 라우트 (v1)
|
|
*
|
|
* - 거래처/거래처그룹 관리
|
|
* - 견적/입찰 관리
|
|
* - 수주 관리
|
|
* - 매출 관리
|
|
* - 회사 추가 관리
|
|
*/
|
|
|
|
use App\Http\Controllers\Api\V1\BiddingController;
|
|
use App\Http\Controllers\Api\V1\ClientController;
|
|
use App\Http\Controllers\Api\V1\ClientGroupController;
|
|
use App\Http\Controllers\Api\V1\CompanyController;
|
|
use App\Http\Controllers\Api\V1\EstimateController;
|
|
use App\Http\Controllers\Api\V1\OrderController;
|
|
use App\Http\Controllers\Api\V1\PricingController;
|
|
use App\Http\Controllers\Api\V1\QuoteController;
|
|
use App\Http\Controllers\Api\V1\SaleController;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
// Client API (거래처 관리)
|
|
Route::prefix('clients')->group(function () {
|
|
Route::get('', [ClientController::class, 'index'])->name('v1.clients.index');
|
|
Route::post('', [ClientController::class, 'store'])->name('v1.clients.store');
|
|
Route::get('/active', [ClientController::class, 'active'])->name('v1.clients.active');
|
|
Route::get('/stats', [ClientController::class, 'stats'])->name('v1.clients.stats');
|
|
Route::delete('/bulk', [ClientController::class, 'bulkDestroy'])->name('v1.clients.bulk-destroy');
|
|
Route::get('/{id}', [ClientController::class, 'show'])->whereNumber('id')->name('v1.clients.show');
|
|
Route::put('/{id}', [ClientController::class, 'update'])->whereNumber('id')->name('v1.clients.update');
|
|
Route::delete('/{id}', [ClientController::class, 'destroy'])->whereNumber('id')->name('v1.clients.destroy');
|
|
Route::patch('/{id}/toggle', [ClientController::class, 'toggle'])->whereNumber('id')->name('v1.clients.toggle');
|
|
});
|
|
|
|
// Client Group API (거래처 그룹 관리)
|
|
Route::prefix('client-groups')->group(function () {
|
|
Route::get('', [ClientGroupController::class, 'index'])->name('v1.client-groups.index');
|
|
Route::post('', [ClientGroupController::class, 'store'])->name('v1.client-groups.store');
|
|
Route::get('/{id}', [ClientGroupController::class, 'show'])->whereNumber('id')->name('v1.client-groups.show');
|
|
Route::put('/{id}', [ClientGroupController::class, 'update'])->whereNumber('id')->name('v1.client-groups.update');
|
|
Route::delete('/{id}', [ClientGroupController::class, 'destroy'])->whereNumber('id')->name('v1.client-groups.destroy');
|
|
Route::patch('/{id}/toggle', [ClientGroupController::class, 'toggle'])->whereNumber('id')->name('v1.client-groups.toggle');
|
|
});
|
|
|
|
// Quote API (견적 관리)
|
|
Route::prefix('quotes')->group(function () {
|
|
Route::get('', [QuoteController::class, 'index'])->name('v1.quotes.index');
|
|
Route::get('/stats', [QuoteController::class, 'stats'])->name('v1.quotes.stats');
|
|
Route::get('/stage-counts', [QuoteController::class, 'stageCounts'])->name('v1.quotes.stage-counts');
|
|
Route::post('', [QuoteController::class, 'store'])->name('v1.quotes.store');
|
|
Route::delete('/bulk', [QuoteController::class, 'bulkDestroy'])->name('v1.quotes.bulk-destroy');
|
|
|
|
// 견적번호 미리보기
|
|
Route::get('/number/preview', [QuoteController::class, 'previewNumber'])->name('v1.quotes.number-preview');
|
|
|
|
// 참조 데이터 (현장명, 부호 등)
|
|
Route::get('/reference-data', [QuoteController::class, 'referenceData'])->name('v1.quotes.reference-data');
|
|
|
|
// 자동산출
|
|
Route::get('/calculation/schema', [QuoteController::class, 'calculationSchema'])->name('v1.quotes.calculation-schema');
|
|
Route::post('/calculate', [QuoteController::class, 'calculate'])->name('v1.quotes.calculate');
|
|
Route::post('/calculate/bom', [QuoteController::class, 'calculateBom'])->name('v1.quotes.calculate-bom');
|
|
Route::post('/calculate/bom/bulk', [QuoteController::class, 'calculateBomBulk'])->name('v1.quotes.calculate-bom-bulk');
|
|
|
|
// 품목 단가 조회 (수동 품목 추가 시 사용)
|
|
Route::post('/items/prices', [QuoteController::class, 'getItemPrices'])->name('v1.quotes.item-prices');
|
|
|
|
// 단건 조회/수정/삭제 (id 경로는 구체적인 경로 뒤에 배치)
|
|
Route::get('/{id}', [QuoteController::class, 'show'])->whereNumber('id')->name('v1.quotes.show');
|
|
Route::put('/{id}', [QuoteController::class, 'update'])->whereNumber('id')->name('v1.quotes.update');
|
|
Route::delete('/{id}', [QuoteController::class, 'destroy'])->whereNumber('id')->name('v1.quotes.destroy');
|
|
Route::post('/{id}/clone', [QuoteController::class, 'clone'])->whereNumber('id')->name('v1.quotes.clone');
|
|
Route::put('/{id}/stage', [QuoteController::class, 'updateStage'])->whereNumber('id')->name('v1.quotes.stage');
|
|
Route::put('/{id}/items', [QuoteController::class, 'updateItems'])->whereNumber('id')->name('v1.quotes.items');
|
|
|
|
// 견적 확정/확정 취소
|
|
Route::post('/{id}/finalize', [QuoteController::class, 'finalize'])->whereNumber('id')->name('v1.quotes.finalize');
|
|
Route::post('/{id}/cancel-finalize', [QuoteController::class, 'cancelFinalize'])->whereNumber('id')->name('v1.quotes.cancel-finalize');
|
|
|
|
// 견적 변환
|
|
Route::post('/{id}/convert-to-bidding', [QuoteController::class, 'convertToBidding'])->whereNumber('id')->name('v1.quotes.convert-to-bidding');
|
|
Route::post('/{id}/convert-to-order', [QuoteController::class, 'convertToOrder'])->whereNumber('id')->name('v1.quotes.convert-to-order');
|
|
|
|
// 히스토리
|
|
Route::get('/{id}/histories', [QuoteController::class, 'histories'])->whereNumber('id')->name('v1.quotes.histories');
|
|
Route::post('/{id}/histories', [QuoteController::class, 'addHistory'])->whereNumber('id')->name('v1.quotes.histories.store');
|
|
Route::put('/{id}/histories/{historyId}', [QuoteController::class, 'updateHistory'])->whereNumber('id')->whereNumber('historyId')->name('v1.quotes.histories.update');
|
|
Route::delete('/{id}/histories/{historyId}', [QuoteController::class, 'deleteHistory'])->whereNumber('id')->whereNumber('historyId')->name('v1.quotes.histories.destroy');
|
|
Route::post('/{id}/send-history', [QuoteController::class, 'sendHistory'])->whereNumber('id')->name('v1.quotes.send-history');
|
|
|
|
// 견적서 문서 관리
|
|
Route::get('/{id}/document', [QuoteController::class, 'getDocument'])->whereNumber('id')->name('v1.quotes.document.show');
|
|
Route::post('/{id}/document/issue', [QuoteController::class, 'issueDocument'])->whereNumber('id')->name('v1.quotes.document.issue');
|
|
Route::post('/{id}/document/send', [QuoteController::class, 'sendDocument'])->whereNumber('id')->name('v1.quotes.document.send');
|
|
Route::post('/{id}/pdf', [QuoteController::class, 'generatePdf'])->whereNumber('id')->name('v1.quotes.pdf');
|
|
Route::post('/{id}/send/email', [QuoteController::class, 'sendEmail'])->whereNumber('id')->name('v1.quotes.send-email');
|
|
Route::post('/{id}/send/kakao', [QuoteController::class, 'sendKakao'])->whereNumber('id')->name('v1.quotes.send-kakao');
|
|
});
|
|
|
|
// Bidding API (입찰 관리)
|
|
Route::prefix('biddings')->group(function () {
|
|
Route::get('', [BiddingController::class, 'index'])->name('v1.biddings.index');
|
|
Route::post('', [BiddingController::class, 'store'])->name('v1.biddings.store');
|
|
Route::get('/stats', [BiddingController::class, 'stats'])->name('v1.biddings.stats');
|
|
Route::delete('/bulk', [BiddingController::class, 'bulkDestroy'])->name('v1.biddings.bulk-destroy');
|
|
Route::get('/{id}', [BiddingController::class, 'show'])->whereNumber('id')->name('v1.biddings.show');
|
|
Route::put('/{id}', [BiddingController::class, 'update'])->whereNumber('id')->name('v1.biddings.update');
|
|
Route::delete('/{id}', [BiddingController::class, 'destroy'])->whereNumber('id')->name('v1.biddings.destroy');
|
|
Route::patch('/{id}/status', [BiddingController::class, 'updateStatus'])->whereNumber('id')->name('v1.biddings.status');
|
|
});
|
|
|
|
// Pricing API (단가 관리)
|
|
Route::prefix('pricing')->group(function () {
|
|
Route::get('', [PricingController::class, 'index'])->name('v1.pricing.index');
|
|
Route::post('', [PricingController::class, 'store'])->name('v1.pricing.store');
|
|
Route::get('/types', [PricingController::class, 'types'])->name('v1.pricing.types');
|
|
Route::get('/summary', [PricingController::class, 'summary'])->name('v1.pricing.summary');
|
|
Route::get('/stats', [PricingController::class, 'stats'])->name('v1.pricing.stats');
|
|
Route::get('/cost', [PricingController::class, 'cost'])->name('v1.pricing.cost');
|
|
Route::post('/by-items', [PricingController::class, 'byItems'])->name('v1.pricing.by-items');
|
|
Route::get('/history/{itemId}', [PricingController::class, 'history'])->whereNumber('itemId')->name('v1.pricing.history');
|
|
Route::post('/bulk-upsert', [PricingController::class, 'bulkUpsert'])->name('v1.pricing.bulk-upsert');
|
|
Route::delete('/bulk', [PricingController::class, 'bulkDestroy'])->name('v1.pricing.bulk-destroy');
|
|
Route::get('/{id}', [PricingController::class, 'show'])->whereNumber('id')->name('v1.pricing.show');
|
|
Route::put('/{id}', [PricingController::class, 'update'])->whereNumber('id')->name('v1.pricing.update');
|
|
Route::delete('/{id}', [PricingController::class, 'destroy'])->whereNumber('id')->name('v1.pricing.destroy');
|
|
Route::post('/{id}/finalize', [PricingController::class, 'finalize'])->whereNumber('id')->name('v1.pricing.finalize');
|
|
Route::get('/{id}/revisions', [PricingController::class, 'revisions'])->whereNumber('id')->name('v1.pricing.revisions');
|
|
});
|
|
|
|
// Estimate API (견적/설계)
|
|
Route::prefix('estimates')->group(function () {
|
|
Route::get('', [EstimateController::class, 'index'])->name('v1.estimates.index'); // 견적 목록
|
|
Route::get('/stats', [EstimateController::class, 'stats'])->name('v1.estimates.stats'); // 견적 통계
|
|
Route::post('', [EstimateController::class, 'store'])->name('v1.estimates.store'); // 견적 생성
|
|
Route::get('/{id}', [EstimateController::class, 'show'])->whereNumber('id')->name('v1.estimates.show'); // 견적 상세
|
|
Route::put('/{id}', [EstimateController::class, 'update'])->whereNumber('id')->name('v1.estimates.update'); // 견적 수정
|
|
Route::delete('/{id}', [EstimateController::class, 'destroy'])->whereNumber('id')->name('v1.estimates.destroy'); // 견적 삭제
|
|
Route::post('/{id}/clone', [EstimateController::class, 'clone'])->name('v1.estimates.clone'); // 견적 복제
|
|
Route::put('/{id}/status', [EstimateController::class, 'changeStatus'])->name('v1.estimates.status'); // 견적 상태 변경
|
|
|
|
// 견적 폼 및 계산 기능
|
|
Route::get('/form-schema/{model_set_id}', [EstimateController::class, 'getFormSchema'])->name('v1.estimates.form-schema'); // 견적 폼 스키마
|
|
Route::post('/preview/{model_set_id}', [EstimateController::class, 'previewCalculation'])->name('v1.estimates.preview'); // 견적 계산 미리보기
|
|
});
|
|
|
|
// Order API (수주관리)
|
|
Route::prefix('orders')->group(function () {
|
|
// 기본 CRUD
|
|
Route::get('', [OrderController::class, 'index'])->name('v1.orders.index'); // 목록
|
|
Route::get('/stats', [OrderController::class, 'stats'])->name('v1.orders.stats'); // 통계
|
|
Route::post('', [OrderController::class, 'store'])->name('v1.orders.store'); // 생성
|
|
Route::delete('/bulk', [OrderController::class, 'bulkDestroy'])->name('v1.orders.bulk-destroy'); // 일괄 삭제
|
|
Route::get('/{id}', [OrderController::class, 'show'])->whereNumber('id')->name('v1.orders.show'); // 상세
|
|
Route::put('/{id}', [OrderController::class, 'update'])->whereNumber('id')->name('v1.orders.update'); // 수정
|
|
Route::delete('/{id}', [OrderController::class, 'destroy'])->whereNumber('id')->name('v1.orders.destroy'); // 삭제
|
|
|
|
// 상태 관리
|
|
Route::patch('/{id}/status', [OrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.orders.status'); // 상태 변경
|
|
|
|
// 견적에서 수주 생성
|
|
Route::post('/from-quote/{quoteId}', [OrderController::class, 'createFromQuote'])->whereNumber('quoteId')->name('v1.orders.from-quote');
|
|
|
|
// 절곡 재고 현황 확인
|
|
Route::get('/{id}/bending-stock', [OrderController::class, 'checkBendingStock'])->whereNumber('id')->name('v1.orders.bending-stock');
|
|
|
|
// 생산지시 생성
|
|
Route::post('/{id}/production-order', [OrderController::class, 'createProductionOrder'])->whereNumber('id')->name('v1.orders.production-order');
|
|
|
|
// 수주확정 되돌리기
|
|
Route::post('/{id}/revert-confirmation', [OrderController::class, 'revertOrderConfirmation'])->whereNumber('id')->name('v1.orders.revert-confirmation');
|
|
|
|
// 생산지시 되돌리기
|
|
Route::post('/{id}/revert-production', [OrderController::class, 'revertProductionOrder'])->whereNumber('id')->name('v1.orders.revert-production');
|
|
});
|
|
|
|
// Sale API (매출 관리)
|
|
Route::prefix('sales')->group(function () {
|
|
Route::get('', [SaleController::class, 'index'])->name('v1.sales.index');
|
|
Route::post('', [SaleController::class, 'store'])->name('v1.sales.store');
|
|
Route::get('/summary', [SaleController::class, 'summary'])->name('v1.sales.summary');
|
|
Route::get('/{id}', [SaleController::class, 'show'])->whereNumber('id')->name('v1.sales.show');
|
|
Route::put('/{id}', [SaleController::class, 'update'])->whereNumber('id')->name('v1.sales.update');
|
|
Route::delete('/{id}', [SaleController::class, 'destroy'])->whereNumber('id')->name('v1.sales.destroy');
|
|
Route::post('/{id}/confirm', [SaleController::class, 'confirm'])->whereNumber('id')->name('v1.sales.confirm');
|
|
Route::put('/bulk-update-account', [SaleController::class, 'bulkUpdateAccountCode'])->name('v1.sales.bulk-update-account');
|
|
// 거래명세서 API
|
|
Route::get('/{id}/statement', [SaleController::class, 'getStatement'])->whereNumber('id')->name('v1.sales.statement.show');
|
|
Route::post('/{id}/statement/issue', [SaleController::class, 'issueStatement'])->whereNumber('id')->name('v1.sales.statement.issue');
|
|
Route::post('/{id}/statement/send', [SaleController::class, 'sendStatement'])->whereNumber('id')->name('v1.sales.statement.send');
|
|
Route::post('/bulk-issue-statement', [SaleController::class, 'bulkIssueStatement'])->name('v1.sales.bulk-issue-statement');
|
|
});
|
|
|
|
// Company API (회사 추가 관리)
|
|
Route::prefix('companies')->group(function () {
|
|
Route::post('/check', [CompanyController::class, 'check'])->name('v1.companies.check'); // 사업자등록번호 검증
|
|
Route::post('/request', [CompanyController::class, 'request'])->name('v1.companies.request'); // 회사 추가 신청
|
|
Route::get('/requests', [CompanyController::class, 'requests'])->name('v1.companies.requests.index'); // 신청 목록 (관리자)
|
|
Route::get('/requests/{id}', [CompanyController::class, 'showRequest'])->whereNumber('id')->name('v1.companies.requests.show'); // 신청 상세
|
|
Route::post('/requests/{id}/approve', [CompanyController::class, 'approve'])->whereNumber('id')->name('v1.companies.requests.approve'); // 승인
|
|
Route::post('/requests/{id}/reject', [CompanyController::class, 'reject'])->whereNumber('id')->name('v1.companies.requests.reject'); // 반려
|
|
Route::get('/my-requests', [CompanyController::class, 'myRequests'])->name('v1.companies.my-requests'); // 내 신청 목록
|
|
});
|