Merge branch 'develop' of http://114.203.209.83:3000/SamProject/sam-api into develop
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 논리적 데이터베이스 관계 문서
|
# 논리적 데이터베이스 관계 문서
|
||||||
|
|
||||||
> **자동 생성**: 2026-02-07 09:56:46
|
> **자동 생성**: 2026-02-07 01:10:55
|
||||||
> **소스**: Eloquent 모델 관계 분석
|
> **소스**: Eloquent 모델 관계 분석
|
||||||
|
|
||||||
## 📊 모델별 관계 현황
|
## 📊 모델별 관계 현황
|
||||||
@@ -499,6 +499,8 @@ ### orders
|
|||||||
- **item()**: belongsTo → `items`
|
- **item()**: belongsTo → `items`
|
||||||
- **sale()**: belongsTo → `sales`
|
- **sale()**: belongsTo → `sales`
|
||||||
- **items()**: hasMany → `order_items`
|
- **items()**: hasMany → `order_items`
|
||||||
|
- **nodes()**: hasMany → `order_nodes`
|
||||||
|
- **rootNodes()**: hasMany → `order_nodes`
|
||||||
- **histories()**: hasMany → `order_histories`
|
- **histories()**: hasMany → `order_histories`
|
||||||
- **versions()**: hasMany → `order_versions`
|
- **versions()**: hasMany → `order_versions`
|
||||||
- **workOrders()**: hasMany → `work_orders`
|
- **workOrders()**: hasMany → `work_orders`
|
||||||
@@ -514,6 +516,7 @@ ### order_items
|
|||||||
**모델**: `App\Models\Orders\OrderItem`
|
**모델**: `App\Models\Orders\OrderItem`
|
||||||
|
|
||||||
- **order()**: belongsTo → `orders`
|
- **order()**: belongsTo → `orders`
|
||||||
|
- **node()**: belongsTo → `order_nodes`
|
||||||
- **item()**: belongsTo → `items`
|
- **item()**: belongsTo → `items`
|
||||||
- **quote()**: belongsTo → `quotes`
|
- **quote()**: belongsTo → `quotes`
|
||||||
- **quoteItem()**: belongsTo → `quote_items`
|
- **quoteItem()**: belongsTo → `quote_items`
|
||||||
@@ -524,6 +527,14 @@ ### order_item_components
|
|||||||
|
|
||||||
- **orderItem()**: belongsTo → `order_items`
|
- **orderItem()**: belongsTo → `order_items`
|
||||||
|
|
||||||
|
### order_nodes
|
||||||
|
**모델**: `App\Models\Orders\OrderNode`
|
||||||
|
|
||||||
|
- **parent()**: belongsTo → `order_nodes`
|
||||||
|
- **order()**: belongsTo → `orders`
|
||||||
|
- **children()**: hasMany → `order_nodes`
|
||||||
|
- **items()**: hasMany → `order_items`
|
||||||
|
|
||||||
### order_versions
|
### order_versions
|
||||||
**모델**: `App\Models\Orders\OrderVersion`
|
**모델**: `App\Models\Orders\OrderVersion`
|
||||||
|
|
||||||
@@ -597,6 +608,7 @@ ### work_orders
|
|||||||
- **primaryAssignee()**: hasMany → `work_order_assignees`
|
- **primaryAssignee()**: hasMany → `work_order_assignees`
|
||||||
- **items()**: hasMany → `work_order_items`
|
- **items()**: hasMany → `work_order_items`
|
||||||
- **issues()**: hasMany → `work_order_issues`
|
- **issues()**: hasMany → `work_order_issues`
|
||||||
|
- **stepProgress()**: hasMany → `work_order_step_progress`
|
||||||
- **shipments()**: hasMany → `shipments`
|
- **shipments()**: hasMany → `shipments`
|
||||||
- **bendingDetail()**: hasOne → `work_order_bending_details`
|
- **bendingDetail()**: hasOne → `work_order_bending_details`
|
||||||
|
|
||||||
@@ -624,6 +636,14 @@ ### work_order_items
|
|||||||
- **workOrder()**: belongsTo → `work_orders`
|
- **workOrder()**: belongsTo → `work_orders`
|
||||||
- **item()**: belongsTo → `items`
|
- **item()**: belongsTo → `items`
|
||||||
|
|
||||||
|
### work_order_step_progress
|
||||||
|
**모델**: `App\Models\Production\WorkOrderStepProgress`
|
||||||
|
|
||||||
|
- **workOrder()**: belongsTo → `work_orders`
|
||||||
|
- **processStep()**: belongsTo → `process_steps`
|
||||||
|
- **workOrderItem()**: belongsTo → `work_order_items`
|
||||||
|
- **completedByUser()**: belongsTo → `users`
|
||||||
|
|
||||||
### work_results
|
### work_results
|
||||||
**모델**: `App\Models\Production\WorkResult`
|
**모델**: `App\Models\Production\WorkResult`
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ public function update(int $id, array $data)
|
|||||||
public function destroy(int $id)
|
public function destroy(int $id)
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
$order = Order::where('tenant_id', $tenantId)->find($id);
|
$order = Order::where('tenant_id', $tenantId)->find($id);
|
||||||
if (! $order) {
|
if (! $order) {
|
||||||
@@ -263,16 +264,49 @@ public function destroy(int $id)
|
|||||||
// 진행 중이거나 완료된 수주는 삭제 불가
|
// 진행 중이거나 완료된 수주는 삭제 불가
|
||||||
if (in_array($order->status_code, [
|
if (in_array($order->status_code, [
|
||||||
Order::STATUS_IN_PROGRESS,
|
Order::STATUS_IN_PROGRESS,
|
||||||
|
Order::STATUS_IN_PRODUCTION,
|
||||||
|
Order::STATUS_PRODUCED,
|
||||||
|
Order::STATUS_SHIPPING,
|
||||||
|
Order::STATUS_SHIPPED,
|
||||||
Order::STATUS_COMPLETED,
|
Order::STATUS_COMPLETED,
|
||||||
])) {
|
])) {
|
||||||
throw new BadRequestHttpException(__('error.order.cannot_delete_in_progress'));
|
throw new BadRequestHttpException(__('error.order.cannot_delete_in_progress'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$order->deleted_by = $this->apiUserId();
|
// 작업지시가 존재하면 삭제 불가
|
||||||
$order->save();
|
if ($order->workOrders()->exists()) {
|
||||||
$order->delete();
|
throw new BadRequestHttpException(__('error.order.cannot_delete_has_work_orders'));
|
||||||
|
}
|
||||||
|
|
||||||
return 'success';
|
// 출하 정보가 존재하면 삭제 불가
|
||||||
|
if ($order->shipments()->exists()) {
|
||||||
|
throw new BadRequestHttpException(__('error.order.cannot_delete_has_shipments'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($order, $userId) {
|
||||||
|
// 1. order_item_components soft delete
|
||||||
|
foreach ($order->items as $item) {
|
||||||
|
$item->components()->update(['deleted_by' => $userId]);
|
||||||
|
$item->components()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. order_items soft delete
|
||||||
|
$order->items()->update(['deleted_by' => $userId]);
|
||||||
|
$order->items()->delete();
|
||||||
|
|
||||||
|
// 3. order_nodes soft delete
|
||||||
|
$order->nodes()->update(['deleted_by' => $userId]);
|
||||||
|
$order->nodes()->delete();
|
||||||
|
|
||||||
|
// 4. order 마스터 soft delete
|
||||||
|
$order->deleted_by = $userId;
|
||||||
|
$order->save();
|
||||||
|
$order->delete();
|
||||||
|
|
||||||
|
// order_histories, order_versions는 감사 기록이므로 보존
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1092,7 +1092,7 @@ public function updateItemStatus(int $workOrderId, int $itemId, string $status)
|
|||||||
* 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 로트별 재고)
|
* 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 로트별 재고)
|
||||||
*
|
*
|
||||||
* 작업지시 품목의 BOM 자재별로 StockLot(입고 로트)를 FIFO 순서로 반환합니다.
|
* 작업지시 품목의 BOM 자재별로 StockLot(입고 로트)를 FIFO 순서로 반환합니다.
|
||||||
* 로트번호는 입고관리(Receiving)에서 생성된 실제 로트번호입니다.
|
* 동일 자재가 여러 작업지시 품목에 걸쳐 있으면 필요수량을 합산하고 로트는 중복 없이 반환합니다.
|
||||||
*
|
*
|
||||||
* @param int $workOrderId 작업지시 ID
|
* @param int $workOrderId 작업지시 ID
|
||||||
* @return array 자재 목록 (로트 단위)
|
* @return array 자재 목록 (로트 단위)
|
||||||
@@ -1109,8 +1109,8 @@ public function getMaterials(int $workOrderId): array
|
|||||||
throw new NotFoundHttpException(__('error.not_found'));
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$materials = [];
|
// Phase 1: 작업지시 품목들에서 유니크 자재 목록 수집 (item_id 기준 합산)
|
||||||
$rank = 1;
|
$uniqueMaterials = [];
|
||||||
|
|
||||||
foreach ($workOrder->items as $woItem) {
|
foreach ($workOrder->items as $woItem) {
|
||||||
$materialItems = [];
|
$materialItems = [];
|
||||||
@@ -1140,7 +1140,6 @@ public function getMaterials(int $workOrderId): array
|
|||||||
'item' => $childItem,
|
'item' => $childItem,
|
||||||
'bom_qty' => $bomQty,
|
'bom_qty' => $bomQty,
|
||||||
'required_qty' => $bomQty * ($woItem->quantity ?? 1),
|
'required_qty' => $bomQty * ($woItem->quantity ?? 1),
|
||||||
'work_order_item_id' => $woItem->id,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1152,73 +1151,83 @@ public function getMaterials(int $workOrderId): array
|
|||||||
'item' => $woItem->item,
|
'item' => $woItem->item,
|
||||||
'bom_qty' => 1,
|
'bom_qty' => 1,
|
||||||
'required_qty' => $woItem->quantity ?? 1,
|
'required_qty' => $woItem->quantity ?? 1,
|
||||||
'work_order_item_id' => $woItem->id,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 각 자재별로 StockLot(입고 로트) 조회
|
// 유니크 자재 수집 (같은 item_id면 required_qty 합산)
|
||||||
foreach ($materialItems as $matInfo) {
|
foreach ($materialItems as $matInfo) {
|
||||||
$materialItem = $matInfo['item'];
|
$itemId = $matInfo['item']->id;
|
||||||
|
if (isset($uniqueMaterials[$itemId])) {
|
||||||
// Stock 조회
|
$uniqueMaterials[$itemId]['required_qty'] += $matInfo['required_qty'];
|
||||||
$stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
|
} else {
|
||||||
->where('item_id', $materialItem->id)
|
$uniqueMaterials[$itemId] = $matInfo;
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($stock) {
|
|
||||||
// 가용 로트를 FIFO 순서로 조회
|
|
||||||
$lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
|
|
||||||
->where('stock_id', $stock->id)
|
|
||||||
->where('status', 'available')
|
|
||||||
->where('available_qty', '>', 0)
|
|
||||||
->orderBy('fifo_order', 'asc')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
foreach ($lots as $lot) {
|
|
||||||
$materials[] = [
|
|
||||||
'stock_lot_id' => $lot->id,
|
|
||||||
'item_id' => $materialItem->id,
|
|
||||||
'work_order_item_id' => $matInfo['work_order_item_id'],
|
|
||||||
'lot_no' => $lot->lot_no,
|
|
||||||
'material_code' => $materialItem->code,
|
|
||||||
'material_name' => $materialItem->name,
|
|
||||||
'specification' => $materialItem->specification,
|
|
||||||
'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
|
|
||||||
'bom_qty' => $matInfo['bom_qty'],
|
|
||||||
'required_qty' => $matInfo['required_qty'],
|
|
||||||
'lot_qty' => (float) $lot->qty,
|
|
||||||
'lot_available_qty' => (float) $lot->available_qty,
|
|
||||||
'lot_reserved_qty' => (float) $lot->reserved_qty,
|
|
||||||
'receipt_date' => $lot->receipt_date,
|
|
||||||
'supplier' => $lot->supplier,
|
|
||||||
'fifo_rank' => $rank++,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시)
|
// Phase 2: 유니크 자재별로 StockLot 조회
|
||||||
$hasLots = collect($materials)->where('item_id', $materialItem->id)->isNotEmpty();
|
$materials = [];
|
||||||
if (! $hasLots) {
|
$rank = 1;
|
||||||
|
|
||||||
|
foreach ($uniqueMaterials as $matInfo) {
|
||||||
|
$materialItem = $matInfo['item'];
|
||||||
|
|
||||||
|
$stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
|
||||||
|
->where('item_id', $materialItem->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$lotsFound = false;
|
||||||
|
|
||||||
|
if ($stock) {
|
||||||
|
$lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
|
||||||
|
->where('stock_id', $stock->id)
|
||||||
|
->where('status', 'available')
|
||||||
|
->where('available_qty', '>', 0)
|
||||||
|
->orderBy('fifo_order', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($lots as $lot) {
|
||||||
|
$lotsFound = true;
|
||||||
$materials[] = [
|
$materials[] = [
|
||||||
'stock_lot_id' => null,
|
'stock_lot_id' => $lot->id,
|
||||||
'item_id' => $materialItem->id,
|
'item_id' => $materialItem->id,
|
||||||
'work_order_item_id' => $matInfo['work_order_item_id'],
|
'lot_no' => $lot->lot_no,
|
||||||
'lot_no' => null,
|
|
||||||
'material_code' => $materialItem->code,
|
'material_code' => $materialItem->code,
|
||||||
'material_name' => $materialItem->name,
|
'material_name' => $materialItem->name,
|
||||||
'specification' => $materialItem->specification,
|
'specification' => $materialItem->specification,
|
||||||
'unit' => $materialItem->unit ?? 'EA',
|
'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
|
||||||
'bom_qty' => $matInfo['bom_qty'],
|
'bom_qty' => $matInfo['bom_qty'],
|
||||||
'required_qty' => $matInfo['required_qty'],
|
'required_qty' => $matInfo['required_qty'],
|
||||||
'lot_qty' => 0,
|
'lot_qty' => (float) $lot->qty,
|
||||||
'lot_available_qty' => 0,
|
'lot_available_qty' => (float) $lot->available_qty,
|
||||||
'lot_reserved_qty' => 0,
|
'lot_reserved_qty' => (float) $lot->reserved_qty,
|
||||||
'receipt_date' => null,
|
'receipt_date' => $lot->receipt_date,
|
||||||
'supplier' => null,
|
'supplier' => $lot->supplier,
|
||||||
'fifo_rank' => $rank++,
|
'fifo_rank' => $rank++,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시)
|
||||||
|
if (! $lotsFound) {
|
||||||
|
$materials[] = [
|
||||||
|
'stock_lot_id' => null,
|
||||||
|
'item_id' => $materialItem->id,
|
||||||
|
'lot_no' => null,
|
||||||
|
'material_code' => $materialItem->code,
|
||||||
|
'material_name' => $materialItem->name,
|
||||||
|
'specification' => $materialItem->specification,
|
||||||
|
'unit' => $materialItem->unit ?? 'EA',
|
||||||
|
'bom_qty' => $matInfo['bom_qty'],
|
||||||
|
'required_qty' => $matInfo['required_qty'],
|
||||||
|
'lot_qty' => 0,
|
||||||
|
'lot_available_qty' => 0,
|
||||||
|
'lot_reserved_qty' => 0,
|
||||||
|
'receipt_date' => null,
|
||||||
|
'supplier' => null,
|
||||||
|
'fifo_rank' => $rank++,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $materials;
|
return $materials;
|
||||||
|
|||||||
234
app/Swagger/v1/TriggerAuditLogApi.php
Normal file
234
app/Swagger/v1/TriggerAuditLogApi.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Trigger Audit",
|
||||||
|
* description="DB 트리거 기반 데이터 변경 추적 로그"
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TriggerAuditLog",
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="table_name", type="string", example="products"),
|
||||||
|
* @OA\Property(property="row_id", type="string", example="42"),
|
||||||
|
* @OA\Property(property="dml_type", type="string", enum={"INSERT","UPDATE","DELETE"}),
|
||||||
|
* @OA\Property(property="old_values", type="object", nullable=true),
|
||||||
|
* @OA\Property(property="new_values", type="object", nullable=true),
|
||||||
|
* @OA\Property(property="changed_columns", type="array", nullable=true, @OA\Items(type="string")),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="actor_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="session_info", type="object", nullable=true,
|
||||||
|
* @OA\Property(property="ip", type="string"),
|
||||||
|
* @OA\Property(property="ua", type="string"),
|
||||||
|
* @OA\Property(property="route", type="string")
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="db_user", type="string", nullable=true, example="samuser@%"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class TriggerAuditLogApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/trigger-audit-logs",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="감사 로그 목록 조회",
|
||||||
|
* description="DB 트리거 기반 변경 로그를 페이지네이션으로 조회합니다.",
|
||||||
|
* 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="table_name", in="query", description="테이블명 필터", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="row_id", in="query", description="레코드 PK 필터", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="dml_type", in="query", description="DML 유형 필터", @OA\Schema(type="string", enum={"INSERT","UPDATE","DELETE"})),
|
||||||
|
* @OA\Parameter(name="tenant_id", in="query", @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="actor_id", in="query", @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="db_user", in="query", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="from", in="query", description="시작일", @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="to", in="query", description="종료일", @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="sort", in="query", @OA\Schema(type="string", enum={"created_at","id"})),
|
||||||
|
* @OA\Parameter(name="order", in="query", @OA\Schema(type="string", enum={"asc","desc"})),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="목록 조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="current_page", type="integer"),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/TriggerAuditLog")),
|
||||||
|
* @OA\Property(property="last_page", type="integer"),
|
||||||
|
* @OA\Property(property="per_page", type="integer"),
|
||||||
|
* @OA\Property(property="total", type="integer")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/trigger-audit-logs/stats",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="감사 로그 통계",
|
||||||
|
* description="전체/오늘/DML별 건수, 상위 테이블, 저장소 크기 통계를 반환합니다.",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="tenant_id", in="query", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="통계 조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="total", type="integer"),
|
||||||
|
* @OA\Property(property="today", type="integer"),
|
||||||
|
* @OA\Property(property="by_dml_type", type="object",
|
||||||
|
* @OA\Property(property="INSERT", type="integer"),
|
||||||
|
* @OA\Property(property="UPDATE", type="integer"),
|
||||||
|
* @OA\Property(property="DELETE", type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="top_tables", type="object"),
|
||||||
|
* @OA\Property(property="storage_mb", type="number")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function stats() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/trigger-audit-logs/{id}",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="감사 로그 상세 조회",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="상세 조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TriggerAuditLog")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="Not Found")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/trigger-audit-logs/{tableName}/{rowId}/history",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="레코드 변경 이력 조회",
|
||||||
|
* description="특정 테이블의 특정 레코드에 대한 전체 변경 이력을 조회합니다.",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="tableName", in="path", required=true, description="테이블명", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="rowId", in="path", required=true, description="레코드 PK", @OA\Schema(type="string")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="이력 조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/TriggerAuditLog"))
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function recordHistory() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/trigger-audit-logs/{id}/rollback-preview",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="롤백 SQL 미리보기",
|
||||||
|
* description="해당 변경을 되돌리기 위한 SQL문을 미리 확인합니다.",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="롤백 SQL 반환",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="audit_id", type="integer"),
|
||||||
|
* @OA\Property(property="rollback_sql", type="string")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="Not Found")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function rollbackPreview() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/trigger-audit-logs/{id}/rollback",
|
||||||
|
* tags={"Trigger Audit"},
|
||||||
|
* summary="롤백 실행",
|
||||||
|
* description="해당 변경을 실제로 되돌립니다. confirm=true 필수.",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* required={"confirm"},
|
||||||
|
*
|
||||||
|
* @OA\Property(property="confirm", type="boolean", example=true, description="롤백 확인 (true 필수)")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="롤백 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="rolled_back", type="boolean"),
|
||||||
|
* @OA\Property(property="sql_executed", type="string")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=404, description="Not Found"),
|
||||||
|
* @OA\Response(response=422, description="Validation Error")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function rollbackExecute() {}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
'failed_jobs',
|
'failed_jobs',
|
||||||
'migrations',
|
'migrations',
|
||||||
'password_reset_tokens',
|
'password_reset_tokens',
|
||||||
|
'api_request_logs',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 변경 추적 제외 컬럼 */
|
/** 변경 추적 제외 컬럼 */
|
||||||
@@ -56,6 +57,7 @@ public function up(): void
|
|||||||
|
|
||||||
if (in_array($tableName, $this->excludeTables, true)) {
|
if (in_array($tableName, $this->excludeTables, true)) {
|
||||||
$skipped++;
|
$skipped++;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +108,12 @@ private function createTriggersForTable(string $dbName, string $tableName): void
|
|||||||
$pk = $pkRow->COLUMN_NAME;
|
$pk = $pkRow->COLUMN_NAME;
|
||||||
|
|
||||||
// 컬럼 목록 (제외 컬럼 필터링)
|
// 컬럼 목록 (제외 컬럼 필터링)
|
||||||
$columns = DB::select("
|
$columns = DB::select('
|
||||||
SELECT COLUMN_NAME
|
SELECT COLUMN_NAME
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||||
ORDER BY ORDINAL_POSITION
|
ORDER BY ORDINAL_POSITION
|
||||||
", [$dbName, $tableName]);
|
', [$dbName, $tableName]);
|
||||||
|
|
||||||
$cols = [];
|
$cols = [];
|
||||||
$hasTenantId = false;
|
$hasTenantId = false;
|
||||||
@@ -144,8 +146,8 @@ private function createTriggersForTable(string $dbName, string $tableName): void
|
|||||||
$cols
|
$cols
|
||||||
));
|
));
|
||||||
|
|
||||||
$tenantNew = $hasTenantId ? "NEW.`tenant_id`" : 'NULL';
|
$tenantNew = $hasTenantId ? 'NEW.`tenant_id`' : 'NULL';
|
||||||
$tenantOld = $hasTenantId ? "OLD.`tenant_id`" : 'NULL';
|
$tenantOld = $hasTenantId ? 'OLD.`tenant_id`' : 'NULL';
|
||||||
|
|
||||||
// 기존 트리거 삭제
|
// 기존 트리거 삭제
|
||||||
DB::unprepared("DROP TRIGGER IF EXISTS `trg_{$tableName}_ai`");
|
DB::unprepared("DROP TRIGGER IF EXISTS `trg_{$tableName}_ai`");
|
||||||
|
|||||||
@@ -400,6 +400,8 @@
|
|||||||
'order' => [
|
'order' => [
|
||||||
'cannot_update_completed' => '완료 또는 취소된 수주는 수정할 수 없습니다.',
|
'cannot_update_completed' => '완료 또는 취소된 수주는 수정할 수 없습니다.',
|
||||||
'cannot_delete_in_progress' => '진행 중이거나 완료된 수주는 삭제할 수 없습니다.',
|
'cannot_delete_in_progress' => '진행 중이거나 완료된 수주는 삭제할 수 없습니다.',
|
||||||
|
'cannot_delete_has_work_orders' => '작업지시가 존재하는 수주는 삭제할 수 없습니다. 작업지시를 먼저 삭제해주세요.',
|
||||||
|
'cannot_delete_has_shipments' => '출하 정보가 존재하는 수주는 삭제할 수 없습니다. 출하를 먼저 삭제해주세요.',
|
||||||
'invalid_status_transition' => '유효하지 않은 상태 전환입니다.',
|
'invalid_status_transition' => '유효하지 않은 상태 전환입니다.',
|
||||||
'already_created_from_quote' => '이미 해당 견적에서 수주가 생성되었습니다.',
|
'already_created_from_quote' => '이미 해당 견적에서 수주가 생성되었습니다.',
|
||||||
'must_be_confirmed_for_production' => '확정 상태의 수주만 생산지시를 생성할 수 있습니다.',
|
'must_be_confirmed_for_production' => '확정 상태의 수주만 생산지시를 생성할 수 있습니다.',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user