refactor(work-orders): 코드 리뷰 기반 전면 개선
## Critical 수정 - Multi-tenancy: WorkOrderItem, BendingDetail, Issue에 BelongsToTenant 적용 - 감사 로그: 상태변경, 품목수정, 이슈 등록/해결 시 로깅 추가 - 상태 전이 규칙: STATUS_TRANSITIONS + canTransitionTo() 구현 ## High 수정 - 다중 담당자: work_order_assignees 피벗 테이블 및 관계 추가 - 부분 수정: 품목 ID 기반 upsert/delete 로직 구현 ## 변경 파일 - Models: WorkOrder, WorkOrderAssignee(신규), 하위 모델들 - Services: WorkOrderService (assign, update 메서드 개선) - Migrations: tenant_id 추가, assignees 테이블 생성 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,19 @@ class WorkOrder extends Model
|
||||
self::STATUS_SHIPPED,
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 전이 규칙
|
||||
* [현재 상태 => [허용되는 다음 상태들]]
|
||||
*/
|
||||
public const STATUS_TRANSITIONS = [
|
||||
self::STATUS_UNASSIGNED => [self::STATUS_PENDING],
|
||||
self::STATUS_PENDING => [self::STATUS_UNASSIGNED, self::STATUS_WAITING],
|
||||
self::STATUS_WAITING => [self::STATUS_PENDING, self::STATUS_IN_PROGRESS],
|
||||
self::STATUS_IN_PROGRESS => [self::STATUS_WAITING, self::STATUS_COMPLETED],
|
||||
self::STATUS_COMPLETED => [self::STATUS_IN_PROGRESS, self::STATUS_SHIPPED],
|
||||
self::STATUS_SHIPPED => [self::STATUS_COMPLETED],
|
||||
];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 관계
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
@@ -111,13 +124,29 @@ public function salesOrder(): BelongsTo
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자
|
||||
* 담당자 (주 담당자 - 하위 호환)
|
||||
*/
|
||||
public function assignee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assignee_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자들 (다중 담당자)
|
||||
*/
|
||||
public function assignees(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkOrderAssignee::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주 담당자 (다중 담당자 중)
|
||||
*/
|
||||
public function primaryAssignee(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkOrderAssignee::class)->where('is_primary', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 팀
|
||||
*/
|
||||
@@ -291,4 +320,41 @@ public function getOpenIssuesCountAttribute(): int
|
||||
{
|
||||
return $this->issues()->where('status', '!=', 'resolved')->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 상태로 전이 가능한지 확인
|
||||
*/
|
||||
public function canTransitionTo(string $newStatus): bool
|
||||
{
|
||||
$allowedTransitions = self::STATUS_TRANSITIONS[$this->status] ?? [];
|
||||
|
||||
return in_array($newStatus, $allowedTransitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 상태에서 허용되는 다음 상태 목록 조회
|
||||
*/
|
||||
public function getAllowedTransitions(): array
|
||||
{
|
||||
return self::STATUS_TRANSITIONS[$this->status] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 전이 실행 (유효성 검증 포함)
|
||||
*
|
||||
* @throws \InvalidArgumentException 허용되지 않은 전이인 경우
|
||||
*/
|
||||
public function transitionTo(string $newStatus): bool
|
||||
{
|
||||
if (! $this->canTransitionTo($newStatus)) {
|
||||
$allowed = implode(', ', $this->getAllowedTransitions());
|
||||
throw new \InvalidArgumentException(
|
||||
"상태를 '{$this->status}'에서 '{$newStatus}'(으)로 변경할 수 없습니다. 허용된 상태: {$allowed}"
|
||||
);
|
||||
}
|
||||
|
||||
$this->status = $newStatus;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Models/Production/WorkOrderAssignee.php
Normal file
64
app/Models/Production/WorkOrderAssignee.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 작업지시 담당자 피벗 모델
|
||||
*
|
||||
* 다중 담당자 지원을 위한 피벗 테이블 모델
|
||||
*/
|
||||
class WorkOrderAssignee extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'work_order_assignees';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'work_order_id',
|
||||
'user_id',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 관계
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 작업지시
|
||||
*/
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 스코프
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 주 담당자만
|
||||
*/
|
||||
public function scopePrimary($query)
|
||||
{
|
||||
return $query->where('is_primary', true);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -12,9 +13,12 @@
|
||||
*/
|
||||
class WorkOrderBendingDetail extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_bending_details';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'work_order_id',
|
||||
'shaft_cutting',
|
||||
'bearing',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -13,9 +14,12 @@
|
||||
*/
|
||||
class WorkOrderIssue extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_issues';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'work_order_id',
|
||||
'title',
|
||||
'description',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -11,9 +12,12 @@
|
||||
*/
|
||||
class WorkOrderItem extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_items';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'work_order_id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
|
||||
@@ -3,13 +3,21 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Production\WorkOrder;
|
||||
use App\Models\Production\WorkOrderAssignee;
|
||||
use App\Models\Production\WorkOrderBendingDetail;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class WorkOrderService extends Service
|
||||
{
|
||||
private const AUDIT_TARGET = 'work_order';
|
||||
|
||||
public function __construct(
|
||||
private readonly AuditLogger $auditLogger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 목록 조회 (검색/필터링/페이징)
|
||||
*/
|
||||
@@ -29,7 +37,7 @@ public function index(array $params)
|
||||
|
||||
$query = WorkOrder::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['assignee:id,name', 'team:id,name', 'salesOrder:id,order_no']);
|
||||
->with(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'salesOrder:id,order_no']);
|
||||
|
||||
// 검색어
|
||||
if ($q !== '') {
|
||||
@@ -106,6 +114,7 @@ public function show(int $id)
|
||||
$workOrder = WorkOrder::where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'assignee:id,name',
|
||||
'assignees.user:id,name',
|
||||
'team:id,name',
|
||||
'salesOrder:id,order_no,project_name',
|
||||
'items',
|
||||
@@ -149,16 +158,28 @@ public function store(array $data)
|
||||
|
||||
// 품목 저장
|
||||
foreach ($items as $index => $item) {
|
||||
$item['tenant_id'] = $tenantId;
|
||||
$item['sort_order'] = $index;
|
||||
$workOrder->items()->create($item);
|
||||
}
|
||||
|
||||
// 벤딩 상세 저장 (벤딩 공정인 경우)
|
||||
if ($data['process_type'] === WorkOrder::PROCESS_BENDING && $bendingDetail) {
|
||||
$bendingDetail['tenant_id'] = $tenantId;
|
||||
$workOrder->bendingDetail()->create($bendingDetail);
|
||||
}
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'team:id,name', 'items', 'bendingDetail']);
|
||||
// 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'created',
|
||||
null,
|
||||
$workOrder->toArray()
|
||||
);
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'items', 'bendingDetail']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,33 +196,89 @@ public function update(int $id, array $data)
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($workOrder, $data, $userId) {
|
||||
$beforeData = $workOrder->toArray();
|
||||
|
||||
return DB::transaction(function () use ($workOrder, $data, $userId, $beforeData) {
|
||||
$data['updated_by'] = $userId;
|
||||
|
||||
$items = $data['items'] ?? null;
|
||||
$bendingDetail = $data['bending_detail'] ?? null;
|
||||
unset($data['items'], $data['bending_detail'], $data['work_order_no']); // 번호 변경 불가
|
||||
|
||||
// 품목 수정 시 기존 품목 기록
|
||||
$oldItems = null;
|
||||
if ($items !== null) {
|
||||
$oldItems = $workOrder->items()->get()->toArray();
|
||||
}
|
||||
|
||||
$workOrder->update($data);
|
||||
|
||||
// 품목 교체 (있는 경우)
|
||||
// 품목 부분 수정 (ID 기반 upsert/delete)
|
||||
if ($items !== null) {
|
||||
$workOrder->items()->delete();
|
||||
$existingIds = $workOrder->items()->pluck('id')->toArray();
|
||||
$incomingIds = [];
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$item['sort_order'] = $index;
|
||||
$workOrder->items()->create($item);
|
||||
$itemData = [
|
||||
'tenant_id' => $workOrder->tenant_id,
|
||||
'item_name' => $item['item_name'] ?? null,
|
||||
'specification' => $item['specification'] ?? null,
|
||||
'quantity' => $item['quantity'] ?? 1,
|
||||
'unit' => $item['unit'] ?? null,
|
||||
'sort_order' => $index,
|
||||
];
|
||||
|
||||
if (isset($item['id']) && $item['id']) {
|
||||
// ID가 있으면 업데이트
|
||||
$existingItem = $workOrder->items()->find($item['id']);
|
||||
if ($existingItem) {
|
||||
$existingItem->update($itemData);
|
||||
$incomingIds[] = (int) $item['id'];
|
||||
}
|
||||
} else {
|
||||
// ID가 없으면 신규 생성
|
||||
$newItem = $workOrder->items()->create($itemData);
|
||||
$incomingIds[] = $newItem->id;
|
||||
}
|
||||
}
|
||||
|
||||
// 요청에 없는 기존 품목 삭제
|
||||
$toDelete = array_diff($existingIds, $incomingIds);
|
||||
if (! empty($toDelete)) {
|
||||
$workOrder->items()->whereIn('id', $toDelete)->delete();
|
||||
}
|
||||
|
||||
// 품목 수정 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$workOrder->tenant_id,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'items_updated',
|
||||
['items' => $oldItems],
|
||||
['items' => $workOrder->items()->get()->toArray()]
|
||||
);
|
||||
}
|
||||
|
||||
// 벤딩 상세 업데이트
|
||||
if ($bendingDetail !== null && $workOrder->process_type === WorkOrder::PROCESS_BENDING) {
|
||||
$bendingDetail['tenant_id'] = $workOrder->tenant_id;
|
||||
$workOrder->bendingDetail()->updateOrCreate(
|
||||
['work_order_id' => $workOrder->id],
|
||||
$bendingDetail
|
||||
);
|
||||
}
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'team:id,name', 'items', 'bendingDetail']);
|
||||
// 수정 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$workOrder->tenant_id,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'updated',
|
||||
$beforeData,
|
||||
$workOrder->fresh()->toArray()
|
||||
);
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'items', 'bendingDetail']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,8 +303,19 @@ public function destroy(int $id)
|
||||
throw new BadRequestHttpException(__('error.work_order.cannot_delete_in_progress'));
|
||||
}
|
||||
|
||||
$beforeData = $workOrder->toArray();
|
||||
$workOrder->delete();
|
||||
|
||||
// 삭제 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'deleted',
|
||||
$beforeData,
|
||||
null
|
||||
);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
@@ -249,6 +337,19 @@ public function updateStatus(int $id, string $status)
|
||||
throw new BadRequestHttpException(__('error.invalid_status'));
|
||||
}
|
||||
|
||||
// 상태 전이 규칙 검증
|
||||
if (! $workOrder->canTransitionTo($status)) {
|
||||
$allowed = implode(', ', $workOrder->getAllowedTransitions());
|
||||
throw new BadRequestHttpException(
|
||||
__('error.work_order.invalid_transition', [
|
||||
'from' => $workOrder->status,
|
||||
'to' => $status,
|
||||
'allowed' => $allowed,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$oldStatus = $workOrder->status;
|
||||
$workOrder->status = $status;
|
||||
$workOrder->updated_by = $userId;
|
||||
|
||||
@@ -267,11 +368,24 @@ public function updateStatus(int $id, string $status)
|
||||
|
||||
$workOrder->save();
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'team:id,name']);
|
||||
// 상태 변경 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'status_changed',
|
||||
['status' => $oldStatus],
|
||||
['status' => $status]
|
||||
);
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 배정
|
||||
* 담당자 배정 (다중 담당자 지원)
|
||||
*
|
||||
* @param int $id 작업지시 ID
|
||||
* @param array $data 배정 데이터 (assignee_ids: int[], team_id?: int)
|
||||
*/
|
||||
public function assign(int $id, array $data)
|
||||
{
|
||||
@@ -283,18 +397,65 @@ public function assign(int $id, array $data)
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$workOrder->assignee_id = $data['assignee_id'];
|
||||
$workOrder->team_id = $data['team_id'] ?? $workOrder->team_id;
|
||||
$workOrder->updated_by = $userId;
|
||||
return DB::transaction(function () use ($workOrder, $data, $tenantId, $userId) {
|
||||
// 이전 상태 기록
|
||||
$beforeAssignees = $workOrder->assignees()->pluck('user_id')->toArray();
|
||||
$beforePrimaryAssignee = $workOrder->assignee_id;
|
||||
$beforeTeam = $workOrder->team_id;
|
||||
|
||||
// 미배정이었으면 대기로 변경
|
||||
if ($workOrder->status === WorkOrder::STATUS_UNASSIGNED) {
|
||||
$workOrder->status = WorkOrder::STATUS_PENDING;
|
||||
}
|
||||
// 담당자 ID 배열 처리 (단일 값도 배열로 변환)
|
||||
$assigneeIds = $data['assignee_ids'] ?? [];
|
||||
if (isset($data['assignee_id']) && ! empty($data['assignee_id'])) {
|
||||
// 하위 호환: 단일 assignee_id도 지원
|
||||
$assigneeIds = is_array($data['assignee_id']) ? $data['assignee_id'] : [$data['assignee_id']];
|
||||
}
|
||||
$assigneeIds = array_unique(array_filter($assigneeIds));
|
||||
|
||||
$workOrder->save();
|
||||
// 기존 담당자 삭제 후 새로 추가
|
||||
$workOrder->assignees()->delete();
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'team:id,name']);
|
||||
foreach ($assigneeIds as $index => $assigneeId) {
|
||||
WorkOrderAssignee::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'work_order_id' => $workOrder->id,
|
||||
'user_id' => $assigneeId,
|
||||
'is_primary' => $index === 0, // 첫 번째가 주 담당자
|
||||
]);
|
||||
}
|
||||
|
||||
// 주 담당자는 work_orders 테이블에도 설정 (하위 호환)
|
||||
$primaryAssigneeId = $assigneeIds[0] ?? null;
|
||||
$workOrder->assignee_id = $primaryAssigneeId;
|
||||
$workOrder->team_id = $data['team_id'] ?? $workOrder->team_id;
|
||||
$workOrder->updated_by = $userId;
|
||||
|
||||
// 미배정이었으면 대기로 변경
|
||||
if ($workOrder->status === WorkOrder::STATUS_UNASSIGNED && $primaryAssigneeId) {
|
||||
$workOrder->status = WorkOrder::STATUS_PENDING;
|
||||
}
|
||||
|
||||
$workOrder->save();
|
||||
|
||||
// 배정 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'assigned',
|
||||
[
|
||||
'assignee_id' => $beforePrimaryAssignee,
|
||||
'assignee_ids' => $beforeAssignees,
|
||||
'team_id' => $beforeTeam,
|
||||
],
|
||||
[
|
||||
'assignee_id' => $workOrder->assignee_id,
|
||||
'assignee_ids' => $assigneeIds,
|
||||
'team_id' => $workOrder->team_id,
|
||||
]
|
||||
);
|
||||
|
||||
return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,15 +476,28 @@ public function toggleBendingField(int $id, string $field)
|
||||
|
||||
$detail = $workOrder->bendingDetail;
|
||||
if (! $detail) {
|
||||
$detail = $workOrder->bendingDetail()->create([]);
|
||||
$detail = $workOrder->bendingDetail()->create([
|
||||
'tenant_id' => $workOrder->tenant_id,
|
||||
]);
|
||||
}
|
||||
|
||||
if (! in_array($field, WorkOrderBendingDetail::PROCESS_FIELDS)) {
|
||||
throw new BadRequestHttpException(__('error.invalid_field'));
|
||||
}
|
||||
|
||||
$beforeValue = $detail->{$field};
|
||||
$detail->toggleField($field);
|
||||
|
||||
// 벤딩 토글 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$workOrder->tenant_id,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'bending_toggled',
|
||||
[$field => $beforeValue],
|
||||
[$field => $detail->{$field}]
|
||||
);
|
||||
|
||||
return $detail;
|
||||
}
|
||||
|
||||
@@ -340,9 +514,22 @@ public function addIssue(int $workOrderId, array $data)
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$data['tenant_id'] = $tenantId;
|
||||
$data['reported_by'] = $userId;
|
||||
|
||||
return $workOrder->issues()->create($data);
|
||||
$issue = $workOrder->issues()->create($data);
|
||||
|
||||
// 이슈 추가 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrderId,
|
||||
'issue_added',
|
||||
null,
|
||||
['issue_id' => $issue->id, 'title' => $issue->title, 'priority' => $issue->priority]
|
||||
);
|
||||
|
||||
return $issue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,6 +552,16 @@ public function resolveIssue(int $workOrderId, int $issueId)
|
||||
|
||||
$issue->resolve($userId);
|
||||
|
||||
// 이슈 해결 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrderId,
|
||||
'issue_resolved',
|
||||
['issue_id' => $issueId, 'status' => 'open'],
|
||||
['issue_id' => $issueId, 'status' => 'resolved', 'resolved_by' => $userId]
|
||||
);
|
||||
|
||||
return $issue;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 작업지시 하위 테이블에 tenant_id 컬럼 추가
|
||||
* - work_order_items
|
||||
* - work_order_bending_details
|
||||
* - work_order_issues
|
||||
*
|
||||
* 기존 데이터는 work_orders 테이블의 tenant_id를 참조하여 업데이트
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. work_order_items
|
||||
Schema::table('work_order_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable()->after('id')->comment('테넌트ID');
|
||||
$table->index('tenant_id', 'idx_work_order_items_tenant');
|
||||
});
|
||||
|
||||
// 기존 데이터 업데이트
|
||||
DB::statement('
|
||||
UPDATE work_order_items wi
|
||||
JOIN work_orders wo ON wi.work_order_id = wo.id
|
||||
SET wi.tenant_id = wo.tenant_id
|
||||
');
|
||||
|
||||
// nullable 제거
|
||||
Schema::table('work_order_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
|
||||
});
|
||||
|
||||
// 2. work_order_bending_details
|
||||
Schema::table('work_order_bending_details', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable()->after('id')->comment('테넌트ID');
|
||||
$table->index('tenant_id', 'idx_work_order_bending_details_tenant');
|
||||
});
|
||||
|
||||
DB::statement('
|
||||
UPDATE work_order_bending_details wbd
|
||||
JOIN work_orders wo ON wbd.work_order_id = wo.id
|
||||
SET wbd.tenant_id = wo.tenant_id
|
||||
');
|
||||
|
||||
Schema::table('work_order_bending_details', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
|
||||
});
|
||||
|
||||
// 3. work_order_issues
|
||||
Schema::table('work_order_issues', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable()->after('id')->comment('테넌트ID');
|
||||
$table->index('tenant_id', 'idx_work_order_issues_tenant');
|
||||
});
|
||||
|
||||
DB::statement('
|
||||
UPDATE work_order_issues woi
|
||||
JOIN work_orders wo ON woi.work_order_id = wo.id
|
||||
SET woi.tenant_id = wo.tenant_id
|
||||
');
|
||||
|
||||
Schema::table('work_order_issues', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('work_order_items', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_work_order_items_tenant');
|
||||
$table->dropColumn('tenant_id');
|
||||
});
|
||||
|
||||
Schema::table('work_order_bending_details', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_work_order_bending_details_tenant');
|
||||
$table->dropColumn('tenant_id');
|
||||
});
|
||||
|
||||
Schema::table('work_order_issues', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_work_order_issues_tenant');
|
||||
$table->dropColumn('tenant_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 작업지시 담당자 피벗 테이블 (Work Order Assignees)
|
||||
* - 다중 담당자 지원
|
||||
* - 주 담당자 구분 (is_primary)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('work_order_assignees', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->unsignedBigInteger('work_order_id')->comment('작업지시ID');
|
||||
$table->unsignedBigInteger('user_id')->comment('담당자ID');
|
||||
$table->boolean('is_primary')->default(false)->comment('주담당자 여부');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->unique(['work_order_id', 'user_id'], 'uq_work_order_assignees');
|
||||
$table->index(['tenant_id', 'work_order_id'], 'idx_wo_assignees_tenant_wo');
|
||||
$table->index(['tenant_id', 'user_id'], 'idx_wo_assignees_tenant_user');
|
||||
|
||||
// Foreign keys
|
||||
$table->foreign('work_order_id')
|
||||
->references('id')
|
||||
->on('work_orders')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('work_order_assignees');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user