feat: 매입 일괄 업데이트 API 추가

- 매입유형 일괄 변경 API (POST /purchases/bulk-update-type)
- 세금계산서 수취 일괄 설정 API (POST /purchases/bulk-update-tax-received)
- FormRequest 검증 클래스 추가
- Swagger 문서 추가
This commit is contained in:
2026-01-19 19:42:05 +09:00
parent 121c888c7c
commit 7282c1ee07
6 changed files with 335 additions and 9 deletions

View File

@@ -4,6 +4,8 @@
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Purchase\BulkUpdatePurchaseTypeRequest;
use App\Http\Requests\V1\Purchase\BulkUpdateTaxReceivedRequest;
use App\Http\Requests\V1\Purchase\StorePurchaseRequest;
use App\Http\Requests\V1\Purchase\UpdatePurchaseRequest;
use App\Services\PurchaseService;
@@ -103,4 +105,36 @@ public function summary(Request $request)
return ApiResponse::success($summary, __('message.fetched'));
}
/**
* 매입유형 일괄 변경
*/
public function bulkUpdatePurchaseType(BulkUpdatePurchaseTypeRequest $request)
{
$updatedCount = $this->service->bulkUpdatePurchaseType(
$request->getIds(),
$request->getPurchaseType()
);
return ApiResponse::success(
['updated_count' => $updatedCount],
__('message.bulk_updated')
);
}
/**
* 세금계산서 수취 일괄 설정
*/
public function bulkUpdateTaxReceived(BulkUpdateTaxReceivedRequest $request)
{
$updatedCount = $this->service->bulkUpdateTaxReceived(
$request->getIds(),
$request->getTaxInvoiceReceived()
);
return ApiResponse::success(
['updated_count' => $updatedCount],
__('message.bulk_updated')
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Purchase;
use App\Models\Tenants\Purchase;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 매입유형 일괄 변경 요청 검증
*/
class BulkUpdatePurchaseTypeRequest extends FormRequest
{
/**
* 권한 확인
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검사 규칙
*
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['required', 'integer', 'min:1'],
'purchase_type' => ['required', 'string', Rule::in(array_keys(Purchase::PURCHASE_TYPES))],
];
}
/**
* 유효성 검사 메시지
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'ids.required' => __('validation.required', ['attribute' => 'ID 목록']),
'ids.array' => __('validation.array', ['attribute' => 'ID 목록']),
'ids.min' => __('validation.min.array', ['attribute' => 'ID 목록', 'min' => 1]),
'ids.*.required' => __('validation.required', ['attribute' => 'ID']),
'ids.*.integer' => __('validation.integer', ['attribute' => 'ID']),
'ids.*.min' => __('validation.min.numeric', ['attribute' => 'ID', 'min' => 1]),
'purchase_type.required' => __('validation.required', ['attribute' => '매입유형']),
'purchase_type.string' => __('validation.string', ['attribute' => '매입유형']),
'purchase_type.in' => __('validation.in', ['attribute' => '매입유형']),
];
}
/**
* 검증된 ID 배열 반환
*
* @return array<int, int>
*/
public function getIds(): array
{
return $this->validated('ids');
}
/**
* 검증된 매입유형 반환
*/
public function getPurchaseType(): string
{
return $this->validated('purchase_type');
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Purchase;
use Illuminate\Foundation\Http\FormRequest;
/**
* 세금계산서 수취 일괄 설정 요청 검증
*/
class BulkUpdateTaxReceivedRequest extends FormRequest
{
/**
* 권한 확인
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검사 규칙
*
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['required', 'integer', 'min:1'],
'tax_invoice_received' => ['required', 'boolean'],
];
}
/**
* 유효성 검사 메시지
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'ids.required' => __('validation.required', ['attribute' => 'ID 목록']),
'ids.array' => __('validation.array', ['attribute' => 'ID 목록']),
'ids.min' => __('validation.min.array', ['attribute' => 'ID 목록', 'min' => 1]),
'ids.*.required' => __('validation.required', ['attribute' => 'ID']),
'ids.*.integer' => __('validation.integer', ['attribute' => 'ID']),
'ids.*.min' => __('validation.min.numeric', ['attribute' => 'ID', 'min' => 1]),
'tax_invoice_received.required' => __('validation.required', ['attribute' => '세금계산서 수취 여부']),
'tax_invoice_received.boolean' => __('validation.boolean', ['attribute' => '세금계산서 수취 여부']),
];
}
/**
* 검증된 ID 배열 반환
*
* @return array<int, int>
*/
public function getIds(): array
{
return $this->validated('ids');
}
/**
* 검증된 세금계산서 수취 여부 반환
*/
public function getTaxInvoiceReceived(): bool
{
return $this->validated('tax_invoice_received');
}
}

View File

@@ -264,6 +264,44 @@ public function summary(array $params): array
];
}
/**
* 매입유형 일괄 변경
*
* @param array<int> $ids
*/
public function bulkUpdatePurchaseType(array $ids, string $purchaseType): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return Purchase::query()
->where('tenant_id', $tenantId)
->whereIn('id', $ids)
->update([
'purchase_type' => $purchaseType,
'updated_by' => $userId,
]);
}
/**
* 세금계산서 수취 일괄 설정
*
* @param array<int> $ids
*/
public function bulkUpdateTaxReceived(array $ids, bool $taxInvoiceReceived): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return Purchase::query()
->where('tenant_id', $tenantId)
->whereIn('id', $ids)
->update([
'tax_invoice_received' => $taxInvoiceReceived,
'updated_by' => $userId,
]);
}
/**
* 매입번호 자동 생성
*/

View File

@@ -333,4 +333,92 @@ public function destroy() {}
* )
*/
public function confirm() {}
/**
* @OA\Post(
* path="/api/v1/purchases/bulk-update-type",
* tags={"Purchases"},
* summary="매입유형 일괄 변경",
* description="선택한 매입 건들의 매입유형을 일괄 변경합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(
* required={"ids","purchase_type"},
*
* @OA\Property(property="ids", type="array", description="매입 ID 목록", @OA\Items(type="integer"), example={1,2,3}),
* @OA\Property(property="purchase_type", type="string", description="매입유형", enum={"unset","raw_material","subsidiary_material","product","outsourcing","consumables","repair","transportation","office_supplies","rent","utilities","communication","vehicle","entertainment","insurance","other_service"}, example="raw_material")
* )
* ),
*
* @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="updated_count", type="integer", example=3, description="변경된 건수")
* )
* )
* }
* )
* ),
*
* @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=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bulkUpdatePurchaseType() {}
/**
* @OA\Post(
* path="/api/v1/purchases/bulk-update-tax-received",
* tags={"Purchases"},
* summary="세금계산서 수취 일괄 설정",
* description="선택한 매입 건들의 세금계산서 수취 여부를 일괄 설정합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(
* required={"ids","tax_invoice_received"},
*
* @OA\Property(property="ids", type="array", description="매입 ID 목록", @OA\Items(type="integer"), example={1,2,3}),
* @OA\Property(property="tax_invoice_received", type="boolean", description="세금계산서 수취 여부", example=true)
* )
* ),
*
* @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="updated_count", type="integer", example=3, description="변경된 건수")
* )
* )
* }
* )
* ),
*
* @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=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function bulkUpdateTaxReceived() {}
}

View File

@@ -2,6 +2,7 @@
use App\Http\Controllers\Api\Admin\GlobalMenuController;
use App\Http\Controllers\Api\V1\AccountController;
use App\Http\Controllers\Api\V1\Admin\FcmController;
use App\Http\Controllers\Api\V1\AdminController;
use App\Http\Controllers\Api\V1\AiReportController;
use App\Http\Controllers\Api\V1\ApiController;
@@ -13,6 +14,7 @@
use App\Http\Controllers\Api\V1\BankAccountController;
use App\Http\Controllers\Api\V1\BankTransactionController;
use App\Http\Controllers\Api\V1\BarobillSettingController;
use App\Http\Controllers\Api\V1\BiddingController;
use App\Http\Controllers\Api\V1\BillController;
use App\Http\Controllers\Api\V1\BoardController;
use App\Http\Controllers\Api\V1\CardController;
@@ -55,8 +57,8 @@
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
use App\Http\Controllers\Api\V1\ItemsBomController;
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
use App\Http\Controllers\Api\V1\ItemsBomController;
use App\Http\Controllers\Api\V1\ItemsController;
use App\Http\Controllers\Api\V1\ItemsFileController;
use App\Http\Controllers\Api\V1\LaborController;
@@ -70,9 +72,9 @@
use App\Http\Controllers\Api\V1\PaymentController;
use App\Http\Controllers\Api\V1\PayrollController;
use App\Http\Controllers\Api\V1\PermissionController;
use App\Http\Controllers\Api\V1\PlanController;
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
use App\Http\Controllers\Api\V1\PlanController;
use App\Http\Controllers\Api\V1\PopupController;
use App\Http\Controllers\Api\V1\PositionController;
use App\Http\Controllers\Api\V1\PostController;
@@ -684,6 +686,8 @@
Route::get('', [PurchaseController::class, 'index'])->name('v1.purchases.index');
Route::post('', [PurchaseController::class, 'store'])->name('v1.purchases.store');
Route::get('/summary', [PurchaseController::class, 'summary'])->name('v1.purchases.summary');
Route::post('/bulk-update-type', [PurchaseController::class, 'bulkUpdatePurchaseType'])->name('v1.purchases.bulk-update-type');
Route::post('/bulk-update-tax-received', [PurchaseController::class, 'bulkUpdateTaxReceived'])->name('v1.purchases.bulk-update-tax-received');
Route::get('/{id}', [PurchaseController::class, 'show'])->whereNumber('id')->name('v1.purchases.show');
Route::put('/{id}', [PurchaseController::class, 'update'])->whereNumber('id')->name('v1.purchases.update');
Route::delete('/{id}', [PurchaseController::class, 'destroy'])->whereNumber('id')->name('v1.purchases.destroy');
@@ -870,13 +874,13 @@
// Admin FCM API (MNG 관리자용 FCM 발송) - API Key 인증만 사용
Route::prefix('admin/fcm')->group(function () {
Route::post('/send', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'send'])->name('v1.admin.fcm.send'); // FCM 발송
Route::get('/preview-count', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'previewCount'])->name('v1.admin.fcm.preview'); // 대상 토큰 수 미리보기
Route::get('/tokens', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'tokens'])->name('v1.admin.fcm.tokens'); // 토큰 목록
Route::get('/tokens/stats', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'tokenStats'])->name('v1.admin.fcm.tokens.stats'); // 토큰 통계
Route::patch('/tokens/{id}/toggle', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'toggleToken'])->name('v1.admin.fcm.tokens.toggle'); // 토큰 상태 토글
Route::delete('/tokens/{id}', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'deleteToken'])->name('v1.admin.fcm.tokens.delete'); // 토큰 삭제
Route::get('/history', [\App\Http\Controllers\Api\V1\Admin\FcmController::class, 'history'])->name('v1.admin.fcm.history'); // 발송 이력
Route::post('/send', [FcmController::class, 'send'])->name('v1.admin.fcm.send'); // FCM 발송
Route::get('/preview-count', [FcmController::class, 'previewCount'])->name('v1.admin.fcm.preview'); // 대상 토큰 수 미리보기
Route::get('/tokens', [FcmController::class, 'tokens'])->name('v1.admin.fcm.tokens'); // 토큰 목록
Route::get('/tokens/stats', [FcmController::class, 'tokenStats'])->name('v1.admin.fcm.tokens.stats'); // 토큰 통계
Route::patch('/tokens/{id}/toggle', [FcmController::class, 'toggleToken'])->name('v1.admin.fcm.tokens.toggle'); // 토큰 상태 토글
Route::delete('/tokens/{id}', [FcmController::class, 'deleteToken'])->name('v1.admin.fcm.tokens.delete'); // 토큰 삭제
Route::get('/history', [FcmController::class, 'history'])->name('v1.admin.fcm.history'); // 발송 이력
});
// 회원 프로필(테넌트 기준)
@@ -1003,6 +1007,21 @@
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'); // 카카오톡 발송
Route::get('/{id}/send/history', [QuoteController::class, 'sendHistory'])->whereNumber('id')->name('v1.quotes.send-history'); // 발송 이력
// 입찰 전환
Route::post('/{id}/convert-to-bidding', [QuoteController::class, 'convertToBidding'])->whereNumber('id')->name('v1.quotes.convert-to-bidding'); // 입찰 전환
});
// Biddings (입찰관리)
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 (단가 관리)