Compare commits
37 Commits
ad93743bdc
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| e061faadc2 | |||
| 30c2484440 | |||
| 334e39d2de | |||
| e372b9543b | |||
|
|
4e192d1c00 | ||
|
|
6d1925fcd1 | ||
| 22f72f1bbc | |||
|
|
6f0ad1cf2d | ||
|
|
d8f2361c88 | ||
| 74e3c21ee0 | |||
| 45a207d4a8 | |||
| 3fc5f511bc | |||
|
|
ee9f4d0b8f | ||
|
|
ca259ccb18 | ||
|
|
3929c5fd1e | ||
|
|
56c60ec3df | ||
|
|
60c4256bd0 | ||
|
|
1861f4daf2 | ||
|
|
c62e59ad17 | ||
|
|
e6f13e3870 | ||
|
|
1d5d161e05 | ||
|
|
0044779eb4 | ||
| 3ac64d5b76 | |||
| 2231c9a48f | |||
| ff8553055c | |||
| f2eede6e3a | |||
| c5d5b5d076 | |||
| 5ebf940873 | |||
| 293330c418 | |||
| a845f52fc0 | |||
| 0f26ea546a | |||
| 3600c7b12b | |||
| a6e29bc1f3 | |||
| 0aa0a8592d | |||
| 38c2402771 | |||
| 59d13eeb9f | |||
| 2df8ecf765 |
@@ -54,4 +54,4 @@ ## 관련 파일
|
||||
|
||||
- `api/app/Services/ComprehensiveAnalysisService.php`
|
||||
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
||||
- `docs/plans/react-mock-remaining-tasks.md`
|
||||
- `docs/dev/dev_plans/react-mock-remaining-tasks.md`
|
||||
|
||||
@@ -15,7 +15,7 @@ ## Phase 구성
|
||||
- Phase 5: MNG 관리자 패널 (4항목) — mng/ /system/alerts
|
||||
|
||||
## 핵심 파일
|
||||
- 계획 문서: docs/plans/db-backup-system-plan.md
|
||||
- 계획 문서: docs/dev/dev_plans/db-backup-system-plan.md
|
||||
- 개발서버: 114.203.209.83 (SSH: hskwon)
|
||||
- DB: sam (메인) + sam_stat (통계)
|
||||
- Slack 웹훅: api/.env → LOG_SLACK_WEBHOOK_URL (이미 설정됨)
|
||||
|
||||
@@ -16,7 +16,7 @@ ### 생성된 파일
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` | 다건 BOM 산출 FormRequest |
|
||||
| `api/docs/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
| `api/docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일 | 설명 |
|
||||
@@ -93,9 +93,9 @@ ### QuoteCalculationService::calculateBomBulk()
|
||||
- 개별 품목 실패가 전체에 영향 없음 (예외 처리)
|
||||
|
||||
## 관련 문서
|
||||
- 계획 문서: `docs/plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
- 계획 문서: `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/dev/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
|
||||
## 다음 단계
|
||||
- React 프론트엔드에서 `/calculate/bom/bulk` API 연동
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-03-04 22:33:37
|
||||
> **자동 생성**: 2026-03-06 21:25:05
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -753,6 +753,38 @@ ### lot_sales
|
||||
|
||||
- **lot()**: belongsTo → `lots`
|
||||
|
||||
### performance_reports
|
||||
**모델**: `App\Models\Qualitys\PerformanceReport`
|
||||
|
||||
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||
- **confirmer()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### quality_documents
|
||||
**모델**: `App\Models\Qualitys\QualityDocument`
|
||||
|
||||
- **client()**: belongsTo → `clients`
|
||||
- **inspector()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **documentOrders()**: hasMany → `quality_document_orders`
|
||||
- **locations()**: hasMany → `quality_document_locations`
|
||||
- **performanceReport()**: hasOne → `performance_reports`
|
||||
|
||||
### quality_document_locations
|
||||
**모델**: `App\Models\Qualitys\QualityDocumentLocation`
|
||||
|
||||
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||
- **qualityDocumentOrder()**: belongsTo → `quality_document_orders`
|
||||
- **orderItem()**: belongsTo → `order_items`
|
||||
- **document()**: belongsTo → `documents`
|
||||
|
||||
### quality_document_orders
|
||||
**모델**: `App\Models\Qualitys\QualityDocumentOrder`
|
||||
|
||||
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||
- **order()**: belongsTo → `orders`
|
||||
- **locations()**: hasMany → `quality_document_locations`
|
||||
|
||||
### quotes
|
||||
**모델**: `App\Models\Quote\Quote`
|
||||
|
||||
@@ -929,6 +961,16 @@ ### expense_accounts
|
||||
|
||||
- **vendor()**: belongsTo → `clients`
|
||||
|
||||
### journal_entrys
|
||||
**모델**: `App\Models\Tenants\JournalEntry`
|
||||
|
||||
- **lines()**: hasMany → `journal_entry_lines`
|
||||
|
||||
### journal_entry_lines
|
||||
**모델**: `App\Models\Tenants\JournalEntryLine`
|
||||
|
||||
- **journalEntry()**: belongsTo → `journal_entries`
|
||||
|
||||
### leaves
|
||||
**모델**: `App\Models\Tenants\Leave`
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* - 두께 매핑 (normalizeThickness)
|
||||
* - 면적 계산 (calculateArea)
|
||||
*
|
||||
* @see docs/plans/5130-sam-data-migration-plan.md 섹션 4.5
|
||||
* @see docs/dev_plans/5130-sam-data-migration-plan.md 섹션 4.5
|
||||
*/
|
||||
class Legacy5130Calculator
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\AccountSubject\StoreAccountSubjectRequest;
|
||||
use App\Http\Requests\V1\AccountSubject\UpdateAccountSubjectRequest;
|
||||
use App\Services\AccountCodeService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -19,7 +20,10 @@ public function __construct(
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only(['search', 'category']);
|
||||
$params = $request->only([
|
||||
'search', 'category', 'sub_category',
|
||||
'department_type', 'depth', 'is_active', 'selectable',
|
||||
]);
|
||||
|
||||
$subjects = $this->service->index($params);
|
||||
|
||||
@@ -36,6 +40,16 @@ public function store(StoreAccountSubjectRequest $request)
|
||||
return ApiResponse::success($subject, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 수정
|
||||
*/
|
||||
public function update(int $id, UpdateAccountSubjectRequest $request)
|
||||
{
|
||||
$subject = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($subject, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 활성/비활성 토글
|
||||
*/
|
||||
@@ -57,4 +71,17 @@ public function destroy(int $id)
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 계정과목표 일괄 생성 (더존 표준)
|
||||
*/
|
||||
public function seedDefaults()
|
||||
{
|
||||
$count = $this->service->seedDefaults();
|
||||
|
||||
return ApiResponse::success(
|
||||
['inserted_count' => $count],
|
||||
__('message.created')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
112
app/Http/Controllers/Api/V1/AuditChecklistController.php
Normal file
112
app/Http/Controllers/Api/V1/AuditChecklistController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Qms\AuditChecklistStoreRequest;
|
||||
use App\Http\Requests\Qms\AuditChecklistUpdateRequest;
|
||||
use App\Services\AuditChecklistService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuditChecklistController extends Controller
|
||||
{
|
||||
public function __construct(private AuditChecklistService $service) {}
|
||||
|
||||
/**
|
||||
* 점검표 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 생성 (카테고리+항목 일괄)
|
||||
*/
|
||||
public function store(AuditChecklistStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 수정
|
||||
*/
|
||||
public function update(AuditChecklistUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 완료 처리
|
||||
*/
|
||||
public function complete(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->complete($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 완료/미완료 토글
|
||||
*/
|
||||
public function toggleItem(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->toggleItem($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목별 기준 문서 조회
|
||||
*/
|
||||
public function itemDocuments(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->itemDocuments($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결
|
||||
*/
|
||||
public function attachDocument(Request $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->attachDocument($id, $request->validate([
|
||||
'title' => 'required|string|max:200',
|
||||
'version' => 'nullable|string|max:20',
|
||||
'date' => 'nullable|date',
|
||||
'document_id' => 'nullable|integer|exists:documents,id',
|
||||
]));
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결 해제
|
||||
*/
|
||||
public function detachDocument(int $id, int $docId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $docId) {
|
||||
$this->service->detachDocument($id, $docId);
|
||||
|
||||
return null;
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,9 @@ public function __construct(
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$setting = $this->barobillService->getSetting();
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $setting,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->barobillService->getSetting();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,12 +28,9 @@ public function show()
|
||||
*/
|
||||
public function save(SaveBarobillSettingRequest $request)
|
||||
{
|
||||
$setting = $this->barobillService->saveSetting($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $setting,
|
||||
message: __('message.saved')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->barobillService->saveSetting($request->validated());
|
||||
}, __('message.saved'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,11 +38,8 @@ public function save(SaveBarobillSettingRequest $request)
|
||||
*/
|
||||
public function testConnection()
|
||||
{
|
||||
$result = $this->barobillService->testConnection();
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $result,
|
||||
message: __('message.barobill.connection_success')
|
||||
);
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->barobillService->testConnection();
|
||||
}, __('message.barobill.connection_success'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\CardTransactionService;
|
||||
use App\Services\JournalSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -14,7 +16,8 @@
|
||||
class CardTransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected CardTransactionService $service
|
||||
protected CardTransactionService $service,
|
||||
protected JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -148,4 +151,105 @@ public function destroy(int $id): JsonResponse
|
||||
return $this->service->destroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "card_{$id}";
|
||||
$data = $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_CARD_TRANSACTION,
|
||||
$sourceKey
|
||||
);
|
||||
|
||||
if (! $data) {
|
||||
return ['items' => []];
|
||||
}
|
||||
|
||||
// 프론트엔드가 기대하는 items 형식으로 변환
|
||||
$items = array_map(fn ($row) => [
|
||||
'id' => $row['id'],
|
||||
'supply_amount' => $row['debit_amount'],
|
||||
'tax_amount' => 0,
|
||||
'account_code' => $row['account_code'],
|
||||
'deduction_type' => 'deductible',
|
||||
'vendor_name' => $row['vendor_name'],
|
||||
'description' => $row['memo'],
|
||||
'memo' => '',
|
||||
], $data['rows']);
|
||||
|
||||
return ['items' => $items];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 저장
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.supply_amount' => 'required|integer|min:0',
|
||||
'items.*.tax_amount' => 'required|integer|min:0',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.deduction_type' => 'nullable|string|max:20',
|
||||
'items.*.vendor_name' => 'nullable|string|max:200',
|
||||
'items.*.description' => 'nullable|string|max:500',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
// 카드 거래 정보 조회 (날짜용)
|
||||
$transaction = $this->service->show($id);
|
||||
if (! $transaction) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
$entryDate = $transaction->used_at
|
||||
? $transaction->used_at->format('Y-m-d')
|
||||
: ($transaction->withdrawal_date?->format('Y-m-d') ?? now()->format('Y-m-d'));
|
||||
|
||||
// items → journal rows 변환 (각 item을 차변 행으로)
|
||||
$rows = [];
|
||||
foreach ($validated['items'] as $item) {
|
||||
$amount = ($item['supply_amount'] ?? 0) + ($item['tax_amount'] ?? 0);
|
||||
$rows[] = [
|
||||
'side' => 'debit',
|
||||
'account_code' => $item['account_code'],
|
||||
'debit_amount' => $amount,
|
||||
'credit_amount' => 0,
|
||||
'vendor_name' => $item['vendor_name'] ?? '',
|
||||
'memo' => $item['description'] ?? $item['memo'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// 대변 합계 행 (카드미지급금)
|
||||
$totalAmount = array_sum(array_column($rows, 'debit_amount'));
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25300', // 미지급금 (표준 코드)
|
||||
'account_name' => '미지급금',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $totalAmount,
|
||||
'vendor_name' => $transaction->merchant_name ?? '',
|
||||
'memo' => '카드결제',
|
||||
];
|
||||
|
||||
$sourceKey = "card_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_CARD_TRANSACTION,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
"카드거래 분개 (#{$id})",
|
||||
$rows,
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,22 @@ public function destroy(int $id): JsonResponse
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* rendered_html 스냅샷 저장 (Lazy Snapshot)
|
||||
* PATCH /v1/documents/{id}/snapshot
|
||||
*/
|
||||
public function patchSnapshot(int $id, UpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
$renderedHtml = $request->validated()['rendered_html'] ?? null;
|
||||
if (! $renderedHtml) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException('rendered_html is required');
|
||||
}
|
||||
|
||||
return $this->service->patchSnapshot($id, $renderedHtml);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FQC 일괄생성 (제품검사)
|
||||
// =========================================================================
|
||||
|
||||
59
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
59
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Quality\PerformanceReportConfirmRequest;
|
||||
use App\Http\Requests\Quality\PerformanceReportMemoRequest;
|
||||
use App\Services\PerformanceReportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PerformanceReportController extends Controller
|
||||
{
|
||||
public function __construct(private PerformanceReportService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function stats(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->stats($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function confirm(PerformanceReportConfirmRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->confirm($request->validated()['ids']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function unconfirm(PerformanceReportConfirmRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->unconfirm($request->validated()['ids']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function updateMemo(PerformanceReportMemoRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$data = $request->validated();
|
||||
|
||||
return $this->service->updateMemo($data['ids'], $data['memo']);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function missing(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->missing($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
50
app/Http/Controllers/Api/V1/ProductionOrderController.php
Normal file
50
app/Http/Controllers/Api/V1/ProductionOrderController.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProductionOrder\ProductionOrderIndexRequest;
|
||||
use App\Services\ProductionOrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ProductionOrderController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductionOrderService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 생산지시 목록 조회
|
||||
*/
|
||||
public function index(ProductionOrderIndexRequest $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->index($request->validated());
|
||||
|
||||
return ApiResponse::success($result, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 상태별 통계
|
||||
*/
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$stats = $this->service->stats();
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 상세 조회
|
||||
*/
|
||||
public function show(int $orderId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$detail = $this->service->show($orderId);
|
||||
|
||||
return ApiResponse::success($detail, __('message.fetched'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.order.not_found'), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/Http/Controllers/Api/V1/QmsLotAuditController.php
Normal file
65
app/Http/Controllers/Api/V1/QmsLotAuditController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Qms\QmsLotAuditConfirmRequest;
|
||||
use App\Http\Requests\Qms\QmsLotAuditDocumentDetailRequest;
|
||||
use App\Http\Requests\Qms\QmsLotAuditIndexRequest;
|
||||
use App\Services\QmsLotAuditService;
|
||||
|
||||
class QmsLotAuditController extends Controller
|
||||
{
|
||||
public function __construct(private QmsLotAuditService $service) {}
|
||||
|
||||
/**
|
||||
* 품질관리서 목록 (로트 추적 심사용)
|
||||
*/
|
||||
public function index(QmsLotAuditIndexRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품질관리서 상세 — 수주/개소 목록
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 루트별 8종 서류 목록
|
||||
*/
|
||||
public function routeDocuments(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->routeDocuments($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 상세 조회 (2단계 로딩)
|
||||
*/
|
||||
public function documentDetail(QmsLotAuditDocumentDetailRequest $request, string $type, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($type, $id) {
|
||||
return $this->service->documentDetail($type, $id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 개소별 로트 심사 확인 토글
|
||||
*/
|
||||
public function confirm(QmsLotAuditConfirmRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->confirm($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
}
|
||||
127
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
127
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Quality\QualityDocumentStoreRequest;
|
||||
use App\Http\Requests\Quality\QualityDocumentUpdateRequest;
|
||||
use App\Services\QualityDocumentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class QualityDocumentController extends Controller
|
||||
{
|
||||
public function __construct(private QualityDocumentService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function stats(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->stats($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function calendar(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->calendar($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function availableOrders(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->availableOrders($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function store(QualityDocumentStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
public function update(QualityDocumentUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->destroy($id);
|
||||
|
||||
return 'success';
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
public function complete(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->complete($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function attachOrders(Request $request, int $id)
|
||||
{
|
||||
$request->validate([
|
||||
'order_ids' => ['required', 'array', 'min:1'],
|
||||
'order_ids.*' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->attachOrders($id, $request->input('order_ids'));
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function detachOrder(int $id, int $orderId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $orderId) {
|
||||
return $this->service->detachOrder($id, $orderId);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function inspectLocation(Request $request, int $id, int $locId)
|
||||
{
|
||||
$request->validate([
|
||||
'post_width' => ['nullable', 'integer'],
|
||||
'post_height' => ['nullable', 'integer'],
|
||||
'change_reason' => ['nullable', 'string', 'max:500'],
|
||||
'inspection_status' => ['nullable', 'string', 'in:pending,completed'],
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request, $id, $locId) {
|
||||
return $this->service->inspectLocation($id, $locId, $request->all());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
public function requestDocument(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->requestDocument($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
public function resultDocument(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->resultDocument($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,17 @@
|
||||
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
||||
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
||||
use App\Http\Requests\V1\TaxInvoice\BulkIssueRequest;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\JournalSyncService;
|
||||
use App\Services\TaxInvoiceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TaxInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private TaxInvoiceService $taxInvoiceService
|
||||
private TaxInvoiceService $taxInvoiceService,
|
||||
private JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -23,12 +28,9 @@ public function __construct(
|
||||
*/
|
||||
public function index(TaxInvoiceListRequest $request)
|
||||
{
|
||||
$taxInvoices = $this->taxInvoiceService->list($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoices,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->list($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,12 +38,9 @@ public function index(TaxInvoiceListRequest $request)
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +48,9 @@ public function show(int $id)
|
||||
*/
|
||||
public function store(CreateTaxInvoiceRequest $request)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->create($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.created'),
|
||||
status: 201
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->create($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +58,9 @@ public function store(CreateTaxInvoiceRequest $request)
|
||||
*/
|
||||
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.updated')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->taxInvoiceService->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,12 +68,11 @@ public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->taxInvoiceService->delete($id);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->taxInvoiceService->delete($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: null,
|
||||
message: __('message.deleted')
|
||||
);
|
||||
return null;
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,12 +80,9 @@ public function destroy(int $id)
|
||||
*/
|
||||
public function issue(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->issue($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.issued')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->issue($id);
|
||||
}, __('message.tax_invoice.issued'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,12 +90,9 @@ public function issue(int $id)
|
||||
*/
|
||||
public function bulkIssue(BulkIssueRequest $request)
|
||||
{
|
||||
$result = $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $result,
|
||||
message: __('message.tax_invoice.bulk_issued')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||
}, __('message.tax_invoice.bulk_issued'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,12 +100,9 @@ public function bulkIssue(BulkIssueRequest $request)
|
||||
*/
|
||||
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.cancelled')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||
}, __('message.tax_invoice.cancelled'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,12 +110,9 @@ public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function checkStatus(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->checkStatus($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->checkStatus($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,11 +120,79 @@ public function checkStatus(int $id)
|
||||
*/
|
||||
public function summary(TaxInvoiceSummaryRequest $request)
|
||||
{
|
||||
$summary = $this->taxInvoiceService->summary($request->validated());
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->summary($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $summary,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
$data = $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey
|
||||
);
|
||||
|
||||
return $data ?? ['rows' => []];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 저장/수정
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'rows' => 'required|array|min:1',
|
||||
'rows.*.side' => 'required|in:debit,credit',
|
||||
'rows.*.account_subject' => 'required|string|max:20',
|
||||
'rows.*.debit_amount' => 'required|integer|min:0',
|
||||
'rows.*.credit_amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
// 세금계산서 정보 조회 (entry_date용)
|
||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||
|
||||
$rows = array_map(fn ($row) => [
|
||||
'side' => $row['side'],
|
||||
'account_code' => $row['account_subject'],
|
||||
'debit_amount' => $row['debit_amount'],
|
||||
'credit_amount' => $row['credit_amount'],
|
||||
], $validated['rows']);
|
||||
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey,
|
||||
$taxInvoice->issue_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
||||
"세금계산서 분개 (#{$id})",
|
||||
$rows,
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 삭제
|
||||
*/
|
||||
public function deleteJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
|
||||
return $this->journalSyncService->deleteForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey
|
||||
);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,14 +61,18 @@ public function detail(Request $request): JsonResponse
|
||||
: 0.05;
|
||||
$year = $request->query('year') ? (int) $request->query('year') : null;
|
||||
$quarter = $request->query('quarter') ? (int) $request->query('quarter') : null;
|
||||
$startDate = $request->query('start_date');
|
||||
$endDate = $request->query('end_date');
|
||||
|
||||
return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter) {
|
||||
return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter, $startDate, $endDate) {
|
||||
return $this->welfareService->getDetail(
|
||||
$calculationType,
|
||||
$fixedAmountPerMonth,
|
||||
$ratio,
|
||||
$year,
|
||||
$quarter
|
||||
$quarter,
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ public function rules(): array
|
||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||
'approvers.*.role' => 'nullable|string|max:50',
|
||||
|
||||
// HTML 스냅샷
|
||||
'rendered_html' => 'nullable|string',
|
||||
|
||||
// 문서 데이터 (EAV)
|
||||
'data' => 'nullable|array',
|
||||
'data.*.section_id' => 'nullable|integer',
|
||||
|
||||
@@ -27,6 +27,9 @@ public function rules(): array
|
||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||
'approvers.*.role' => 'nullable|string|max:50',
|
||||
|
||||
// HTML 스냅샷
|
||||
'rendered_html' => 'nullable|string',
|
||||
|
||||
// 문서 데이터 (EAV)
|
||||
'data' => 'nullable|array',
|
||||
'data.*.section_id' => 'nullable|integer',
|
||||
|
||||
@@ -30,6 +30,9 @@ public function rules(): array
|
||||
'data.*.field_key' => 'required_with:data|string|max:100',
|
||||
'data.*.field_value' => 'nullable|string',
|
||||
|
||||
// HTML 스냅샷
|
||||
'rendered_html' => 'nullable|string',
|
||||
|
||||
// 첨부파일
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*.file_id' => 'required_with:attachments|integer|exists:files,id',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ProductionOrder;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProductionOrderIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => 'nullable|string|max:100',
|
||||
'production_status' => 'nullable|in:waiting,in_production,completed',
|
||||
'sort_by' => 'nullable|in:created_at,delivery_date,order_no',
|
||||
'sort_dir' => 'nullable|in:asc,desc',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Qms/AuditChecklistStoreRequest.php
Normal file
39
app/Http/Requests/Qms/AuditChecklistStoreRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuditChecklistStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'required|integer|min:2020|max:2100',
|
||||
'quarter' => 'required|integer|in:1,2,3,4',
|
||||
'type' => 'nullable|string|max:30',
|
||||
'categories' => 'required|array|min:1',
|
||||
'categories.*.title' => 'required|string|max:200',
|
||||
'categories.*.sort_order' => 'nullable|integer|min:0',
|
||||
'categories.*.items' => 'required|array|min:1',
|
||||
'categories.*.items.*.name' => 'required|string|max:200',
|
||||
'categories.*.items.*.description' => 'nullable|string',
|
||||
'categories.*.items.*.sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'categories.required' => __('validation.required', ['attribute' => '카테고리']),
|
||||
'categories.*.title.required' => __('validation.required', ['attribute' => '카테고리 제목']),
|
||||
'categories.*.items.required' => __('validation.required', ['attribute' => '점검 항목']),
|
||||
'categories.*.items.*.name.required' => __('validation.required', ['attribute' => '항목명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Qms/AuditChecklistUpdateRequest.php
Normal file
28
app/Http/Requests/Qms/AuditChecklistUpdateRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuditChecklistUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'categories' => 'sometimes|array|min:1',
|
||||
'categories.*.id' => 'nullable|integer|exists:audit_checklist_categories,id',
|
||||
'categories.*.title' => 'required|string|max:200',
|
||||
'categories.*.sort_order' => 'nullable|integer|min:0',
|
||||
'categories.*.items' => 'required|array|min:1',
|
||||
'categories.*.items.*.id' => 'nullable|integer|exists:audit_checklist_items,id',
|
||||
'categories.*.items.*.name' => 'required|string|max:200',
|
||||
'categories.*.items.*.description' => 'nullable|string',
|
||||
'categories.*.items.*.sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php
Normal file
28
app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditConfirmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'confirmed' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'confirmed.required' => __('validation.required', ['attribute' => '확인 상태']),
|
||||
'confirmed.boolean' => __('validation.boolean', ['attribute' => '확인 상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/Qms/QmsLotAuditDocumentDetailRequest.php
Normal file
34
app/Http/Requests/Qms/QmsLotAuditDocumentDetailRequest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditDocumentDetailRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'type' => $this->route('type'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'required|string|in:import,order,log,report,confirmation,shipping,product,quality',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'type.in' => __('validation.in', ['attribute' => '서류 타입']),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/Qms/QmsLotAuditIndexRequest.php
Normal file
23
app/Http/Requests/Qms/QmsLotAuditIndexRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'nullable|integer|min:2020|max:2100',
|
||||
'quarter' => 'nullable|integer|in:1,2,3,4',
|
||||
'q' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PerformanceReportConfirmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||
'ids.min' => __('validation.min.array', ['attribute' => '실적신고 ID', 'min' => 1]),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PerformanceReportMemoRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => ['required', 'array', 'min:1'],
|
||||
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||
'memo' => ['required', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||
'memo.required' => __('validation.required', ['attribute' => '메모']),
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
45
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QualityDocumentStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'site_name' => ['required', 'string', 'max:200'],
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'received_date' => ['nullable', 'date'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.manager' => ['nullable', 'array'],
|
||||
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||
'options.inspection' => ['nullable', 'array'],
|
||||
'options.inspection.request_date' => ['nullable', 'date'],
|
||||
'options.inspection.start_date' => ['nullable', 'date'],
|
||||
'options.inspection.end_date' => ['nullable', 'date'],
|
||||
'options.site_address' => ['nullable', 'array'],
|
||||
'options.construction_site' => ['nullable', 'array'],
|
||||
'options.material_distributor' => ['nullable', 'array'],
|
||||
'options.contractor' => ['nullable', 'array'],
|
||||
'options.supervisor' => ['nullable', 'array'],
|
||||
'order_ids' => ['nullable', 'array'],
|
||||
'order_ids.*' => ['integer', 'exists:orders,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'site_name.required' => __('validation.required', ['attribute' => '현장명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
44
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QualityDocumentUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'site_name' => ['sometimes', 'string', 'max:200'],
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'received_date' => ['nullable', 'date'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.manager' => ['nullable', 'array'],
|
||||
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||
'options.inspection' => ['nullable', 'array'],
|
||||
'options.inspection.request_date' => ['nullable', 'date'],
|
||||
'options.inspection.start_date' => ['nullable', 'date'],
|
||||
'options.inspection.end_date' => ['nullable', 'date'],
|
||||
'options.site_address' => ['nullable', 'array'],
|
||||
'options.construction_site' => ['nullable', 'array'],
|
||||
'options.material_distributor' => ['nullable', 'array'],
|
||||
'options.contractor' => ['nullable', 'array'],
|
||||
'options.supervisor' => ['nullable', 'array'],
|
||||
'order_ids' => ['nullable', 'array'],
|
||||
'order_ids.*' => ['integer', 'exists:orders,id'],
|
||||
'locations' => ['nullable', 'array'],
|
||||
'locations.*.id' => ['required', 'integer'],
|
||||
'locations.*.post_width' => ['nullable', 'integer'],
|
||||
'locations.*.post_height' => ['nullable', 'integer'],
|
||||
'locations.*.change_reason' => ['nullable', 'string', 'max:500'],
|
||||
'locations.*.inspection_data' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,18 @@ public function rules(): array
|
||||
'issue_type' => ['required', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||
'direction' => ['required', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||
|
||||
// 공급자 정보
|
||||
'supplier_corp_num' => ['required', 'string', 'max:20'],
|
||||
'supplier_corp_name' => ['required', 'string', 'max:100'],
|
||||
// 공급자 정보 (매입 시 필수, 매출 시 선택)
|
||||
'supplier_corp_num' => ['required_if:direction,purchases', 'nullable', 'string', 'max:20'],
|
||||
'supplier_corp_name' => ['required_if:direction,purchases', 'nullable', 'string', 'max:100'],
|
||||
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
||||
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
||||
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
||||
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||
|
||||
// 공급받는자 정보
|
||||
'buyer_corp_num' => ['required', 'string', 'max:20'],
|
||||
'buyer_corp_name' => ['required', 'string', 'max:100'],
|
||||
// 공급받는자 정보 (매출 시 필수, 매입 시 선택)
|
||||
'buyer_corp_num' => ['required_if:direction,sales', 'nullable', 'string', 'max:20'],
|
||||
'buyer_corp_name' => ['required_if:direction,sales', 'nullable', 'string', 'max:100'],
|
||||
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
||||
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
||||
|
||||
@@ -17,6 +17,12 @@ public function rules(): array
|
||||
'code' => ['required', 'string', 'max:10'],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
||||
'sub_category' => ['nullable', 'string', 'max:50'],
|
||||
'parent_code' => ['nullable', 'string', 'max:10'],
|
||||
'depth' => ['nullable', 'integer', 'in:1,2,3'],
|
||||
'department_type' => ['nullable', 'string', 'in:common,manufacturing,admin'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'sort_order' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,6 +32,8 @@ public function messages(): array
|
||||
'code.required' => '계정과목 코드를 입력하세요.',
|
||||
'name.required' => '계정과목명을 입력하세요.',
|
||||
'category.in' => '유효한 분류를 선택하세요.',
|
||||
'depth.in' => '계층은 1(대), 2(중), 3(소) 중 하나여야 합니다.',
|
||||
'department_type.in' => '부문은 common, manufacturing, admin 중 하나여야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\AccountSubject;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAccountSubjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
||||
'sub_category' => ['nullable', 'string', 'max:50'],
|
||||
'parent_code' => ['nullable', 'string', 'max:10'],
|
||||
'depth' => ['nullable', 'integer', 'in:1,2,3'],
|
||||
'department_type' => ['nullable', 'string', 'in:common,manufacturing,admin'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'sort_order' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'category.in' => '유효한 분류를 선택하세요.',
|
||||
'depth.in' => '계층은 1(대), 2(중), 3(소) 중 하나여야 합니다.',
|
||||
'department_type.in' => '부문은 common, manufacturing, admin 중 하나여야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ class Document extends Model
|
||||
'linkable_id',
|
||||
'submitted_at',
|
||||
'completed_at',
|
||||
'rendered_html',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
|
||||
@@ -22,6 +22,7 @@ class DocumentTemplateSection extends Model
|
||||
protected $fillable = [
|
||||
'template_id',
|
||||
'title',
|
||||
'description',
|
||||
'image_path',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
57
app/Models/Qualitys/AuditChecklist.php
Normal file
57
app/Models/Qualitys/AuditChecklist.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class AuditChecklist extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'audit_checklists';
|
||||
|
||||
const STATUS_DRAFT = 'draft';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
const TYPE_STANDARD_MANUAL = 'standard_manual';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'year',
|
||||
'quarter',
|
||||
'type',
|
||||
'status',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'year' => 'integer',
|
||||
'quarter' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function categories(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditChecklistCategory::class, 'checklist_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
}
|
||||
35
app/Models/Qualitys/AuditChecklistCategory.php
Normal file
35
app/Models/Qualitys/AuditChecklistCategory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AuditChecklistCategory extends Model
|
||||
{
|
||||
protected $table = 'audit_checklist_categories';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'checklist_id',
|
||||
'title',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function checklist(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklist::class, 'checklist_id');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditChecklistItem::class, 'category_id')->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
47
app/Models/Qualitys/AuditChecklistItem.php
Normal file
47
app/Models/Qualitys/AuditChecklistItem.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AuditChecklistItem extends Model
|
||||
{
|
||||
protected $table = 'audit_checklist_items';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'category_id',
|
||||
'name',
|
||||
'description',
|
||||
'is_completed',
|
||||
'completed_at',
|
||||
'completed_by',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_completed' => 'boolean',
|
||||
'completed_at' => 'datetime',
|
||||
'sort_order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklistCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function completedByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'completed_by');
|
||||
}
|
||||
|
||||
public function standardDocuments(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditStandardDocument::class, 'checklist_item_id');
|
||||
}
|
||||
}
|
||||
37
app/Models/Qualitys/AuditStandardDocument.php
Normal file
37
app/Models/Qualitys/AuditStandardDocument.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AuditStandardDocument extends Model
|
||||
{
|
||||
protected $table = 'audit_standard_documents';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'checklist_item_id',
|
||||
'title',
|
||||
'version',
|
||||
'date',
|
||||
'document_id',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function checklistItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklistItem::class, 'checklist_item_id');
|
||||
}
|
||||
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
}
|
||||
76
app/Models/Qualitys/PerformanceReport.php
Normal file
76
app/Models/Qualitys/PerformanceReport.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PerformanceReport extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'performance_reports';
|
||||
|
||||
const STATUS_UNCONFIRMED = 'unconfirmed';
|
||||
|
||||
const STATUS_CONFIRMED = 'confirmed';
|
||||
|
||||
const STATUS_REPORTED = 'reported';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'quality_document_id',
|
||||
'year',
|
||||
'quarter',
|
||||
'confirmation_status',
|
||||
'confirmed_date',
|
||||
'confirmed_by',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'confirmed_date' => 'date',
|
||||
'year' => 'integer',
|
||||
'quarter' => 'integer',
|
||||
];
|
||||
|
||||
// ===== Relationships =====
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function confirmer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'confirmed_by');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// ===== Status Helpers =====
|
||||
|
||||
public function isUnconfirmed(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_UNCONFIRMED;
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_CONFIRMED;
|
||||
}
|
||||
|
||||
public function isReported(): bool
|
||||
{
|
||||
return $this->confirmation_status === self::STATUS_REPORTED;
|
||||
}
|
||||
}
|
||||
131
app/Models/Qualitys/QualityDocument.php
Normal file
131
app/Models/Qualitys/QualityDocument.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class QualityDocument extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'quality_documents';
|
||||
|
||||
const STATUS_RECEIVED = 'received';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'quality_doc_number',
|
||||
'site_name',
|
||||
'status',
|
||||
'client_id',
|
||||
'inspector_id',
|
||||
'received_date',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'options' => 'array',
|
||||
'received_date' => 'date',
|
||||
];
|
||||
|
||||
// ===== Relationships =====
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id');
|
||||
}
|
||||
|
||||
public function inspector()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'inspector_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function documentOrders()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentOrder::class);
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentLocation::class);
|
||||
}
|
||||
|
||||
public function performanceReport()
|
||||
{
|
||||
return $this->hasOne(PerformanceReport::class);
|
||||
}
|
||||
|
||||
// ===== 채번 =====
|
||||
|
||||
public static function generateDocNumber(int $tenantId): string
|
||||
{
|
||||
$prefix = 'KD-QD';
|
||||
$yearMonth = now()->format('Ym');
|
||||
|
||||
$lastNo = static::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('quality_doc_number', 'like', "{$prefix}-{$yearMonth}-%")
|
||||
->orderByDesc('quality_doc_number')
|
||||
->value('quality_doc_number');
|
||||
|
||||
if ($lastNo) {
|
||||
$seq = (int) substr($lastNo, -4) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
return sprintf('%s-%s-%04d', $prefix, $yearMonth, $seq);
|
||||
}
|
||||
|
||||
// ===== Status Helpers =====
|
||||
|
||||
public function isReceived(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_RECEIVED;
|
||||
}
|
||||
|
||||
public function isInProgress(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
public static function mapStatusToFrontend(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
self::STATUS_RECEIVED => 'reception',
|
||||
self::STATUS_IN_PROGRESS => 'in_progress',
|
||||
self::STATUS_COMPLETED => 'completed',
|
||||
default => $status,
|
||||
};
|
||||
}
|
||||
|
||||
public static function mapStatusFromFrontend(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'reception' => self::STATUS_RECEIVED,
|
||||
default => $status,
|
||||
};
|
||||
}
|
||||
}
|
||||
66
app/Models/Qualitys/QualityDocumentLocation.php
Normal file
66
app/Models/Qualitys/QualityDocumentLocation.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use App\Models\Orders\OrderItem;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class QualityDocumentLocation extends Model
|
||||
{
|
||||
protected $table = 'quality_document_locations';
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
protected $fillable = [
|
||||
'quality_document_id',
|
||||
'quality_document_order_id',
|
||||
'order_item_id',
|
||||
'post_width',
|
||||
'post_height',
|
||||
'change_reason',
|
||||
'inspection_data',
|
||||
'document_id',
|
||||
'inspection_status',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'inspection_data' => 'array',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function qualityDocumentOrder()
|
||||
{
|
||||
return $this->belongsTo(QualityDocumentOrder::class);
|
||||
}
|
||||
|
||||
public function orderItem()
|
||||
{
|
||||
return $this->belongsTo(OrderItem::class);
|
||||
}
|
||||
|
||||
public function document()
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->inspection_status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->inspection_status === self::STATUS_COMPLETED;
|
||||
}
|
||||
}
|
||||
31
app/Models/Qualitys/QualityDocumentOrder.php
Normal file
31
app/Models/Qualitys/QualityDocumentOrder.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class QualityDocumentOrder extends Model
|
||||
{
|
||||
protected $table = 'quality_document_orders';
|
||||
|
||||
protected $fillable = [
|
||||
'quality_document_id',
|
||||
'order_id',
|
||||
];
|
||||
|
||||
public function qualityDocument()
|
||||
{
|
||||
return $this->belongsTo(QualityDocument::class);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(QualityDocumentLocation::class);
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,22 @@ class AccountCode extends Model
|
||||
'code',
|
||||
'name',
|
||||
'category',
|
||||
'sub_category',
|
||||
'parent_code',
|
||||
'depth',
|
||||
'department_type',
|
||||
'description',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'depth' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// Categories
|
||||
// Categories (대분류)
|
||||
public const CATEGORY_ASSET = 'asset';
|
||||
public const CATEGORY_LIABILITY = 'liability';
|
||||
public const CATEGORY_CAPITAL = 'capital';
|
||||
@@ -39,6 +45,36 @@ class AccountCode extends Model
|
||||
self::CATEGORY_EXPENSE => '비용',
|
||||
];
|
||||
|
||||
// Sub-categories (중분류)
|
||||
public const SUB_CATEGORIES = [
|
||||
'current_asset' => '유동자산',
|
||||
'fixed_asset' => '비유동자산',
|
||||
'current_liability' => '유동부채',
|
||||
'long_term_liability' => '비유동부채',
|
||||
'capital' => '자본',
|
||||
'sales_revenue' => '매출',
|
||||
'other_revenue' => '영업외수익',
|
||||
'cogs' => '매출원가',
|
||||
'selling_admin' => '판매비와관리비',
|
||||
'other_expense' => '영업외비용',
|
||||
];
|
||||
|
||||
// Department types (부문)
|
||||
public const DEPT_COMMON = 'common';
|
||||
public const DEPT_MANUFACTURING = 'manufacturing';
|
||||
public const DEPT_ADMIN = 'admin';
|
||||
|
||||
public const DEPARTMENT_TYPES = [
|
||||
self::DEPT_COMMON => '공통',
|
||||
self::DEPT_MANUFACTURING => '제조',
|
||||
self::DEPT_ADMIN => '관리',
|
||||
];
|
||||
|
||||
// Depth levels (계층)
|
||||
public const DEPTH_MAJOR = 1;
|
||||
public const DEPTH_MIDDLE = 2;
|
||||
public const DEPTH_MINOR = 3;
|
||||
|
||||
/**
|
||||
* 활성 계정과목만 조회
|
||||
*/
|
||||
@@ -46,4 +82,21 @@ public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 소분류(입력 가능 계정)만 조회
|
||||
*/
|
||||
public function scopeSelectable(Builder $query): Builder
|
||||
{
|
||||
return $query->where('depth', self::DEPTH_MINOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 계정과목 관계
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_code', 'code')
|
||||
->where('tenant_id', $this->tenant_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class ExpenseAccount extends Model
|
||||
'payment_method',
|
||||
'card_no',
|
||||
'loan_id',
|
||||
'journal_entry_id',
|
||||
'journal_entry_line_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
|
||||
@@ -39,6 +39,8 @@ class JournalEntry extends Model
|
||||
// Source type
|
||||
public const SOURCE_MANUAL = 'manual';
|
||||
public const SOURCE_BANK_TRANSACTION = 'bank_transaction';
|
||||
public const SOURCE_TAX_INVOICE = 'tax_invoice';
|
||||
public const SOURCE_CARD_TRANSACTION = 'card_transaction';
|
||||
|
||||
// Entry type
|
||||
public const TYPE_GENERAL = 'general';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Tenants\AccountCode;
|
||||
use App\Models\Tenants\JournalEntryLine;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class AccountCodeService extends Service
|
||||
@@ -27,12 +28,37 @@ public function index(array $params): array
|
||||
});
|
||||
}
|
||||
|
||||
// 분류 필터
|
||||
// 분류 필터 (대분류)
|
||||
if (! empty($params['category'])) {
|
||||
$query->where('category', $params['category']);
|
||||
}
|
||||
|
||||
return $query->orderBy('sort_order')->orderBy('code')->get()->toArray();
|
||||
// 중분류 필터
|
||||
if (! empty($params['sub_category'])) {
|
||||
$query->where('sub_category', $params['sub_category']);
|
||||
}
|
||||
|
||||
// 부문 필터
|
||||
if (! empty($params['department_type'])) {
|
||||
$query->where('department_type', $params['department_type']);
|
||||
}
|
||||
|
||||
// 계층 필터
|
||||
if (! empty($params['depth'])) {
|
||||
$query->where('depth', (int) $params['depth']);
|
||||
}
|
||||
|
||||
// 활성 상태 필터
|
||||
if (isset($params['is_active'])) {
|
||||
$query->where('is_active', filter_var($params['is_active'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
|
||||
// 선택 가능한 계정만 (소분류만 = Select용)
|
||||
if (! empty($params['selectable'])) {
|
||||
$query->selectable();
|
||||
}
|
||||
|
||||
return $query->orderBy('code')->orderBy('sort_order')->get()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,6 +83,11 @@ public function store(array $data): AccountCode
|
||||
$accountCode->code = $data['code'];
|
||||
$accountCode->name = $data['name'];
|
||||
$accountCode->category = $data['category'] ?? null;
|
||||
$accountCode->sub_category = $data['sub_category'] ?? null;
|
||||
$accountCode->parent_code = $data['parent_code'] ?? null;
|
||||
$accountCode->depth = $data['depth'] ?? AccountCode::DEPTH_MINOR;
|
||||
$accountCode->department_type = $data['department_type'] ?? AccountCode::DEPT_COMMON;
|
||||
$accountCode->description = $data['description'] ?? null;
|
||||
$accountCode->sort_order = $data['sort_order'] ?? 0;
|
||||
$accountCode->is_active = true;
|
||||
$accountCode->save();
|
||||
@@ -64,6 +95,36 @@ public function store(array $data): AccountCode
|
||||
return $accountCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 수정
|
||||
*/
|
||||
public function update(int $id, array $data): AccountCode
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$accountCode = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 코드 변경 시 중복 체크
|
||||
if (isset($data['code']) && $data['code'] !== $accountCode->code) {
|
||||
$exists = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['code'])
|
||||
->where('id', '!=', $id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new BadRequestHttpException(__('error.account_subject.duplicate_code'));
|
||||
}
|
||||
}
|
||||
|
||||
$accountCode->fill($data);
|
||||
$accountCode->save();
|
||||
|
||||
return $accountCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 활성/비활성 토글
|
||||
*/
|
||||
@@ -106,4 +167,242 @@ public function destroy(int $id): bool
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 계정과목표 일괄 생성 (초기 세팅)
|
||||
*/
|
||||
public function seedDefaults(): int
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$defaults = $this->getDefaultAccountCodes();
|
||||
$insertedCount = 0;
|
||||
|
||||
DB::transaction(function () use ($tenantId, $defaults, &$insertedCount) {
|
||||
foreach ($defaults as $item) {
|
||||
$exists = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $item['code'])
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
AccountCode::create(array_merge($item, ['tenant_id' => $tenantId]));
|
||||
$insertedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $insertedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 계정과목표 데이터 (더존 Smart A 표준 기반)
|
||||
*
|
||||
* 코드 체계: 5자리 (10100~99900)
|
||||
* - 10100~24000: 자산
|
||||
* - 25000~31700: 부채
|
||||
* - 33100~38700: 자본
|
||||
* - 40100~41000: 매출
|
||||
* - 50100~53700: 매출원가/제조경비 (제조부문)
|
||||
* - 80100~84800: 판매비와관리비 (관리부문)
|
||||
* - 90100~99900: 영업외수익/비용
|
||||
*
|
||||
* 계층: depth 1(대분류) → depth 2(중분류) → depth 3(소분류=더존 실제코드)
|
||||
*/
|
||||
private function getDefaultAccountCodes(): array
|
||||
{
|
||||
$c = fn ($code, $name, $cat, $sub, $parent, $depth, $dept, $sort) => [
|
||||
'code' => $code, 'name' => $name, 'category' => $cat,
|
||||
'sub_category' => $sub, 'parent_code' => $parent,
|
||||
'depth' => $depth, 'department_type' => $dept, 'sort_order' => $sort,
|
||||
];
|
||||
|
||||
return [
|
||||
// ============================================================
|
||||
// 자산 (Assets)
|
||||
// ============================================================
|
||||
$c('1', '자산', 'asset', null, null, 1, 'common', 100),
|
||||
|
||||
// -- 유동자산 --
|
||||
$c('11', '유동자산', 'asset', 'current_asset', '1', 2, 'common', 110),
|
||||
$c('10100', '현금', 'asset', 'current_asset', '11', 3, 'common', 1010),
|
||||
$c('10200', '당좌예금', 'asset', 'current_asset', '11', 3, 'common', 1020),
|
||||
$c('10300', '보통예금', 'asset', 'current_asset', '11', 3, 'common', 1030),
|
||||
$c('10400', '기타제예금', 'asset', 'current_asset', '11', 3, 'common', 1040),
|
||||
$c('10500', '정기적금', 'asset', 'current_asset', '11', 3, 'common', 1050),
|
||||
$c('10800', '외상매출금', 'asset', 'current_asset', '11', 3, 'common', 1080),
|
||||
$c('10900', '대손충당금(외상매출금)', 'asset', 'current_asset', '11', 3, 'common', 1090),
|
||||
$c('11000', '받을어음', 'asset', 'current_asset', '11', 3, 'common', 1100),
|
||||
$c('11400', '단기대여금', 'asset', 'current_asset', '11', 3, 'common', 1140),
|
||||
$c('11600', '미수수익', 'asset', 'current_asset', '11', 3, 'common', 1160),
|
||||
$c('12000', '미수금', 'asset', 'current_asset', '11', 3, 'common', 1200),
|
||||
$c('12200', '소모품', 'asset', 'current_asset', '11', 3, 'common', 1220),
|
||||
$c('12500', '미환급세금', 'asset', 'current_asset', '11', 3, 'common', 1250),
|
||||
$c('13100', '선급금', 'asset', 'current_asset', '11', 3, 'common', 1310),
|
||||
$c('13300', '선급비용', 'asset', 'current_asset', '11', 3, 'common', 1330),
|
||||
$c('13400', '가지급금', 'asset', 'current_asset', '11', 3, 'common', 1340),
|
||||
$c('13500', '부가세대급금', 'asset', 'current_asset', '11', 3, 'common', 1350),
|
||||
$c('13600', '선납세금', 'asset', 'current_asset', '11', 3, 'common', 1360),
|
||||
$c('14000', '선납법인세', 'asset', 'current_asset', '11', 3, 'common', 1400),
|
||||
|
||||
// -- 재고자산 --
|
||||
$c('12', '재고자산', 'asset', 'current_asset', '1', 2, 'common', 120),
|
||||
$c('14600', '상품', 'asset', 'current_asset', '12', 3, 'common', 1460),
|
||||
$c('15000', '제품', 'asset', 'current_asset', '12', 3, 'common', 1500),
|
||||
$c('15300', '원재료', 'asset', 'current_asset', '12', 3, 'common', 1530),
|
||||
$c('16200', '부재료', 'asset', 'current_asset', '12', 3, 'common', 1620),
|
||||
$c('16700', '저장품', 'asset', 'current_asset', '12', 3, 'common', 1670),
|
||||
$c('16900', '재공품', 'asset', 'current_asset', '12', 3, 'common', 1690),
|
||||
|
||||
// -- 비유동자산 --
|
||||
$c('13', '비유동자산', 'asset', 'fixed_asset', '1', 2, 'common', 130),
|
||||
$c('17600', '장기성예금', 'asset', 'fixed_asset', '13', 3, 'common', 1760),
|
||||
$c('17900', '장기대여금', 'asset', 'fixed_asset', '13', 3, 'common', 1790),
|
||||
$c('18700', '투자부동산', 'asset', 'fixed_asset', '13', 3, 'common', 1870),
|
||||
$c('19200', '단체퇴직보험예치금', 'asset', 'fixed_asset', '13', 3, 'common', 1920),
|
||||
$c('20100', '토지', 'asset', 'fixed_asset', '13', 3, 'common', 2010),
|
||||
$c('20200', '건물', 'asset', 'fixed_asset', '13', 3, 'common', 2020),
|
||||
$c('20300', '감가상각누계액(건물)', 'asset', 'fixed_asset', '13', 3, 'common', 2030),
|
||||
$c('20400', '구축물', 'asset', 'fixed_asset', '13', 3, 'common', 2040),
|
||||
$c('20500', '감가상각누계액(구축물)', 'asset', 'fixed_asset', '13', 3, 'common', 2050),
|
||||
$c('20600', '기계장치', 'asset', 'fixed_asset', '13', 3, 'common', 2060),
|
||||
$c('20700', '감가상각누계액(기계장치)', 'asset', 'fixed_asset', '13', 3, 'common', 2070),
|
||||
$c('20800', '차량운반구', 'asset', 'fixed_asset', '13', 3, 'common', 2080),
|
||||
$c('20900', '감가상각누계액(차량운반구)', 'asset', 'fixed_asset', '13', 3, 'common', 2090),
|
||||
$c('21000', '공구와기구', 'asset', 'fixed_asset', '13', 3, 'common', 2100),
|
||||
$c('21200', '비품', 'asset', 'fixed_asset', '13', 3, 'common', 2120),
|
||||
$c('21300', '건설중인자산', 'asset', 'fixed_asset', '13', 3, 'common', 2130),
|
||||
$c('24000', '소프트웨어', 'asset', 'fixed_asset', '13', 3, 'common', 2400),
|
||||
|
||||
// ============================================================
|
||||
// 부채 (Liabilities)
|
||||
// ============================================================
|
||||
$c('2', '부채', 'liability', null, null, 1, 'common', 200),
|
||||
|
||||
// -- 유동부채 --
|
||||
$c('21', '유동부채', 'liability', 'current_liability', '2', 2, 'common', 210),
|
||||
$c('25100', '외상매입금', 'liability', 'current_liability', '21', 3, 'common', 2510),
|
||||
$c('25200', '지급어음', 'liability', 'current_liability', '21', 3, 'common', 2520),
|
||||
$c('25300', '미지급금', 'liability', 'current_liability', '21', 3, 'common', 2530),
|
||||
$c('25400', '예수금', 'liability', 'current_liability', '21', 3, 'common', 2540),
|
||||
$c('25500', '부가세예수금', 'liability', 'current_liability', '21', 3, 'common', 2550),
|
||||
$c('25900', '선수금', 'liability', 'current_liability', '21', 3, 'common', 2590),
|
||||
$c('26000', '단기차입금', 'liability', 'current_liability', '21', 3, 'common', 2600),
|
||||
$c('26100', '미지급세금', 'liability', 'current_liability', '21', 3, 'common', 2610),
|
||||
$c('26200', '미지급비용', 'liability', 'current_liability', '21', 3, 'common', 2620),
|
||||
$c('26400', '유동성장기차입금', 'liability', 'current_liability', '21', 3, 'common', 2640),
|
||||
$c('26500', '미지급배당금', 'liability', 'current_liability', '21', 3, 'common', 2650),
|
||||
|
||||
// -- 비유동부채 --
|
||||
$c('22', '비유동부채', 'liability', 'long_term_liability', '2', 2, 'common', 220),
|
||||
$c('29300', '장기차입금', 'liability', 'long_term_liability', '22', 3, 'common', 2930),
|
||||
$c('29400', '임대보증금', 'liability', 'long_term_liability', '22', 3, 'common', 2940),
|
||||
$c('29500', '퇴직급여충당부채', 'liability', 'long_term_liability', '22', 3, 'common', 2950),
|
||||
$c('30700', '장기임대보증금', 'liability', 'long_term_liability', '22', 3, 'common', 3070),
|
||||
|
||||
// ============================================================
|
||||
// 자본 (Capital)
|
||||
// ============================================================
|
||||
$c('3', '자본', 'capital', null, null, 1, 'common', 300),
|
||||
|
||||
// -- 자본금 --
|
||||
$c('31', '자본금', 'capital', 'capital', '3', 2, 'common', 310),
|
||||
$c('33100', '자본금', 'capital', 'capital', '31', 3, 'common', 3310),
|
||||
$c('33200', '우선주자본금', 'capital', 'capital', '31', 3, 'common', 3320),
|
||||
|
||||
// -- 잉여금 --
|
||||
$c('32', '잉여금', 'capital', 'capital', '3', 2, 'common', 320),
|
||||
$c('34100', '주식발행초과금', 'capital', 'capital', '32', 3, 'common', 3410),
|
||||
$c('35100', '이익준비금', 'capital', 'capital', '32', 3, 'common', 3510),
|
||||
$c('37500', '이월이익잉여금', 'capital', 'capital', '32', 3, 'common', 3750),
|
||||
$c('37900', '당기순이익', 'capital', 'capital', '32', 3, 'common', 3790),
|
||||
|
||||
// ============================================================
|
||||
// 수익 (Revenue)
|
||||
// ============================================================
|
||||
$c('4', '수익', 'revenue', null, null, 1, 'common', 400),
|
||||
|
||||
// -- 매출 --
|
||||
$c('41', '매출', 'revenue', 'sales_revenue', '4', 2, 'common', 410),
|
||||
$c('40100', '상품매출', 'revenue', 'sales_revenue', '41', 3, 'common', 4010),
|
||||
$c('40400', '제품매출', 'revenue', 'sales_revenue', '41', 3, 'common', 4040),
|
||||
$c('40700', '공사수입금', 'revenue', 'sales_revenue', '41', 3, 'common', 4070),
|
||||
$c('41000', '임대료수입', 'revenue', 'sales_revenue', '41', 3, 'common', 4100),
|
||||
|
||||
// -- 영업외수익 --
|
||||
$c('42', '영업외수익', 'revenue', 'other_revenue', '4', 2, 'common', 420),
|
||||
$c('90100', '이자수익', 'revenue', 'other_revenue', '42', 3, 'common', 9010),
|
||||
$c('90300', '배당금수익', 'revenue', 'other_revenue', '42', 3, 'common', 9030),
|
||||
$c('90400', '수입임대료', 'revenue', 'other_revenue', '42', 3, 'common', 9040),
|
||||
$c('90700', '외환차익', 'revenue', 'other_revenue', '42', 3, 'common', 9070),
|
||||
$c('93000', '잡이익', 'revenue', 'other_revenue', '42', 3, 'common', 9300),
|
||||
|
||||
// ============================================================
|
||||
// 비용 (Expenses)
|
||||
// ============================================================
|
||||
$c('5', '비용', 'expense', null, null, 1, 'common', 500),
|
||||
|
||||
// -- 매출원가/제조원가 (제조부문) --
|
||||
$c('51', '매출원가', 'expense', 'cogs', '5', 2, 'manufacturing', 510),
|
||||
$c('50100', '원재료비', 'expense', 'cogs', '51', 3, 'manufacturing', 5010),
|
||||
$c('50200', '외주가공비', 'expense', 'cogs', '51', 3, 'manufacturing', 5020),
|
||||
$c('50300', '급여(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5030),
|
||||
$c('50400', '임금', 'expense', 'cogs', '51', 3, 'manufacturing', 5040),
|
||||
$c('50500', '상여금(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5050),
|
||||
$c('50800', '퇴직급여(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5080),
|
||||
$c('51100', '복리후생비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5110),
|
||||
$c('51200', '여비교통비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5120),
|
||||
$c('51300', '접대비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5130),
|
||||
$c('51400', '통신비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5140),
|
||||
$c('51600', '전력비', 'expense', 'cogs', '51', 3, 'manufacturing', 5160),
|
||||
$c('51700', '세금과공과금(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5170),
|
||||
$c('51800', '감가상각비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5180),
|
||||
$c('51900', '지급임차료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5190),
|
||||
$c('52000', '수선비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5200),
|
||||
$c('52100', '보험료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5210),
|
||||
$c('52200', '차량유지비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5220),
|
||||
$c('52400', '운반비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5240),
|
||||
$c('53000', '소모품비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5300),
|
||||
$c('53100', '지급수수료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5310),
|
||||
|
||||
// -- 판매비와관리비 (관리부문) --
|
||||
$c('52', '판매비와관리비', 'expense', 'selling_admin', '5', 2, 'admin', 520),
|
||||
$c('80100', '임원급여', 'expense', 'selling_admin', '52', 3, 'admin', 8010),
|
||||
$c('80200', '직원급여', 'expense', 'selling_admin', '52', 3, 'admin', 8020),
|
||||
$c('80300', '상여금', 'expense', 'selling_admin', '52', 3, 'admin', 8030),
|
||||
$c('80600', '퇴직급여', 'expense', 'selling_admin', '52', 3, 'admin', 8060),
|
||||
$c('81100', '복리후생비', 'expense', 'selling_admin', '52', 3, 'admin', 8110),
|
||||
$c('81200', '여비교통비', 'expense', 'selling_admin', '52', 3, 'admin', 8120),
|
||||
$c('81300', '접대비', 'expense', 'selling_admin', '52', 3, 'admin', 8130),
|
||||
$c('81400', '통신비', 'expense', 'selling_admin', '52', 3, 'admin', 8140),
|
||||
$c('81500', '수도광열비', 'expense', 'selling_admin', '52', 3, 'admin', 8150),
|
||||
$c('81700', '세금과공과금', 'expense', 'selling_admin', '52', 3, 'admin', 8170),
|
||||
$c('81800', '감가상각비', 'expense', 'selling_admin', '52', 3, 'admin', 8180),
|
||||
$c('81900', '지급임차료', 'expense', 'selling_admin', '52', 3, 'admin', 8190),
|
||||
$c('82000', '수선비', 'expense', 'selling_admin', '52', 3, 'admin', 8200),
|
||||
$c('82100', '보험료', 'expense', 'selling_admin', '52', 3, 'admin', 8210),
|
||||
$c('82200', '차량유지비', 'expense', 'selling_admin', '52', 3, 'admin', 8220),
|
||||
$c('82300', '경상연구개발비', 'expense', 'selling_admin', '52', 3, 'admin', 8230),
|
||||
$c('82400', '운반비', 'expense', 'selling_admin', '52', 3, 'admin', 8240),
|
||||
$c('82500', '교육훈련비', 'expense', 'selling_admin', '52', 3, 'admin', 8250),
|
||||
$c('82600', '도서인쇄비', 'expense', 'selling_admin', '52', 3, 'admin', 8260),
|
||||
$c('82700', '회의비', 'expense', 'selling_admin', '52', 3, 'admin', 8270),
|
||||
$c('82900', '사무용품비', 'expense', 'selling_admin', '52', 3, 'admin', 8290),
|
||||
$c('83000', '소모품비', 'expense', 'selling_admin', '52', 3, 'admin', 8300),
|
||||
$c('83100', '지급수수료', 'expense', 'selling_admin', '52', 3, 'admin', 8310),
|
||||
$c('83200', '보관료', 'expense', 'selling_admin', '52', 3, 'admin', 8320),
|
||||
$c('83300', '광고선전비', 'expense', 'selling_admin', '52', 3, 'admin', 8330),
|
||||
$c('83500', '대손상각비', 'expense', 'selling_admin', '52', 3, 'admin', 8350),
|
||||
$c('84800', '잡비', 'expense', 'selling_admin', '52', 3, 'admin', 8480),
|
||||
|
||||
// -- 영업외비용 --
|
||||
$c('53', '영업외비용', 'expense', 'other_expense', '5', 2, 'common', 530),
|
||||
$c('93100', '이자비용', 'expense', 'other_expense', '53', 3, 'common', 9310),
|
||||
$c('93200', '외환차손', 'expense', 'other_expense', '53', 3, 'common', 9320),
|
||||
$c('93300', '기부금', 'expense', 'other_expense', '53', 3, 'common', 9330),
|
||||
$c('96000', '잡손실', 'expense', 'other_expense', '53', 3, 'common', 9600),
|
||||
$c('99800', '법인세', 'expense', 'other_expense', '53', 3, 'common', 9980),
|
||||
$c('99900', '소득세등', 'expense', 'other_expense', '53', 3, 'common', 9990),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
392
app/Services/AuditChecklistService.php
Normal file
392
app/Services/AuditChecklistService.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Qualitys\AuditChecklist;
|
||||
use App\Models\Qualitys\AuditChecklistCategory;
|
||||
use App\Models\Qualitys\AuditChecklistItem;
|
||||
use App\Models\Qualitys\AuditStandardDocument;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class AuditChecklistService extends Service
|
||||
{
|
||||
/**
|
||||
* 점검표 목록 (year, quarter 필터)
|
||||
*/
|
||||
public function index(array $params): array
|
||||
{
|
||||
$query = AuditChecklist::with(['categories.items']);
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->where('year', (int) $params['year']);
|
||||
}
|
||||
if (! empty($params['quarter'])) {
|
||||
$query->where('quarter', (int) $params['quarter']);
|
||||
}
|
||||
if (! empty($params['type'])) {
|
||||
$query->where('type', $params['type']);
|
||||
}
|
||||
|
||||
$query->orderByDesc('year')->orderByDesc('quarter');
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$items = $paginated->getCollection()->map(fn ($checklist) => $this->transformListItem($checklist));
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 생성 (카테고리+항목 일괄)
|
||||
*/
|
||||
public function store(array $data): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 중복 체크
|
||||
$exists = AuditChecklist::where('year', $data['year'])
|
||||
->where('quarter', $data['quarter'])
|
||||
->where('type', $data['type'] ?? AuditChecklist::TYPE_STANDARD_MANUAL)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new BadRequestHttpException(__('error.duplicate', ['attribute' => '해당 분기 점검표']));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$checklist = AuditChecklist::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'year' => $data['year'],
|
||||
'quarter' => $data['quarter'],
|
||||
'type' => $data['type'] ?? AuditChecklist::TYPE_STANDARD_MANUAL,
|
||||
'status' => AuditChecklist::STATUS_DRAFT,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$this->syncCategories($checklist, $data['categories'], $tenantId);
|
||||
|
||||
return $this->show($checklist->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 상세 (카테고리→항목→문서 중첩)
|
||||
*/
|
||||
public function show(int $id): array
|
||||
{
|
||||
$checklist = AuditChecklist::with([
|
||||
'categories.items.standardDocuments.document',
|
||||
])->findOrFail($id);
|
||||
|
||||
return $this->transformDetail($checklist);
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 수정
|
||||
*/
|
||||
public function update(int $id, array $data): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$checklist = AuditChecklist::findOrFail($id);
|
||||
|
||||
if ($checklist->isCompleted()) {
|
||||
throw new BadRequestHttpException('완료된 점검표는 수정할 수 없습니다.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($checklist, $data, $tenantId) {
|
||||
$checklist->update([
|
||||
'updated_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
if (isset($data['categories'])) {
|
||||
$this->syncCategories($checklist, $data['categories'], $tenantId);
|
||||
}
|
||||
|
||||
return $this->show($checklist->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 완료 처리
|
||||
*/
|
||||
public function complete(int $id): array
|
||||
{
|
||||
$checklist = AuditChecklist::with('categories.items')->findOrFail($id);
|
||||
|
||||
// 미완료 항목 확인
|
||||
$totalItems = 0;
|
||||
$completedItems = 0;
|
||||
foreach ($checklist->categories as $category) {
|
||||
foreach ($category->items as $item) {
|
||||
$totalItems++;
|
||||
if ($item->is_completed) {
|
||||
$completedItems++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($completedItems < $totalItems) {
|
||||
throw new BadRequestHttpException("미완료 항목이 있습니다. ({$completedItems}/{$totalItems})");
|
||||
}
|
||||
|
||||
$checklist->update([
|
||||
'status' => AuditChecklist::STATUS_COMPLETED,
|
||||
'updated_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
return $this->show($checklist->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 완료/미완료 토글
|
||||
*/
|
||||
public function toggleItem(int $itemId): array
|
||||
{
|
||||
$item = AuditChecklistItem::findOrFail($itemId);
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
DB::transaction(function () use ($item, $userId) {
|
||||
$item->lockForUpdate();
|
||||
|
||||
$newCompleted = ! $item->is_completed;
|
||||
$item->update([
|
||||
'is_completed' => $newCompleted,
|
||||
'completed_at' => $newCompleted ? now() : null,
|
||||
'completed_by' => $newCompleted ? $userId : null,
|
||||
]);
|
||||
|
||||
// 점검표 상태 자동 업데이트: draft → in_progress
|
||||
$category = $item->category;
|
||||
$checklist = $category->checklist;
|
||||
if ($checklist->isDraft()) {
|
||||
$checklist->update([
|
||||
'status' => AuditChecklist::STATUS_IN_PROGRESS,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$item->refresh();
|
||||
|
||||
return [
|
||||
'id' => (string) $item->id,
|
||||
'name' => $item->name,
|
||||
'is_completed' => $item->is_completed,
|
||||
'completed_at' => $item->completed_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목별 기준 문서 조회
|
||||
*/
|
||||
public function itemDocuments(int $itemId): array
|
||||
{
|
||||
$item = AuditChecklistItem::findOrFail($itemId);
|
||||
|
||||
return $item->standardDocuments()->with('document')->get()
|
||||
->map(fn ($doc) => $this->transformStandardDocument($doc))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결
|
||||
*/
|
||||
public function attachDocument(int $itemId, array $data): array
|
||||
{
|
||||
$item = AuditChecklistItem::findOrFail($itemId);
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$doc = AuditStandardDocument::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'checklist_item_id' => $item->id,
|
||||
'title' => $data['title'],
|
||||
'version' => $data['version'] ?? null,
|
||||
'date' => $data['date'] ?? null,
|
||||
'document_id' => $data['document_id'] ?? null,
|
||||
]);
|
||||
|
||||
$doc->load('document');
|
||||
|
||||
return $this->transformStandardDocument($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결 해제
|
||||
*/
|
||||
public function detachDocument(int $itemId, int $docId): void
|
||||
{
|
||||
$doc = AuditStandardDocument::where('checklist_item_id', $itemId)
|
||||
->where('id', $docId)
|
||||
->firstOrFail();
|
||||
|
||||
$doc->delete();
|
||||
}
|
||||
|
||||
// ===== Private: Sync & Transform =====
|
||||
|
||||
private function syncCategories(AuditChecklist $checklist, array $categoriesData, int $tenantId): void
|
||||
{
|
||||
// 기존 카테고리 ID 추적 (삭제 감지용)
|
||||
$existingCategoryIds = $checklist->categories()->pluck('id')->all();
|
||||
$keptCategoryIds = [];
|
||||
|
||||
foreach ($categoriesData as $catIdx => $catData) {
|
||||
if (! empty($catData['id'])) {
|
||||
// 기존 카테고리 업데이트
|
||||
$category = AuditChecklistCategory::findOrFail($catData['id']);
|
||||
$category->update([
|
||||
'title' => $catData['title'],
|
||||
'sort_order' => $catData['sort_order'] ?? $catIdx,
|
||||
]);
|
||||
$keptCategoryIds[] = $category->id;
|
||||
} else {
|
||||
// 새 카테고리 생성
|
||||
$category = AuditChecklistCategory::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'checklist_id' => $checklist->id,
|
||||
'title' => $catData['title'],
|
||||
'sort_order' => $catData['sort_order'] ?? $catIdx,
|
||||
]);
|
||||
$keptCategoryIds[] = $category->id;
|
||||
}
|
||||
|
||||
// 하위 항목 동기화
|
||||
$this->syncItems($category, $catData['items'] ?? [], $tenantId);
|
||||
}
|
||||
|
||||
// 삭제된 카테고리 제거 (cascade로 items도 삭제)
|
||||
$deletedIds = array_diff($existingCategoryIds, $keptCategoryIds);
|
||||
if (! empty($deletedIds)) {
|
||||
AuditChecklistCategory::whereIn('id', $deletedIds)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function syncItems(AuditChecklistCategory $category, array $itemsData, int $tenantId): void
|
||||
{
|
||||
$existingItemIds = $category->items()->pluck('id')->all();
|
||||
$keptItemIds = [];
|
||||
|
||||
foreach ($itemsData as $itemIdx => $itemData) {
|
||||
if (! empty($itemData['id'])) {
|
||||
$item = AuditChecklistItem::findOrFail($itemData['id']);
|
||||
$item->update([
|
||||
'name' => $itemData['name'],
|
||||
'description' => $itemData['description'] ?? null,
|
||||
'sort_order' => $itemData['sort_order'] ?? $itemIdx,
|
||||
]);
|
||||
$keptItemIds[] = $item->id;
|
||||
} else {
|
||||
$item = AuditChecklistItem::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'category_id' => $category->id,
|
||||
'name' => $itemData['name'],
|
||||
'description' => $itemData['description'] ?? null,
|
||||
'sort_order' => $itemData['sort_order'] ?? $itemIdx,
|
||||
]);
|
||||
$keptItemIds[] = $item->id;
|
||||
}
|
||||
}
|
||||
|
||||
$deletedIds = array_diff($existingItemIds, $keptItemIds);
|
||||
if (! empty($deletedIds)) {
|
||||
AuditChecklistItem::whereIn('id', $deletedIds)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function transformListItem(AuditChecklist $checklist): array
|
||||
{
|
||||
$total = 0;
|
||||
$completed = 0;
|
||||
foreach ($checklist->categories as $category) {
|
||||
foreach ($category->items as $item) {
|
||||
$total++;
|
||||
if ($item->is_completed) {
|
||||
$completed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (string) $checklist->id,
|
||||
'year' => $checklist->year,
|
||||
'quarter' => $checklist->quarter,
|
||||
'type' => $checklist->type,
|
||||
'status' => $checklist->status,
|
||||
'progress' => [
|
||||
'completed' => $completed,
|
||||
'total' => $total,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function transformDetail(AuditChecklist $checklist): array
|
||||
{
|
||||
$total = 0;
|
||||
$completed = 0;
|
||||
|
||||
$categories = $checklist->categories->map(function ($category) use (&$total, &$completed) {
|
||||
$subItems = $category->items->map(function ($item) use (&$total, &$completed) {
|
||||
$total++;
|
||||
if ($item->is_completed) {
|
||||
$completed++;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (string) $item->id,
|
||||
'name' => $item->name,
|
||||
'description' => $item->description,
|
||||
'is_completed' => $item->is_completed,
|
||||
'completed_at' => $item->completed_at?->toIso8601String(),
|
||||
'sort_order' => $item->sort_order,
|
||||
'standard_documents' => $item->standardDocuments->map(
|
||||
fn ($doc) => $this->transformStandardDocument($doc)
|
||||
)->all(),
|
||||
];
|
||||
})->all();
|
||||
|
||||
return [
|
||||
'id' => (string) $category->id,
|
||||
'title' => $category->title,
|
||||
'sort_order' => $category->sort_order,
|
||||
'sub_items' => $subItems,
|
||||
];
|
||||
})->all();
|
||||
|
||||
return [
|
||||
'id' => (string) $checklist->id,
|
||||
'year' => $checklist->year,
|
||||
'quarter' => $checklist->quarter,
|
||||
'type' => $checklist->type,
|
||||
'status' => $checklist->status,
|
||||
'progress' => [
|
||||
'completed' => $completed,
|
||||
'total' => $total,
|
||||
],
|
||||
'categories' => $categories,
|
||||
];
|
||||
}
|
||||
|
||||
private function transformStandardDocument(AuditStandardDocument $doc): array
|
||||
{
|
||||
$file = $doc->document;
|
||||
|
||||
return [
|
||||
'id' => (string) $doc->id,
|
||||
'title' => $doc->title,
|
||||
'version' => $doc->version ?? '-',
|
||||
'date' => $doc->date?->toDateString() ?? '',
|
||||
'file_name' => $file?->original_name ?? null,
|
||||
'file_url' => $file ? "/api/v1/documents/{$file->id}/download" : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -86,8 +86,8 @@ public function summary(array $params = []): array
|
||||
|
||||
// is_active=true인 악성채권만 통계
|
||||
$query = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true);
|
||||
->where('bad_debts.tenant_id', $tenantId)
|
||||
->where('bad_debts.is_active', true);
|
||||
|
||||
// 거래처 필터
|
||||
if (! empty($params['client_id'])) {
|
||||
@@ -110,6 +110,9 @@ public function summary(array $params = []): array
|
||||
->distinct('client_id')
|
||||
->count('client_id');
|
||||
|
||||
// per-card sub_label: 각 상태별 최다 금액 거래처명 + 건수
|
||||
$subLabels = $this->buildPerCardSubLabels($query);
|
||||
|
||||
return [
|
||||
'total_amount' => (float) $totalAmount,
|
||||
'collecting_amount' => (float) $collectingAmount,
|
||||
@@ -117,9 +120,56 @@ public function summary(array $params = []): array
|
||||
'recovered_amount' => (float) $recoveredAmount,
|
||||
'bad_debt_amount' => (float) $badDebtAmount,
|
||||
'client_count' => $clientCount,
|
||||
'sub_labels' => $subLabels,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드별 sub_label 생성 (최다 금액 거래처명 + 건수)
|
||||
*/
|
||||
private function buildPerCardSubLabels($baseQuery): array
|
||||
{
|
||||
$result = [];
|
||||
$statusScopes = [
|
||||
'dc1' => null, // 전체 (누적)
|
||||
'dc2' => 'collecting', // 추심중
|
||||
'dc3' => 'legalAction', // 법적조치
|
||||
'dc4' => 'recovered', // 회수완료
|
||||
];
|
||||
|
||||
foreach ($statusScopes as $cardId => $scope) {
|
||||
$q = clone $baseQuery;
|
||||
if ($scope) {
|
||||
$q = $q->$scope();
|
||||
}
|
||||
|
||||
$clientCount = (clone $q)->distinct('client_id')->count('client_id');
|
||||
|
||||
if ($clientCount <= 0) {
|
||||
$result[$cardId] = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$topClient = (clone $q)
|
||||
->join('clients', 'bad_debts.client_id', '=', 'clients.id')
|
||||
->selectRaw('clients.name, SUM(bad_debts.debt_amount) as total_amount')
|
||||
->groupBy('clients.id', 'clients.name')
|
||||
->orderByDesc('total_amount')
|
||||
->first();
|
||||
|
||||
if ($topClient) {
|
||||
$result[$cardId] = $clientCount > 1
|
||||
? $topClient->name.' 외 '.($clientCount - 1).'건'
|
||||
: $topClient->name;
|
||||
} else {
|
||||
$result[$cardId] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 상세 조회
|
||||
*/
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
|
||||
use App\Models\Construction\Contract;
|
||||
use App\Models\Production\WorkOrder;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Tenants\Bill;
|
||||
use App\Models\Tenants\ExpectedExpense;
|
||||
use App\Models\Tenants\Leave;
|
||||
use App\Models\Tenants\Schedule;
|
||||
use App\Models\Tenants\Shipment;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
@@ -16,6 +20,7 @@
|
||||
* - 계약(Contract): 시공 일정
|
||||
* - 휴가(Leave): 직원 휴가 일정
|
||||
* - 일정(Schedule): 본사 공통 일정 + 테넌트 일정 (세금 신고, 공휴일 등)
|
||||
* - 어음(Bill): 어음 만기일 일정
|
||||
*/
|
||||
class CalendarService extends Service
|
||||
{
|
||||
@@ -24,7 +29,7 @@ class CalendarService extends Service
|
||||
*
|
||||
* @param string $startDate 조회 시작일 (Y-m-d)
|
||||
* @param string $endDate 조회 종료일 (Y-m-d)
|
||||
* @param string|null $type 일정 타입 필터 (schedule|order|construction|other|null=전체)
|
||||
* @param string|null $type 일정 타입 필터 (schedule|order|construction|other|bill|expected_expense|delivery|shipment|null=전체)
|
||||
* @param string|null $departmentFilter 부서 필터 (all|department|personal)
|
||||
*/
|
||||
public function getSchedules(
|
||||
@@ -64,6 +69,34 @@ public function getSchedules(
|
||||
);
|
||||
}
|
||||
|
||||
// 어음 만기일
|
||||
if ($type === null || $type === 'bill') {
|
||||
$schedules = $schedules->merge(
|
||||
$this->getBillSchedules($tenantId, $startDate, $endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 매입 결제 예정일
|
||||
if ($type === null || $type === 'expected_expense') {
|
||||
$schedules = $schedules->merge(
|
||||
$this->getExpectedExpenseSchedules($tenantId, $startDate, $endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 수주 납기일
|
||||
if ($type === null || $type === 'delivery') {
|
||||
$schedules = $schedules->merge(
|
||||
$this->getDeliverySchedules($tenantId, $startDate, $endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 출고 예정일
|
||||
if ($type === null || $type === 'shipment') {
|
||||
$schedules = $schedules->merge(
|
||||
$this->getShipmentSchedules($tenantId, $startDate, $endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// startDate 기준 정렬
|
||||
$sortedSchedules = $schedules
|
||||
->sortBy('startDate')
|
||||
@@ -331,4 +364,170 @@ private function getGeneralSchedules(
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 어음 만기일 일정 조회
|
||||
*/
|
||||
private function getBillSchedules(
|
||||
int $tenantId,
|
||||
string $startDate,
|
||||
string $endDate
|
||||
): Collection {
|
||||
$excludedStatuses = [
|
||||
'paymentComplete',
|
||||
'dishonored',
|
||||
];
|
||||
|
||||
$bills = Bill::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotNull('maturity_date')
|
||||
->where('maturity_date', '>=', $startDate)
|
||||
->where('maturity_date', '<=', $endDate)
|
||||
->whereNotIn('status', $excludedStatuses)
|
||||
->orderBy('maturity_date')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $bills->map(function ($bill) {
|
||||
$clientName = $bill->display_client_name ?? $bill->client_name ?? '';
|
||||
|
||||
return [
|
||||
'id' => 'bill_'.$bill->id,
|
||||
'title' => '[만기] '.$clientName.' '.number_format($bill->amount).'원',
|
||||
'startDate' => $bill->maturity_date->format('Y-m-d'),
|
||||
'endDate' => $bill->maturity_date->format('Y-m-d'),
|
||||
'startTime' => null,
|
||||
'endTime' => null,
|
||||
'isAllDay' => true,
|
||||
'type' => 'bill',
|
||||
'department' => null,
|
||||
'personName' => null,
|
||||
'color' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매입 결제 예정일 조회
|
||||
*/
|
||||
private function getExpectedExpenseSchedules(
|
||||
int $tenantId,
|
||||
string $startDate,
|
||||
string $endDate
|
||||
): Collection {
|
||||
$expenses = ExpectedExpense::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotNull('expected_payment_date')
|
||||
->where('expected_payment_date', '>=', $startDate)
|
||||
->where('expected_payment_date', '<=', $endDate)
|
||||
->where('payment_status', '!=', 'paid')
|
||||
->with(['client:id,name'])
|
||||
->orderBy('expected_payment_date')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $expenses->map(function ($expense) {
|
||||
$clientName = $expense->client?->name ?? $expense->client_name ?? '';
|
||||
|
||||
return [
|
||||
'id' => 'expense_'.$expense->id,
|
||||
'title' => '[결제] '.$clientName.' '.number_format($expense->amount).'원',
|
||||
'startDate' => $expense->expected_payment_date->format('Y-m-d'),
|
||||
'endDate' => $expense->expected_payment_date->format('Y-m-d'),
|
||||
'startTime' => null,
|
||||
'endTime' => null,
|
||||
'isAllDay' => true,
|
||||
'type' => 'expected_expense',
|
||||
'department' => null,
|
||||
'personName' => null,
|
||||
'color' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 납기일 조회
|
||||
*/
|
||||
private function getDeliverySchedules(
|
||||
int $tenantId,
|
||||
string $startDate,
|
||||
string $endDate
|
||||
): Collection {
|
||||
$activeStatuses = [
|
||||
'CONFIRMED',
|
||||
'IN_PROGRESS',
|
||||
'IN_PRODUCTION',
|
||||
'PRODUCED',
|
||||
'SHIPPING',
|
||||
];
|
||||
|
||||
$orders = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotNull('delivery_date')
|
||||
->where('delivery_date', '>=', $startDate)
|
||||
->where('delivery_date', '<=', $endDate)
|
||||
->whereIn('status_code', $activeStatuses)
|
||||
->with(['client:id,name'])
|
||||
->orderBy('delivery_date')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $orders->map(function ($order) {
|
||||
$clientName = $order->client?->name ?? $order->client_name ?? '';
|
||||
$siteName = $order->site_name ?? $order->order_no;
|
||||
|
||||
return [
|
||||
'id' => 'delivery_'.$order->id,
|
||||
'title' => '[납기] '.$clientName.' '.$siteName,
|
||||
'startDate' => $order->delivery_date->format('Y-m-d'),
|
||||
'endDate' => $order->delivery_date->format('Y-m-d'),
|
||||
'startTime' => null,
|
||||
'endTime' => null,
|
||||
'isAllDay' => true,
|
||||
'type' => 'delivery',
|
||||
'department' => null,
|
||||
'personName' => null,
|
||||
'color' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출고 예정일 조회
|
||||
*/
|
||||
private function getShipmentSchedules(
|
||||
int $tenantId,
|
||||
string $startDate,
|
||||
string $endDate
|
||||
): Collection {
|
||||
$shipments = Shipment::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotNull('scheduled_date')
|
||||
->where('scheduled_date', '>=', $startDate)
|
||||
->where('scheduled_date', '<=', $endDate)
|
||||
->whereIn('status', ['scheduled', 'ready'])
|
||||
->with(['client:id,name', 'order:id,site_name'])
|
||||
->orderBy('scheduled_date')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $shipments->map(function ($shipment) {
|
||||
$clientName = $shipment->client?->name ?? $shipment->customer_name ?? '';
|
||||
$siteName = $shipment->site_name ?? $shipment->order?->site_name ?? $shipment->shipment_no;
|
||||
|
||||
return [
|
||||
'id' => 'shipment_'.$shipment->id,
|
||||
'title' => '[출고] '.$clientName.' '.$siteName,
|
||||
'startDate' => $shipment->scheduled_date->format('Y-m-d'),
|
||||
'endDate' => $shipment->scheduled_date->format('Y-m-d'),
|
||||
'startTime' => null,
|
||||
'endTime' => null,
|
||||
'isAllDay' => true,
|
||||
'type' => 'shipment',
|
||||
'department' => null,
|
||||
'personName' => null,
|
||||
'color' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ public function create(array $data): Document
|
||||
'status' => Document::STATUS_DRAFT,
|
||||
'linkable_type' => $data['linkable_type'] ?? null,
|
||||
'linkable_id' => $data['linkable_id'] ?? null,
|
||||
'rendered_html' => $data['rendered_html'] ?? null,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
@@ -170,12 +171,16 @@ public function update(int $id, array $data): Document
|
||||
}
|
||||
|
||||
// 기본 정보 수정
|
||||
$document->fill([
|
||||
$updateFields = [
|
||||
'title' => $data['title'] ?? $document->title,
|
||||
'linkable_type' => $data['linkable_type'] ?? $document->linkable_type,
|
||||
'linkable_id' => $data['linkable_id'] ?? $document->linkable_id,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
];
|
||||
if (isset($data['rendered_html'])) {
|
||||
$updateFields['rendered_html'] = $data['rendered_html'];
|
||||
}
|
||||
$document->fill($updateFields);
|
||||
|
||||
// 반려 상태에서 수정 시 DRAFT로 변경
|
||||
if ($document->status === Document::STATUS_REJECTED) {
|
||||
@@ -658,20 +663,32 @@ public function fqcStatus(int $orderId, int $templateId): array
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$order = \App\Models\Orders\Order::where('tenant_id', $tenantId)
|
||||
->with('items')
|
||||
->with(['rootNodes.items' => fn ($q) => $q->orderBy('sort_order')])
|
||||
->findOrFail($orderId);
|
||||
|
||||
// 해당 수주의 FQC 문서 조회
|
||||
// 개소별 대표 OrderItem ID 수집
|
||||
$representativeItemIds = $order->rootNodes
|
||||
->map(fn ($node) => $node->items->first()?->id)
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
// 해당 대표 품목의 FQC 문서 조회
|
||||
$documents = Document::where('tenant_id', $tenantId)
|
||||
->where('template_id', $templateId)
|
||||
->where('linkable_type', \App\Models\Orders\OrderItem::class)
|
||||
->whereIn('linkable_id', $order->items->pluck('id'))
|
||||
->whereIn('linkable_id', $representativeItemIds)
|
||||
->with('data')
|
||||
->get()
|
||||
->keyBy('linkable_id');
|
||||
|
||||
$items = $order->items->map(function ($orderItem) use ($documents) {
|
||||
$doc = $documents->get($orderItem->id);
|
||||
// 개소(root node)별 진행현황
|
||||
$items = $order->rootNodes->map(function ($node) use ($documents) {
|
||||
$representativeItem = $node->items->first();
|
||||
if (! $representativeItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$doc = $documents->get($representativeItem->id);
|
||||
|
||||
// 종합판정 값 추출
|
||||
$judgement = null;
|
||||
@@ -681,17 +698,17 @@ public function fqcStatus(int $orderId, int $templateId): array
|
||||
}
|
||||
|
||||
return [
|
||||
'order_item_id' => $orderItem->id,
|
||||
'floor_code' => $orderItem->floor_code,
|
||||
'symbol_code' => $orderItem->symbol_code,
|
||||
'specification' => $orderItem->specification,
|
||||
'item_name' => $orderItem->item_name,
|
||||
'order_item_id' => $representativeItem->id,
|
||||
'floor_code' => $representativeItem->floor_code,
|
||||
'symbol_code' => $representativeItem->symbol_code,
|
||||
'specification' => $representativeItem->specification,
|
||||
'item_name' => $representativeItem->item_name,
|
||||
'document_id' => $doc?->id,
|
||||
'document_no' => $doc?->document_no,
|
||||
'status' => $doc?->status ?? 'NONE',
|
||||
'judgement' => $judgement,
|
||||
];
|
||||
});
|
||||
})->filter()->values();
|
||||
|
||||
// 통계
|
||||
$total = $items->count();
|
||||
@@ -713,6 +730,28 @@ public function fqcStatus(int $orderId, int $templateId): array
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Snapshot (Lazy Snapshot)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* rendered_html만 업데이트 (상태 무관, canEdit 체크 없음)
|
||||
* Lazy Snapshot: 조회 시 rendered_html이 없으면 프론트에서 캡처 후 저장
|
||||
*/
|
||||
public function patchSnapshot(int $id, string $renderedHtml): Document
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$document = Document::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$document->rendered_html = $renderedHtml;
|
||||
$document->save();
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Resolve/Upsert (React 연동용)
|
||||
// =========================================================================
|
||||
@@ -852,24 +891,34 @@ public function upsert(array $data): Document
|
||||
|
||||
if ($existingDocument) {
|
||||
// UPDATE: 기존 update 로직 재사용
|
||||
return $this->update($existingDocument->id, [
|
||||
$updatePayload = [
|
||||
'title' => $data['title'] ?? $existingDocument->title,
|
||||
'linkable_type' => 'item',
|
||||
'linkable_id' => $itemId,
|
||||
'data' => $data['data'] ?? [],
|
||||
'attachments' => $data['attachments'] ?? [],
|
||||
]);
|
||||
];
|
||||
if (isset($data['rendered_html'])) {
|
||||
$updatePayload['rendered_html'] = $data['rendered_html'];
|
||||
}
|
||||
|
||||
return $this->update($existingDocument->id, $updatePayload);
|
||||
}
|
||||
|
||||
// CREATE: 기존 create 로직 재사용
|
||||
return $this->create([
|
||||
$createPayload = [
|
||||
'template_id' => $templateId,
|
||||
'title' => $data['title'] ?? '',
|
||||
'linkable_type' => 'item',
|
||||
'linkable_id' => $itemId,
|
||||
'data' => $data['data'] ?? [],
|
||||
'attachments' => $data['attachments'] ?? [],
|
||||
]);
|
||||
];
|
||||
if (isset($data['rendered_html'])) {
|
||||
$createPayload['rendered_html'] = $data['rendered_html'];
|
||||
}
|
||||
|
||||
return $this->create($createPayload);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
use App\Models\Tenants\AccountCode;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Models\Tenants\JournalEntryLine;
|
||||
use App\Traits\SyncsExpenseAccounts;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class GeneralJournalEntryService extends Service
|
||||
{
|
||||
use SyncsExpenseAccounts;
|
||||
/**
|
||||
* 일반전표입력 통합 목록 (입금 + 출금 + 수기전표)
|
||||
* deposits/withdrawals는 계좌이체 건만, LEFT JOIN journal_entries로 분개 여부 표시
|
||||
@@ -326,6 +328,9 @@ public function store(array $data): JournalEntry
|
||||
// 분개 행 생성
|
||||
$this->createLines($entry, $data['rows'], $tenantId);
|
||||
|
||||
// expense_accounts 동기화 (복리후생비/접대비 → CEO 대시보드)
|
||||
$this->syncExpenseAccounts($entry);
|
||||
|
||||
return $entry->load('lines');
|
||||
});
|
||||
}
|
||||
@@ -373,6 +378,9 @@ public function updateJournal(int $id, array $data): JournalEntry
|
||||
|
||||
$entry->save();
|
||||
|
||||
// expense_accounts 동기화 (복리후생비/접대비 → CEO 대시보드)
|
||||
$this->syncExpenseAccounts($entry);
|
||||
|
||||
return $entry->load('lines');
|
||||
});
|
||||
}
|
||||
@@ -389,6 +397,9 @@ public function destroyJournal(int $id): bool
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// expense_accounts 정리 (복리후생비/접대비 → CEO 대시보드)
|
||||
$this->cleanupExpenseAccounts($tenantId, $entry->id);
|
||||
|
||||
// lines 먼저 삭제 (soft delete가 아니므로 물리 삭제)
|
||||
JournalEntryLine::query()
|
||||
->where('journal_entry_id', $entry->id)
|
||||
@@ -503,6 +514,9 @@ private function resolveVendorName(?int $vendorId): string
|
||||
return $vendor ? $vendor->name : '';
|
||||
}
|
||||
|
||||
// syncExpenseAccounts, cleanupExpenseAccounts, getExpenseAccountType
|
||||
// → SyncsExpenseAccounts 트레이트로 이관
|
||||
|
||||
/**
|
||||
* 원본 거래 정보 조회 (입금/출금)
|
||||
*/
|
||||
|
||||
214
app/Services/JournalSyncService.php
Normal file
214
app/Services/JournalSyncService.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\AccountCode;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Models\Tenants\JournalEntryLine;
|
||||
use App\Traits\SyncsExpenseAccounts;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 세금계산서/카드거래 등 외부 소스의 분개 통합 관리 서비스
|
||||
*
|
||||
* journal_entries + journal_entry_lines에 저장하고
|
||||
* 복리후생비/접대비는 expense_accounts에 동기화 (CEO 대시보드)
|
||||
*/
|
||||
class JournalSyncService extends Service
|
||||
{
|
||||
use SyncsExpenseAccounts;
|
||||
|
||||
/**
|
||||
* 소스에 대한 분개 저장 (생성 또는 교체)
|
||||
*
|
||||
* @param string $sourceType JournalEntry::SOURCE_TAX_INVOICE 등
|
||||
* @param string $sourceKey 'tax_invoice_123' 등
|
||||
* @param string $entryDate 전표일자 (Y-m-d)
|
||||
* @param string|null $description 적요
|
||||
* @param array $rows 분개 행 [{side, account_code, account_name?, debit_amount, credit_amount, vendor_id?, vendor_name?, memo?}]
|
||||
*/
|
||||
public function saveForSource(
|
||||
string $sourceType,
|
||||
string $sourceKey,
|
||||
string $entryDate,
|
||||
?string $description,
|
||||
array $rows,
|
||||
): JournalEntry {
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($sourceType, $sourceKey, $entryDate, $description, $rows, $tenantId) {
|
||||
// 기존 전표가 있으면 삭제 후 재생성 (교체 방식)
|
||||
$existing = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_type', $sourceType)
|
||||
->where('source_key', $sourceKey)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->cleanupExpenseAccounts($tenantId, $existing->id);
|
||||
JournalEntryLine::where('journal_entry_id', $existing->id)->delete();
|
||||
$existing->forceDelete();
|
||||
}
|
||||
|
||||
// 합계 계산
|
||||
$totalDebit = 0;
|
||||
$totalCredit = 0;
|
||||
foreach ($rows as $row) {
|
||||
$totalDebit += (int) ($row['debit_amount'] ?? 0);
|
||||
$totalCredit += (int) ($row['credit_amount'] ?? 0);
|
||||
}
|
||||
|
||||
// 전표번호 생성
|
||||
$entryNo = $this->generateEntryNo($tenantId, $entryDate);
|
||||
|
||||
// 전표 생성
|
||||
$entry = new JournalEntry;
|
||||
$entry->tenant_id = $tenantId;
|
||||
$entry->entry_no = $entryNo;
|
||||
$entry->entry_date = $entryDate;
|
||||
$entry->entry_type = JournalEntry::TYPE_GENERAL;
|
||||
$entry->description = $description;
|
||||
$entry->total_debit = $totalDebit;
|
||||
$entry->total_credit = $totalCredit;
|
||||
$entry->status = JournalEntry::STATUS_CONFIRMED;
|
||||
$entry->source_type = $sourceType;
|
||||
$entry->source_key = $sourceKey;
|
||||
$entry->save();
|
||||
|
||||
// 분개 행 생성
|
||||
foreach ($rows as $index => $row) {
|
||||
$accountCode = $row['account_code'] ?? '';
|
||||
$accountName = $row['account_name'] ?? $this->resolveAccountName($tenantId, $accountCode);
|
||||
|
||||
$line = new JournalEntryLine;
|
||||
$line->tenant_id = $tenantId;
|
||||
$line->journal_entry_id = $entry->id;
|
||||
$line->line_no = $index + 1;
|
||||
$line->dc_type = $row['side'];
|
||||
$line->account_code = $accountCode;
|
||||
$line->account_name = $accountName;
|
||||
$line->trading_partner_id = ! empty($row['vendor_id']) ? (int) $row['vendor_id'] : null;
|
||||
$line->trading_partner_name = $row['vendor_name'] ?? '';
|
||||
$line->debit_amount = (int) ($row['debit_amount'] ?? 0);
|
||||
$line->credit_amount = (int) ($row['credit_amount'] ?? 0);
|
||||
$line->description = $row['memo'] ?? null;
|
||||
$line->save();
|
||||
}
|
||||
|
||||
// expense_accounts 동기화 (복리후생비/접대비 → CEO 대시보드)
|
||||
$this->syncExpenseAccounts($entry);
|
||||
|
||||
return $entry->load('lines');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스에 대한 분개 조회
|
||||
*/
|
||||
public function getForSource(string $sourceType, string $sourceKey): ?array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$entry = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_type', $sourceType)
|
||||
->where('source_key', $sourceKey)
|
||||
->whereNull('deleted_at')
|
||||
->with('lines')
|
||||
->first();
|
||||
|
||||
if (! $entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $entry->id,
|
||||
'entry_no' => $entry->entry_no,
|
||||
'entry_date' => $entry->entry_date->format('Y-m-d'),
|
||||
'description' => $entry->description,
|
||||
'total_debit' => $entry->total_debit,
|
||||
'total_credit' => $entry->total_credit,
|
||||
'rows' => $entry->lines->map(function ($line) {
|
||||
return [
|
||||
'id' => $line->id,
|
||||
'side' => $line->dc_type,
|
||||
'account_code' => $line->account_code,
|
||||
'account_name' => $line->account_name,
|
||||
'vendor_id' => $line->trading_partner_id,
|
||||
'vendor_name' => $line->trading_partner_name ?? '',
|
||||
'debit_amount' => (int) $line->debit_amount,
|
||||
'credit_amount' => (int) $line->credit_amount,
|
||||
'memo' => $line->description ?? '',
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스에 대한 분개 삭제
|
||||
*/
|
||||
public function deleteForSource(string $sourceType, string $sourceKey): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return DB::transaction(function () use ($sourceType, $sourceKey, $tenantId) {
|
||||
$entry = JournalEntry::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('source_type', $sourceType)
|
||||
->where('source_key', $sourceKey)
|
||||
->first();
|
||||
|
||||
if (! $entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->cleanupExpenseAccounts($tenantId, $entry->id);
|
||||
JournalEntryLine::where('journal_entry_id', $entry->id)->delete();
|
||||
$entry->delete(); // soft delete
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표번호 생성: JE-YYYYMMDD-NNN
|
||||
*/
|
||||
private function generateEntryNo(int $tenantId, string $date): string
|
||||
{
|
||||
$dateStr = str_replace('-', '', substr($date, 0, 10));
|
||||
$prefix = "JE-{$dateStr}-";
|
||||
|
||||
$lastEntry = DB::table('journal_entries')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('entry_no', 'like', "{$prefix}%")
|
||||
->lockForUpdate()
|
||||
->orderBy('entry_no', 'desc')
|
||||
->first(['entry_no']);
|
||||
|
||||
if ($lastEntry) {
|
||||
$lastSeq = (int) substr($lastEntry->entry_no, -3);
|
||||
$nextSeq = $lastSeq + 1;
|
||||
} else {
|
||||
$nextSeq = 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($nextSeq, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 코드 → 이름 조회
|
||||
*/
|
||||
private function resolveAccountName(int $tenantId, string $code): string
|
||||
{
|
||||
if (empty($code)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$account = AccountCode::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $code)
|
||||
->first(['name']);
|
||||
|
||||
return $account ? $account->name : $code;
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,7 @@ public static function getUserInfoForLogin(int $userId): array
|
||||
$dept = DB::table('departments')->where('id', $profile->department_id)->first();
|
||||
if ($dept) {
|
||||
$userInfo['department'] = $dept->name;
|
||||
$userInfo['department_id'] = $dept->id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1325,9 +1325,13 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
// 작업지시번호 생성
|
||||
$workOrderNo = $this->generateWorkOrderNo($tenantId);
|
||||
|
||||
// 절곡 공정이면 bending_info 자동 생성
|
||||
// 공정 옵션 초기화 (보조 공정 플래그 포함)
|
||||
$workOrderOptions = null;
|
||||
if ($processId) {
|
||||
$process = \App\Models\Process::find($processId);
|
||||
if ($process && ! empty($process->options['is_auxiliary'])) {
|
||||
$workOrderOptions = ['is_auxiliary' => true];
|
||||
}
|
||||
// 이 작업지시에 포함되는 노드 ID만 추출
|
||||
$nodeIds = collect($items)
|
||||
->pluck('order_node_id')
|
||||
@@ -1338,7 +1342,7 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
|
||||
$buildResult = app(BendingInfoBuilder::class)->build($order, $processId, $nodeIds ?: null);
|
||||
if ($buildResult) {
|
||||
$workOrderOptions = ['bending_info' => $buildResult['bending_info']];
|
||||
$workOrderOptions = array_merge($workOrderOptions ?? [], ['bending_info' => $buildResult['bending_info']]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
258
app/Services/PerformanceReportService.php
Normal file
258
app/Services/PerformanceReportService.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Qualitys\PerformanceReport;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class PerformanceReportService extends Service
|
||||
{
|
||||
private const AUDIT_TARGET = 'performance_report';
|
||||
|
||||
public function __construct(
|
||||
private readonly AuditLogger $auditLogger,
|
||||
private readonly QualityDocumentService $qualityDocumentService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
*/
|
||||
public function index(array $params)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$q = trim((string) ($params['q'] ?? ''));
|
||||
$year = $params['year'] ?? null;
|
||||
$quarter = $params['quarter'] ?? null;
|
||||
$confirmStatus = $params['confirm_status'] ?? null;
|
||||
|
||||
$query = PerformanceReport::query()
|
||||
->where('performance_reports.tenant_id', $tenantId)
|
||||
->with(['qualityDocument.client', 'qualityDocument.locations', 'confirmer:id,name']);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->whereHas('qualityDocument', function ($qq) use ($q) {
|
||||
$qq->where('quality_doc_number', 'like', "%{$q}%")
|
||||
->orWhere('site_name', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($year !== null) {
|
||||
$query->where('year', $year);
|
||||
}
|
||||
if ($quarter !== null) {
|
||||
$query->where('quarter', $quarter);
|
||||
}
|
||||
if ($confirmStatus !== null) {
|
||||
$query->where('confirmation_status', $confirmStatus);
|
||||
}
|
||||
|
||||
$query->orderByDesc('performance_reports.id');
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$transformedData = $paginated->getCollection()->map(fn ($report) => $this->transformToFrontend($report));
|
||||
|
||||
return [
|
||||
'items' => $transformedData,
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(array $params = []): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = PerformanceReport::where('performance_reports.tenant_id', $tenantId);
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->where('performance_reports.year', $params['year']);
|
||||
}
|
||||
if (! empty($params['quarter'])) {
|
||||
$query->where('performance_reports.quarter', $params['quarter']);
|
||||
}
|
||||
|
||||
$counts = (clone $query)
|
||||
->select('confirmation_status', DB::raw('count(*) as count'))
|
||||
->groupBy('confirmation_status')
|
||||
->pluck('count', 'confirmation_status')
|
||||
->toArray();
|
||||
|
||||
$totalLocations = (clone $query)
|
||||
->join('quality_documents', 'quality_documents.id', '=', 'performance_reports.quality_document_id')
|
||||
->join('quality_document_locations', 'quality_document_locations.quality_document_id', '=', 'quality_documents.id')
|
||||
->count('quality_document_locations.id');
|
||||
|
||||
return [
|
||||
'total_count' => array_sum($counts),
|
||||
'confirmed_count' => $counts[PerformanceReport::STATUS_CONFIRMED] ?? 0,
|
||||
'unconfirmed_count' => $counts[PerformanceReport::STATUS_UNCONFIRMED] ?? 0,
|
||||
'total_locations' => $totalLocations,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 확정
|
||||
*/
|
||||
public function confirm(array $ids)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $tenantId, $userId) {
|
||||
$reports = PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->with(['qualityDocument'])
|
||||
->get();
|
||||
|
||||
$errors = [];
|
||||
foreach ($reports as $report) {
|
||||
if ($report->isConfirmed() || $report->isReported()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 필수정보 검증
|
||||
$requiredInfo = $this->qualityDocumentService->calculateRequiredInfo($report->qualityDocument);
|
||||
if ($requiredInfo !== '완료') {
|
||||
$errors[] = [
|
||||
'id' => $report->id,
|
||||
'quality_doc_number' => $report->qualityDocument->quality_doc_number,
|
||||
'reason' => $requiredInfo,
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$report->update([
|
||||
'confirmation_status' => PerformanceReport::STATUS_CONFIRMED,
|
||||
'confirmed_date' => now()->toDateString(),
|
||||
'confirmed_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
throw new BadRequestHttpException(json_encode([
|
||||
'message' => __('error.quality.confirm_failed'),
|
||||
'errors' => $errors,
|
||||
]));
|
||||
}
|
||||
|
||||
return ['confirmed_count' => count($ids) - count($errors)];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 확정 해제
|
||||
*/
|
||||
public function unconfirm(array $ids)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($ids, $tenantId, $userId) {
|
||||
PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->where('confirmation_status', PerformanceReport::STATUS_CONFIRMED)
|
||||
->update([
|
||||
'confirmation_status' => PerformanceReport::STATUS_UNCONFIRMED,
|
||||
'confirmed_date' => null,
|
||||
'confirmed_by' => null,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ['unconfirmed_count' => count($ids)];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 메모 업데이트
|
||||
*/
|
||||
public function updateMemo(array $ids, string $memo)
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
PerformanceReport::where('tenant_id', $tenantId)
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'memo' => $memo,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ['updated_count' => count($ids)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 누락체크 (출고완료 but 제품검사 미등록)
|
||||
*/
|
||||
public function missing(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 품질관리서가 등록된 수주 ID
|
||||
$registeredOrderIds = DB::table('quality_document_orders')
|
||||
->join('quality_documents', 'quality_documents.id', '=', 'quality_document_orders.quality_document_id')
|
||||
->where('quality_documents.tenant_id', $tenantId)
|
||||
->pluck('quality_document_orders.order_id');
|
||||
|
||||
// 출고완료 상태이지만 품질관리서 미등록 수주
|
||||
$query = DB::table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNotIn('id', $registeredOrderIds)
|
||||
->where('status_code', 'SHIPPED'); // TODO: 출고완료 상태 추가 시 상수 확인
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->whereYear('created_at', $params['year']);
|
||||
}
|
||||
if (! empty($params['quarter'])) {
|
||||
$quarter = (int) $params['quarter'];
|
||||
$startMonth = ($quarter - 1) * 3 + 1;
|
||||
$endMonth = $quarter * 3;
|
||||
$query->whereMonth('created_at', '>=', $startMonth)
|
||||
->whereMonth('created_at', '<=', $endMonth);
|
||||
}
|
||||
|
||||
return $query->orderByDesc('id')
|
||||
->limit(100)
|
||||
->get()
|
||||
->map(fn ($order) => [
|
||||
'id' => $order->id,
|
||||
'order_number' => $order->order_no ?? '',
|
||||
'site_name' => $order->site_name ?? '',
|
||||
'client' => '', // 별도 조인 필요
|
||||
'delivery_date' => $order->delivery_date ?? '',
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* DB → 프론트엔드 변환
|
||||
*/
|
||||
private function transformToFrontend(PerformanceReport $report): array
|
||||
{
|
||||
$doc = $report->qualityDocument;
|
||||
|
||||
return [
|
||||
'id' => $report->id,
|
||||
'quality_doc_number' => $doc?->quality_doc_number ?? '',
|
||||
'created_date' => $report->created_at?->format('Y-m-d') ?? '',
|
||||
'site_name' => $doc?->site_name ?? '',
|
||||
'client' => $doc?->client?->name ?? '',
|
||||
'location_count' => $doc?->locations?->count() ?? 0,
|
||||
'required_info' => $doc ? $this->qualityDocumentService->calculateRequiredInfo($doc) : '',
|
||||
'confirm_status' => $report->confirmation_status === PerformanceReport::STATUS_CONFIRMED ? 'confirmed' : 'unconfirmed',
|
||||
'confirm_date' => $report->confirmed_date?->format('Y-m-d'),
|
||||
'memo' => $report->memo ?? '',
|
||||
'year' => $report->year,
|
||||
'quarter' => $report->quarter,
|
||||
];
|
||||
}
|
||||
}
|
||||
285
app/Services/ProductionOrderService.php
Normal file
285
app/Services/ProductionOrderService.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ProductionOrderService extends Service
|
||||
{
|
||||
/**
|
||||
* 생산지시 대상 상태 코드
|
||||
*/
|
||||
private const PRODUCTION_STATUSES = [
|
||||
Order::STATUS_IN_PROGRESS,
|
||||
Order::STATUS_IN_PRODUCTION,
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
];
|
||||
|
||||
/**
|
||||
* 생산지시 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', self::PRODUCTION_STATUSES)
|
||||
->with(['client', 'workOrders.process', 'workOrders.assignees.user'])
|
||||
->withCount([
|
||||
'workOrders' => fn ($q) => $q->whereNotNull('process_id')
|
||||
->where(fn ($q2) => $q2->whereNull('options->is_auxiliary')
|
||||
->orWhere('options->is_auxiliary', false)),
|
||||
'nodes',
|
||||
]);
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('order_no', 'like', "%{$search}%")
|
||||
->orWhere('client_name', 'like', "%{$search}%")
|
||||
->orWhere('site_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 생산 상태 필터
|
||||
if (! empty($params['production_status'])) {
|
||||
switch ($params['production_status']) {
|
||||
case 'waiting':
|
||||
$query->where('status_code', Order::STATUS_IN_PROGRESS);
|
||||
break;
|
||||
case 'in_production':
|
||||
$query->where('status_code', Order::STATUS_IN_PRODUCTION);
|
||||
break;
|
||||
case 'completed':
|
||||
$query->whereIn('status_code', [
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
$result = $query->paginate($perPage);
|
||||
|
||||
// 가공 필드 추가
|
||||
$result->getCollection()->transform(function (Order $order) {
|
||||
$minCreatedAt = $order->workOrders->min('created_at');
|
||||
$order->production_ordered_at = $minCreatedAt
|
||||
? $minCreatedAt->format('Y-m-d')
|
||||
: null;
|
||||
|
||||
// 개소수 (order_nodes 수)
|
||||
$order->node_count = $order->nodes_count ?? 0;
|
||||
|
||||
// 주요 생산 공정 WO만 (구매품 + 보조 공정 제외)
|
||||
$productionWOs = $this->filterMainProductionWOs($order->workOrders);
|
||||
$order->work_order_progress = [
|
||||
'total' => $productionWOs->count(),
|
||||
'completed' => $productionWOs->where('status', 'completed')->count()
|
||||
+ $productionWOs->where('status', 'shipped')->count(),
|
||||
'in_progress' => $productionWOs->where('status', 'in_progress')->count(),
|
||||
];
|
||||
|
||||
// 프론트 탭용 production_status 매핑
|
||||
$order->production_status = $this->mapProductionStatus($order->status_code);
|
||||
|
||||
return $order;
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 통계
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$waiting = Order::where('tenant_id', $tenantId)
|
||||
->where('status_code', Order::STATUS_IN_PROGRESS)
|
||||
->count();
|
||||
|
||||
$inProduction = Order::where('tenant_id', $tenantId)
|
||||
->where('status_code', Order::STATUS_IN_PRODUCTION)
|
||||
->count();
|
||||
|
||||
$completed = Order::where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', [
|
||||
Order::STATUS_PRODUCED,
|
||||
Order::STATUS_SHIPPING,
|
||||
Order::STATUS_SHIPPED,
|
||||
])
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $waiting + $inProduction + $completed,
|
||||
'waiting' => $waiting,
|
||||
'in_production' => $inProduction,
|
||||
'completed' => $completed,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산지시 상세 조회
|
||||
*/
|
||||
public function show(int $orderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$order = Order::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('status_code', self::PRODUCTION_STATUSES)
|
||||
->with([
|
||||
'client',
|
||||
'workOrders.process',
|
||||
'workOrders.items',
|
||||
'workOrders.assignees.user',
|
||||
'nodes',
|
||||
])
|
||||
->withCount('nodes')
|
||||
->findOrFail($orderId);
|
||||
|
||||
// 생산지시일 (날짜만)
|
||||
$minCreatedAt = $order->workOrders->min('created_at');
|
||||
$order->production_ordered_at = $minCreatedAt
|
||||
? $minCreatedAt->format('Y-m-d')
|
||||
: null;
|
||||
$order->production_status = $this->mapProductionStatus($order->status_code);
|
||||
|
||||
// 주요 생산 공정 WO만 필터 (구매품 + 보조 공정 제외)
|
||||
$productionWorkOrders = $this->filterMainProductionWOs($order->workOrders);
|
||||
|
||||
// WorkOrder 진행 현황 (생산 공정 기준)
|
||||
$workOrderProgress = [
|
||||
'total' => $productionWorkOrders->count(),
|
||||
'completed' => $productionWorkOrders->where('status', 'completed')->count()
|
||||
+ $productionWorkOrders->where('status', 'shipped')->count(),
|
||||
'in_progress' => $productionWorkOrders->where('status', 'in_progress')->count(),
|
||||
];
|
||||
|
||||
// WorkOrder 목록 가공 (생산 공정만)
|
||||
$workOrders = $productionWorkOrders->values()->map(function ($wo) {
|
||||
return [
|
||||
'id' => $wo->id,
|
||||
'work_order_no' => $wo->work_order_no,
|
||||
'process_name' => $wo->process?->process_name ?? '',
|
||||
'quantity' => $wo->items->count(),
|
||||
'status' => $wo->status,
|
||||
'assignees' => $wo->assignees->map(fn ($a) => $a->user?->name ?? '')->filter()->values()->toArray(),
|
||||
];
|
||||
});
|
||||
|
||||
// BOM 데이터 (order_nodes에서 추출)
|
||||
$bomProcessGroups = $this->extractBomProcessGroups($order->nodes);
|
||||
|
||||
return [
|
||||
'order' => $order->makeHidden(['workOrders', 'nodes']),
|
||||
'production_ordered_at' => $order->production_ordered_at,
|
||||
'production_status' => $order->production_status,
|
||||
'node_count' => $order->nodes_count ?? 0,
|
||||
'work_order_progress' => $workOrderProgress,
|
||||
'work_orders' => $workOrders,
|
||||
'bom_process_groups' => $bomProcessGroups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Order status_code → 프론트 production_status 매핑
|
||||
*/
|
||||
private function mapProductionStatus(string $statusCode): string
|
||||
{
|
||||
return match ($statusCode) {
|
||||
Order::STATUS_IN_PROGRESS => 'waiting',
|
||||
Order::STATUS_IN_PRODUCTION => 'in_production',
|
||||
default => 'completed',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* order_nodes에서 BOM 공정 분류 추출
|
||||
*
|
||||
* bom_result 구조: { items: [...], success, subtotals, ... }
|
||||
* 각 item: { item_id, item_code, item_name, process_group, specification, quantity, unit, ... }
|
||||
*/
|
||||
private function extractBomProcessGroups($nodes): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$bomResult = $node->options['bom_result'] ?? null;
|
||||
if (! $bomResult || ! is_array($bomResult)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// bom_result.items 배열에서 추출
|
||||
$items = $bomResult['items'] ?? [];
|
||||
if (! is_array($items)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nodeName = $node->name ?? '';
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$processGroup = $item['process_group'] ?? $item['category_group'] ?? '기타';
|
||||
|
||||
if (! isset($groups[$processGroup])) {
|
||||
$groups[$processGroup] = [
|
||||
'process_name' => $processGroup,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$groups[$processGroup]['items'][] = [
|
||||
'id' => $item['item_id'] ?? null,
|
||||
'item_code' => $item['item_code'] ?? '',
|
||||
'item_name' => $item['item_name'] ?? '',
|
||||
'spec' => $item['specification'] ?? '',
|
||||
'unit' => $item['unit'] ?? '',
|
||||
'quantity' => $item['quantity'] ?? 0,
|
||||
'unit_price' => $item['unit_price'] ?? 0,
|
||||
'total_price' => $item['total_price'] ?? 0,
|
||||
'node_name' => $nodeName,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주요 생산 공정 WO만 필터 (구매품/서비스 + 보조 공정 제외)
|
||||
*
|
||||
* 제외 대상:
|
||||
* - process_id가 null인 WO (구매품/서비스)
|
||||
* - options.is_auxiliary가 true인 WO (재고생산 등 보조 공정)
|
||||
*/
|
||||
private function filterMainProductionWOs($workOrders): \Illuminate\Support\Collection
|
||||
{
|
||||
return $workOrders->filter(function ($wo) {
|
||||
if (empty($wo->process_id)) {
|
||||
return false;
|
||||
}
|
||||
$options = is_array($wo->options) ? $wo->options : (json_decode($wo->options, true) ?? []);
|
||||
|
||||
return empty($options['is_auxiliary']);
|
||||
});
|
||||
}
|
||||
}
|
||||
517
app/Services/QmsLotAuditService.php
Normal file
517
app/Services/QmsLotAuditService.php
Normal file
@@ -0,0 +1,517 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Production\WorkOrder;
|
||||
use App\Models\Production\WorkOrderMaterialInput;
|
||||
use App\Models\Qualitys\Inspection;
|
||||
use App\Models\Qualitys\QualityDocument;
|
||||
use App\Models\Qualitys\QualityDocumentLocation;
|
||||
use App\Models\Qualitys\QualityDocumentOrder;
|
||||
use App\Models\Tenants\Shipment;
|
||||
use App\Models\Tenants\StockLot;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class QmsLotAuditService extends Service
|
||||
{
|
||||
/**
|
||||
* 품질관리서 목록 (로트 추적 심사용)
|
||||
* completed 상태의 품질관리서를 PerformanceReport 기반으로 필터링
|
||||
*/
|
||||
public function index(array $params): array
|
||||
{
|
||||
$query = QualityDocument::with([
|
||||
'documentOrders.order.nodes' => fn ($q) => $q->whereNull('parent_id'),
|
||||
'documentOrders.order.nodes.items.item',
|
||||
'locations',
|
||||
'performanceReport',
|
||||
])
|
||||
->where('status', QualityDocument::STATUS_COMPLETED);
|
||||
|
||||
// 연도 필터
|
||||
if (! empty($params['year'])) {
|
||||
$year = (int) $params['year'];
|
||||
$query->where(function ($q) use ($year) {
|
||||
$q->whereHas('performanceReport', fn ($pr) => $pr->where('year', $year))
|
||||
->orWhereDoesntHave('performanceReport');
|
||||
});
|
||||
}
|
||||
|
||||
// 분기 필터
|
||||
if (! empty($params['quarter'])) {
|
||||
$quarter = (int) $params['quarter'];
|
||||
$query->whereHas('performanceReport', fn ($pr) => $pr->where('quarter', $quarter));
|
||||
}
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['q'])) {
|
||||
$term = trim($params['q']);
|
||||
$query->where(function ($q) use ($term) {
|
||||
$q->where('quality_doc_number', 'like', "%{$term}%")
|
||||
->orWhere('site_name', 'like', "%{$term}%");
|
||||
});
|
||||
}
|
||||
|
||||
$query->orderByDesc('id');
|
||||
$perPage = (int) ($params['per_page'] ?? 20);
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$items = $paginated->getCollection()->map(fn ($doc) => $this->transformReportToFrontend($doc));
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'total' => $paginated->total(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 품질관리서 상세 — 수주/개소 목록 (RouteItem[])
|
||||
*/
|
||||
public function show(int $id): array
|
||||
{
|
||||
$doc = QualityDocument::with([
|
||||
'documentOrders.order',
|
||||
'documentOrders.locations.orderItem',
|
||||
])->findOrFail($id);
|
||||
|
||||
return $doc->documentOrders->map(fn ($docOrder) => $this->transformRouteToFrontend($docOrder, $doc))->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 루트별 8종 서류 목록 (Document[])
|
||||
*/
|
||||
public function routeDocuments(int $qualityDocumentOrderId): array
|
||||
{
|
||||
$docOrder = QualityDocumentOrder::with([
|
||||
'order.workOrders.process',
|
||||
'locations',
|
||||
'qualityDocument',
|
||||
])->findOrFail($qualityDocumentOrderId);
|
||||
|
||||
$order = $docOrder->order;
|
||||
$qualityDoc = $docOrder->qualityDocument;
|
||||
$workOrders = $order->workOrders;
|
||||
|
||||
$documents = [];
|
||||
|
||||
// 1. 수입검사 성적서 (IQC): WorkOrderMaterialInput → StockLot → lot_no → Inspection(IQC)
|
||||
$investedLotIds = WorkOrderMaterialInput::whereIn('work_order_id', $workOrders->pluck('id'))
|
||||
->pluck('stock_lot_id')
|
||||
->unique();
|
||||
|
||||
$investedLotNos = StockLot::whereIn('id', $investedLotIds)
|
||||
->whereNotNull('lot_no')
|
||||
->pluck('lot_no')
|
||||
->unique();
|
||||
|
||||
$iqcInspections = Inspection::where('inspection_type', 'IQC')
|
||||
->whereIn('lot_no', $investedLotNos)
|
||||
->where('status', 'completed')
|
||||
->get();
|
||||
|
||||
$documents[] = $this->formatDocument('import', '수입검사 성적서', $iqcInspections);
|
||||
|
||||
// 2. 수주서
|
||||
$documents[] = $this->formatDocument('order', '수주서', collect([$order]));
|
||||
|
||||
// 3. 작업일지 (subType: process.process_name 기반)
|
||||
$documents[] = $this->formatDocumentWithSubType('log', '작업일지', $workOrders);
|
||||
|
||||
// 4. 중간검사 성적서 (PQC)
|
||||
$pqcInspections = Inspection::where('inspection_type', 'PQC')
|
||||
->whereIn('work_order_id', $workOrders->pluck('id'))
|
||||
->where('status', 'completed')
|
||||
->with('workOrder.process')
|
||||
->get();
|
||||
|
||||
$documents[] = $this->formatDocumentWithSubType('report', '중간검사 성적서', $pqcInspections, 'workOrder');
|
||||
|
||||
// 5. 납품확인서
|
||||
$shipments = $order->shipments()->get();
|
||||
$documents[] = $this->formatDocument('confirmation', '납품확인서', $shipments);
|
||||
|
||||
// 6. 출고증
|
||||
$documents[] = $this->formatDocument('shipping', '출고증', $shipments);
|
||||
|
||||
// 7. 제품검사 성적서
|
||||
$locationsWithDoc = $docOrder->locations->filter(fn ($loc) => $loc->document_id);
|
||||
$documents[] = $this->formatDocument('product', '제품검사 성적서', $locationsWithDoc);
|
||||
|
||||
// 8. 품질관리서
|
||||
$documents[] = $this->formatDocument('quality', '품질관리서', collect([$qualityDoc]));
|
||||
|
||||
return $documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 상세 조회 (2단계 로딩 — 모달 렌더링용)
|
||||
*/
|
||||
public function documentDetail(string $type, int $id): array
|
||||
{
|
||||
return match ($type) {
|
||||
'import' => $this->getInspectionDetail($id, 'IQC'),
|
||||
'order' => $this->getOrderDetail($id),
|
||||
'log' => $this->getWorkOrderLogDetail($id),
|
||||
'report' => $this->getInspectionDetail($id, 'PQC'),
|
||||
'confirmation', 'shipping' => $this->getShipmentDetail($id),
|
||||
'product' => $this->getLocationDetail($id),
|
||||
'quality' => $this->getQualityDocDetail($id),
|
||||
default => throw new NotFoundHttpException(__('error.not_found')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 개소별 로트 심사 확인 토글
|
||||
*/
|
||||
public function confirm(int $locationId, array $data): array
|
||||
{
|
||||
$location = QualityDocumentLocation::findOrFail($locationId);
|
||||
$confirmed = (bool) $data['confirmed'];
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
DB::transaction(function () use ($location, $confirmed, $userId) {
|
||||
$location->lockForUpdate();
|
||||
|
||||
$options = $location->options ?? [];
|
||||
$options['lot_audit_confirmed'] = $confirmed;
|
||||
$options['lot_audit_confirmed_at'] = $confirmed ? now()->toIso8601String() : null;
|
||||
$options['lot_audit_confirmed_by'] = $confirmed ? $userId : null;
|
||||
$location->options = $options;
|
||||
$location->save();
|
||||
});
|
||||
|
||||
$location->refresh();
|
||||
|
||||
return [
|
||||
'id' => (string) $location->id,
|
||||
'name' => $this->buildLocationName($location),
|
||||
'location' => $this->buildLocationCode($location),
|
||||
'is_completed' => (bool) data_get($location->options, 'lot_audit_confirmed', false),
|
||||
];
|
||||
}
|
||||
|
||||
// ===== Private: Transform Methods =====
|
||||
|
||||
private function transformReportToFrontend(QualityDocument $doc): array
|
||||
{
|
||||
$performanceReport = $doc->performanceReport;
|
||||
$confirmedCount = $doc->locations->filter(function ($loc) {
|
||||
return data_get($loc->options, 'lot_audit_confirmed', false);
|
||||
})->count();
|
||||
|
||||
return [
|
||||
'id' => (string) $doc->id,
|
||||
'code' => $doc->quality_doc_number,
|
||||
'site_name' => $doc->site_name,
|
||||
'item' => $this->getFgProductName($doc),
|
||||
'route_count' => $confirmedCount,
|
||||
'total_routes' => $doc->locations->count(),
|
||||
'quarter' => $performanceReport
|
||||
? $performanceReport->year.'년 '.$performanceReport->quarter.'분기'
|
||||
: '',
|
||||
'year' => $performanceReport?->year ?? now()->year,
|
||||
'quarter_num' => $performanceReport?->quarter ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 최상위(FG) 제품명 추출
|
||||
* Order → root OrderNode(parent_id=null) → 대표 OrderItem → Item(FG).name
|
||||
*/
|
||||
private function getFgProductName(QualityDocument $doc): string
|
||||
{
|
||||
$firstDocOrder = $doc->documentOrders->first();
|
||||
if (! $firstDocOrder) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$order = $firstDocOrder->order;
|
||||
if (! $order) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// eager loaded with whereNull('parent_id') filter
|
||||
$rootNode = $order->nodes->first();
|
||||
if (! $rootNode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$representativeItem = $rootNode->items->first();
|
||||
|
||||
return $representativeItem?->item?->name ?? '';
|
||||
}
|
||||
|
||||
private function transformRouteToFrontend(QualityDocumentOrder $docOrder, QualityDocument $qualityDoc): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $docOrder->id,
|
||||
'code' => $docOrder->order->order_no,
|
||||
'date' => $docOrder->order->received_at?->toDateString(),
|
||||
'site' => $docOrder->order->site_name ?? '',
|
||||
'location_count' => $docOrder->locations->count(),
|
||||
'sub_items' => $docOrder->locations->values()->map(fn ($loc, $idx) => [
|
||||
'id' => (string) $loc->id,
|
||||
'name' => $qualityDoc->quality_doc_number.'-'.str_pad($idx + 1, 2, '0', STR_PAD_LEFT),
|
||||
'location' => $this->buildLocationCode($loc),
|
||||
'is_completed' => (bool) data_get($loc->options, 'lot_audit_confirmed', false),
|
||||
])->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildLocationName(QualityDocumentLocation $location): string
|
||||
{
|
||||
$qualityDoc = $location->qualityDocument;
|
||||
if (! $qualityDoc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// location의 순번을 구하기 위해 같은 문서의 location 목록 조회
|
||||
$locations = QualityDocumentLocation::where('quality_document_id', $qualityDoc->id)
|
||||
->orderBy('id')
|
||||
->pluck('id');
|
||||
|
||||
$index = $locations->search($location->id);
|
||||
|
||||
return $qualityDoc->quality_doc_number.'-'.str_pad(($index !== false ? $index + 1 : 1), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function buildLocationCode(QualityDocumentLocation $location): string
|
||||
{
|
||||
$orderItem = $location->orderItem;
|
||||
if (! $orderItem) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim(($orderItem->floor_code ?? '').' '.($orderItem->symbol_code ?? ''));
|
||||
}
|
||||
|
||||
// ===== Private: Document Format Helpers =====
|
||||
|
||||
private function formatDocument(string $type, string $title, $collection): array
|
||||
{
|
||||
return [
|
||||
'id' => $type,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'count' => $collection->count(),
|
||||
'items' => $collection->values()->map(fn ($item) => $this->formatDocumentItem($type, $item))->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatDocumentWithSubType(string $type, string $title, $collection, ?string $workOrderRelation = null): array
|
||||
{
|
||||
return [
|
||||
'id' => $type,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'count' => $collection->count(),
|
||||
'items' => $collection->values()->map(function ($item) use ($type, $workOrderRelation) {
|
||||
$formatted = $this->formatDocumentItem($type, $item);
|
||||
|
||||
// subType: process.process_name 기반
|
||||
$workOrder = $workOrderRelation ? $item->{$workOrderRelation} : $item;
|
||||
if ($workOrder instanceof WorkOrder) {
|
||||
$processName = $workOrder->process?->process_name;
|
||||
$formatted['sub_type'] = $this->mapProcessToSubType($processName);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
})->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatDocumentItem(string $type, $item): array
|
||||
{
|
||||
return match ($type) {
|
||||
'import', 'report' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => $item->inspection_no ?? '',
|
||||
'date' => $item->inspection_date?->toDateString() ?? $item->request_date?->toDateString() ?? '',
|
||||
'code' => $item->inspection_no ?? '',
|
||||
],
|
||||
'order' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => $item->order_no,
|
||||
'date' => $item->received_at?->toDateString() ?? '',
|
||||
'code' => $item->order_no,
|
||||
],
|
||||
'log' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => $item->project_name ?? '작업일지',
|
||||
'date' => $item->created_at?->toDateString() ?? '',
|
||||
'code' => $item->id,
|
||||
],
|
||||
'confirmation', 'shipping' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => $item->shipment_no ?? '',
|
||||
'date' => $item->scheduled_date?->toDateString() ?? '',
|
||||
'code' => $item->shipment_no ?? '',
|
||||
],
|
||||
'product' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => '제품검사 성적서',
|
||||
'date' => $item->updated_at?->toDateString() ?? '',
|
||||
'code' => '',
|
||||
],
|
||||
'quality' => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => $item->quality_doc_number ?? '',
|
||||
'date' => $item->received_date?->toDateString() ?? '',
|
||||
'code' => $item->quality_doc_number ?? '',
|
||||
],
|
||||
default => [
|
||||
'id' => (string) $item->id,
|
||||
'title' => '',
|
||||
'date' => '',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* process_name → subType 매핑
|
||||
*/
|
||||
private function mapProcessToSubType(?string $processName): ?string
|
||||
{
|
||||
if (! $processName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = mb_strtolower($processName);
|
||||
|
||||
return match (true) {
|
||||
str_contains($name, 'screen') || str_contains($name, '스크린') => 'screen',
|
||||
str_contains($name, 'bending') || str_contains($name, '절곡') => 'bending',
|
||||
str_contains($name, 'slat') || str_contains($name, '슬랫') => 'slat',
|
||||
str_contains($name, 'jointbar') || str_contains($name, '조인트바') || str_contains($name, 'joint') => 'jointbar',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Private: Document Detail Methods (2단계 로딩) =====
|
||||
|
||||
private function getInspectionDetail(int $id, string $type): array
|
||||
{
|
||||
$inspection = Inspection::where('inspection_type', $type)
|
||||
->with(['item', 'workOrder.process'])
|
||||
->findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => $type === 'IQC' ? 'import' : 'report',
|
||||
'data' => [
|
||||
'id' => $inspection->id,
|
||||
'inspection_no' => $inspection->inspection_no,
|
||||
'inspection_type' => $inspection->inspection_type,
|
||||
'status' => $inspection->status,
|
||||
'result' => $inspection->result,
|
||||
'request_date' => $inspection->request_date?->toDateString(),
|
||||
'inspection_date' => $inspection->inspection_date?->toDateString(),
|
||||
'lot_no' => $inspection->lot_no,
|
||||
'item_name' => $inspection->item?->name,
|
||||
'process_name' => $inspection->workOrder?->process?->process_name,
|
||||
'meta' => $inspection->meta,
|
||||
'items' => $inspection->items,
|
||||
'attachments' => $inspection->attachments,
|
||||
'extra' => $inspection->extra,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getOrderDetail(int $id): array
|
||||
{
|
||||
$order = Order::with(['nodes' => fn ($q) => $q->whereNull('parent_id')])->findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => 'order',
|
||||
'data' => [
|
||||
'id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'status' => $order->status,
|
||||
'received_at' => $order->received_at?->toDateString(),
|
||||
'site_name' => $order->site_name,
|
||||
'nodes_count' => $order->nodes->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getWorkOrderLogDetail(int $id): array
|
||||
{
|
||||
$workOrder = WorkOrder::with('process')->findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => 'log',
|
||||
'data' => [
|
||||
'id' => $workOrder->id,
|
||||
'project_name' => $workOrder->project_name,
|
||||
'status' => $workOrder->status,
|
||||
'process_name' => $workOrder->process?->process_name,
|
||||
'options' => $workOrder->options,
|
||||
'created_at' => $workOrder->created_at?->toDateString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getShipmentDetail(int $id): array
|
||||
{
|
||||
$shipment = Shipment::findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => 'shipping',
|
||||
'data' => [
|
||||
'id' => $shipment->id,
|
||||
'shipment_no' => $shipment->shipment_no,
|
||||
'status' => $shipment->status,
|
||||
'scheduled_date' => $shipment->scheduled_date?->toDateString(),
|
||||
'customer_name' => $shipment->customer_name,
|
||||
'site_name' => $shipment->site_name,
|
||||
'delivery_address' => $shipment->delivery_address,
|
||||
'delivery_method' => $shipment->delivery_method,
|
||||
'vehicle_no' => $shipment->vehicle_no,
|
||||
'driver_name' => $shipment->driver_name,
|
||||
'remarks' => $shipment->remarks,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getLocationDetail(int $id): array
|
||||
{
|
||||
$location = QualityDocumentLocation::with(['orderItem', 'document'])->findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => 'product',
|
||||
'data' => [
|
||||
'id' => $location->id,
|
||||
'inspection_status' => $location->inspection_status,
|
||||
'inspection_data' => $location->inspection_data,
|
||||
'post_width' => $location->post_width,
|
||||
'post_height' => $location->post_height,
|
||||
'floor_code' => $location->orderItem?->floor_code,
|
||||
'symbol_code' => $location->orderItem?->symbol_code,
|
||||
'document_id' => $location->document_id,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getQualityDocDetail(int $id): array
|
||||
{
|
||||
$doc = QualityDocument::with(['client', 'inspector:id,name'])->findOrFail($id);
|
||||
|
||||
return [
|
||||
'type' => 'quality',
|
||||
'data' => [
|
||||
'id' => $doc->id,
|
||||
'quality_doc_number' => $doc->quality_doc_number,
|
||||
'site_name' => $doc->site_name,
|
||||
'status' => $doc->status,
|
||||
'received_date' => $doc->received_date?->toDateString(),
|
||||
'client_name' => $doc->client?->name,
|
||||
'inspector_name' => $doc->inspector?->name,
|
||||
'options' => $doc->options,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
1251
app/Services/QualityDocumentService.php
Normal file
1251
app/Services/QualityDocumentService.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -44,23 +44,8 @@ public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
")
|
||||
->first();
|
||||
|
||||
// 상담 (sales_prospect_consultations)
|
||||
$consultationCount = DB::connection('mysql')
|
||||
->table('sales_prospect_consultations')
|
||||
->whereDate('created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
// 영업 기회 (sales_prospects - tenant_id 없음, created_at 기반)
|
||||
$prospectStats = DB::connection('mysql')
|
||||
->table('sales_prospects')
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
COUNT(*) as created_count,
|
||||
SUM(CASE WHEN status = 'contracted' THEN 1 ELSE 0 END) as won_count,
|
||||
SUM(CASE WHEN status = 'lost' THEN 1 ELSE 0 END) as lost_count
|
||||
")
|
||||
->first();
|
||||
// sales_prospect_consultations, sales_prospects는 codebridge DB에 이관되었고
|
||||
// tenant_id가 없어 테넌트별 집계 불가 → 제외
|
||||
|
||||
StatQuotePipelineDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
@@ -71,14 +56,9 @@ public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
'quote_rejected_count' => $quoteStats->rejected_count ?? 0,
|
||||
'quote_conversion_count' => $conversionCount,
|
||||
'quote_conversion_rate' => $conversionRate,
|
||||
'prospect_created_count' => $prospectStats->created_count ?? 0,
|
||||
'prospect_won_count' => $prospectStats->won_count ?? 0,
|
||||
'prospect_lost_count' => $prospectStats->lost_count ?? 0,
|
||||
'prospect_amount' => 0, // sales_prospects에 금액 컬럼 없음
|
||||
'bidding_count' => $biddingStats->cnt ?? 0,
|
||||
'bidding_won_count' => $biddingStats->won_count ?? 0,
|
||||
'bidding_amount' => $biddingStats->total_amount ?? 0,
|
||||
'consultation_count' => $consultationCount,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -90,4 +70,4 @@ public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
// 견적 도메인은 일간 테이블만 운영 (월간은 Phase 4에서 필요시 추가)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,16 +67,35 @@ private function getOrdersStatus(int $tenantId, Carbon $today): array
|
||||
*/
|
||||
private function getBadDebtStatus(int $tenantId): array
|
||||
{
|
||||
$count = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('status', BadDebt::STATUS_COLLECTING) // 추심 진행 중
|
||||
->where('is_active', true) // 활성 채권만 (목록 페이지와 일치)
|
||||
->count();
|
||||
$query = BadDebt::query()
|
||||
->where('bad_debts.tenant_id', $tenantId)
|
||||
->where('bad_debts.status', BadDebt::STATUS_COLLECTING)
|
||||
->where('bad_debts.is_active', true);
|
||||
|
||||
$count = (clone $query)->count();
|
||||
|
||||
// 최다 금액 거래처명 조회
|
||||
$subLabel = null;
|
||||
if ($count > 0) {
|
||||
$topClient = (clone $query)
|
||||
->join('clients', 'bad_debts.client_id', '=', 'clients.id')
|
||||
->selectRaw('clients.name, SUM(bad_debts.debt_amount) as total_amount')
|
||||
->groupBy('clients.id', 'clients.name')
|
||||
->orderByDesc('total_amount')
|
||||
->first();
|
||||
|
||||
if ($topClient) {
|
||||
$subLabel = $count > 1
|
||||
? $topClient->name.' 외 '.($count - 1).'건'
|
||||
: $topClient->name;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => 'bad_debts',
|
||||
'label' => __('message.status_board.bad_debts'),
|
||||
'count' => $count,
|
||||
'sub_label' => $subLabel,
|
||||
'path' => '/accounting/bad-debt-collection',
|
||||
'isHighlighted' => false,
|
||||
];
|
||||
@@ -152,15 +171,31 @@ private function getTaxDeadlineStatus(int $tenantId, Carbon $today): array
|
||||
*/
|
||||
private function getNewClientStatus(int $tenantId, Carbon $today): array
|
||||
{
|
||||
$count = Client::query()
|
||||
$query = Client::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('created_at', '>=', $today->copy()->subDays(7))
|
||||
->count();
|
||||
->where('created_at', '>=', $today->copy()->subDays(7));
|
||||
|
||||
$count = (clone $query)->count();
|
||||
|
||||
// 가장 최근 등록 업체명 조회
|
||||
$subLabel = null;
|
||||
if ($count > 0) {
|
||||
$latestClient = (clone $query)
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
|
||||
if ($latestClient) {
|
||||
$subLabel = $count > 1
|
||||
? $latestClient->name.' 외 '.($count - 1).'건'
|
||||
: $latestClient->name;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => 'new_clients',
|
||||
'label' => __('message.status_board.new_clients'),
|
||||
'count' => $count,
|
||||
'sub_label' => $subLabel,
|
||||
'path' => '/accounting/vendors',
|
||||
'isHighlighted' => false,
|
||||
];
|
||||
@@ -211,19 +246,34 @@ private function getPurchaseStatus(int $tenantId): array
|
||||
*/
|
||||
private function getApprovalStatus(int $tenantId, int $userId): array
|
||||
{
|
||||
$count = ApprovalStep::query()
|
||||
->whereHas('approval', function ($query) use ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId)
|
||||
$query = ApprovalStep::query()
|
||||
->whereHas('approval', function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId)
|
||||
->where('status', 'pending');
|
||||
})
|
||||
->where('approver_id', $userId)
|
||||
->where('status', 'pending')
|
||||
->count();
|
||||
->approvalOnly();
|
||||
|
||||
$count = (clone $query)->count();
|
||||
|
||||
// 최근 결재 유형 조회
|
||||
$subLabel = null;
|
||||
if ($count > 0) {
|
||||
$latestStep = (clone $query)->with('approval')->latest()->first();
|
||||
if ($latestStep && $latestStep->approval) {
|
||||
$typeLabel = $latestStep->approval->title ?? '결재';
|
||||
$subLabel = $count > 1
|
||||
? $typeLabel.' 외 '.($count - 1).'건'
|
||||
: $typeLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => 'approvals',
|
||||
'label' => __('message.status_board.approvals'),
|
||||
'count' => $count,
|
||||
'sub_label' => $subLabel,
|
||||
'path' => '/approval/inbox',
|
||||
'isHighlighted' => $count > 0,
|
||||
];
|
||||
|
||||
@@ -112,6 +112,12 @@ public function create(array $data): TaxInvoice
|
||||
// 합계금액 계산
|
||||
$data['total_amount'] = ($data['supply_amount'] ?? 0) + ($data['tax_amount'] ?? 0);
|
||||
|
||||
// NOT NULL 컬럼: Laravel ConvertEmptyStringsToNull 미들웨어가 ''→null 변환하므로 보정
|
||||
$data['supplier_corp_num'] = $data['supplier_corp_num'] ?? '';
|
||||
$data['supplier_corp_name'] = $data['supplier_corp_name'] ?? '';
|
||||
$data['buyer_corp_num'] = $data['buyer_corp_num'] ?? '';
|
||||
$data['buyer_corp_name'] = $data['buyer_corp_name'] ?? '';
|
||||
|
||||
$taxInvoice = TaxInvoice::create(array_merge($data, [
|
||||
'tenant_id' => $tenantId,
|
||||
'status' => TaxInvoice::STATUS_DRAFT,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\TenantBootstrap;
|
||||
|
||||
use App\Services\TenantBootstrap\Steps\ApprovalFormsStep;
|
||||
use App\Services\TenantBootstrap\Steps\CapabilityProfilesStep;
|
||||
use App\Services\TenantBootstrap\Steps\CategoriesStep;
|
||||
use App\Services\TenantBootstrap\Steps\MenusStep;
|
||||
@@ -24,6 +25,7 @@ public function steps(string $recipe = 'STANDARD'): array
|
||||
new CategoriesStep,
|
||||
// new MenusStep, // Disabled: Use MenuBootstrapService in RegisterService instead
|
||||
new SettingsStep,
|
||||
new ApprovalFormsStep,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
105
app/Services/TenantBootstrap/Steps/ApprovalFormsStep.php
Normal file
105
app/Services/TenantBootstrap/Steps/ApprovalFormsStep.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\TenantBootstrap\Steps;
|
||||
|
||||
use App\Services\TenantBootstrap\Contracts\TenantBootstrapStep;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ApprovalFormsStep implements TenantBootstrapStep
|
||||
{
|
||||
public function key(): string
|
||||
{
|
||||
return 'approval_forms_seed';
|
||||
}
|
||||
|
||||
public function run(int $tenantId): void
|
||||
{
|
||||
if (! DB::getSchemaBuilder()->hasTable('approval_forms')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$forms = [
|
||||
[
|
||||
'name' => '품의서',
|
||||
'code' => 'proposal',
|
||||
'category' => '일반',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'title', 'type' => 'text', 'label' => '제목', 'required' => true],
|
||||
['name' => 'vendor', 'type' => 'text', 'label' => '거래처', 'required' => false],
|
||||
['name' => 'description', 'type' => 'textarea', 'label' => '내용', 'required' => true],
|
||||
['name' => 'reason', 'type' => 'textarea', 'label' => '사유', 'required' => true],
|
||||
['name' => 'estimatedCost', 'type' => 'number', 'label' => '예상비용', 'required' => false],
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => '지출결의서',
|
||||
'code' => 'expenseReport',
|
||||
'category' => '경비',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'requestDate', 'type' => 'date', 'label' => '신청일', 'required' => true],
|
||||
['name' => 'paymentDate', 'type' => 'date', 'label' => '지급일', 'required' => true],
|
||||
['name' => 'items', 'type' => 'array', 'label' => '지출항목', 'required' => true],
|
||||
['name' => 'totalAmount', 'type' => 'number', 'label' => '총액', 'required' => true],
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => '비용견적서',
|
||||
'code' => 'expenseEstimate',
|
||||
'category' => '경비',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'items', 'type' => 'array', 'label' => '비용항목', 'required' => true],
|
||||
['name' => 'totalExpense', 'type' => 'number', 'label' => '총지출', 'required' => true],
|
||||
['name' => 'accountBalance', 'type' => 'number', 'label' => '계좌잔액', 'required' => true],
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => '근태신청',
|
||||
'code' => 'attendance_request',
|
||||
'category' => '일반',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'user_name', 'type' => 'text', 'label' => '신청자', 'required' => true],
|
||||
['name' => 'request_type', 'type' => 'select', 'label' => '신청유형', 'required' => true],
|
||||
['name' => 'period', 'type' => 'daterange', 'label' => '기간', 'required' => true],
|
||||
['name' => 'days', 'type' => 'number', 'label' => '일수', 'required' => true],
|
||||
['name' => 'reason', 'type' => 'textarea', 'label' => '사유', 'required' => true],
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => '사유서',
|
||||
'code' => 'reason_report',
|
||||
'category' => '일반',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'user_name', 'type' => 'text', 'label' => '작성자', 'required' => true],
|
||||
['name' => 'report_type', 'type' => 'select', 'label' => '사유유형', 'required' => true],
|
||||
['name' => 'target_date', 'type' => 'date', 'label' => '대상일', 'required' => true],
|
||||
['name' => 'reason', 'type' => 'textarea', 'label' => '사유', 'required' => true],
|
||||
],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($forms as $form) {
|
||||
DB::table('approval_forms')->updateOrInsert(
|
||||
['tenant_id' => $tenantId, 'code' => $form['code']],
|
||||
[
|
||||
'name' => $form['name'],
|
||||
'category' => $form['category'],
|
||||
'template' => $form['template'],
|
||||
'is_active' => true,
|
||||
'updated_at' => $now,
|
||||
'created_at' => $now,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,7 +498,9 @@ public function getDetail(
|
||||
?int $fixedAmountPerMonth = 200000,
|
||||
?float $ratio = 0.05,
|
||||
?int $year = null,
|
||||
?int $quarter = null
|
||||
?int $quarter = null,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null
|
||||
): array {
|
||||
$tenantId = $this->tenantId();
|
||||
$now = Carbon::now();
|
||||
@@ -562,8 +564,10 @@ public function getDetail(
|
||||
// 3. 항목별 분포
|
||||
$categoryDistribution = $this->getCategoryDistribution($tenantId, $annualStartDate, $annualEndDate);
|
||||
|
||||
// 4. 일별 사용 내역
|
||||
$transactions = $this->getTransactions($tenantId, $quarterStartDate, $quarterEndDate);
|
||||
// 4. 일별 사용 내역 (커스텀 날짜 범위가 있으면 해당 범위, 없으면 분기 기준)
|
||||
$txStartDate = $startDate ?? $quarterStartDate;
|
||||
$txEndDate = $endDate ?? $quarterEndDate;
|
||||
$transactions = $this->getTransactions($tenantId, $txStartDate, $txEndDate);
|
||||
|
||||
// 5. 계산 정보
|
||||
$calculation = [
|
||||
|
||||
@@ -259,6 +259,17 @@ public function store(array $data)
|
||||
$salesOrderId = $data['sales_order_id'] ?? null;
|
||||
unset($data['items'], $data['bending_detail']);
|
||||
|
||||
// 공정의 is_auxiliary 플래그를 WO options에 복사
|
||||
if (! empty($data['process_id'])) {
|
||||
$process = \App\Models\Process::find($data['process_id']);
|
||||
if ($process && ! empty($process->options['is_auxiliary'])) {
|
||||
$opts = $data['options'] ?? [];
|
||||
$opts = is_array($opts) ? $opts : (json_decode($opts, true) ?? []);
|
||||
$opts['is_auxiliary'] = true;
|
||||
$data['options'] = $opts;
|
||||
}
|
||||
}
|
||||
|
||||
$workOrder = WorkOrder::create($data);
|
||||
|
||||
// process 관계 로드 (isBending 체크용)
|
||||
@@ -815,6 +826,11 @@ private function syncOrderStatus(WorkOrder $workOrder, int $tenantId): void
|
||||
return;
|
||||
}
|
||||
|
||||
// 보조 공정(재고생산 등)은 수주 상태에 영향 주지 않음
|
||||
if ($this->isAuxiliaryWorkOrder($workOrder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = Order::where('tenant_id', $tenantId)->find($workOrder->sales_order_id);
|
||||
if (! $order) {
|
||||
return;
|
||||
@@ -850,6 +866,47 @@ private function syncOrderStatus(WorkOrder $workOrder, int $tenantId): void
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자재 투입 시 작업지시가 대기 상태이면 자동으로 진행중으로 전환
|
||||
*
|
||||
* pending/waiting 상태에서 첫 자재 투입이 발생하면
|
||||
* 작업지시 → in_progress, 수주 → IN_PRODUCTION 으로 자동 전환
|
||||
*/
|
||||
private function autoStartWorkOrderOnMaterialInput(WorkOrder $workOrder, int $tenantId): void
|
||||
{
|
||||
// 보조 공정(재고생산 등)은 WO 자체는 진행중으로 전환하되, 수주 상태는 변경하지 않음
|
||||
$isAuxiliary = $this->isAuxiliaryWorkOrder($workOrder);
|
||||
|
||||
// 아직 진행 전인 상태에서만 자동 전환 (자재투입 = 실질적 작업 시작)
|
||||
if (! in_array($workOrder->status, [
|
||||
WorkOrder::STATUS_UNASSIGNED,
|
||||
WorkOrder::STATUS_PENDING,
|
||||
WorkOrder::STATUS_WAITING,
|
||||
])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldStatus = $workOrder->status;
|
||||
$workOrder->status = WorkOrder::STATUS_IN_PROGRESS;
|
||||
$workOrder->updated_by = $this->apiUserId();
|
||||
$workOrder->save();
|
||||
|
||||
// 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrder->id,
|
||||
'status_auto_changed_on_material_input',
|
||||
['status' => $oldStatus],
|
||||
['status' => WorkOrder::STATUS_IN_PROGRESS]
|
||||
);
|
||||
|
||||
// 보조 공정이 아닌 경우만 수주 상태 동기화
|
||||
if (! $isAuxiliary) {
|
||||
$this->syncOrderStatus($workOrder, $tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업지시 품목에 결과 데이터 저장
|
||||
*/
|
||||
@@ -890,6 +947,16 @@ private function saveItemResults(WorkOrder $workOrder, ?array $resultData, int $
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 보조 공정(재고생산 등) 여부 판단
|
||||
*/
|
||||
private function isAuxiliaryWorkOrder(WorkOrder $workOrder): bool
|
||||
{
|
||||
$options = is_array($workOrder->options) ? $workOrder->options : (json_decode($workOrder->options, true) ?? []);
|
||||
|
||||
return ! empty($options['is_auxiliary']);
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT 번호 생성
|
||||
*/
|
||||
@@ -1458,6 +1525,9 @@ public function registerMaterialInput(int $workOrderId, array $inputs): array
|
||||
$totalCount = array_sum(array_column($delegatedResults, 'material_count'));
|
||||
$allResults = array_merge(...array_map(fn ($r) => $r['input_results'], $delegatedResults));
|
||||
|
||||
// 자재 투입 시 작업지시가 대기 상태면 자동으로 진행중으로 전환
|
||||
$this->autoStartWorkOrderOnMaterialInput($workOrder, $tenantId);
|
||||
|
||||
return [
|
||||
'work_order_id' => $workOrderId,
|
||||
'material_count' => $totalCount,
|
||||
@@ -1536,6 +1606,9 @@ public function registerMaterialInput(int $workOrderId, array $inputs): array
|
||||
$allResults = array_merge($allResults, $dr['input_results']);
|
||||
}
|
||||
|
||||
// 자재 투입 시 작업지시가 대기 상태면 자동으로 진행중으로 전환
|
||||
$this->autoStartWorkOrderOnMaterialInput($workOrder, $tenantId);
|
||||
|
||||
return [
|
||||
'work_order_id' => $workOrderId,
|
||||
'material_count' => count($allResults),
|
||||
@@ -2358,11 +2431,26 @@ public function resolveInspectionDocument(int $workOrderId, array $params = []):
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
// Lazy Snapshot 대상: rendered_html이 없는 문서 (상태 무관)
|
||||
$snapshotDocumentId = null;
|
||||
$snapshotCandidate = Document::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('template_id', $templateId)
|
||||
->where('linkable_type', 'work_order')
|
||||
->where('linkable_id', $workOrderId)
|
||||
->whereNull('rendered_html')
|
||||
->latest()
|
||||
->value('id');
|
||||
if ($snapshotCandidate) {
|
||||
$snapshotDocumentId = $snapshotCandidate;
|
||||
}
|
||||
|
||||
return [
|
||||
'work_order_id' => $workOrderId,
|
||||
'template_id' => $templateId,
|
||||
'template' => $formattedTemplate,
|
||||
'existing_document' => $existingDocument,
|
||||
'snapshot_document_id' => $snapshotDocumentId,
|
||||
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
|
||||
];
|
||||
}
|
||||
@@ -2436,10 +2524,14 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
|
||||
])
|
||||
->toArray();
|
||||
|
||||
$document = $documentService->update($existingDocument->id, [
|
||||
$updateData = [
|
||||
'title' => $inspectionData['title'] ?? $existingDocument->title,
|
||||
'data' => array_merge($existingBasicFields, $documentDataRecords),
|
||||
]);
|
||||
];
|
||||
if (isset($inspectionData['rendered_html'])) {
|
||||
$updateData['rendered_html'] = $inspectionData['rendered_html'];
|
||||
}
|
||||
$document = $documentService->update($existingDocument->id, $updateData);
|
||||
|
||||
$action = 'inspection_document_updated';
|
||||
} else {
|
||||
@@ -2451,6 +2543,9 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
|
||||
'data' => $documentDataRecords,
|
||||
'approvers' => $inspectionData['approvers'] ?? [],
|
||||
];
|
||||
if (isset($inspectionData['rendered_html'])) {
|
||||
$documentData['rendered_html'] = $inspectionData['rendered_html'];
|
||||
}
|
||||
|
||||
$document = $documentService->create($documentData);
|
||||
$action = 'inspection_document_created';
|
||||
@@ -3067,20 +3162,28 @@ public function createWorkLog(int $workOrderId, array $workLogData): array
|
||||
$documentDataRecords = $this->transformWorkLogDataToRecords($workLogData, $workOrder, $template);
|
||||
|
||||
if ($existingDocument) {
|
||||
$document = $documentService->update($existingDocument->id, [
|
||||
$updateData = [
|
||||
'title' => $workLogData['title'] ?? $existingDocument->title,
|
||||
'data' => $documentDataRecords,
|
||||
]);
|
||||
];
|
||||
if (isset($workLogData['rendered_html'])) {
|
||||
$updateData['rendered_html'] = $workLogData['rendered_html'];
|
||||
}
|
||||
$document = $documentService->update($existingDocument->id, $updateData);
|
||||
$action = 'work_log_updated';
|
||||
} else {
|
||||
$document = $documentService->create([
|
||||
$createData = [
|
||||
'template_id' => $templateId,
|
||||
'title' => $workLogData['title'] ?? "작업일지 - {$workOrder->work_order_no}",
|
||||
'linkable_type' => 'work_order',
|
||||
'linkable_id' => $workOrderId,
|
||||
'data' => $documentDataRecords,
|
||||
'approvers' => $workLogData['approvers'] ?? [],
|
||||
]);
|
||||
];
|
||||
if (isset($workLogData['rendered_html'])) {
|
||||
$createData['rendered_html'] = $workLogData['rendered_html'];
|
||||
}
|
||||
$document = $documentService->create($createData);
|
||||
$action = 'work_log_created';
|
||||
}
|
||||
|
||||
|
||||
188
app/Swagger/v1/ProductionOrderApi.php
Normal file
188
app/Swagger/v1/ProductionOrderApi.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="ProductionOrders", description="생산지시 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ProductionOrderListItem",
|
||||
* type="object",
|
||||
* description="생산지시 목록 아이템",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1, description="수주 ID"),
|
||||
* @OA\Property(property="order_no", type="string", example="ORD-20260301-0001", description="수주번호 (= 생산지시번호)"),
|
||||
* @OA\Property(property="site_name", type="string", example="서울현장", nullable=true, description="현장명"),
|
||||
* @OA\Property(property="client_name", type="string", example="(주)고객사", nullable=true, description="거래처명"),
|
||||
* @OA\Property(property="quantity", type="number", example=232, description="부품수량 합계"),
|
||||
* @OA\Property(property="node_count", type="integer", example=4, description="개소수 (order_nodes 수)"),
|
||||
* @OA\Property(property="delivery_date", type="string", format="date", example="2026-03-15", nullable=true, description="납기일"),
|
||||
* @OA\Property(property="production_ordered_at", type="string", format="date", example="2026-02-21", nullable=true, description="생산지시일 (첫 WorkOrder 생성일, Y-m-d)"),
|
||||
* @OA\Property(property="production_status", type="string", enum={"waiting","in_production","completed"}, example="waiting", description="생산 상태"),
|
||||
* @OA\Property(property="work_orders_count", type="integer", example=2, description="작업지시 수 (공정별 1건)"),
|
||||
* @OA\Property(property="work_order_progress", type="object",
|
||||
* @OA\Property(property="total", type="integer", example=3),
|
||||
* @OA\Property(property="completed", type="integer", example=1),
|
||||
* @OA\Property(property="in_progress", type="integer", example=1)
|
||||
* ),
|
||||
* @OA\Property(property="client", type="object", nullable=true,
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="(주)고객사")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ProductionOrderStats",
|
||||
* type="object",
|
||||
* description="생산지시 통계",
|
||||
*
|
||||
* @OA\Property(property="total", type="integer", example=25, description="전체"),
|
||||
* @OA\Property(property="waiting", type="integer", example=10, description="생산대기"),
|
||||
* @OA\Property(property="in_production", type="integer", example=8, description="생산중"),
|
||||
* @OA\Property(property="completed", type="integer", example=7, description="생산완료")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ProductionOrderDetail",
|
||||
* type="object",
|
||||
* description="생산지시 상세",
|
||||
*
|
||||
* @OA\Property(property="order", ref="#/components/schemas/ProductionOrderListItem"),
|
||||
* @OA\Property(property="production_ordered_at", type="string", format="date", example="2026-02-21", nullable=true),
|
||||
* @OA\Property(property="production_status", type="string", enum={"waiting","in_production","completed"}),
|
||||
* @OA\Property(property="node_count", type="integer", example=4, description="개소수"),
|
||||
* @OA\Property(property="work_order_progress", type="object",
|
||||
* @OA\Property(property="total", type="integer"),
|
||||
* @OA\Property(property="completed", type="integer"),
|
||||
* @OA\Property(property="in_progress", type="integer")
|
||||
* ),
|
||||
* @OA\Property(property="work_orders", type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="work_order_no", type="string"),
|
||||
* @OA\Property(property="process_name", type="string"),
|
||||
* @OA\Property(property="quantity", type="integer"),
|
||||
* @OA\Property(property="status", type="string"),
|
||||
* @OA\Property(property="assignees", type="array", @OA\Items(type="string"))
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="bom_process_groups", type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="process_name", type="string"),
|
||||
* @OA\Property(property="size_spec", type="string", nullable=true),
|
||||
* @OA\Property(property="items", type="array",
|
||||
*
|
||||
* @OA\Items(type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", nullable=true),
|
||||
* @OA\Property(property="item_code", type="string"),
|
||||
* @OA\Property(property="item_name", type="string"),
|
||||
* @OA\Property(property="spec", type="string"),
|
||||
* @OA\Property(property="lot_no", type="string"),
|
||||
* @OA\Property(property="required_qty", type="number"),
|
||||
* @OA\Property(property="qty", type="number")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
class ProductionOrderApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/production-orders",
|
||||
* tags={"ProductionOrders"},
|
||||
* summary="생산지시 목록 조회",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="search", in="query", required=false,
|
||||
*
|
||||
* @OA\Schema(type="string"), description="검색어 (수주번호, 거래처명, 현장명)"
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(name="production_status", in="query", required=false,
|
||||
*
|
||||
* @OA\Schema(type="string", enum={"waiting","in_production","completed"}), description="생산 상태 필터"
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(name="sort_by", in="query", required=false,
|
||||
*
|
||||
* @OA\Schema(type="string", enum={"created_at","delivery_date","order_no"}), description="정렬 기준"
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(name="sort_dir", in="query", required=false,
|
||||
*
|
||||
* @OA\Schema(type="string", enum={"asc","desc"}), description="정렬 방향"
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", type="object",
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ProductionOrderListItem")),
|
||||
* @OA\Property(property="current_page", type="integer"),
|
||||
* @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/production-orders/stats",
|
||||
* tags={"ProductionOrders"},
|
||||
* summary="생산지시 상태별 통계",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Response(response=200, description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ProductionOrderStats")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function stats() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/production-orders/{orderId}",
|
||||
* tags={"ProductionOrders"},
|
||||
* summary="생산지시 상세 조회",
|
||||
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="orderId", in="path", required=true, @OA\Schema(type="integer"), description="수주 ID"),
|
||||
*
|
||||
* @OA\Response(response=200, description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ProductionOrderDetail")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="생산지시를 찾을 수 없음")
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
}
|
||||
98
app/Traits/SyncsExpenseAccounts.php
Normal file
98
app/Traits/SyncsExpenseAccounts.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\Tenants\ExpenseAccount;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
|
||||
/**
|
||||
* 전표 저장/수정/삭제 시 expense_accounts 동기화 (복리후생비/접대비 → CEO 대시보드)
|
||||
*
|
||||
* 사용처: GeneralJournalEntryService, JournalSyncService
|
||||
*/
|
||||
trait SyncsExpenseAccounts
|
||||
{
|
||||
/**
|
||||
* 계정과목명 → expense_accounts account_type 매핑
|
||||
*/
|
||||
private static array $expenseAccountMap = [
|
||||
'복리후생비' => ExpenseAccount::TYPE_WELFARE,
|
||||
'접대비' => ExpenseAccount::TYPE_ENTERTAINMENT,
|
||||
];
|
||||
|
||||
/**
|
||||
* 전표 저장/수정 후 expense_accounts 동기화
|
||||
* 복리후생비/접대비 계정과목이 포함된 lines → expense_accounts에 반영
|
||||
*/
|
||||
protected function syncExpenseAccounts(JournalEntry $entry): void
|
||||
{
|
||||
$tenantId = $entry->tenant_id;
|
||||
|
||||
// 1. 기존 이 전표에서 생성된 expense_accounts 삭제
|
||||
ExpenseAccount::where('tenant_id', $tenantId)
|
||||
->where('journal_entry_id', $entry->id)
|
||||
->forceDelete();
|
||||
|
||||
// 2. 현재 lines 중 복리후생비/접대비 해당하는 것만 insert
|
||||
$lines = $entry->lines()->get();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$accountType = $this->getExpenseAccountType($line->account_name);
|
||||
if (! $accountType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 차변(debit)이 대변보다 큰 경우만 비용 발생으로 처리
|
||||
$amount = $line->debit_amount - $line->credit_amount;
|
||||
if ($amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// source_type에 따라 payment_method 결정
|
||||
$paymentMethod = match ($entry->source_type) {
|
||||
JournalEntry::SOURCE_CARD_TRANSACTION => ExpenseAccount::PAYMENT_CARD,
|
||||
default => ExpenseAccount::PAYMENT_TRANSFER,
|
||||
};
|
||||
|
||||
ExpenseAccount::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'account_type' => $accountType,
|
||||
'sub_type' => null,
|
||||
'expense_date' => $entry->entry_date,
|
||||
'amount' => $amount,
|
||||
'description' => $line->description ?? $entry->description,
|
||||
'receipt_no' => null,
|
||||
'vendor_id' => $line->trading_partner_id,
|
||||
'vendor_name' => $line->trading_partner_name,
|
||||
'payment_method' => $paymentMethod,
|
||||
'card_no' => null,
|
||||
'journal_entry_id' => $entry->id,
|
||||
'journal_entry_line_id' => $line->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전표 삭제 시 expense_accounts 정리
|
||||
*/
|
||||
protected function cleanupExpenseAccounts(int $tenantId, int $entryId): void
|
||||
{
|
||||
ExpenseAccount::where('tenant_id', $tenantId)
|
||||
->where('journal_entry_id', $entryId)
|
||||
->forceDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목명에서 비용 유형 판별
|
||||
*/
|
||||
private function getExpenseAccountType(string $accountName): ?string
|
||||
{
|
||||
foreach (self::$expenseAccountMap as $keyword => $type) {
|
||||
if (str_contains($accountName, $keyword)) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,26 @@
|
||||
]) : [],
|
||||
],
|
||||
|
||||
// Codebridge DB (이관된 Sales/Finance/Admin 등)
|
||||
'codebridge' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('CODEBRIDGE_DB_HOST', env('DB_HOST', '127.0.0.1')),
|
||||
'port' => env('CODEBRIDGE_DB_PORT', env('DB_PORT', '3306')),
|
||||
'database' => env('CODEBRIDGE_DB_DATABASE', 'codebridge'),
|
||||
'username' => env('CODEBRIDGE_DB_USERNAME', env('DB_USERNAME', 'root')),
|
||||
'password' => env('CODEBRIDGE_DB_PASSWORD', env('DB_PASSWORD', '')),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
// 5130 레거시 DB (chandj)
|
||||
'chandj' => [
|
||||
'driver' => 'mysql',
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'permission' => 0664,
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
@@ -138,6 +139,7 @@
|
||||
*/
|
||||
'api' => [
|
||||
'driver' => 'daily',
|
||||
'permission' => 0664,
|
||||
'path' => storage_path('logs/api/api.log'),
|
||||
'level' => 'info',
|
||||
'days' => env('API_LOG_DAYS', 14),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('quality_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained();
|
||||
$table->string('quality_doc_number', 30)->comment('품질관리서 번호');
|
||||
$table->string('site_name')->comment('현장명');
|
||||
$table->string('status', 20)->default('received')->comment('received/in_progress/completed');
|
||||
$table->foreignId('client_id')->nullable()->constrained('clients')->comment('수주처');
|
||||
$table->foreignId('inspector_id')->nullable()->constrained('users')->comment('검사자');
|
||||
$table->date('received_date')->nullable()->comment('접수일');
|
||||
$table->json('options')->nullable()->comment('관련자정보, 검사정보, 현장주소 등');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'quality_doc_number']);
|
||||
$table->index(['tenant_id', 'status']);
|
||||
$table->index(['tenant_id', 'client_id']);
|
||||
$table->index(['tenant_id', 'inspector_id']);
|
||||
$table->index(['tenant_id', 'received_date']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('quality_document_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('order_id')->constrained('orders');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['quality_document_id', 'order_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_document_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('quality_document_locations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('quality_document_order_id')->constrained('quality_document_orders', 'id', 'qdl_qdo_id_fk')->cascadeOnDelete();
|
||||
$table->foreignId('order_item_id')->constrained('order_items');
|
||||
$table->integer('post_width')->nullable()->comment('시공후 가로');
|
||||
$table->integer('post_height')->nullable()->comment('시공후 세로');
|
||||
$table->string('change_reason')->nullable()->comment('규격 변경사유');
|
||||
$table->foreignId('document_id')->nullable()->comment('검사성적서 문서 ID');
|
||||
$table->string('inspection_status', 20)->default('pending')->comment('pending/completed');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['quality_document_id', 'inspection_status'], 'qdl_doc_id_status_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quality_document_locations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('performance_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained();
|
||||
$table->foreignId('quality_document_id')->constrained();
|
||||
$table->unsignedSmallInteger('year')->comment('연도');
|
||||
$table->unsignedTinyInteger('quarter')->comment('분기 1-4');
|
||||
$table->string('confirmation_status', 20)->default('unconfirmed')->comment('unconfirmed/confirmed/reported');
|
||||
$table->date('confirmed_date')->nullable();
|
||||
$table->foreignId('confirmed_by')->nullable()->constrained('users');
|
||||
$table->text('memo')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'quality_document_id']);
|
||||
$table->index(['tenant_id', 'year', 'quarter']);
|
||||
$table->index(['tenant_id', 'confirmation_status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('performance_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('quality_document_locations', function (Blueprint $table) {
|
||||
$table->json('inspection_data')->nullable()->after('change_reason')->comment('검사 데이터 JSON');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('quality_document_locations', function (Blueprint $table) {
|
||||
$table->dropColumn('inspection_data');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('expense_accounts', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('journal_entry_id')->nullable()->after('loan_id');
|
||||
$table->unsignedBigInteger('journal_entry_line_id')->nullable()->after('journal_entry_id');
|
||||
|
||||
$table->index(['tenant_id', 'journal_entry_id']);
|
||||
$table->index(['journal_entry_line_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('expense_accounts', function (Blueprint $table) {
|
||||
$table->dropIndex(['tenant_id', 'journal_entry_id']);
|
||||
$table->dropIndex(['journal_entry_line_id']);
|
||||
$table->dropColumn(['journal_entry_id', 'journal_entry_line_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('document_template_sections', function (Blueprint $table) {
|
||||
$table->text('description')->nullable()->after('title')->comment('섹션 설명/안내문');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('document_template_sections', function (Blueprint $table) {
|
||||
$table->dropColumn('description');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 계정과목 마스터 확장
|
||||
*
|
||||
* - sub_category: 중분류 (유동자산, 판관비, 매출원가 등)
|
||||
* - parent_code: 상위 계정과목 코드 (계층 구조)
|
||||
* - depth: 계층 깊이 (1=대, 2=중, 3=소)
|
||||
* - department_type: 부문 (common=공통, manufacturing=제조, admin=관리)
|
||||
* - description: 계정과목 설명
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('account_codes', function (Blueprint $table) {
|
||||
$table->string('sub_category', 50)->nullable()->after('category')
|
||||
->comment('중분류 (current_asset, fixed_asset, selling_admin, cogs 등)');
|
||||
$table->string('parent_code', 10)->nullable()->after('sub_category')
|
||||
->comment('상위 계정과목 코드 (계층 구조)');
|
||||
$table->tinyInteger('depth')->default(3)->after('parent_code')
|
||||
->comment('계층 깊이 (1=대분류, 2=중분류, 3=소분류)');
|
||||
$table->string('department_type', 20)->default('common')->after('depth')
|
||||
->comment('부문 (common=공통, manufacturing=제조, admin=관리)');
|
||||
$table->string('description', 500)->nullable()->after('department_type')
|
||||
->comment('계정과목 설명');
|
||||
|
||||
$table->index(['tenant_id', 'category'], 'account_codes_tenant_category_idx');
|
||||
$table->index(['tenant_id', 'parent_code'], 'account_codes_tenant_parent_idx');
|
||||
$table->index(['tenant_id', 'depth'], 'account_codes_tenant_depth_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('account_codes', function (Blueprint $table) {
|
||||
$table->dropIndex('account_codes_tenant_category_idx');
|
||||
$table->dropIndex('account_codes_tenant_parent_idx');
|
||||
$table->dropIndex('account_codes_tenant_depth_idx');
|
||||
|
||||
$table->dropColumn([
|
||||
'sub_category',
|
||||
'parent_code',
|
||||
'depth',
|
||||
'department_type',
|
||||
'description',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 모든 기존 테넌트에 더존 Smart A 표준 계정과목 128건 자동 시드
|
||||
*
|
||||
* 조건: tenant_id + code 중복 시 skip (기존 데이터 보호)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenantIds = DB::table('tenants')
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id');
|
||||
|
||||
$defaults = $this->getDefaultAccountCodes();
|
||||
$now = now();
|
||||
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
// 이미 등록된 코드 조회
|
||||
$existingCodes = DB::table('account_codes')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('code')
|
||||
->toArray();
|
||||
|
||||
$inserts = [];
|
||||
foreach ($defaults as $item) {
|
||||
if (! in_array($item['code'], $existingCodes)) {
|
||||
$inserts[] = array_merge($item, [
|
||||
'tenant_id' => $tenantId,
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($inserts)) {
|
||||
// 500건 단위 청크 insert
|
||||
foreach (array_chunk($inserts, 500) as $chunk) {
|
||||
DB::table('account_codes')->insert($chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 시드 데이터만 롤백 (수동 추가 데이터는 보호)
|
||||
$defaultCodes = array_column($this->getDefaultAccountCodes(), 'code');
|
||||
|
||||
DB::table('account_codes')
|
||||
->whereIn('code', $defaultCodes)
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function getDefaultAccountCodes(): array
|
||||
{
|
||||
$c = fn ($code, $name, $cat, $sub, $parent, $depth, $dept, $sort) => [
|
||||
'code' => $code, 'name' => $name, 'category' => $cat,
|
||||
'sub_category' => $sub, 'parent_code' => $parent,
|
||||
'depth' => $depth, 'department_type' => $dept, 'sort_order' => $sort,
|
||||
];
|
||||
|
||||
return [
|
||||
// 자산 (Assets)
|
||||
$c('1', '자산', 'asset', null, null, 1, 'common', 100),
|
||||
$c('11', '유동자산', 'asset', 'current_asset', '1', 2, 'common', 110),
|
||||
$c('10100', '현금', 'asset', 'current_asset', '11', 3, 'common', 1010),
|
||||
$c('10200', '당좌예금', 'asset', 'current_asset', '11', 3, 'common', 1020),
|
||||
$c('10300', '보통예금', 'asset', 'current_asset', '11', 3, 'common', 1030),
|
||||
$c('10400', '기타제예금', 'asset', 'current_asset', '11', 3, 'common', 1040),
|
||||
$c('10500', '정기적금', 'asset', 'current_asset', '11', 3, 'common', 1050),
|
||||
$c('10800', '외상매출금', 'asset', 'current_asset', '11', 3, 'common', 1080),
|
||||
$c('10900', '대손충당금(외상매출금)', 'asset', 'current_asset', '11', 3, 'common', 1090),
|
||||
$c('11000', '받을어음', 'asset', 'current_asset', '11', 3, 'common', 1100),
|
||||
$c('11400', '단기대여금', 'asset', 'current_asset', '11', 3, 'common', 1140),
|
||||
$c('11600', '미수수익', 'asset', 'current_asset', '11', 3, 'common', 1160),
|
||||
$c('12000', '미수금', 'asset', 'current_asset', '11', 3, 'common', 1200),
|
||||
$c('12200', '소모품', 'asset', 'current_asset', '11', 3, 'common', 1220),
|
||||
$c('12500', '미환급세금', 'asset', 'current_asset', '11', 3, 'common', 1250),
|
||||
$c('13100', '선급금', 'asset', 'current_asset', '11', 3, 'common', 1310),
|
||||
$c('13300', '선급비용', 'asset', 'current_asset', '11', 3, 'common', 1330),
|
||||
$c('13400', '가지급금', 'asset', 'current_asset', '11', 3, 'common', 1340),
|
||||
$c('13500', '부가세대급금', 'asset', 'current_asset', '11', 3, 'common', 1350),
|
||||
$c('13600', '선납세금', 'asset', 'current_asset', '11', 3, 'common', 1360),
|
||||
$c('14000', '선납법인세', 'asset', 'current_asset', '11', 3, 'common', 1400),
|
||||
$c('12', '재고자산', 'asset', 'current_asset', '1', 2, 'common', 120),
|
||||
$c('14600', '상품', 'asset', 'current_asset', '12', 3, 'common', 1460),
|
||||
$c('15000', '제품', 'asset', 'current_asset', '12', 3, 'common', 1500),
|
||||
$c('15300', '원재료', 'asset', 'current_asset', '12', 3, 'common', 1530),
|
||||
$c('16200', '부재료', 'asset', 'current_asset', '12', 3, 'common', 1620),
|
||||
$c('16700', '저장품', 'asset', 'current_asset', '12', 3, 'common', 1670),
|
||||
$c('16900', '재공품', 'asset', 'current_asset', '12', 3, 'common', 1690),
|
||||
$c('13', '비유동자산', 'asset', 'fixed_asset', '1', 2, 'common', 130),
|
||||
$c('17600', '장기성예금', 'asset', 'fixed_asset', '13', 3, 'common', 1760),
|
||||
$c('17900', '장기대여금', 'asset', 'fixed_asset', '13', 3, 'common', 1790),
|
||||
$c('18700', '투자부동산', 'asset', 'fixed_asset', '13', 3, 'common', 1870),
|
||||
$c('19200', '단체퇴직보험예치금', 'asset', 'fixed_asset', '13', 3, 'common', 1920),
|
||||
$c('20100', '토지', 'asset', 'fixed_asset', '13', 3, 'common', 2010),
|
||||
$c('20200', '건물', 'asset', 'fixed_asset', '13', 3, 'common', 2020),
|
||||
$c('20300', '감가상각누계액(건물)', 'asset', 'fixed_asset', '13', 3, 'common', 2030),
|
||||
$c('20400', '구축물', 'asset', 'fixed_asset', '13', 3, 'common', 2040),
|
||||
$c('20500', '감가상각누계액(구축물)', 'asset', 'fixed_asset', '13', 3, 'common', 2050),
|
||||
$c('20600', '기계장치', 'asset', 'fixed_asset', '13', 3, 'common', 2060),
|
||||
$c('20700', '감가상각누계액(기계장치)', 'asset', 'fixed_asset', '13', 3, 'common', 2070),
|
||||
$c('20800', '차량운반구', 'asset', 'fixed_asset', '13', 3, 'common', 2080),
|
||||
$c('20900', '감가상각누계액(차량운반구)', 'asset', 'fixed_asset', '13', 3, 'common', 2090),
|
||||
$c('21000', '공구와기구', 'asset', 'fixed_asset', '13', 3, 'common', 2100),
|
||||
$c('21200', '비품', 'asset', 'fixed_asset', '13', 3, 'common', 2120),
|
||||
$c('21300', '건설중인자산', 'asset', 'fixed_asset', '13', 3, 'common', 2130),
|
||||
$c('24000', '소프트웨어', 'asset', 'fixed_asset', '13', 3, 'common', 2400),
|
||||
// 부채 (Liabilities)
|
||||
$c('2', '부채', 'liability', null, null, 1, 'common', 200),
|
||||
$c('21', '유동부채', 'liability', 'current_liability', '2', 2, 'common', 210),
|
||||
$c('25100', '외상매입금', 'liability', 'current_liability', '21', 3, 'common', 2510),
|
||||
$c('25200', '지급어음', 'liability', 'current_liability', '21', 3, 'common', 2520),
|
||||
$c('25300', '미지급금', 'liability', 'current_liability', '21', 3, 'common', 2530),
|
||||
$c('25400', '예수금', 'liability', 'current_liability', '21', 3, 'common', 2540),
|
||||
$c('25500', '부가세예수금', 'liability', 'current_liability', '21', 3, 'common', 2550),
|
||||
$c('25900', '선수금', 'liability', 'current_liability', '21', 3, 'common', 2590),
|
||||
$c('26000', '단기차입금', 'liability', 'current_liability', '21', 3, 'common', 2600),
|
||||
$c('26100', '미지급세금', 'liability', 'current_liability', '21', 3, 'common', 2610),
|
||||
$c('26200', '미지급비용', 'liability', 'current_liability', '21', 3, 'common', 2620),
|
||||
$c('26400', '유동성장기차입금', 'liability', 'current_liability', '21', 3, 'common', 2640),
|
||||
$c('26500', '미지급배당금', 'liability', 'current_liability', '21', 3, 'common', 2650),
|
||||
$c('22', '비유동부채', 'liability', 'long_term_liability', '2', 2, 'common', 220),
|
||||
$c('29300', '장기차입금', 'liability', 'long_term_liability', '22', 3, 'common', 2930),
|
||||
$c('29400', '임대보증금', 'liability', 'long_term_liability', '22', 3, 'common', 2940),
|
||||
$c('29500', '퇴직급여충당부채', 'liability', 'long_term_liability', '22', 3, 'common', 2950),
|
||||
$c('30700', '장기임대보증금', 'liability', 'long_term_liability', '22', 3, 'common', 3070),
|
||||
// 자본 (Capital)
|
||||
$c('3', '자본', 'capital', null, null, 1, 'common', 300),
|
||||
$c('31', '자본금', 'capital', 'capital', '3', 2, 'common', 310),
|
||||
$c('33100', '자본금', 'capital', 'capital', '31', 3, 'common', 3310),
|
||||
$c('33200', '우선주자본금', 'capital', 'capital', '31', 3, 'common', 3320),
|
||||
$c('32', '잉여금', 'capital', 'capital', '3', 2, 'common', 320),
|
||||
$c('34100', '주식발행초과금', 'capital', 'capital', '32', 3, 'common', 3410),
|
||||
$c('35100', '이익준비금', 'capital', 'capital', '32', 3, 'common', 3510),
|
||||
$c('37500', '이월이익잉여금', 'capital', 'capital', '32', 3, 'common', 3750),
|
||||
$c('37900', '당기순이익', 'capital', 'capital', '32', 3, 'common', 3790),
|
||||
// 수익 (Revenue)
|
||||
$c('4', '수익', 'revenue', null, null, 1, 'common', 400),
|
||||
$c('41', '매출', 'revenue', 'sales_revenue', '4', 2, 'common', 410),
|
||||
$c('40100', '상품매출', 'revenue', 'sales_revenue', '41', 3, 'common', 4010),
|
||||
$c('40400', '제품매출', 'revenue', 'sales_revenue', '41', 3, 'common', 4040),
|
||||
$c('40700', '공사수입금', 'revenue', 'sales_revenue', '41', 3, 'common', 4070),
|
||||
$c('41000', '임대료수입', 'revenue', 'sales_revenue', '41', 3, 'common', 4100),
|
||||
$c('42', '영업외수익', 'revenue', 'other_revenue', '4', 2, 'common', 420),
|
||||
$c('90100', '이자수익', 'revenue', 'other_revenue', '42', 3, 'common', 9010),
|
||||
$c('90300', '배당금수익', 'revenue', 'other_revenue', '42', 3, 'common', 9030),
|
||||
$c('90400', '수입임대료', 'revenue', 'other_revenue', '42', 3, 'common', 9040),
|
||||
$c('90700', '외환차익', 'revenue', 'other_revenue', '42', 3, 'common', 9070),
|
||||
$c('93000', '잡이익', 'revenue', 'other_revenue', '42', 3, 'common', 9300),
|
||||
// 비용 (Expenses)
|
||||
$c('5', '비용', 'expense', null, null, 1, 'common', 500),
|
||||
$c('51', '매출원가', 'expense', 'cogs', '5', 2, 'manufacturing', 510),
|
||||
$c('50100', '원재료비', 'expense', 'cogs', '51', 3, 'manufacturing', 5010),
|
||||
$c('50200', '외주가공비', 'expense', 'cogs', '51', 3, 'manufacturing', 5020),
|
||||
$c('50300', '급여(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5030),
|
||||
$c('50400', '임금', 'expense', 'cogs', '51', 3, 'manufacturing', 5040),
|
||||
$c('50500', '상여금(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5050),
|
||||
$c('50800', '퇴직급여(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5080),
|
||||
$c('51100', '복리후생비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5110),
|
||||
$c('51200', '여비교통비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5120),
|
||||
$c('51300', '접대비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5130),
|
||||
$c('51400', '통신비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5140),
|
||||
$c('51600', '전력비', 'expense', 'cogs', '51', 3, 'manufacturing', 5160),
|
||||
$c('51700', '세금과공과금(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5170),
|
||||
$c('51800', '감가상각비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5180),
|
||||
$c('51900', '지급임차료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5190),
|
||||
$c('52000', '수선비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5200),
|
||||
$c('52100', '보험료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5210),
|
||||
$c('52200', '차량유지비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5220),
|
||||
$c('52400', '운반비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5240),
|
||||
$c('53000', '소모품비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5300),
|
||||
$c('53100', '지급수수료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5310),
|
||||
$c('52', '판매비와관리비', 'expense', 'selling_admin', '5', 2, 'admin', 520),
|
||||
$c('80100', '임원급여', 'expense', 'selling_admin', '52', 3, 'admin', 8010),
|
||||
$c('80200', '직원급여', 'expense', 'selling_admin', '52', 3, 'admin', 8020),
|
||||
$c('80300', '상여금', 'expense', 'selling_admin', '52', 3, 'admin', 8030),
|
||||
$c('80600', '퇴직급여', 'expense', 'selling_admin', '52', 3, 'admin', 8060),
|
||||
$c('81100', '복리후생비', 'expense', 'selling_admin', '52', 3, 'admin', 8110),
|
||||
$c('81200', '여비교통비', 'expense', 'selling_admin', '52', 3, 'admin', 8120),
|
||||
$c('81300', '접대비', 'expense', 'selling_admin', '52', 3, 'admin', 8130),
|
||||
$c('81400', '통신비', 'expense', 'selling_admin', '52', 3, 'admin', 8140),
|
||||
$c('81500', '수도광열비', 'expense', 'selling_admin', '52', 3, 'admin', 8150),
|
||||
$c('81700', '세금과공과금', 'expense', 'selling_admin', '52', 3, 'admin', 8170),
|
||||
$c('81800', '감가상각비', 'expense', 'selling_admin', '52', 3, 'admin', 8180),
|
||||
$c('81900', '지급임차료', 'expense', 'selling_admin', '52', 3, 'admin', 8190),
|
||||
$c('82000', '수선비', 'expense', 'selling_admin', '52', 3, 'admin', 8200),
|
||||
$c('82100', '보험료', 'expense', 'selling_admin', '52', 3, 'admin', 8210),
|
||||
$c('82200', '차량유지비', 'expense', 'selling_admin', '52', 3, 'admin', 8220),
|
||||
$c('82300', '경상연구개발비', 'expense', 'selling_admin', '52', 3, 'admin', 8230),
|
||||
$c('82400', '운반비', 'expense', 'selling_admin', '52', 3, 'admin', 8240),
|
||||
$c('82500', '교육훈련비', 'expense', 'selling_admin', '52', 3, 'admin', 8250),
|
||||
$c('82600', '도서인쇄비', 'expense', 'selling_admin', '52', 3, 'admin', 8260),
|
||||
$c('82700', '회의비', 'expense', 'selling_admin', '52', 3, 'admin', 8270),
|
||||
$c('82900', '사무용품비', 'expense', 'selling_admin', '52', 3, 'admin', 8290),
|
||||
$c('83000', '소모품비', 'expense', 'selling_admin', '52', 3, 'admin', 8300),
|
||||
$c('83100', '지급수수료', 'expense', 'selling_admin', '52', 3, 'admin', 8310),
|
||||
$c('83200', '보관료', 'expense', 'selling_admin', '52', 3, 'admin', 8320),
|
||||
$c('83300', '광고선전비', 'expense', 'selling_admin', '52', 3, 'admin', 8330),
|
||||
$c('83500', '대손상각비', 'expense', 'selling_admin', '52', 3, 'admin', 8350),
|
||||
$c('84800', '잡비', 'expense', 'selling_admin', '52', 3, 'admin', 8480),
|
||||
$c('53', '영업외비용', 'expense', 'other_expense', '5', 2, 'common', 530),
|
||||
$c('93100', '이자비용', 'expense', 'other_expense', '53', 3, 'common', 9310),
|
||||
$c('93200', '외환차손', 'expense', 'other_expense', '53', 3, 'common', 9320),
|
||||
$c('93300', '기부금', 'expense', 'other_expense', '53', 3, 'common', 9330),
|
||||
$c('96000', '잡손실', 'expense', 'other_expense', '53', 3, 'common', 9600),
|
||||
$c('99800', '법인세', 'expense', 'other_expense', '53', 3, 'common', 9980),
|
||||
$c('99900', '소득세등', 'expense', 'other_expense', '53', 3, 'common', 9990),
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 더존 Smart A 표준 계정과목 추가 시드 — 기획서 14장 기준 누락분 보완
|
||||
*
|
||||
* 조건: tenant_id + code 중복 시 skip (기존 데이터 보호)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenantIds = DB::table('tenants')
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id');
|
||||
|
||||
$defaults = $this->getAdditionalAccountCodes();
|
||||
$now = now();
|
||||
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
$existingCodes = DB::table('account_codes')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('code')
|
||||
->toArray();
|
||||
|
||||
$inserts = [];
|
||||
foreach ($defaults as $item) {
|
||||
if (! in_array($item['code'], $existingCodes)) {
|
||||
$inserts[] = array_merge($item, [
|
||||
'tenant_id' => $tenantId,
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($inserts)) {
|
||||
foreach (array_chunk($inserts, 500) as $chunk) {
|
||||
DB::table('account_codes')->insert($chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$codes = array_column($this->getAdditionalAccountCodes(), 'code');
|
||||
DB::table('account_codes')->whereIn('code', $codes)->delete();
|
||||
}
|
||||
|
||||
private function getAdditionalAccountCodes(): array
|
||||
{
|
||||
$c = fn ($code, $name, $cat, $sub, $parent, $depth, $dept, $sort) => [
|
||||
'code' => $code, 'name' => $name, 'category' => $cat,
|
||||
'sub_category' => $sub, 'parent_code' => $parent,
|
||||
'depth' => $depth, 'department_type' => $dept, 'sort_order' => $sort,
|
||||
];
|
||||
|
||||
return [
|
||||
// ================================================================
|
||||
// 새 depth-2 카테고리
|
||||
// ================================================================
|
||||
$c('33', '자본조정', 'capital', 'capital_adjustment', '3', 2, 'common', 330),
|
||||
$c('54', '건설원가', 'expense', 'construction_cost', '5', 2, 'construction', 540),
|
||||
|
||||
// ================================================================
|
||||
// 자산 — 유동자산 추가 (parent: '11')
|
||||
// ================================================================
|
||||
$c('10600', '기타단기금융상품예금', 'asset', 'current_asset', '11', 3, 'common', 1060),
|
||||
$c('10700', '단기투자자산', 'asset', 'current_asset', '11', 3, 'common', 1070),
|
||||
$c('11100', '대손충당금(받을어음)', 'asset', 'current_asset', '11', 3, 'common', 1110),
|
||||
$c('11200', '공사미수금', 'asset', 'current_asset', '11', 3, 'common', 1120),
|
||||
$c('11300', '대손충당금(공사미수금)', 'asset', 'current_asset', '11', 3, 'common', 1130),
|
||||
$c('11500', '대손충당금(단기대여금)', 'asset', 'current_asset', '11', 3, 'common', 1150),
|
||||
$c('11700', '대손충당금(미수수익)', 'asset', 'current_asset', '11', 3, 'common', 1170),
|
||||
$c('11800', '분양미수금', 'asset', 'current_asset', '11', 3, 'common', 1180),
|
||||
$c('11900', '대손충당금(분양미수금)', 'asset', 'current_asset', '11', 3, 'common', 1190),
|
||||
$c('12100', '대손충당금(미수금)', 'asset', 'current_asset', '11', 3, 'common', 1210),
|
||||
$c('12300', '매도가능증권', 'asset', 'current_asset', '11', 3, 'common', 1230),
|
||||
$c('12400', '만기보유증권', 'asset', 'current_asset', '11', 3, 'common', 1240),
|
||||
$c('13200', '대손충당금(선급금)', 'asset', 'current_asset', '11', 3, 'common', 1320),
|
||||
$c('13700', '주임종단기채권', 'asset', 'current_asset', '11', 3, 'common', 1370),
|
||||
$c('13800', '전도금', 'asset', 'current_asset', '11', 3, 'common', 1380),
|
||||
$c('13900', '선급공사비', 'asset', 'current_asset', '11', 3, 'common', 1390),
|
||||
|
||||
// ================================================================
|
||||
// 자산 — 재고자산 추가 (parent: '12')
|
||||
// ================================================================
|
||||
$c('14700', '매입환출및에누리(상품)', 'asset', 'current_asset', '12', 3, 'common', 1470),
|
||||
$c('14800', '매입할인(상품)', 'asset', 'current_asset', '12', 3, 'common', 1480),
|
||||
$c('14900', '관세환급금(상품)', 'asset', 'current_asset', '12', 3, 'common', 1490),
|
||||
$c('15100', '관세환급금(제품)', 'asset', 'current_asset', '12', 3, 'common', 1510),
|
||||
$c('15200', '완성건물', 'asset', 'current_asset', '12', 3, 'common', 1520),
|
||||
$c('15400', '매입환출및에누리(원재료)', 'asset', 'current_asset', '12', 3, 'common', 1540),
|
||||
$c('15500', '매입할인(원재료)', 'asset', 'current_asset', '12', 3, 'common', 1550),
|
||||
$c('15600', '원재료(도급)', 'asset', 'current_asset', '12', 3, 'common', 1560),
|
||||
$c('15700', '매입환출및에누리(원재료-도급)', 'asset', 'current_asset', '12', 3, 'common', 1570),
|
||||
$c('15800', '매입할인(원재료-도급)', 'asset', 'current_asset', '12', 3, 'common', 1580),
|
||||
$c('15900', '원재료(분양)', 'asset', 'current_asset', '12', 3, 'common', 1590),
|
||||
$c('16000', '매입환출및에누리(원재료-분양)', 'asset', 'current_asset', '12', 3, 'common', 1600),
|
||||
$c('16100', '매입할인(원재료-분양)', 'asset', 'current_asset', '12', 3, 'common', 1610),
|
||||
$c('16300', '매입환출및에누리(부재료)', 'asset', 'current_asset', '12', 3, 'common', 1630),
|
||||
$c('16400', '매입할인(부재료)', 'asset', 'current_asset', '12', 3, 'common', 1640),
|
||||
$c('16500', '건설용지', 'asset', 'current_asset', '12', 3, 'common', 1650),
|
||||
$c('16600', '가설재', 'asset', 'current_asset', '12', 3, 'common', 1660),
|
||||
$c('16800', '미착품', 'asset', 'current_asset', '12', 3, 'common', 1680),
|
||||
$c('17000', '미완성공사(도급)', 'asset', 'current_asset', '12', 3, 'common', 1700),
|
||||
$c('17100', '미완성공사(분양)', 'asset', 'current_asset', '12', 3, 'common', 1710),
|
||||
|
||||
// ================================================================
|
||||
// 자산 — 비유동자산 추가 (parent: '13')
|
||||
// ================================================================
|
||||
$c('17700', '특정현금과예금', 'asset', 'fixed_asset', '13', 3, 'common', 1770),
|
||||
$c('17800', '장기투자증권', 'asset', 'fixed_asset', '13', 3, 'common', 1780),
|
||||
$c('18000', '대손충당금(장기대여금)', 'asset', 'fixed_asset', '13', 3, 'common', 1800),
|
||||
$c('18100', '만기보유증권(장기)', 'asset', 'fixed_asset', '13', 3, 'common', 1810),
|
||||
$c('18200', '지분법적용투자주식', 'asset', 'fixed_asset', '13', 3, 'common', 1820),
|
||||
$c('19100', '출자금', 'asset', 'fixed_asset', '13', 3, 'common', 1910),
|
||||
$c('19700', '투자임대계약자산', 'asset', 'fixed_asset', '13', 3, 'common', 1970),
|
||||
$c('19800', '출자금(장기)', 'asset', 'fixed_asset', '13', 3, 'common', 1980),
|
||||
$c('19900', '퇴직보험예치금', 'asset', 'fixed_asset', '13', 3, 'common', 1990),
|
||||
$c('20000', '국민연금전환금', 'asset', 'fixed_asset', '13', 3, 'common', 2000),
|
||||
$c('21100', '감가상각누계액(공구와기구)', 'asset', 'fixed_asset', '13', 3, 'common', 2110),
|
||||
$c('21400', '건설중인자산(임대)', 'asset', 'fixed_asset', '13', 3, 'common', 2140),
|
||||
$c('21500', '미착기계', 'asset', 'fixed_asset', '13', 3, 'common', 2150),
|
||||
$c('21600', '감가상각누계액(비품)', 'asset', 'fixed_asset', '13', 3, 'common', 2160),
|
||||
// 무형자산
|
||||
$c('23100', '영업권', 'asset', 'fixed_asset', '13', 3, 'common', 2310),
|
||||
$c('23200', '특허권', 'asset', 'fixed_asset', '13', 3, 'common', 2320),
|
||||
$c('23300', '상표권', 'asset', 'fixed_asset', '13', 3, 'common', 2330),
|
||||
$c('23400', '실용신안권', 'asset', 'fixed_asset', '13', 3, 'common', 2340),
|
||||
$c('23500', '의장권', 'asset', 'fixed_asset', '13', 3, 'common', 2350),
|
||||
$c('23600', '면허권', 'asset', 'fixed_asset', '13', 3, 'common', 2360),
|
||||
$c('23700', '광업권', 'asset', 'fixed_asset', '13', 3, 'common', 2370),
|
||||
$c('23800', '창업비', 'asset', 'fixed_asset', '13', 3, 'common', 2380),
|
||||
$c('23900', '개발비', 'asset', 'fixed_asset', '13', 3, 'common', 2390),
|
||||
// 기타비유동자산 (96xxx-97xxx)
|
||||
$c('96100', '이연법인세자산', 'asset', 'fixed_asset', '13', 3, 'common', 9610),
|
||||
$c('96200', '임차보증금', 'asset', 'fixed_asset', '13', 3, 'common', 9620),
|
||||
$c('96300', '전세금', 'asset', 'fixed_asset', '13', 3, 'common', 9630),
|
||||
$c('96400', '기타보증금', 'asset', 'fixed_asset', '13', 3, 'common', 9640),
|
||||
$c('96500', '장기외상매출금', 'asset', 'fixed_asset', '13', 3, 'common', 9650),
|
||||
$c('96600', '현재가치할인차금(장기외상매출금)', 'asset', 'fixed_asset', '13', 3, 'common', 9660),
|
||||
$c('96700', '대손충당금(장기외상매출금)', 'asset', 'fixed_asset', '13', 3, 'common', 9670),
|
||||
$c('96800', '장기받을어음', 'asset', 'fixed_asset', '13', 3, 'common', 9680),
|
||||
$c('96900', '현재가치할인차금(장기받을어음)', 'asset', 'fixed_asset', '13', 3, 'common', 9690),
|
||||
$c('97000', '대손충당금(장기받을어음)', 'asset', 'fixed_asset', '13', 3, 'common', 9700),
|
||||
$c('97100', '장기미수금', 'asset', 'fixed_asset', '13', 3, 'common', 9710),
|
||||
$c('97200', '현재가치할인차금(장기미수금)', 'asset', 'fixed_asset', '13', 3, 'common', 9720),
|
||||
$c('97300', '대손충당금(장기미수금)', 'asset', 'fixed_asset', '13', 3, 'common', 9730),
|
||||
$c('97400', '장기선급비용', 'asset', 'fixed_asset', '13', 3, 'common', 9740),
|
||||
$c('97500', '장기선급금', 'asset', 'fixed_asset', '13', 3, 'common', 9750),
|
||||
$c('97600', '부도어음과수표', 'asset', 'fixed_asset', '13', 3, 'common', 9760),
|
||||
$c('97700', '대손충당금(부도어음)', 'asset', 'fixed_asset', '13', 3, 'common', 9770),
|
||||
$c('97800', '전신전화가입권', 'asset', 'fixed_asset', '13', 3, 'common', 9780),
|
||||
|
||||
// ================================================================
|
||||
// 부채 — 유동부채 추가 (parent: '21')
|
||||
// ================================================================
|
||||
$c('25600', '당좌차월', 'liability', 'current_liability', '21', 3, 'common', 2560),
|
||||
$c('25700', '가수금', 'liability', 'current_liability', '21', 3, 'common', 2570),
|
||||
$c('25800', '예수보증금', 'liability', 'current_liability', '21', 3, 'common', 2580),
|
||||
$c('26300', '수입금', 'liability', 'current_liability', '21', 3, 'common', 2630),
|
||||
$c('26600', '지급보증채무', 'liability', 'current_liability', '21', 3, 'common', 2660),
|
||||
$c('26700', '수출금융', 'liability', 'current_liability', '21', 3, 'common', 2670),
|
||||
$c('26800', '수입금융', 'liability', 'current_liability', '21', 3, 'common', 2680),
|
||||
$c('26900', '공사손실충당금', 'liability', 'current_liability', '21', 3, 'common', 2690),
|
||||
$c('27000', '하자보수충당금', 'liability', 'current_liability', '21', 3, 'common', 2700),
|
||||
$c('27100', '공사선수금', 'liability', 'current_liability', '21', 3, 'common', 2710),
|
||||
$c('27200', '분양선수금', 'liability', 'current_liability', '21', 3, 'common', 2720),
|
||||
$c('27300', '이연법인세부채', 'liability', 'current_liability', '21', 3, 'common', 2730),
|
||||
|
||||
// ================================================================
|
||||
// 부채 — 비유동부채 추가 (parent: '22')
|
||||
// ================================================================
|
||||
$c('29100', '사채', 'liability', 'long_term_liability', '22', 3, 'common', 2910),
|
||||
$c('29200', '사채할인발행차금', 'liability', 'long_term_liability', '22', 3, 'common', 2920),
|
||||
$c('29600', '퇴직보험충당부채', 'liability', 'long_term_liability', '22', 3, 'common', 2960),
|
||||
$c('29700', '중소기업투자준비금', 'liability', 'long_term_liability', '22', 3, 'common', 2970),
|
||||
$c('29800', '기술개발준비금', 'liability', 'long_term_liability', '22', 3, 'common', 2980),
|
||||
$c('29900', '해외시장개척준비금', 'liability', 'long_term_liability', '22', 3, 'common', 2990),
|
||||
$c('30100', '지방이전준비금', 'liability', 'long_term_liability', '22', 3, 'common', 3010),
|
||||
$c('30200', '수출손실준비금', 'liability', 'long_term_liability', '22', 3, 'common', 3020),
|
||||
$c('30300', '주임종장기차입금', 'liability', 'long_term_liability', '22', 3, 'common', 3030),
|
||||
$c('30400', '관계회사장기차입금', 'liability', 'long_term_liability', '22', 3, 'common', 3040),
|
||||
$c('30500', '외화장기차입금', 'liability', 'long_term_liability', '22', 3, 'common', 3050),
|
||||
$c('30600', '공사선수금(장기)', 'liability', 'long_term_liability', '22', 3, 'common', 3060),
|
||||
$c('30800', '장기성지급어음', 'liability', 'long_term_liability', '22', 3, 'common', 3080),
|
||||
$c('30900', '환율조정대', 'liability', 'long_term_liability', '22', 3, 'common', 3090),
|
||||
$c('31000', '이연법인세대', 'liability', 'long_term_liability', '22', 3, 'common', 3100),
|
||||
$c('31100', '신주인수권부사채', 'liability', 'long_term_liability', '22', 3, 'common', 3110),
|
||||
$c('31200', '전환사채', 'liability', 'long_term_liability', '22', 3, 'common', 3120),
|
||||
$c('31300', '사채할증발행차금', 'liability', 'long_term_liability', '22', 3, 'common', 3130),
|
||||
$c('31400', '장기제품보증부채', 'liability', 'long_term_liability', '22', 3, 'common', 3140),
|
||||
|
||||
// ================================================================
|
||||
// 자본 — 잉여금 추가 (parent: '32')
|
||||
// ================================================================
|
||||
$c('34200', '감자차익', 'capital', 'capital', '32', 3, 'common', 3420),
|
||||
$c('34300', '자기주식처분이익', 'capital', 'capital', '32', 3, 'common', 3430),
|
||||
$c('34900', '기타자본잉여금', 'capital', 'capital', '32', 3, 'common', 3490),
|
||||
$c('35000', '재평가적립금', 'capital', 'capital', '32', 3, 'common', 3500),
|
||||
$c('35200', '기업합리화적립금', 'capital', 'capital', '32', 3, 'common', 3520),
|
||||
$c('35300', '법정적립금', 'capital', 'capital', '32', 3, 'common', 3530),
|
||||
$c('35400', '재무구조개선적립금', 'capital', 'capital', '32', 3, 'common', 3540),
|
||||
$c('35500', '임의적립금', 'capital', 'capital', '32', 3, 'common', 3550),
|
||||
$c('35600', '사업확장적립금', 'capital', 'capital', '32', 3, 'common', 3560),
|
||||
$c('35700', '감채적립금', 'capital', 'capital', '32', 3, 'common', 3570),
|
||||
$c('35800', '배당평균적립금', 'capital', 'capital', '32', 3, 'common', 3580),
|
||||
$c('35900', '주식할인발행차손', 'capital', 'capital', '32', 3, 'common', 3590),
|
||||
$c('36000', '배당건설이자상각', 'capital', 'capital', '32', 3, 'common', 3600),
|
||||
$c('36100', '자기주식상환액', 'capital', 'capital', '32', 3, 'common', 3610),
|
||||
$c('36200', '자기주식처분차금', 'capital', 'capital', '32', 3, 'common', 3620),
|
||||
$c('36300', '중소기업투자준비금(자본)', 'capital', 'capital', '32', 3, 'common', 3630),
|
||||
$c('36400', '기술개발준비금(자본)', 'capital', 'capital', '32', 3, 'common', 3640),
|
||||
$c('36500', '해외시장개척준비금(자본)', 'capital', 'capital', '32', 3, 'common', 3650),
|
||||
$c('36600', '지방이전준비금(자본)', 'capital', 'capital', '32', 3, 'common', 3660),
|
||||
$c('36700', '수출손실준비금(자본)', 'capital', 'capital', '32', 3, 'common', 3670),
|
||||
$c('36800', '기타임의적립금', 'capital', 'capital', '32', 3, 'common', 3680),
|
||||
$c('36900', '회계변경의누적효과', 'capital', 'capital', '32', 3, 'common', 3690),
|
||||
$c('37000', '전기오류수정이익', 'capital', 'capital', '32', 3, 'common', 3700),
|
||||
$c('37100', '전기오류수정손실', 'capital', 'capital', '32', 3, 'common', 3710),
|
||||
$c('37200', '중간배당금', 'capital', 'capital', '32', 3, 'common', 3720),
|
||||
$c('37400', '기타이익잉여금', 'capital', 'capital', '32', 3, 'common', 3740),
|
||||
$c('37600', '이월결손금', 'capital', 'capital', '32', 3, 'common', 3760),
|
||||
$c('37800', '처분전이익잉여금', 'capital', 'capital', '32', 3, 'common', 3780),
|
||||
|
||||
// ================================================================
|
||||
// 자본 — 자본조정 (parent: '33')
|
||||
// ================================================================
|
||||
$c('38000', '당기순손실', 'capital', 'capital_adjustment', '33', 3, 'common', 3800),
|
||||
$c('38100', '주식할인발행차금', 'capital', 'capital_adjustment', '33', 3, 'common', 3810),
|
||||
$c('38200', '배당건설이자', 'capital', 'capital_adjustment', '33', 3, 'common', 3820),
|
||||
$c('38300', '자기주식', 'capital', 'capital_adjustment', '33', 3, 'common', 3830),
|
||||
$c('38400', '환전대가', 'capital', 'capital_adjustment', '33', 3, 'common', 3840),
|
||||
$c('38500', '신주인수권대가', 'capital', 'capital_adjustment', '33', 3, 'common', 3850),
|
||||
$c('38600', '신주발행비', 'capital', 'capital_adjustment', '33', 3, 'common', 3860),
|
||||
$c('38700', '미교부주식배당금', 'capital', 'capital_adjustment', '33', 3, 'common', 3870),
|
||||
$c('38800', '신주청약증거금', 'capital', 'capital_adjustment', '33', 3, 'common', 3880),
|
||||
$c('39200', '국고보조금', 'capital', 'capital_adjustment', '33', 3, 'common', 3920),
|
||||
$c('39300', '공사부담금', 'capital', 'capital_adjustment', '33', 3, 'common', 3930),
|
||||
$c('39400', '감자차손', 'capital', 'capital_adjustment', '33', 3, 'common', 3940),
|
||||
$c('39500', '자기주식처분손실', 'capital', 'capital_adjustment', '33', 3, 'common', 3950),
|
||||
$c('39600', '주식매입선택권', 'capital', 'capital_adjustment', '33', 3, 'common', 3960),
|
||||
// 기타포괄손익누계액
|
||||
$c('98100', '매도가능증권평가이익', 'capital', 'capital_adjustment', '33', 3, 'common', 9810),
|
||||
$c('98200', '매도가능증권평가손실', 'capital', 'capital_adjustment', '33', 3, 'common', 9820),
|
||||
$c('98300', '해외사업환산이익', 'capital', 'capital_adjustment', '33', 3, 'common', 9830),
|
||||
$c('98400', '해외사업환산손실', 'capital', 'capital_adjustment', '33', 3, 'common', 9840),
|
||||
$c('98500', '파생상품평가이익', 'capital', 'capital_adjustment', '33', 3, 'common', 9850),
|
||||
$c('98600', '파생상품평가손실', 'capital', 'capital_adjustment', '33', 3, 'common', 9860),
|
||||
|
||||
// ================================================================
|
||||
// 수익 — 매출 추가 (parent: '41')
|
||||
// ================================================================
|
||||
$c('40200', '매출환입및에누리(상품)', 'revenue', 'sales_revenue', '41', 3, 'common', 4020),
|
||||
$c('40300', '매출할인(상품)', 'revenue', 'sales_revenue', '41', 3, 'common', 4030),
|
||||
$c('40500', '매출환입및에누리(제품)', 'revenue', 'sales_revenue', '41', 3, 'common', 4050),
|
||||
$c('40600', '매출할인(제품)', 'revenue', 'sales_revenue', '41', 3, 'common', 4060),
|
||||
$c('40800', '매출할인(공사)', 'revenue', 'sales_revenue', '41', 3, 'common', 4080),
|
||||
$c('40900', '완성건물매출', 'revenue', 'sales_revenue', '41', 3, 'common', 4090),
|
||||
|
||||
// ================================================================
|
||||
// 수익 — 영업외수익 추가 (parent: '42')
|
||||
// ================================================================
|
||||
$c('90200', '만기보유증권이자', 'revenue', 'other_revenue', '42', 3, 'common', 9020),
|
||||
$c('90500', '단기투자자산평가이익', 'revenue', 'other_revenue', '42', 3, 'common', 9050),
|
||||
$c('90600', '단기투자자산처분이익', 'revenue', 'other_revenue', '42', 3, 'common', 9060),
|
||||
$c('90800', '대손충당금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9080),
|
||||
$c('90900', '수입수수료', 'revenue', 'other_revenue', '42', 3, 'common', 9090),
|
||||
$c('91000', '외화환산이익', 'revenue', 'other_revenue', '42', 3, 'common', 9100),
|
||||
$c('91100', '사채상환이익', 'revenue', 'other_revenue', '42', 3, 'common', 9110),
|
||||
$c('91200', '전기오류수정이익', 'revenue', 'other_revenue', '42', 3, 'common', 9120),
|
||||
$c('91300', '하자보수충당금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9130),
|
||||
$c('91400', '유형자산처분이익', 'revenue', 'other_revenue', '42', 3, 'common', 9140),
|
||||
$c('91500', '투자자산처분이익', 'revenue', 'other_revenue', '42', 3, 'common', 9150),
|
||||
$c('91600', '상각채권추심이익', 'revenue', 'other_revenue', '42', 3, 'common', 9160),
|
||||
$c('91700', '자산수증이익', 'revenue', 'other_revenue', '42', 3, 'common', 9170),
|
||||
$c('91800', '채무면제이익', 'revenue', 'other_revenue', '42', 3, 'common', 9180),
|
||||
$c('92000', '투자증권손상차환입', 'revenue', 'other_revenue', '42', 3, 'common', 9200),
|
||||
$c('92100', '지분법이익', 'revenue', 'other_revenue', '42', 3, 'common', 9210),
|
||||
$c('92400', '중소투자준비금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9240),
|
||||
$c('92500', '기술개발준비금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9250),
|
||||
$c('92600', '해외개척준비금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9260),
|
||||
$c('92700', '지방이전준비금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9270),
|
||||
$c('92800', '수출손실준비금환입', 'revenue', 'other_revenue', '42', 3, 'common', 9280),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 매출원가 추가: 45xxx (parent: '51')
|
||||
// ================================================================
|
||||
$c('45100', '상품매출원가', 'expense', 'cogs', '51', 3, 'common', 4510),
|
||||
$c('45200', '도급공사매출원가', 'expense', 'cogs', '51', 3, 'common', 4520),
|
||||
$c('45300', '분양공사매출원가', 'expense', 'cogs', '51', 3, 'common', 4530),
|
||||
$c('45500', '제품매출원가', 'expense', 'cogs', '51', 3, 'common', 4550),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 제조원가 추가: 50xxx-53xxx (parent: '51')
|
||||
// ================================================================
|
||||
$c('50600', '제수당(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5060),
|
||||
$c('50700', '잡급(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5070),
|
||||
$c('50900', '퇴직보험충당금전입(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5090),
|
||||
$c('51000', '퇴직금여(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5100),
|
||||
$c('51500', '가스수도료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5150),
|
||||
$c('52300', '경상연구개발비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5230),
|
||||
$c('52500', '교육훈련비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5250),
|
||||
$c('52600', '도서인쇄비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5260),
|
||||
$c('52700', '회의비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5270),
|
||||
$c('52800', '포장비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5280),
|
||||
$c('52900', '사무용품비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5290),
|
||||
$c('53200', '보관료(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5320),
|
||||
$c('53300', '외주가공비(제조경비)', 'expense', 'cogs', '51', 3, 'manufacturing', 5330),
|
||||
$c('53400', '시험비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5340),
|
||||
$c('53500', '기밀비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5350),
|
||||
$c('53600', '잡비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5360),
|
||||
$c('53700', '폐기물처리비(제조)', 'expense', 'cogs', '51', 3, 'manufacturing', 5370),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 건설원가 60xxx (parent: '54')
|
||||
// ================================================================
|
||||
$c('60100', '원재료비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6010),
|
||||
$c('60200', '외주비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6020),
|
||||
$c('60300', '급여(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6030),
|
||||
$c('60400', '임금(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6040),
|
||||
$c('60500', '상여금(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6050),
|
||||
$c('60600', '잡급(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6060),
|
||||
$c('60700', '퇴직급여(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6070),
|
||||
$c('60800', '퇴직보험충당금전입(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6080),
|
||||
$c('60900', '퇴직금여(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6090),
|
||||
$c('61000', '중기및운반비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6100),
|
||||
$c('61100', '복리후생비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6110),
|
||||
$c('61200', '여비교통비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6120),
|
||||
$c('61300', '접대비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6130),
|
||||
$c('61400', '통신비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6140),
|
||||
$c('61500', '가스수도료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6150),
|
||||
$c('61600', '전력비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6160),
|
||||
$c('61700', '세금과공과금(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6170),
|
||||
$c('61800', '감가상각비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6180),
|
||||
$c('61900', '지급임차료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6190),
|
||||
$c('62000', '수선비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6200),
|
||||
$c('62100', '보험료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6210),
|
||||
$c('62200', '차량유지비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6220),
|
||||
$c('62300', '운반비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6230),
|
||||
$c('62400', '잡자재대(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6240),
|
||||
$c('62500', '교육훈련비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6250),
|
||||
$c('62600', '도서인쇄비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6260),
|
||||
$c('62700', '회의비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6270),
|
||||
$c('62800', '포장비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6280),
|
||||
$c('62900', '사무용품비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6290),
|
||||
$c('63000', '소모품비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6300),
|
||||
$c('63100', '지급수수료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6310),
|
||||
$c('63200', '보관료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6320),
|
||||
$c('63300', '외주용역비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6330),
|
||||
$c('63400', '장비사용료(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6340),
|
||||
$c('63500', '설계용역비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6350),
|
||||
$c('63600', '광고선전비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6360),
|
||||
$c('63700', '소모공구비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6370),
|
||||
$c('63800', '외주시공비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6380),
|
||||
$c('63900', '협비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6390),
|
||||
$c('64000', '잡비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6400),
|
||||
$c('64100', '공사손실충당금전입(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6410),
|
||||
$c('64200', '공사손실충당금환입(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6420),
|
||||
$c('64300', '외주비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 6430),
|
||||
$c('64400', '유류비(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 6440),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 건설원가 70xxx (parent: '54')
|
||||
// ================================================================
|
||||
$c('70100', '원재료비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7010),
|
||||
$c('70200', '중기및운반비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7020),
|
||||
$c('70300', '급여(건설노무비)', 'expense', 'construction_cost', '54', 3, 'construction', 7030),
|
||||
$c('70400', '임금(건설노무비)', 'expense', 'construction_cost', '54', 3, 'construction', 7040),
|
||||
$c('70500', '상여금(건설노무비)', 'expense', 'construction_cost', '54', 3, 'construction', 7050),
|
||||
$c('70600', '제수당(건설노무비)', 'expense', 'construction_cost', '54', 3, 'construction', 7060),
|
||||
$c('70700', '퇴직급여(건설노무비)', 'expense', 'construction_cost', '54', 3, 'construction', 7070),
|
||||
$c('70800', '퇴직보험충당금전입(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7080),
|
||||
$c('70900', '퇴직금여(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7090),
|
||||
$c('71000', '건설용지비', 'expense', 'construction_cost', '54', 3, 'construction', 7100),
|
||||
$c('71100', '복리후생비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7110),
|
||||
$c('71200', '여비교통비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7120),
|
||||
$c('71300', '접대비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7130),
|
||||
$c('71400', '통신비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7140),
|
||||
$c('71500', '가스수도료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7150),
|
||||
$c('71600', '전력비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7160),
|
||||
$c('71700', '세금과공과금(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7170),
|
||||
$c('71800', '감가상각비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7180),
|
||||
$c('71900', '지급임차료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7190),
|
||||
$c('72000', '수선비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7200),
|
||||
$c('72100', '보험료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7210),
|
||||
$c('72200', '차량유지비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7220),
|
||||
$c('72300', '경상연구개발비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7230),
|
||||
$c('72400', '잡자재대(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7240),
|
||||
$c('72500', '교육훈련비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7250),
|
||||
$c('72600', '도서인쇄비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7260),
|
||||
$c('72700', '회의비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7270),
|
||||
$c('72800', '포장비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7280),
|
||||
$c('72900', '사무용품비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7290),
|
||||
$c('73000', '소모품비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7300),
|
||||
$c('73100', '지급수수료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7310),
|
||||
$c('73200', '보관료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7320),
|
||||
$c('73300', '외주가공비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7330),
|
||||
$c('73400', '시험비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7340),
|
||||
$c('73500', '설계용역비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7350),
|
||||
$c('73600', '가설재손료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7360),
|
||||
$c('73700', '잡비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7370),
|
||||
$c('73800', '폐기물처리비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7380),
|
||||
$c('73900', '장비사용료(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7390),
|
||||
$c('74100', '공사손실충당금전입(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7410),
|
||||
$c('74200', '공사손실충당금환입(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7420),
|
||||
$c('74300', '외주비(건설경비)', 'expense', 'construction_cost', '54', 3, 'construction', 7430),
|
||||
$c('74900', '명예퇴직금(건설)', 'expense', 'construction_cost', '54', 3, 'construction', 7490),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 판관비 추가 (parent: '52')
|
||||
// ================================================================
|
||||
$c('80400', '제수당', 'expense', 'selling_admin', '52', 3, 'admin', 8040),
|
||||
$c('80500', '잡급', 'expense', 'selling_admin', '52', 3, 'admin', 8050),
|
||||
$c('80700', '퇴직보험충당금전입', 'expense', 'selling_admin', '52', 3, 'admin', 8070),
|
||||
$c('80800', '퇴직금여', 'expense', 'selling_admin', '52', 3, 'admin', 8080),
|
||||
$c('81600', '전력비', 'expense', 'selling_admin', '52', 3, 'admin', 8160),
|
||||
$c('82800', '포장비', 'expense', 'selling_admin', '52', 3, 'admin', 8280),
|
||||
$c('83400', '판매촉진비', 'expense', 'selling_admin', '52', 3, 'admin', 8340),
|
||||
$c('83600', '기밀비', 'expense', 'selling_admin', '52', 3, 'admin', 8360),
|
||||
$c('83700', '건물관리비', 'expense', 'selling_admin', '52', 3, 'admin', 8370),
|
||||
$c('83800', '수출제비용', 'expense', 'selling_admin', '52', 3, 'admin', 8380),
|
||||
$c('83900', '판매수수료', 'expense', 'selling_admin', '52', 3, 'admin', 8390),
|
||||
$c('84000', '무형고정자산상각', 'expense', 'selling_admin', '52', 3, 'admin', 8400),
|
||||
$c('84100', '환가료', 'expense', 'selling_admin', '52', 3, 'admin', 8410),
|
||||
$c('84200', '견본비', 'expense', 'selling_admin', '52', 3, 'admin', 8420),
|
||||
$c('84300', '해외접대비', 'expense', 'selling_admin', '52', 3, 'admin', 8430),
|
||||
$c('84400', '해외시장개척비', 'expense', 'selling_admin', '52', 3, 'admin', 8440),
|
||||
$c('84500', '미분양주택관리비', 'expense', 'selling_admin', '52', 3, 'admin', 8450),
|
||||
$c('84600', '수주비', 'expense', 'selling_admin', '52', 3, 'admin', 8460),
|
||||
$c('84700', '하자보수충당금전입', 'expense', 'selling_admin', '52', 3, 'admin', 8470),
|
||||
$c('84900', '명예퇴직금', 'expense', 'selling_admin', '52', 3, 'admin', 8490),
|
||||
|
||||
// ================================================================
|
||||
// 비용 — 영업외비용 추가 (parent: '53')
|
||||
// ================================================================
|
||||
$c('93400', '기타의대손상각비', 'expense', 'other_expense', '53', 3, 'common', 9340),
|
||||
$c('93500', '외화환산손실', 'expense', 'other_expense', '53', 3, 'common', 9350),
|
||||
$c('93600', '매출채권처분손실', 'expense', 'other_expense', '53', 3, 'common', 9360),
|
||||
$c('93700', '단기투자자산평가손실', 'expense', 'other_expense', '53', 3, 'common', 9370),
|
||||
$c('93800', '단기투자자산처분손실', 'expense', 'other_expense', '53', 3, 'common', 9380),
|
||||
$c('93900', '재고자산감모손실', 'expense', 'other_expense', '53', 3, 'common', 9390),
|
||||
$c('94000', '재고자산평가손실', 'expense', 'other_expense', '53', 3, 'common', 9400),
|
||||
$c('94100', '재해손실', 'expense', 'other_expense', '53', 3, 'common', 9410),
|
||||
$c('94200', '전기오류수정손실', 'expense', 'other_expense', '53', 3, 'common', 9420),
|
||||
$c('94300', '투자증권손상차손', 'expense', 'other_expense', '53', 3, 'common', 9430),
|
||||
$c('94700', '사채상환손실', 'expense', 'other_expense', '53', 3, 'common', 9470),
|
||||
$c('95000', '투자자산처분손실', 'expense', 'other_expense', '53', 3, 'common', 9500),
|
||||
$c('95100', '중소투자준비금전입', 'expense', 'other_expense', '53', 3, 'common', 9510),
|
||||
$c('95200', '기술개발준비금전입', 'expense', 'other_expense', '53', 3, 'common', 9520),
|
||||
$c('95300', '해외개척준비금전입', 'expense', 'other_expense', '53', 3, 'common', 9530),
|
||||
$c('95400', '지방이전준비금전입', 'expense', 'other_expense', '53', 3, 'common', 9540),
|
||||
$c('95500', '수출손실준비금전입', 'expense', 'other_expense', '53', 3, 'common', 9550),
|
||||
$c('95700', '특별상각', 'expense', 'other_expense', '53', 3, 'common', 9570),
|
||||
// 중단사업
|
||||
$c('99100', '사업중단직접비', 'expense', 'other_expense', '53', 3, 'common', 9910),
|
||||
$c('99200', '중단사업자산손상차손', 'expense', 'other_expense', '53', 3, 'common', 9920),
|
||||
$c('99300', '중단사업손상차환입', 'expense', 'other_expense', '53', 3, 'common', 9930),
|
||||
$c('99700', '중단손익', 'expense', 'other_expense', '53', 3, 'common', 9970),
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* codebridge DB로 이관 완료된 58개 테이블을 sam DB에서 삭제
|
||||
*
|
||||
* 복원: ~/backups/sam_codebridge_tables_20260309.sql
|
||||
*/
|
||||
private array $tables = [
|
||||
// Admin (9)
|
||||
'admin_api_flows',
|
||||
'admin_api_flow_runs',
|
||||
'admin_pm_daily_logs',
|
||||
'admin_pm_daily_log_entries',
|
||||
'admin_pm_issues',
|
||||
'admin_pm_projects',
|
||||
'admin_pm_tasks',
|
||||
'admin_roadmap_milestones',
|
||||
'admin_roadmap_plans',
|
||||
|
||||
// DevTools (5)
|
||||
'admin_api_bookmarks',
|
||||
'admin_api_deprecations',
|
||||
'admin_api_environments',
|
||||
'admin_api_histories',
|
||||
'admin_api_templates',
|
||||
|
||||
// Sales (17)
|
||||
'sales_partners',
|
||||
'sales_managers',
|
||||
'sales_manager_documents',
|
||||
'sales_commissions',
|
||||
'sales_commission_details',
|
||||
'sales_consultations',
|
||||
'sales_contract_products',
|
||||
'sales_products',
|
||||
'sales_product_categories',
|
||||
'sales_prospects',
|
||||
'sales_prospect_consultations',
|
||||
'sales_prospect_products',
|
||||
'sales_prospect_scenarios',
|
||||
'sales_records',
|
||||
'sales_scenario_checklists',
|
||||
'sales_tenant_managements',
|
||||
'tenant_prospects',
|
||||
|
||||
// Finance (9)
|
||||
'condolence_expenses',
|
||||
'consulting_fees',
|
||||
'corporate_cards',
|
||||
'corporate_card_prepayments',
|
||||
'customer_settlements',
|
||||
'daily_fund_memos',
|
||||
'daily_fund_transactions',
|
||||
'incomes',
|
||||
'vat_records',
|
||||
|
||||
// ESign (2)
|
||||
'esign_field_templates',
|
||||
'esign_field_template_items',
|
||||
|
||||
// Equipment (6)
|
||||
'equipments',
|
||||
'equipment_process',
|
||||
'equipment_inspections',
|
||||
'equipment_inspection_details',
|
||||
'equipment_inspection_templates',
|
||||
'equipment_repairs',
|
||||
|
||||
// HR (1)
|
||||
'business_income_payments',
|
||||
|
||||
// System (1)
|
||||
'ai_configs',
|
||||
|
||||
// 기타 (8)
|
||||
'biz_cert',
|
||||
'cm_songs',
|
||||
'construction_site_photos',
|
||||
'construction_site_photo_rows',
|
||||
'admin_meeting_logs',
|
||||
'meeting_minutes',
|
||||
'meeting_minute_segments',
|
||||
'interview_knowledge',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 복원은 백업 파일로 수행
|
||||
// mysql -u codebridge -p sam < ~/backups/sam_codebridge_tables_20260309.sql
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('payrolls', function (Blueprint $table) {
|
||||
$table->json('options')->nullable()->after('note');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payrolls', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('quality_document_locations', function (Blueprint $table) {
|
||||
$table->json('options')->nullable()->after('inspection_status')->comment('QMS 심사 확인 등 추가 데이터');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('quality_document_locations', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1) 심사 점검표 마스터
|
||||
Schema::create('audit_checklists', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->smallInteger('year')->unsigned()->comment('연도');
|
||||
$table->tinyInteger('quarter')->unsigned()->comment('분기 1~4');
|
||||
$table->string('type', 30)->default('standard_manual')->comment('심사유형');
|
||||
$table->string('status', 20)->default('draft')->comment('draft/in_progress/completed');
|
||||
$table->json('options')->nullable()->comment('추가 설정');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'year', 'quarter', 'type'], 'uq_audit_checklists_tenant_period');
|
||||
$table->index(['tenant_id', 'status'], 'idx_audit_checklists_status');
|
||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||||
});
|
||||
|
||||
// 2) 점검표 카테고리
|
||||
Schema::create('audit_checklist_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->unsignedBigInteger('checklist_id')->comment('점검표ID');
|
||||
$table->string('title', 200)->comment('카테고리명');
|
||||
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['checklist_id', 'sort_order'], 'idx_audit_categories_sort');
|
||||
$table->foreign('checklist_id')->references('id')->on('audit_checklists')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 3) 점검표 세부 항목
|
||||
Schema::create('audit_checklist_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->unsignedBigInteger('category_id')->comment('카테고리ID');
|
||||
$table->string('name', 200)->comment('항목명');
|
||||
$table->text('description')->nullable()->comment('항목 설명');
|
||||
$table->boolean('is_completed')->default(false)->comment('완료여부');
|
||||
$table->timestamp('completed_at')->nullable()->comment('완료일시');
|
||||
$table->unsignedBigInteger('completed_by')->nullable()->comment('완료처리자');
|
||||
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['category_id', 'sort_order'], 'idx_audit_items_sort');
|
||||
$table->index(['category_id', 'is_completed'], 'idx_audit_items_completed');
|
||||
$table->foreign('category_id')->references('id')->on('audit_checklist_categories')->onDelete('cascade');
|
||||
$table->foreign('completed_by')->references('id')->on('users')->onDelete('set null');
|
||||
});
|
||||
|
||||
// 4) 기준 문서 연결
|
||||
Schema::create('audit_standard_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->unsignedBigInteger('checklist_item_id')->comment('점검항목ID');
|
||||
$table->string('title', 200)->comment('문서명');
|
||||
$table->string('version', 20)->nullable()->comment('버전');
|
||||
$table->date('date')->nullable()->comment('시행일');
|
||||
$table->unsignedBigInteger('document_id')->nullable()->comment('EAV 파일 FK');
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('checklist_item_id', 'idx_audit_std_docs_item');
|
||||
$table->foreign('checklist_item_id')->references('id')->on('audit_checklist_items')->onDelete('cascade');
|
||||
$table->foreign('document_id')->references('id')->on('documents')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('audit_standard_documents');
|
||||
Schema::dropIfExists('audit_checklist_items');
|
||||
Schema::dropIfExists('audit_checklist_categories');
|
||||
Schema::dropIfExists('audit_checklists');
|
||||
}
|
||||
};
|
||||
@@ -17,7 +17,7 @@
|
||||
* Phase 3.1: chandj.price_motor → items (SM) + prices 누락 품목
|
||||
* Phase 3.2: chandj.price_raw_materials → items (RM) + prices 누락 품목
|
||||
*
|
||||
* @see docs/plans/kd-items-migration-plan.md
|
||||
* @see docs/dev_plans/kd-items-migration-plan.md
|
||||
*/
|
||||
class KyungdongItemSeeder extends Seeder
|
||||
{
|
||||
|
||||
608
database/seeders/QualityDummyDataSeeder.php
Normal file
608
database/seeders/QualityDummyDataSeeder.php
Normal file
@@ -0,0 +1,608 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class QualityDummyDataSeeder extends Seeder
|
||||
{
|
||||
private const TENANT_ID = 287;
|
||||
|
||||
private const USER_ID = 33;
|
||||
|
||||
private const CLIENT_IDS = [9, 10, 11, 12, 13];
|
||||
|
||||
private const ORDER_IDS = [11, 17, 18, 28, 29, 41, 42, 43, 57, 59];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = self::TENANT_ID;
|
||||
$userId = self::USER_ID;
|
||||
$now = Carbon::now();
|
||||
|
||||
// 멱등성: 이미 데이터가 있으면 스킵
|
||||
$existing = DB::table('quality_documents')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('quality_doc_number', 'like', 'KD-QD-202604-%')
|
||||
->count();
|
||||
|
||||
if ($existing > 0) {
|
||||
$this->command->info(' ⚠ quality_documents: 이미 '.$existing.'개 존재 (스킵)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($tenantId, $userId, $now) {
|
||||
// ============================================================
|
||||
// Page 1: 제품검사/품질관리서 (quality_documents + orders + locations)
|
||||
// ============================================================
|
||||
$this->command->info('📋 Page 1: 제품검사/품질관리서 더미 데이터 생성...');
|
||||
|
||||
$qualityDocs = [
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0001',
|
||||
'site_name' => '강남 르네상스 오피스텔 신축공사',
|
||||
'status' => 'completed',
|
||||
'client_id' => self::CLIENT_IDS[0],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-02-15',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '김현수', 'phone' => '010-1111-2222'],
|
||||
'contractor' => ['name' => '이건설', 'phone' => '02-3333-4444', 'address' => '서울시 강남구 역삼동', 'company' => '대한건설(주)'],
|
||||
'inspection' => ['end_date' => '2026-03-05', 'start_date' => '2026-03-03', 'request_date' => '2026-02-28'],
|
||||
'supervisor' => ['name' => '박감리', 'phone' => '02-5555-6666', 'office' => '한국감리사무소', 'address' => '서울시 서초구'],
|
||||
'site_address' => ['detail' => '강남 르네상스 오피스텔 B1~15F', 'address' => '서울시 강남구 역삼동 123-45', 'postal_code' => '06241'],
|
||||
'construction_site' => ['name' => '강남 르네상스 오피스텔 신축공사', 'lot_number' => '123-45', 'land_location' => '서울시 강남구 역삼동'],
|
||||
'material_distributor' => ['ceo' => '최대표', 'phone' => '02-7777-8888', 'address' => '서울시 송파구', 'company' => '경동자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0002',
|
||||
'site_name' => '판교 테크노밸리 물류센터',
|
||||
'status' => 'completed',
|
||||
'client_id' => self::CLIENT_IDS[1],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-02-20',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '정우성', 'phone' => '010-2222-3333'],
|
||||
'contractor' => ['name' => '박시공', 'phone' => '031-4444-5555', 'address' => '경기도 성남시 분당구', 'company' => '판교건설(주)'],
|
||||
'inspection' => ['end_date' => '2026-03-08', 'start_date' => '2026-03-06', 'request_date' => '2026-03-01'],
|
||||
'supervisor' => ['name' => '이감리', 'phone' => '031-6666-7777', 'office' => '성남감리사무소', 'address' => '경기도 성남시 분당구'],
|
||||
'site_address' => ['detail' => '판교 테크노밸리 3단지 물류동', 'address' => '경기도 성남시 분당구 판교동 678-9', 'postal_code' => '13487'],
|
||||
'construction_site' => ['name' => '판교 테크노밸리 물류센터 신축', 'lot_number' => '678-9', 'land_location' => '경기도 성남시 분당구 판교동'],
|
||||
'material_distributor' => ['ceo' => '김대표', 'phone' => '031-8888-9999', 'address' => '경기도 용인시', 'company' => '한국자재유통(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0003',
|
||||
'site_name' => '잠실 롯데월드타워 리모델링',
|
||||
'status' => 'completed',
|
||||
'client_id' => self::CLIENT_IDS[2],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-02-25',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '송민호', 'phone' => '010-3333-4444'],
|
||||
'contractor' => ['name' => '최시공', 'phone' => '02-5555-6666', 'address' => '서울시 송파구 잠실동', 'company' => '잠실건설(주)'],
|
||||
'inspection' => ['end_date' => '2026-03-12', 'start_date' => '2026-03-10', 'request_date' => '2026-03-05'],
|
||||
'supervisor' => ['name' => '강감리', 'phone' => '02-7777-8888', 'office' => '송파감리사무소', 'address' => '서울시 송파구'],
|
||||
'site_address' => ['detail' => '잠실 롯데월드타워 15~20F', 'address' => '서울시 송파구 잠실동 29', 'postal_code' => '05551'],
|
||||
'construction_site' => ['name' => '잠실 롯데월드타워 리모델링 공사', 'lot_number' => '29', 'land_location' => '서울시 송파구 잠실동'],
|
||||
'material_distributor' => ['ceo' => '한대표', 'phone' => '02-9999-0000', 'address' => '서울시 강동구', 'company' => '동부자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0004',
|
||||
'site_name' => '마곡 LG사이언스파크 증축',
|
||||
'status' => 'completed',
|
||||
'client_id' => self::CLIENT_IDS[3],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-03-01',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '윤서연', 'phone' => '010-4444-5555'],
|
||||
'contractor' => ['name' => '임시공', 'phone' => '02-1234-5678', 'address' => '서울시 강서구 마곡동', 'company' => '마곡종합건설(주)'],
|
||||
'inspection' => ['end_date' => '2026-03-15', 'start_date' => '2026-03-13', 'request_date' => '2026-03-08'],
|
||||
'supervisor' => ['name' => '오감리', 'phone' => '02-2345-6789', 'office' => '강서감리사무소', 'address' => '서울시 강서구'],
|
||||
'site_address' => ['detail' => 'LG사이언스파크 E동 증축', 'address' => '서울시 강서구 마곡동 757', 'postal_code' => '07796'],
|
||||
'construction_site' => ['name' => '마곡 LG사이언스파크 증축공사', 'lot_number' => '757', 'land_location' => '서울시 강서구 마곡동'],
|
||||
'material_distributor' => ['ceo' => '장대표', 'phone' => '02-3456-7890', 'address' => '서울시 영등포구', 'company' => '서부자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0005',
|
||||
'site_name' => '인천 송도 스마트시티 아파트',
|
||||
'status' => 'in_progress',
|
||||
'client_id' => self::CLIENT_IDS[3],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-03-05',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '안재현', 'phone' => '010-5555-6666'],
|
||||
'contractor' => ['name' => '배시공', 'phone' => '032-1111-2222', 'address' => '인천시 연수구 송도동', 'company' => '송도종합건설(주)'],
|
||||
'inspection' => ['end_date' => null, 'start_date' => '2026-03-18', 'request_date' => '2026-03-10'],
|
||||
'supervisor' => ['name' => '황감리', 'phone' => '032-3333-4444', 'office' => '인천감리사무소', 'address' => '인천시 연수구'],
|
||||
'site_address' => ['detail' => '송도 스마트시티 A블록 101~105동', 'address' => '인천시 연수구 송도동 100-1', 'postal_code' => '21990'],
|
||||
'construction_site' => ['name' => '인천 송도 스마트시티 아파트 신축', 'lot_number' => '100-1', 'land_location' => '인천시 연수구 송도동'],
|
||||
'material_distributor' => ['ceo' => '서대표', 'phone' => '032-5555-6666', 'address' => '인천시 남동구', 'company' => '인천자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0006',
|
||||
'site_name' => '화성 동탄2 행복주택 단지',
|
||||
'status' => 'in_progress',
|
||||
'client_id' => self::CLIENT_IDS[4],
|
||||
'inspector_id' => $userId,
|
||||
'received_date' => '2026-03-06',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '류준열', 'phone' => '010-6666-7777'],
|
||||
'contractor' => ['name' => '조시공', 'phone' => '031-2222-3333', 'address' => '경기도 화성시 동탄', 'company' => '동탄건설(주)'],
|
||||
'inspection' => ['end_date' => null, 'start_date' => null, 'request_date' => '2026-03-12'],
|
||||
'supervisor' => ['name' => '문감리', 'phone' => '031-4444-5555', 'office' => '화성감리사무소', 'address' => '경기도 화성시'],
|
||||
'site_address' => ['detail' => '동탄2 행복주택 A1~A5동', 'address' => '경기도 화성시 동탄면 200-3', 'postal_code' => '18450'],
|
||||
'construction_site' => ['name' => '화성 동탄2 행복주택 단지 신축공사', 'lot_number' => '200-3', 'land_location' => '경기도 화성시 동탄면'],
|
||||
'material_distributor' => ['ceo' => '남대표', 'phone' => '031-6666-7777', 'address' => '경기도 오산시', 'company' => '경기자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0007',
|
||||
'site_name' => '세종시 정부청사 별관',
|
||||
'status' => 'in_progress',
|
||||
'client_id' => self::CLIENT_IDS[0],
|
||||
'inspector_id' => null,
|
||||
'received_date' => '2026-03-07',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '김세종', 'phone' => '010-7777-8888'],
|
||||
'contractor' => ['name' => '정시공', 'phone' => '044-1111-2222', 'address' => '세종시 어진동', 'company' => '세종건설(주)'],
|
||||
'inspection' => ['end_date' => null, 'start_date' => null, 'request_date' => null],
|
||||
'supervisor' => ['name' => '고감리', 'phone' => '044-3333-4444', 'office' => '세종감리사무소', 'address' => '세종시 나성동'],
|
||||
'site_address' => ['detail' => '정부세종청사 별관동', 'address' => '세종시 어진동 850', 'postal_code' => '30113'],
|
||||
'construction_site' => ['name' => '세종시 정부청사 별관 신축공사', 'lot_number' => '850', 'land_location' => '세종시 어진동'],
|
||||
'material_distributor' => ['ceo' => '윤대표', 'phone' => '044-5555-6666', 'address' => '대전시 유성구', 'company' => '중부자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0008',
|
||||
'site_name' => '부산 해운대 엘시티 주상복합',
|
||||
'status' => 'in_progress',
|
||||
'client_id' => self::CLIENT_IDS[1],
|
||||
'inspector_id' => null,
|
||||
'received_date' => '2026-03-08',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '이부산', 'phone' => '010-8888-9999'],
|
||||
'contractor' => ['name' => '노시공', 'phone' => '051-1111-2222', 'address' => '부산시 해운대구', 'company' => '해운대건설(주)'],
|
||||
'inspection' => ['end_date' => null, 'start_date' => null, 'request_date' => null],
|
||||
'supervisor' => ['name' => '차감리', 'phone' => '051-3333-4444', 'office' => '부산감리사무소', 'address' => '부산시 해운대구'],
|
||||
'site_address' => ['detail' => '해운대 엘시티 B동 전층', 'address' => '부산시 해운대구 우동 1478', 'postal_code' => '48060'],
|
||||
'construction_site' => ['name' => '부산 해운대 엘시티 주상복합 리모델링', 'lot_number' => '1478', 'land_location' => '부산시 해운대구 우동'],
|
||||
'material_distributor' => ['ceo' => '백대표', 'phone' => '051-5555-6666', 'address' => '부산시 사하구', 'company' => '남부자재(주)'],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0009',
|
||||
'site_name' => '수원 광교 복합문화센터',
|
||||
'status' => 'draft',
|
||||
'client_id' => self::CLIENT_IDS[4],
|
||||
'inspector_id' => null,
|
||||
'received_date' => '2026-03-09',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '한지민', 'phone' => '010-5555-6666'],
|
||||
'contractor' => ['name' => '', 'phone' => '', 'address' => '', 'company' => ''],
|
||||
'inspection' => ['end_date' => null, 'start_date' => null, 'request_date' => null],
|
||||
'supervisor' => ['name' => '', 'phone' => '', 'office' => '', 'address' => ''],
|
||||
'site_address' => ['detail' => '광교 복합문화센터 전관', 'address' => '경기도 수원시 영통구 광교동 200', 'postal_code' => '16508'],
|
||||
'construction_site' => ['name' => '수원 광교 복합문화센터 신축공사', 'lot_number' => '200', 'land_location' => '경기도 수원시 영통구 광교동'],
|
||||
'material_distributor' => ['ceo' => '', 'phone' => '', 'address' => '', 'company' => ''],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'quality_doc_number' => 'KD-QD-202604-0010',
|
||||
'site_name' => '대구 수성 의료복합단지',
|
||||
'status' => 'draft',
|
||||
'client_id' => self::CLIENT_IDS[2],
|
||||
'inspector_id' => null,
|
||||
'received_date' => '2026-03-10',
|
||||
'options' => json_encode([
|
||||
'manager' => ['name' => '박대구', 'phone' => '010-9999-0000'],
|
||||
'contractor' => ['name' => '', 'phone' => '', 'address' => '', 'company' => ''],
|
||||
'inspection' => ['end_date' => null, 'start_date' => null, 'request_date' => null],
|
||||
'supervisor' => ['name' => '', 'phone' => '', 'office' => '', 'address' => ''],
|
||||
'site_address' => ['detail' => '수성 의료복합단지 본관', 'address' => '대구시 수성구 범어동 350', 'postal_code' => '42020'],
|
||||
'construction_site' => ['name' => '대구 수성 의료복합단지 신축공사', 'lot_number' => '350', 'land_location' => '대구시 수성구 범어동'],
|
||||
'material_distributor' => ['ceo' => '', 'phone' => '', 'address' => '', 'company' => ''],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$qualityDocIds = [];
|
||||
foreach ($qualityDocs as $doc) {
|
||||
$qualityDocIds[] = DB::table('quality_documents')->insertGetId(array_merge($doc, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' ✅ quality_documents: '.count($qualityDocIds).'개 생성');
|
||||
|
||||
// completed/in_progress 문서에 수주 연결 (draft 제외)
|
||||
$docOrderMapping = [
|
||||
$qualityDocIds[0] => [self::ORDER_IDS[0], self::ORDER_IDS[1]], // completed
|
||||
$qualityDocIds[1] => [self::ORDER_IDS[2], self::ORDER_IDS[3], self::ORDER_IDS[4]], // completed
|
||||
$qualityDocIds[2] => [self::ORDER_IDS[5], self::ORDER_IDS[6]], // completed
|
||||
$qualityDocIds[3] => [self::ORDER_IDS[7], self::ORDER_IDS[8]], // completed
|
||||
$qualityDocIds[4] => [self::ORDER_IDS[9], self::ORDER_IDS[0]], // in_progress
|
||||
$qualityDocIds[5] => [self::ORDER_IDS[1], self::ORDER_IDS[2]], // in_progress
|
||||
$qualityDocIds[6] => [self::ORDER_IDS[3]], // in_progress
|
||||
$qualityDocIds[7] => [self::ORDER_IDS[4], self::ORDER_IDS[5]], // in_progress
|
||||
];
|
||||
|
||||
$qdoMap = [];
|
||||
$qdoCount = 0;
|
||||
foreach ($docOrderMapping as $docId => $orderIds) {
|
||||
foreach ($orderIds as $orderId) {
|
||||
$qdoId = DB::table('quality_document_orders')->insertGetId([
|
||||
'quality_document_id' => $docId,
|
||||
'order_id' => $orderId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$qdoMap[$docId][$orderId] = $qdoId;
|
||||
$qdoCount++;
|
||||
}
|
||||
}
|
||||
$this->command->info(' ✅ quality_document_orders: '.$qdoCount.'개 생성');
|
||||
|
||||
// 각 수주별 order_items 조회 후 locations 생성
|
||||
$inspectionDataSets = [
|
||||
json_encode(['motor' => 'pass', 'material' => 'pass', 'impactTest' => 'pass', 'productName' => '방화셔터', 'guideRailGap' => 'pass', 'openCloseTest' => 'pass']),
|
||||
json_encode(['motor' => 'pass', 'material' => 'pass', 'impactTest' => 'fail', 'productName' => '방화셔터', 'guideRailGap' => 'pass', 'openCloseTest' => 'pass']),
|
||||
json_encode(['motor' => 'pass', 'material' => 'pass', 'impactTest' => 'pass', 'productName' => '방화문', 'guideRailGap' => 'N/A', 'openCloseTest' => 'pass']),
|
||||
json_encode(['motor' => 'N/A', 'material' => 'pass', 'impactTest' => 'pass', 'productName' => '스크린셔터', 'guideRailGap' => 'pass', 'openCloseTest' => 'pass']),
|
||||
json_encode(['motor' => 'pass', 'material' => 'fail', 'impactTest' => 'pass', 'productName' => '방화셔터', 'guideRailGap' => 'pass', 'openCloseTest' => 'fail']),
|
||||
json_encode(['motor' => 'pass', 'material' => 'pass', 'impactTest' => 'pass', 'productName' => '절곡셔터', 'guideRailGap' => 'pass', 'openCloseTest' => 'pass']),
|
||||
];
|
||||
|
||||
$completedDocIds = array_slice($qualityDocIds, 0, 4); // 처음 4개가 completed
|
||||
$locationCount = 0;
|
||||
foreach ($qdoMap as $docId => $orderMap) {
|
||||
foreach ($orderMap as $orderId => $qdoId) {
|
||||
$orderItemIds = DB::table('order_items')
|
||||
->where('order_id', $orderId)
|
||||
->pluck('id')
|
||||
->take(4)
|
||||
->toArray();
|
||||
|
||||
if (empty($orderItemIds)) {
|
||||
// order_items가 없으면 order_nodes → order_items 경로 시도
|
||||
$nodeIds = DB::table('order_nodes')
|
||||
->where('order_id', $orderId)
|
||||
->pluck('id');
|
||||
$orderItemIds = DB::table('order_items')
|
||||
->whereIn('order_node_id', $nodeIds)
|
||||
->pluck('id')
|
||||
->take(4)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if (empty($orderItemIds)) {
|
||||
$this->command->warn(' ⚠ order_id='.$orderId.'에 order_items 없음 (스킵)');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($orderItemIds as $idx => $orderItemId) {
|
||||
$isCompleted = in_array($docId, $completedDocIds);
|
||||
$inspectionStatus = $isCompleted
|
||||
? ($idx === 1 ? 'fail' : 'pass')
|
||||
: 'pending';
|
||||
|
||||
$inspectionData = $isCompleted
|
||||
? $inspectionDataSets[$idx % count($inspectionDataSets)]
|
||||
: null;
|
||||
|
||||
$postWidth = ($isCompleted || $idx < 2) ? rand(1800, 3200) : null;
|
||||
$postHeight = ($isCompleted || $idx < 2) ? rand(2100, 3800) : null;
|
||||
$changeReason = ($isCompleted && $idx === 0) ? '현장 사정으로 규격 변경' : null;
|
||||
|
||||
$locOptions = null;
|
||||
if ($isCompleted) {
|
||||
$locOptions = json_encode([
|
||||
'lot_audit_confirmed' => $idx !== 1,
|
||||
'lot_audit_confirmed_at' => $idx !== 1 ? $now->toDateTimeString() : null,
|
||||
'lot_audit_confirmed_by' => $idx !== 1 ? $userId : null,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('quality_document_locations')->insert([
|
||||
'quality_document_id' => $docId,
|
||||
'quality_document_order_id' => $qdoId,
|
||||
'order_item_id' => $orderItemId,
|
||||
'post_width' => $postWidth,
|
||||
'post_height' => $postHeight,
|
||||
'change_reason' => $changeReason,
|
||||
'inspection_data' => $inspectionData,
|
||||
'document_id' => null,
|
||||
'inspection_status' => $inspectionStatus,
|
||||
'options' => $locOptions,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$locationCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->command->info(' ✅ quality_document_locations: '.$locationCount.'개 생성');
|
||||
|
||||
// ============================================================
|
||||
// Page 2: 실적신고 (performance_reports)
|
||||
// ============================================================
|
||||
$this->command->info('📊 Page 2: 실적신고 더미 데이터 생성...');
|
||||
|
||||
$existingReports = DB::table('performance_reports')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('quality_document_id', $qualityDocIds)
|
||||
->count();
|
||||
|
||||
if ($existingReports > 0) {
|
||||
$this->command->info(' ⚠ performance_reports: 이미 '.$existingReports.'개 존재 (스킵)');
|
||||
} else {
|
||||
$performanceReports = [
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[0],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'confirmed',
|
||||
'confirmed_date' => '2026-03-06',
|
||||
'confirmed_by' => $userId,
|
||||
'memo' => '1분기 검사 완료 - 강남 르네상스 건',
|
||||
],
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[1],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'confirmed',
|
||||
'confirmed_date' => '2026-03-09',
|
||||
'confirmed_by' => $userId,
|
||||
'memo' => '판교 물류센터 건 - 확인 완료',
|
||||
],
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[2],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'unconfirmed',
|
||||
'confirmed_date' => null,
|
||||
'confirmed_by' => null,
|
||||
'memo' => '잠실 리모델링 건 - 확인 대기중',
|
||||
],
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[3],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'confirmed',
|
||||
'confirmed_date' => '2026-03-16',
|
||||
'confirmed_by' => $userId,
|
||||
'memo' => '마곡 LG사이언스파크 건 - 확인 완료',
|
||||
],
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[4],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'pending',
|
||||
'confirmed_date' => null,
|
||||
'confirmed_by' => null,
|
||||
'memo' => '송도 아파트 건 - 검사 진행중',
|
||||
],
|
||||
[
|
||||
'quality_document_id' => $qualityDocIds[5],
|
||||
'year' => 2026, 'quarter' => 1,
|
||||
'confirmation_status' => 'pending',
|
||||
'confirmed_date' => null,
|
||||
'confirmed_by' => null,
|
||||
'memo' => '동탄 행복주택 건 - 검사 진행중',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($performanceReports as $report) {
|
||||
DB::table('performance_reports')->insert(array_merge($report, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' ✅ performance_reports: '.count($performanceReports).'개 생성');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Page 3: 품질인정심사 (audit_checklists + categories + items + standard_documents)
|
||||
// ============================================================
|
||||
$this->command->info('🏅 Page 3: 품질인정심사 더미 데이터 생성...');
|
||||
|
||||
$existingChecklist = DB::table('audit_checklists')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('year', 2026)
|
||||
->where('type', 'standard_manual')
|
||||
->first();
|
||||
|
||||
if ($existingChecklist) {
|
||||
$this->command->info(' ⚠ audit_checklists: 이미 존재 (스킵)');
|
||||
} else {
|
||||
// Q1 점검표 (in_progress)
|
||||
$this->seedChecklist($tenantId, $userId, $now, 2026, 1, 'in_progress', '정기심사');
|
||||
// Q2 점검표 (draft)
|
||||
$this->seedChecklist($tenantId, $userId, $now, 2026, 2, 'draft', '중간심사');
|
||||
}
|
||||
|
||||
$this->command->info('');
|
||||
$this->command->info('🎉 품질 더미 데이터 생성 완료!');
|
||||
});
|
||||
}
|
||||
|
||||
private function seedChecklist(int $tenantId, int $userId, Carbon $now, int $year, int $quarter, string $status, string $auditType): void
|
||||
{
|
||||
$checklistId = DB::table('audit_checklists')->insertGetId([
|
||||
'tenant_id' => $tenantId,
|
||||
'year' => $year,
|
||||
'quarter' => $quarter,
|
||||
'type' => 'standard_manual',
|
||||
'status' => $status,
|
||||
'options' => json_encode([
|
||||
'audit_date' => $year.'-'.str_pad($quarter * 3, 2, '0', STR_PAD_LEFT).'-15',
|
||||
'auditor' => '한국품질인증원',
|
||||
'audit_type' => $auditType,
|
||||
]),
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->command->info(' ✅ audit_checklists: '.$year.' Q'.$quarter.' ('.$status.') 생성');
|
||||
|
||||
$isActive = $status !== 'draft';
|
||||
|
||||
$categoriesData = [
|
||||
[
|
||||
'title' => '품질경영 시스템',
|
||||
'sort_order' => 1,
|
||||
'items' => [
|
||||
['name' => '품질방침 수립 및 공표', 'description' => '최고경영자의 품질방침 수립, 문서화 및 전직원 공표 여부', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '품질매뉴얼 Rev.5', 'version' => '5.0', 'date' => '2025-12-01'], ['title' => '품질방침 선언서', 'version' => '3.0', 'date' => '2025-06-15']]],
|
||||
['name' => '품질목표 설정 및 관리', 'description' => '연간 품질목표 설정, 실행계획 수립 및 주기적 모니터링', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => $year.'년 품질목표 관리대장', 'version' => '1.0', 'date' => $year.'-01-05'], ['title' => '품질목표 관리절차서', 'version' => '2.1', 'date' => '2025-09-10']]],
|
||||
['name' => '내부심사 계획 및 실시', 'description' => '연간 내부심사 계획 수립 및 실시 기록', 'is_completed' => false,
|
||||
'docs' => [['title' => '내부심사 절차서', 'version' => '4.0', 'date' => '2025-03-20'], ['title' => $year.'년 내부심사 계획서', 'version' => '1.0', 'date' => $year.'-01-15'], ['title' => '내부심사 보고서 양식', 'version' => '2.0', 'date' => '2025-07-01']]],
|
||||
['name' => '경영검토 실시', 'description' => '최고경영자 주관 경영검토 실시 및 기록 유지', 'is_completed' => false,
|
||||
'docs' => [['title' => '경영검토 절차서', 'version' => '3.2', 'date' => '2025-08-01'], ['title' => '2025년 하반기 경영검토 회의록', 'version' => '1.0', 'date' => '2025-12-20']]],
|
||||
['name' => '문서 및 기록 관리 체계', 'description' => '품질문서 체계(매뉴얼, 절차서, 지침서) 수립 및 관리', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '문서관리 절차서', 'version' => '4.5', 'date' => '2025-10-01'], ['title' => '기록관리 절차서', 'version' => '4.0', 'date' => '2025-03-10']]],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => '설계 및 개발',
|
||||
'sort_order' => 2,
|
||||
'items' => [
|
||||
['name' => '설계 입력 관리', 'description' => '설계 입력 요구사항 식별, 문서화 및 검토', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '설계관리 절차서', 'version' => '4.1', 'date' => '2025-10-15'], ['title' => '설계입력 검토서 양식', 'version' => '2.0', 'date' => '2025-05-20']]],
|
||||
['name' => '설계 출력 관리', 'description' => '설계 출력물 문서화 및 입력 요구사항 충족 확인', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '설계출력 검증서 양식', 'version' => '2.0', 'date' => '2025-06-15'], ['title' => '도면 관리기준서', 'version' => '3.0', 'date' => '2025-08-20']]],
|
||||
['name' => '설계 검증 및 유효성 확인', 'description' => '설계 출력물에 대한 검증/유효성 확인 절차 운영', 'is_completed' => false,
|
||||
'docs' => [['title' => '설계검증 절차서', 'version' => '3.0', 'date' => '2025-04-10'], ['title' => '설계유효성확인 체크리스트', 'version' => '1.5', 'date' => '2025-11-01'], ['title' => '시제품 시험성적서 양식', 'version' => '2.0', 'date' => '2025-06-30']]],
|
||||
['name' => '설계 변경 관리', 'description' => '설계 변경 요청, 승인 및 이력 관리', 'is_completed' => false,
|
||||
'docs' => [['title' => '설계변경 관리절차서', 'version' => '2.3', 'date' => '2025-07-15'], ['title' => '설계변경 요청서(ECR) 양식', 'version' => '1.0', 'date' => '2025-01-10']]],
|
||||
['name' => 'FMEA 및 위험 분석', 'description' => '설계 고장모드 영향분석(FMEA) 실시 및 관리', 'is_completed' => false,
|
||||
'docs' => [['title' => 'FMEA 절차서', 'version' => '2.0', 'date' => '2025-09-01'], ['title' => 'DFMEA 양식', 'version' => '1.5', 'date' => '2025-11-20']]],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => '구매 관리',
|
||||
'sort_order' => 3,
|
||||
'items' => [
|
||||
['name' => '협력업체 평가 및 선정', 'description' => '협력업체 초기평가, 정기평가 기준 및 실시 기록', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '협력업체 관리절차서', 'version' => '5.0', 'date' => '2025-09-01'], ['title' => '2025년 협력업체 평가결과', 'version' => '1.0', 'date' => '2025-12-15']]],
|
||||
['name' => '수입검사 절차', 'description' => '구매 자재 수입검사 기준 및 합/불합격 처리 절차', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '수입검사 절차서', 'version' => '3.1', 'date' => '2025-08-20'], ['title' => '수입검사 기준서', 'version' => '4.0', 'date' => '2025-11-10'], ['title' => '자재별 검사항목 목록', 'version' => '2.0', 'date' => '2025-10-01']]],
|
||||
['name' => '부적합 자재 처리', 'description' => '수입검사 불합격 자재의 격리, 반품, 특채 처리', 'is_completed' => false,
|
||||
'docs' => [['title' => '부적합품 관리절차서', 'version' => '3.0', 'date' => '2025-05-15'], ['title' => '특채 요청서 양식', 'version' => '1.2', 'date' => '2025-09-20']]],
|
||||
['name' => '구매문서 관리', 'description' => '구매 사양서, 발주서 등 구매문서 관리 체계', 'is_completed' => false,
|
||||
'docs' => [['title' => '구매관리 절차서', 'version' => '4.2', 'date' => '2025-07-01']]],
|
||||
['name' => '입고 및 자재 보관 관리', 'description' => '입고 검수, 자재 보관 조건 및 선입선출 관리', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '자재관리 절차서', 'version' => '3.5', 'date' => '2025-08-01'], ['title' => '창고관리 기준서', 'version' => '2.0', 'date' => '2025-10-15']]],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => '제조 공정 관리',
|
||||
'sort_order' => 4,
|
||||
'items' => [
|
||||
['name' => '공정 관리 계획', 'description' => '제조 공정별 관리항목, 관리기준, 검사방법 수립', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '공정관리 절차서', 'version' => '4.0', 'date' => '2025-07-10'], ['title' => 'QC공정도', 'version' => '3.0', 'date' => '2025-09-15']]],
|
||||
['name' => '작업표준서 관리', 'description' => '공정별 작업표준서 작성, 배포 및 최신본 관리', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '작업표준서 관리절차', 'version' => '2.5', 'date' => '2025-06-20'], ['title' => '방화셔터 조립 작업표준서', 'version' => '5.0', 'date' => '2025-11-01']]],
|
||||
['name' => '공정검사 실시', 'description' => '제조 공정 중 품질검사 기준 및 기록 관리', 'is_completed' => false,
|
||||
'docs' => [['title' => '공정검사 절차서', 'version' => '3.5', 'date' => '2025-10-20'], ['title' => '공정검사 체크시트', 'version' => '2.0', 'date' => '2025-11-15']]],
|
||||
['name' => '부적합품 관리', 'description' => '공정 중 발생한 부적합품 식별, 격리, 처리', 'is_completed' => false,
|
||||
'docs' => [['title' => '부적합품 관리절차서', 'version' => '3.0', 'date' => '2025-05-15'], ['title' => '부적합 처리대장 양식', 'version' => '2.0', 'date' => '2025-08-01']]],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => '검사 및 시험',
|
||||
'sort_order' => 5,
|
||||
'items' => [
|
||||
['name' => '최종검사 및 시험', 'description' => '완제품 출하 전 최종검사 기준 및 기록', 'is_completed' => false,
|
||||
'docs' => [['title' => '최종검사 절차서', 'version' => '4.0', 'date' => '2025-06-01'], ['title' => '방화셔터 시험성적서 양식', 'version' => '3.0', 'date' => '2025-08-10'], ['title' => '제품검사 기준서', 'version' => '5.1', 'date' => '2025-12-05']]],
|
||||
['name' => '검사·측정 장비 관리', 'description' => '검사장비 교정, 유지보수 계획 및 이력 관리', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '계측기 관리절차서', 'version' => '2.8', 'date' => '2025-04-15'], ['title' => $year.'년 교정계획표', 'version' => '1.0', 'date' => $year.'-01-10']]],
|
||||
['name' => '시정 및 예방조치', 'description' => '부적합 발생 시 시정조치, 재발방지 및 예방조치 관리', 'is_completed' => false,
|
||||
'docs' => [['title' => '시정예방조치 절차서', 'version' => '3.3', 'date' => '2025-09-25'], ['title' => '시정조치 보고서 양식', 'version' => '2.0', 'date' => '2025-05-01']]],
|
||||
['name' => '품질기록 관리', 'description' => '품질기록 식별, 보관, 보호, 검색, 폐기 절차', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '기록관리 절차서', 'version' => '4.0', 'date' => '2025-03-10'], ['title' => '기록보존 기간표', 'version' => '2.5', 'date' => '2025-07-20']]],
|
||||
['name' => '출하 및 인도 관리', 'description' => '완제품 출하검사, 포장, 운송 및 인도 절차', 'is_completed' => false,
|
||||
'docs' => [['title' => '출하관리 절차서', 'version' => '3.0', 'date' => '2025-05-20'], ['title' => '포장 및 운송 기준서', 'version' => '2.5', 'date' => '2025-09-10']]],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => '고객만족 및 지속적 개선',
|
||||
'sort_order' => 6,
|
||||
'items' => [
|
||||
['name' => '고객 불만 처리', 'description' => '고객 불만 접수, 처리, 회신 및 재발방지 체계', 'is_completed' => $isActive,
|
||||
'docs' => [['title' => '고객불만 처리절차서', 'version' => '3.0', 'date' => '2025-04-01'], ['title' => '고객불만 처리대장 양식', 'version' => '2.0', 'date' => '2025-07-15']]],
|
||||
['name' => '고객만족도 조사', 'description' => '정기적 고객만족도 조사 실시 및 결과 분석', 'is_completed' => false,
|
||||
'docs' => [['title' => '고객만족도 조사절차서', 'version' => '2.0', 'date' => '2025-06-01'], ['title' => '2025년 고객만족도 조사결과', 'version' => '1.0', 'date' => '2025-12-30']]],
|
||||
['name' => '지속적 개선 활동', 'description' => '품질개선 과제 발굴, 실행 및 효과 확인', 'is_completed' => false,
|
||||
'docs' => [['title' => '지속적개선 절차서', 'version' => '2.5', 'date' => '2025-08-15'], ['title' => '개선활동 보고서 양식', 'version' => '1.5', 'date' => '2025-10-20']]],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$categoryCount = 0;
|
||||
$itemCount = 0;
|
||||
$docCount = 0;
|
||||
|
||||
foreach ($categoriesData as $catData) {
|
||||
$categoryId = DB::table('audit_checklist_categories')->insertGetId([
|
||||
'tenant_id' => $tenantId,
|
||||
'checklist_id' => $checklistId,
|
||||
'title' => $catData['title'],
|
||||
'sort_order' => $catData['sort_order'],
|
||||
'options' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$categoryCount++;
|
||||
|
||||
foreach ($catData['items'] as $itemIdx => $itemData) {
|
||||
$completedAt = $itemData['is_completed'] ? $now->copy()->subDays(rand(1, 15)) : null;
|
||||
|
||||
$itemId = DB::table('audit_checklist_items')->insertGetId([
|
||||
'tenant_id' => $tenantId,
|
||||
'category_id' => $categoryId,
|
||||
'name' => $itemData['name'],
|
||||
'description' => $itemData['description'],
|
||||
'is_completed' => $itemData['is_completed'],
|
||||
'completed_at' => $completedAt,
|
||||
'completed_by' => $itemData['is_completed'] ? $userId : null,
|
||||
'sort_order' => $itemIdx + 1,
|
||||
'options' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$itemCount++;
|
||||
|
||||
foreach ($itemData['docs'] as $docData) {
|
||||
DB::table('audit_standard_documents')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'checklist_item_id' => $itemId,
|
||||
'title' => $docData['title'],
|
||||
'version' => $docData['version'],
|
||||
'date' => $docData['date'],
|
||||
'document_id' => null,
|
||||
'options' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$docCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' ✅ audit_checklist_categories: '.$categoryCount.'개 생성');
|
||||
$this->command->info(' ✅ audit_checklist_items: '.$itemCount.'개 생성');
|
||||
$this->command->info(' ✅ audit_standard_documents: '.$docCount.'개 생성');
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ ## 🔗 프로젝트 문서 (SAM/docs로 이동됨)
|
||||
|
||||
| 이전 위치 | 새 위치 | 설명 |
|
||||
|-----------|---------|------|
|
||||
| `analysis/` | `docs/data/analysis/` | Item DB 분석 문서 |
|
||||
| `analysis/` | `docs/dev/data/analysis/` | Item DB 분석 문서 |
|
||||
| `front/` | `docs/front/` | 프론트엔드 요청 문서 |
|
||||
| `specs/` | `docs/specs/` | 기능 스펙 문서 |
|
||||
|
||||
@@ -49,6 +49,6 @@ ## 📝 문서 추가 가이드
|
||||
|-----------|-----------|
|
||||
| Swagger 스펙 | `api/docs/swagger/` |
|
||||
| API Flow 테스트 | `api/docs/api-flows/` 또는 `flow-tests/` |
|
||||
| 프로젝트 분석 | `SAM/docs/data/` |
|
||||
| 프로젝트 분석 | `SAM/docs/dev/data/` |
|
||||
| 기능 스펙 | `SAM/docs/specs/` |
|
||||
| 프론트 요청 | `SAM/docs/front/` |
|
||||
|
||||
@@ -74,5 +74,5 @@ ## ⚠️ 배포 시 주의사항
|
||||
- 테넌트별 초기 데이터 설정 필요
|
||||
|
||||
## 🔗 관련 문서
|
||||
- `docs/plans/quote-calculation-api-plan.md`
|
||||
- `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||
- `mng/app/Services/Quote/FormulaEvaluatorService.php` (원본)
|
||||
|
||||
@@ -2,7 +2,7 @@ # 변경 내용 요약
|
||||
|
||||
**날짜:** 2026-01-02 13:00
|
||||
**작업명:** Phase 1.2 입력 변수 처리 - React QuoteItem 매핑
|
||||
**계획 문서:** docs/plans/quote-calculation-api-plan.md
|
||||
**계획 문서:** docs/dev/dev_plans/quote-calculation-api-plan.md
|
||||
|
||||
## 변경 개요
|
||||
|
||||
@@ -151,4 +151,4 @@ ## API 사용 예시
|
||||
## 관련 문서
|
||||
|
||||
- Phase 1.1: `20251230_2339_quote_calculation_mng_logic.md` (BOM 단건 산출)
|
||||
- 계획 문서: `docs/plans/quote-calculation-api-plan.md`
|
||||
- 계획 문서: `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||
@@ -444,6 +444,15 @@
|
||||
'already_completed' => '이미 완료된 검사입니다.',
|
||||
],
|
||||
|
||||
// 품질관리서 관련
|
||||
'quality' => [
|
||||
'cannot_delete_completed' => '완료된 품질관리서는 삭제할 수 없습니다.',
|
||||
'already_completed' => '이미 완료된 품질관리서입니다.',
|
||||
'cannot_modify_completed' => '완료된 품질관리서는 수정할 수 없습니다.',
|
||||
'pending_locations' => '미완료 개소가 :count건 있습니다.',
|
||||
'confirm_failed' => '필수정보가 누락된 건이 있어 확정할 수 없습니다.',
|
||||
],
|
||||
|
||||
// 입찰 관련
|
||||
'bidding' => [
|
||||
'not_found' => '입찰을 찾을 수 없습니다.',
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
require __DIR__.'/api/v1/app.php';
|
||||
require __DIR__.'/api/v1/audit.php';
|
||||
require __DIR__.'/api/v1/esign.php';
|
||||
require __DIR__.'/api/v1/quality.php';
|
||||
|
||||
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
||||
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
Route::get('/{id}', [DocumentController::class, 'show'])->whereNumber('id')->name('v1.documents.show');
|
||||
Route::post('/', [DocumentController::class, 'store'])->name('v1.documents.store');
|
||||
Route::patch('/{id}', [DocumentController::class, 'update'])->whereNumber('id')->name('v1.documents.update');
|
||||
Route::patch('/{id}/snapshot', [DocumentController::class, 'patchSnapshot'])->whereNumber('id')->name('v1.documents.snapshot');
|
||||
Route::delete('/{id}', [DocumentController::class, 'destroy'])->whereNumber('id')->name('v1.documents.destroy');
|
||||
|
||||
// 결재 워크플로우
|
||||
|
||||
@@ -163,6 +163,8 @@
|
||||
Route::get('/{id}', [CardTransactionController::class, 'show'])->whereNumber('id')->name('v1.card-transactions.show');
|
||||
Route::put('/{id}', [CardTransactionController::class, 'update'])->whereNumber('id')->name('v1.card-transactions.update');
|
||||
Route::delete('/{id}', [CardTransactionController::class, 'destroy'])->whereNumber('id')->name('v1.card-transactions.destroy');
|
||||
Route::get('/{id}/journal-entries', [CardTransactionController::class, 'getJournalEntries'])->whereNumber('id')->name('v1.card-transactions.journal-entries.show');
|
||||
Route::post('/{id}/journal-entries', [CardTransactionController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.card-transactions.journal-entries.store');
|
||||
});
|
||||
|
||||
// Bank Transaction API (은행 거래 조회)
|
||||
@@ -287,6 +289,10 @@
|
||||
Route::post('/{id}/cancel', [TaxInvoiceController::class, 'cancel'])->whereNumber('id')->name('v1.tax-invoices.cancel');
|
||||
Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status');
|
||||
Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue');
|
||||
Route::get('/{id}/journal-entries', [TaxInvoiceController::class, 'getJournalEntries'])->whereNumber('id')->name('v1.tax-invoices.journal-entries.show');
|
||||
Route::post('/{id}/journal-entries', [TaxInvoiceController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.tax-invoices.journal-entries.store');
|
||||
Route::put('/{id}/journal-entries', [TaxInvoiceController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.tax-invoices.journal-entries.update');
|
||||
Route::delete('/{id}/journal-entries', [TaxInvoiceController::class, 'deleteJournalEntries'])->whereNumber('id')->name('v1.tax-invoices.journal-entries.destroy');
|
||||
});
|
||||
|
||||
// Bad Debt API (악성채권 추심관리)
|
||||
@@ -320,6 +326,8 @@
|
||||
Route::prefix('account-subjects')->group(function () {
|
||||
Route::get('', [AccountSubjectController::class, 'index'])->name('v1.account-subjects.index');
|
||||
Route::post('', [AccountSubjectController::class, 'store'])->name('v1.account-subjects.store');
|
||||
Route::post('/seed-defaults', [AccountSubjectController::class, 'seedDefaults'])->name('v1.account-subjects.seed-defaults');
|
||||
Route::put('/{id}', [AccountSubjectController::class, 'update'])->whereNumber('id')->name('v1.account-subjects.update');
|
||||
Route::patch('/{id}/status', [AccountSubjectController::class, 'toggleStatus'])->whereNumber('id')->name('v1.account-subjects.toggle-status');
|
||||
Route::delete('/{id}', [AccountSubjectController::class, 'destroy'])->whereNumber('id')->name('v1.account-subjects.destroy');
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Api\V1\InspectionController;
|
||||
use App\Http\Controllers\Api\V1\ProductionOrderController;
|
||||
use App\Http\Controllers\Api\V1\WorkOrderController;
|
||||
use App\Http\Controllers\Api\V1\WorkResultController;
|
||||
use App\Http\Controllers\V1\ProcessController;
|
||||
@@ -121,3 +122,10 @@
|
||||
Route::delete('/{id}', [InspectionController::class, 'destroy'])->whereNumber('id')->name('v1.inspections.destroy'); // 삭제
|
||||
Route::patch('/{id}/complete', [InspectionController::class, 'complete'])->whereNumber('id')->name('v1.inspections.complete'); // 완료 처리
|
||||
});
|
||||
|
||||
// Production Order API (생산지시 조회)
|
||||
Route::prefix('production-orders')->group(function () {
|
||||
Route::get('', [ProductionOrderController::class, 'index'])->name('v1.production-orders.index');
|
||||
Route::get('/stats', [ProductionOrderController::class, 'stats'])->name('v1.production-orders.stats');
|
||||
Route::get('/{orderId}', [ProductionOrderController::class, 'show'])->whereNumber('orderId')->name('v1.production-orders.show');
|
||||
});
|
||||
|
||||
64
routes/api/v1/quality.php
Normal file
64
routes/api/v1/quality.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 품질관리 API 라우트 (v1)
|
||||
*
|
||||
* - 제품검사 (품질관리서)
|
||||
* - 실적신고
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Api\V1\AuditChecklistController;
|
||||
use App\Http\Controllers\Api\V1\PerformanceReportController;
|
||||
use App\Http\Controllers\Api\V1\QmsLotAuditController;
|
||||
use App\Http\Controllers\Api\V1\QualityDocumentController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// 제품검사 (품질관리서)
|
||||
Route::prefix('quality/documents')->group(function () {
|
||||
Route::get('', [QualityDocumentController::class, 'index'])->name('v1.quality.documents.index');
|
||||
Route::get('/stats', [QualityDocumentController::class, 'stats'])->name('v1.quality.documents.stats');
|
||||
Route::get('/calendar', [QualityDocumentController::class, 'calendar'])->name('v1.quality.documents.calendar');
|
||||
Route::get('/available-orders', [QualityDocumentController::class, 'availableOrders'])->name('v1.quality.documents.available-orders');
|
||||
Route::post('', [QualityDocumentController::class, 'store'])->name('v1.quality.documents.store');
|
||||
Route::get('/{id}', [QualityDocumentController::class, 'show'])->whereNumber('id')->name('v1.quality.documents.show');
|
||||
Route::put('/{id}', [QualityDocumentController::class, 'update'])->whereNumber('id')->name('v1.quality.documents.update');
|
||||
Route::delete('/{id}', [QualityDocumentController::class, 'destroy'])->whereNumber('id')->name('v1.quality.documents.destroy');
|
||||
Route::patch('/{id}/complete', [QualityDocumentController::class, 'complete'])->whereNumber('id')->name('v1.quality.documents.complete');
|
||||
Route::post('/{id}/orders', [QualityDocumentController::class, 'attachOrders'])->whereNumber('id')->name('v1.quality.documents.attach-orders');
|
||||
Route::delete('/{id}/orders/{orderId}', [QualityDocumentController::class, 'detachOrder'])->whereNumber('id')->whereNumber('orderId')->name('v1.quality.documents.detach-order');
|
||||
Route::post('/{id}/locations/{locId}/inspect', [QualityDocumentController::class, 'inspectLocation'])->whereNumber('id')->whereNumber('locId')->name('v1.quality.documents.inspect-location');
|
||||
Route::get('/{id}/request-document', [QualityDocumentController::class, 'requestDocument'])->whereNumber('id')->name('v1.quality.documents.request-document');
|
||||
Route::get('/{id}/result-document', [QualityDocumentController::class, 'resultDocument'])->whereNumber('id')->name('v1.quality.documents.result-document');
|
||||
});
|
||||
|
||||
// 실적신고
|
||||
Route::prefix('quality/performance-reports')->group(function () {
|
||||
Route::get('', [PerformanceReportController::class, 'index'])->name('v1.quality.performance-reports.index');
|
||||
Route::get('/stats', [PerformanceReportController::class, 'stats'])->name('v1.quality.performance-reports.stats');
|
||||
Route::get('/missing', [PerformanceReportController::class, 'missing'])->name('v1.quality.performance-reports.missing');
|
||||
Route::patch('/confirm', [PerformanceReportController::class, 'confirm'])->name('v1.quality.performance-reports.confirm');
|
||||
Route::patch('/unconfirm', [PerformanceReportController::class, 'unconfirm'])->name('v1.quality.performance-reports.unconfirm');
|
||||
Route::patch('/memo', [PerformanceReportController::class, 'updateMemo'])->name('v1.quality.performance-reports.memo');
|
||||
});
|
||||
|
||||
// QMS 로트 추적 심사
|
||||
Route::prefix('qms/lot-audit')->group(function () {
|
||||
Route::get('/reports', [QmsLotAuditController::class, 'index'])->name('v1.qms.lot-audit.reports');
|
||||
Route::get('/reports/{id}', [QmsLotAuditController::class, 'show'])->whereNumber('id')->name('v1.qms.lot-audit.reports.show');
|
||||
Route::get('/routes/{id}/documents', [QmsLotAuditController::class, 'routeDocuments'])->whereNumber('id')->name('v1.qms.lot-audit.routes.documents');
|
||||
Route::get('/documents/{type}/{id}', [QmsLotAuditController::class, 'documentDetail'])->whereNumber('id')->name('v1.qms.lot-audit.documents.detail');
|
||||
Route::patch('/units/{id}/confirm', [QmsLotAuditController::class, 'confirm'])->whereNumber('id')->name('v1.qms.lot-audit.units.confirm');
|
||||
});
|
||||
|
||||
// QMS 기준/매뉴얼 심사 (1일차)
|
||||
Route::prefix('qms')->group(function () {
|
||||
Route::get('/checklists', [AuditChecklistController::class, 'index'])->name('v1.qms.checklists.index');
|
||||
Route::post('/checklists', [AuditChecklistController::class, 'store'])->name('v1.qms.checklists.store');
|
||||
Route::get('/checklists/{id}', [AuditChecklistController::class, 'show'])->whereNumber('id')->name('v1.qms.checklists.show');
|
||||
Route::put('/checklists/{id}', [AuditChecklistController::class, 'update'])->whereNumber('id')->name('v1.qms.checklists.update');
|
||||
Route::patch('/checklists/{id}/complete', [AuditChecklistController::class, 'complete'])->whereNumber('id')->name('v1.qms.checklists.complete');
|
||||
Route::patch('/checklist-items/{id}/toggle', [AuditChecklistController::class, 'toggleItem'])->whereNumber('id')->name('v1.qms.checklist-items.toggle');
|
||||
Route::get('/checklist-items/{id}/documents', [AuditChecklistController::class, 'itemDocuments'])->whereNumber('id')->name('v1.qms.checklist-items.documents');
|
||||
Route::post('/checklist-items/{id}/documents', [AuditChecklistController::class, 'attachDocument'])->whereNumber('id')->name('v1.qms.checklist-items.documents.attach');
|
||||
Route::delete('/checklist-items/{id}/documents/{docId}', [AuditChecklistController::class, 'detachDocument'])->whereNumber('id')->whereNumber('docId')->name('v1.qms.checklist-items.documents.detach');
|
||||
});
|
||||
Reference in New Issue
Block a user