Compare commits
48 Commits
develop
...
091719e81b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
091719e81b | ||
|
|
b06438cc52 | ||
|
|
1e5cd70081 | ||
|
|
b21c9de6eb | ||
|
|
91567c54bd | ||
|
|
88eb507426 | ||
|
|
0bf56931fa | ||
|
|
c55a4a42e6 | ||
|
|
428b2e2a12 | ||
|
|
64877869e6 | ||
|
|
92efe2e83b | ||
|
|
78eb9363f4 | ||
|
|
c611f551a6 | ||
|
|
48889a7250 | ||
|
|
18f39433ae | ||
|
|
3785d87df4 | ||
|
|
d9075e5da5 | ||
| 1d71b588cb | |||
|
|
521229adcf | ||
|
|
5ce2d2fcbf | ||
|
|
5f5b5db59f | ||
|
|
814b965748 | ||
|
|
c55380f1d2 | ||
|
|
4870b7e6eb | ||
|
|
88ef6a8490 | ||
|
|
2fd122feba | ||
|
|
5a0deddb58 | ||
| d68fd56232 | |||
|
|
d8abc57271 | ||
|
|
d7dd6cdbc5 | ||
|
|
2bb3a2872a | ||
|
|
6df1da9e42 | ||
|
|
93e94901b7 | ||
|
|
7028e27517 | ||
|
|
1d2876d90c | ||
|
|
bfb821698a | ||
|
|
b80f4a0392 | ||
|
|
2ed90dc6db | ||
|
|
347d351d9d | ||
|
|
87a8930c00 | ||
|
|
bbcb0205fe | ||
| 9bf0cc8df2 | |||
|
|
255fad99e7 | ||
|
|
08b07c724a | ||
|
|
91cdfe9917 | ||
|
|
10c09b9fea | ||
|
|
04bb990045 | ||
| 7543054df3 |
36
Jenkinsfile
vendored
36
Jenkinsfile
vendored
@@ -17,7 +17,7 @@ pipeline {
|
||||
script {
|
||||
env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
|
||||
}
|
||||
slackSend channel: '#product_infra', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
slackSend channel: '#deploy_api', color: '#439FE0', tokenCredentialId: 'slack-token',
|
||||
message: "🚀 *api* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,10 @@ pipeline {
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||
sudo chmod -R 775 storage bootstrap/cache &&
|
||||
ln -sfn /home/webservice/api-stage/shared/.env .env &&
|
||||
sudo chmod 640 /home/webservice/api-stage/shared/.env &&
|
||||
ln -sfn /home/webservice/api-stage/shared/storage/app storage/app &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
php artisan config:cache &&
|
||||
@@ -53,18 +56,18 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 운영 배포 승인 ──
|
||||
stage('Production Approval') {
|
||||
when { branch 'main' }
|
||||
steps {
|
||||
slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
message: "🔔 *api* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
|
||||
timeout(time: 24, unit: 'HOURS') {
|
||||
input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr',
|
||||
ok: '운영 배포 진행'
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── 운영 배포 승인 (런칭 후 활성화) ──
|
||||
// stage('Production Approval') {
|
||||
// when { branch 'main' }
|
||||
// steps {
|
||||
// slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
// message: "🔔 *api* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage API: https://stage-api.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>"
|
||||
// timeout(time: 24, unit: 'HOURS') {
|
||||
// input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage API: https://stage-api.sam.it.kr',
|
||||
// ok: '운영 배포 진행'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// ── main → 운영서버 Production 배포 ──
|
||||
stage('Deploy Production') {
|
||||
@@ -81,7 +84,10 @@ pipeline {
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
cd /home/webservice/api/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||
sudo chmod -R 775 storage bootstrap/cache &&
|
||||
ln -sfn /home/webservice/api/shared/.env .env &&
|
||||
sudo chmod 640 /home/webservice/api/shared/.env &&
|
||||
ln -sfn /home/webservice/api/shared/storage/app storage/app &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
php artisan config:cache &&
|
||||
@@ -103,11 +109,11 @@ pipeline {
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#product_infra', color: 'good', tokenCredentialId: 'slack-token',
|
||||
slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#product_infra', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
|
||||
@@ -29,7 +29,7 @@ class RecordStorageUsage extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$tenants = Tenant::where('status', 'active')->get();
|
||||
$tenants = Tenant::active()->get();
|
||||
|
||||
$recorded = 0;
|
||||
foreach ($tenants as $tenant) {
|
||||
|
||||
@@ -17,25 +17,13 @@
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* 특정 IP에서 발생하는 예외를 슬랙/로그에서 무시할지 확인
|
||||
* 슬랙 알림에서 무시할 예외인지 확인
|
||||
*/
|
||||
protected function shouldIgnoreException(Throwable $e): bool
|
||||
{
|
||||
$ignoredIps = array_filter(
|
||||
array_map('trim', explode(',', env('EXCEPTION_IGNORED_IPS', '')))
|
||||
);
|
||||
|
||||
if (empty($ignoredIps)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentIp = request()?->ip();
|
||||
|
||||
// 무시할 IP 목록에 있고, '회원정보 정보 없음' 예외인 경우
|
||||
if (in_array($currentIp, $ignoredIps, true)) {
|
||||
if ($e instanceof AuthenticationException && $e->getMessage() === '회원정보 정보 없음') {
|
||||
return true;
|
||||
}
|
||||
// 세션 만료로 인한 인증 실패는 슬랙 알림 제외 (API Key 검증 통과 후 발생하므로 정상 케이스)
|
||||
if ($e instanceof AuthenticationException && $e->getMessage() === '회원정보 정보 없음') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
133
app/Http/Controllers/Api/V1/CalendarScheduleController.php
Normal file
133
app/Http/Controllers/Api/V1/CalendarScheduleController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CalendarScheduleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CalendarScheduleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CalendarScheduleService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 일정 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'year' => 'required|integer|min:2000|max:2100',
|
||||
'type' => 'nullable|string',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->list(
|
||||
(int) $request->input('year'),
|
||||
$request->input('type')
|
||||
),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'year' => 'required|integer|min:2000|max:2100',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->stats((int) $request->input('year')),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->show($id),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
'type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event',
|
||||
'is_recurring' => 'boolean',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->store($validated),
|
||||
__('message.created')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
'type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event',
|
||||
'is_recurring' => 'boolean',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->update($id, $validated),
|
||||
__('message.updated')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->delete($id),
|
||||
__('message.deleted')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 등록
|
||||
*/
|
||||
public function bulkStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'schedules' => 'required|array|min:1',
|
||||
'schedules.*.name' => 'required|string|max:100',
|
||||
'schedules.*.start_date' => 'required|date',
|
||||
'schedules.*.end_date' => 'required|date|after_or_equal:schedules.*.start_date',
|
||||
'schedules.*.type' => 'required|string|in:public_holiday,temporary_holiday,substitute_holiday,tax_deadline,company_event',
|
||||
'schedules.*.is_recurring' => 'boolean',
|
||||
'schedules.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->bulkStore($validated['schedules']),
|
||||
__('message.created')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public function rules(): array
|
||||
'mobile' => 'nullable|string|max:20',
|
||||
'fax' => 'nullable|string|max:20',
|
||||
'email' => 'nullable|email|max:100',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
// 담당자 정보
|
||||
'manager_name' => 'nullable|string|max:50',
|
||||
'manager_tel' => 'nullable|string|max:20',
|
||||
|
||||
@@ -65,7 +65,7 @@ public function rules(): array
|
||||
'mobile' => 'nullable|string|max:20',
|
||||
'fax' => 'nullable|string|max:20',
|
||||
'email' => 'nullable|email|max:100',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
// 담당자 정보
|
||||
'manager_name' => 'nullable|string|max:50',
|
||||
'manager_tel' => 'nullable|string|max:20',
|
||||
|
||||
@@ -29,7 +29,7 @@ public function rules(): array
|
||||
'briefing_time' => 'nullable|string|max:10',
|
||||
'briefing_type' => ['nullable', 'string', Rule::in(SiteBriefing::TYPES)],
|
||||
'location' => 'nullable|string|max:200',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
|
||||
// 상태 정보
|
||||
'status' => ['nullable', 'string', Rule::in(SiteBriefing::STATUSES)],
|
||||
|
||||
@@ -29,7 +29,7 @@ public function rules(): array
|
||||
'briefing_time' => 'nullable|string|max:10',
|
||||
'briefing_type' => ['nullable', 'string', Rule::in(SiteBriefing::TYPES)],
|
||||
'location' => 'nullable|string|max:200',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
|
||||
// 상태 정보
|
||||
'status' => ['nullable', 'string', Rule::in(SiteBriefing::STATUSES)],
|
||||
|
||||
@@ -17,7 +17,7 @@ public function rules(): array
|
||||
'company_name' => 'required|string|max:100',
|
||||
'email' => 'nullable|email|max:100',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'business_num' => 'nullable|string|max:20',
|
||||
'ceo_name' => 'nullable|string|max:100',
|
||||
];
|
||||
|
||||
@@ -18,7 +18,7 @@ public function rules(): array
|
||||
'company_name' => 'sometimes|string|max:100',
|
||||
'email' => 'nullable|email|max:100',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'business_num' => 'nullable|string|max:20',
|
||||
'ceo_name' => 'nullable|string|max:100',
|
||||
'logo' => 'nullable|string|max:255',
|
||||
|
||||
@@ -15,7 +15,7 @@ public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'address' => ['nullable', 'string', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
|
||||
@@ -15,7 +15,7 @@ public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:100'],
|
||||
'address' => ['nullable', 'string', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
|
||||
41
app/Models/Commons/Holiday.php
Normal file
41
app/Models/Commons/Holiday.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Holiday extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'holidays';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'name',
|
||||
'type',
|
||||
'is_recurring',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'is_recurring' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeForTenant($query, int $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
public function scopeForYear($query, int $year)
|
||||
{
|
||||
return $query->whereYear('start_date', $year);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class Leave extends Model
|
||||
'status',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'approval_id',
|
||||
'reject_reason',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -81,6 +82,18 @@ class Leave extends Model
|
||||
|
||||
public const TYPE_PARENTAL = 'parental'; // 육아
|
||||
|
||||
public const TYPE_BUSINESS_TRIP = 'business_trip'; // 출장
|
||||
|
||||
public const TYPE_REMOTE = 'remote'; // 재택근무
|
||||
|
||||
public const TYPE_FIELD_WORK = 'field_work'; // 외근
|
||||
|
||||
public const TYPE_EARLY_LEAVE = 'early_leave'; // 조퇴
|
||||
|
||||
public const TYPE_LATE_REASON = 'late_reason'; // 지각사유서
|
||||
|
||||
public const TYPE_ABSENT_REASON = 'absent_reason'; // 결근사유서
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_APPROVED = 'approved';
|
||||
@@ -97,6 +110,45 @@ class Leave extends Model
|
||||
self::TYPE_FAMILY,
|
||||
self::TYPE_MATERNITY,
|
||||
self::TYPE_PARENTAL,
|
||||
self::TYPE_BUSINESS_TRIP,
|
||||
self::TYPE_REMOTE,
|
||||
self::TYPE_FIELD_WORK,
|
||||
self::TYPE_EARLY_LEAVE,
|
||||
self::TYPE_LATE_REASON,
|
||||
self::TYPE_ABSENT_REASON,
|
||||
];
|
||||
|
||||
// 그룹 상수
|
||||
public const VACATION_TYPES = [
|
||||
self::TYPE_ANNUAL, self::TYPE_HALF_AM, self::TYPE_HALF_PM,
|
||||
self::TYPE_SICK, self::TYPE_FAMILY, self::TYPE_MATERNITY, self::TYPE_PARENTAL,
|
||||
];
|
||||
|
||||
public const ATTENDANCE_REQUEST_TYPES = [
|
||||
self::TYPE_BUSINESS_TRIP, self::TYPE_REMOTE, self::TYPE_FIELD_WORK, self::TYPE_EARLY_LEAVE,
|
||||
];
|
||||
|
||||
public const REASON_REPORT_TYPES = [
|
||||
self::TYPE_LATE_REASON, self::TYPE_ABSENT_REASON,
|
||||
];
|
||||
|
||||
// 유형 → 결재양식코드 매핑
|
||||
public const FORM_CODE_MAP = [
|
||||
'annual' => 'leave', 'half_am' => 'leave', 'half_pm' => 'leave',
|
||||
'sick' => 'leave', 'family' => 'leave', 'maternity' => 'leave', 'parental' => 'leave',
|
||||
'business_trip' => 'attendance_request', 'remote' => 'attendance_request',
|
||||
'field_work' => 'attendance_request', 'early_leave' => 'attendance_request',
|
||||
'late_reason' => 'reason_report', 'absent_reason' => 'reason_report',
|
||||
];
|
||||
|
||||
// 유형 → 근태상태 매핑 (승인 시 Attendance에 반영할 상태)
|
||||
public const ATTENDANCE_STATUS_MAP = [
|
||||
'annual' => 'vacation', 'half_am' => 'vacation', 'half_pm' => 'vacation',
|
||||
'sick' => 'vacation', 'family' => 'vacation', 'maternity' => 'vacation', 'parental' => 'vacation',
|
||||
'business_trip' => 'businessTrip', 'remote' => 'remote', 'field_work' => 'fieldWork',
|
||||
'early_leave' => null,
|
||||
'late_reason' => null,
|
||||
'absent_reason' => null,
|
||||
];
|
||||
|
||||
public const STATUSES = [
|
||||
@@ -252,6 +304,12 @@ public function getLeaveTypeLabelAttribute(): string
|
||||
self::TYPE_FAMILY => '경조사',
|
||||
self::TYPE_MATERNITY => '출산휴가',
|
||||
self::TYPE_PARENTAL => '육아휴직',
|
||||
self::TYPE_BUSINESS_TRIP => '출장',
|
||||
self::TYPE_REMOTE => '재택근무',
|
||||
self::TYPE_FIELD_WORK => '외근',
|
||||
self::TYPE_EARLY_LEAVE => '조퇴',
|
||||
self::TYPE_LATE_REASON => '지각사유서',
|
||||
self::TYPE_ABSENT_REASON => '결근사유서',
|
||||
default => $this->leave_type,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
|
||||
@@ -44,6 +44,7 @@ class TenantUserProfile extends Model
|
||||
'employee_status',
|
||||
'manager_user_id',
|
||||
'json_extra',
|
||||
'worker_type',
|
||||
'profile_photo_path',
|
||||
'display_name',
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Users\User;
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
180
app/Services/CalendarScheduleService.php
Normal file
180
app/Services/CalendarScheduleService.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Commons\Holiday;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class CalendarScheduleService extends Service
|
||||
{
|
||||
/**
|
||||
* 연도별 일정 목록 조회
|
||||
*/
|
||||
public function list(int $year, ?string $type = null): array
|
||||
{
|
||||
$query = Holiday::forTenant($this->tenantId())
|
||||
->forYear($year)
|
||||
->orderBy('start_date');
|
||||
|
||||
if ($type) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
return $query->get()->map(function ($h) {
|
||||
return [
|
||||
'id' => $h->id,
|
||||
'name' => $h->name,
|
||||
'type' => $h->type,
|
||||
'start_date' => $h->start_date->format('Y-m-d'),
|
||||
'end_date' => $h->end_date->format('Y-m-d'),
|
||||
'days' => $h->start_date->diffInDays($h->end_date) + 1,
|
||||
'is_recurring' => $h->is_recurring,
|
||||
'memo' => $h->memo,
|
||||
'created_at' => $h->created_at?->toIso8601String(),
|
||||
'updated_at' => $h->updated_at?->toIso8601String(),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function stats(int $year): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$holidays = Holiday::forTenant($tenantId)->forYear($year)->get();
|
||||
|
||||
$totalDays = $holidays->sum(function ($h) {
|
||||
return $h->start_date->diffInDays($h->end_date) + 1;
|
||||
});
|
||||
|
||||
return [
|
||||
'total_count' => $holidays->count(),
|
||||
'total_holiday_days' => $totalDays,
|
||||
'public_holiday_count' => $holidays->where('type', 'public_holiday')->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 조회
|
||||
*/
|
||||
public function show(int $id): array
|
||||
{
|
||||
$h = Holiday::forTenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
return [
|
||||
'id' => $h->id,
|
||||
'name' => $h->name,
|
||||
'type' => $h->type,
|
||||
'start_date' => $h->start_date->format('Y-m-d'),
|
||||
'end_date' => $h->end_date->format('Y-m-d'),
|
||||
'days' => $h->start_date->diffInDays($h->end_date) + 1,
|
||||
'is_recurring' => $h->is_recurring,
|
||||
'memo' => $h->memo,
|
||||
'created_at' => $h->created_at?->toIso8601String(),
|
||||
'updated_at' => $h->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록
|
||||
*/
|
||||
public function store(array $data): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$exists = Holiday::forTenant($tenantId)
|
||||
->where('start_date', $data['start_date'])
|
||||
->where('end_date', $data['end_date'])
|
||||
->where('name', $data['name'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new HttpException(422, __('error.duplicate'));
|
||||
}
|
||||
|
||||
$holiday = Holiday::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'start_date' => $data['start_date'],
|
||||
'end_date' => $data['end_date'],
|
||||
'name' => $data['name'],
|
||||
'type' => $data['type'] ?? 'public_holiday',
|
||||
'is_recurring' => $data['is_recurring'] ?? false,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
return $this->show($holiday->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(int $id, array $data): array
|
||||
{
|
||||
$holiday = Holiday::forTenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$holiday->update([
|
||||
'start_date' => $data['start_date'],
|
||||
'end_date' => $data['end_date'],
|
||||
'name' => $data['name'],
|
||||
'type' => $data['type'],
|
||||
'is_recurring' => $data['is_recurring'] ?? false,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'updated_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
return $this->show($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$holiday = Holiday::forTenant($this->tenantId())->findOrFail($id);
|
||||
$holiday->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 등록
|
||||
*/
|
||||
public function bulkStore(array $schedules): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
$count = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($schedules as $item) {
|
||||
$exists = Holiday::forTenant($tenantId)
|
||||
->where('start_date', $item['start_date'])
|
||||
->where('end_date', $item['end_date'])
|
||||
->where('name', $item['name'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Holiday::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'start_date' => $item['start_date'],
|
||||
'end_date' => $item['end_date'],
|
||||
'name' => $item['name'],
|
||||
'type' => $item['type'] ?? 'public_holiday',
|
||||
'is_recurring' => $item['is_recurring'] ?? false,
|
||||
'memo' => $item['memo'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return [
|
||||
'created' => $count,
|
||||
'skipped' => $skipped,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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('equipments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('equipment_code', 20)->comment('설비코드 (KD-M-001 형식)');
|
||||
$table->string('name', 100)->comment('설비명');
|
||||
$table->string('equipment_type', 50)->nullable()->comment('설비유형 (포밍기/미싱기/샤링기/V컷팅기/절곡기/프레스/드릴)');
|
||||
$table->string('specification', 255)->nullable()->comment('규격');
|
||||
$table->string('manufacturer', 100)->nullable()->comment('제조사');
|
||||
$table->string('model_name', 100)->nullable()->comment('모델명');
|
||||
$table->string('serial_no', 100)->nullable()->comment('제조번호');
|
||||
$table->string('location', 100)->nullable()->comment('위치 (1공장-1F, 2공장-절곡 등)');
|
||||
$table->string('production_line', 50)->nullable()->comment('생산라인 (스라트/스크린/절곡)');
|
||||
$table->date('purchase_date')->nullable()->comment('구입일');
|
||||
$table->date('install_date')->nullable()->comment('설치일');
|
||||
$table->decimal('purchase_price', 15, 2)->nullable()->comment('구입가격');
|
||||
$table->integer('useful_life')->nullable()->comment('내용연수');
|
||||
$table->string('status', 20)->default('active')->comment('상태: active/idle/disposed');
|
||||
$table->date('disposed_date')->nullable()->comment('폐기일');
|
||||
$table->foreignId('manager_id')->nullable()->comment('담당자 ID (users.id)');
|
||||
$table->string('photo_path', 500)->nullable()->comment('설비사진 경로');
|
||||
$table->text('memo')->nullable()->comment('비고');
|
||||
$table->tinyInteger('is_active')->default(1)->comment('사용여부');
|
||||
$table->integer('sort_order')->default(0)->comment('정렬순서');
|
||||
$table->foreignId('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->foreignId('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->foreignId('deleted_by')->nullable()->comment('삭제자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'equipment_code'], 'uq_equipment_code');
|
||||
$table->index(['tenant_id', 'status'], 'idx_equipment_status');
|
||||
$table->index(['tenant_id', 'production_line'], 'idx_equipment_line');
|
||||
$table->index(['tenant_id', 'equipment_type'], 'idx_equipment_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipments');
|
||||
}
|
||||
};
|
||||
@@ -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('equipment_inspection_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('equipment_id')->comment('설비 ID');
|
||||
$table->integer('item_no')->comment('항목번호 (1,2,3,4)');
|
||||
$table->string('check_point', 50)->comment('점검개소 (겉모양, 스위치, 롤러 등)');
|
||||
$table->string('check_item', 100)->comment('점검항목 (청결상태, 작동상태 등)');
|
||||
$table->string('check_timing', 20)->nullable()->comment('시기: operating/stopped');
|
||||
$table->string('check_frequency', 50)->nullable()->comment('주기 (1회/일)');
|
||||
$table->text('check_method')->nullable()->comment('점검방법 및 기준');
|
||||
$table->integer('sort_order')->default(0)->comment('정렬순서');
|
||||
$table->tinyInteger('is_active')->default(1)->comment('사용여부');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['equipment_id', 'item_no'], 'uq_equipment_item_no');
|
||||
$table->index('tenant_id', 'idx_insp_tmpl_tenant');
|
||||
|
||||
$table->foreign('equipment_id')
|
||||
->references('id')
|
||||
->on('equipments')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_inspection_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('equipment_inspections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('equipment_id')->comment('설비 ID');
|
||||
$table->string('year_month', 7)->comment('점검년월 (2026-02)');
|
||||
$table->string('overall_judgment', 10)->nullable()->comment('종합판정: OK/NG');
|
||||
$table->foreignId('inspector_id')->nullable()->comment('점검자 ID (users.id)');
|
||||
$table->text('repair_note')->nullable()->comment('수리내역');
|
||||
$table->text('issue_note')->nullable()->comment('이상내용');
|
||||
$table->foreignId('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->foreignId('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'equipment_id', 'year_month'], 'uq_inspection_month');
|
||||
$table->index(['tenant_id', 'year_month'], 'idx_inspection_ym');
|
||||
|
||||
$table->foreign('equipment_id')
|
||||
->references('id')
|
||||
->on('equipments')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_inspections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('equipment_inspection_details', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('inspection_id')->comment('점검 헤더 ID');
|
||||
$table->unsignedBigInteger('template_item_id')->comment('점검항목 템플릿 ID');
|
||||
$table->date('check_date')->comment('점검일');
|
||||
$table->string('result', 10)->nullable()->comment('결과: good/bad/repaired');
|
||||
$table->string('note', 500)->nullable()->comment('비고');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['inspection_id', 'template_item_id', 'check_date'], 'uq_inspection_detail');
|
||||
|
||||
$table->foreign('inspection_id')
|
||||
->references('id')
|
||||
->on('equipment_inspections')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('template_item_id')
|
||||
->references('id')
|
||||
->on('equipment_inspection_templates')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_inspection_details');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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('equipment_repairs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('equipment_id')->comment('설비 ID');
|
||||
$table->date('repair_date')->comment('수리일');
|
||||
$table->string('repair_type', 20)->comment('보전구분: internal/external');
|
||||
$table->decimal('repair_hours', 5, 1)->nullable()->comment('수리시간');
|
||||
$table->text('description')->nullable()->comment('수리내용');
|
||||
$table->decimal('cost', 15, 2)->nullable()->comment('수리비용');
|
||||
$table->string('vendor', 100)->nullable()->comment('외주업체');
|
||||
$table->foreignId('repaired_by')->nullable()->comment('수리자 ID (users.id)');
|
||||
$table->text('memo')->nullable()->comment('비고');
|
||||
$table->foreignId('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->foreignId('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['tenant_id', 'repair_date'], 'idx_repair_date');
|
||||
$table->index(['tenant_id', 'equipment_id'], 'idx_repair_equipment');
|
||||
|
||||
$table->foreign('equipment_id')
|
||||
->references('id')
|
||||
->on('equipments')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_repairs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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('equipment_process', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('equipment_id')->comment('설비 ID');
|
||||
$table->unsignedBigInteger('process_id')->comment('공정 ID');
|
||||
$table->tinyInteger('is_primary')->default(0)->comment('주 설비 여부');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['equipment_id', 'process_id'], 'uq_equipment_process');
|
||||
|
||||
$table->foreign('equipment_id')
|
||||
->references('id')
|
||||
->on('equipments')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('process_id')
|
||||
->references('id')
|
||||
->on('processes')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_process');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('files', function (Blueprint $table) {
|
||||
$table->string('gcs_object_name', 500)->nullable()->after('file_type')->comment('GCS 객체 경로');
|
||||
$table->string('gcs_uri', 600)->nullable()->after('gcs_object_name')->comment('gs://bucket/object URI');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->dropColumn(['gcs_object_name', 'gcs_uri']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('attendance_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('user_id')->comment('신청자');
|
||||
$table->enum('request_type', ['vacation', 'businessTrip', 'remote', 'fieldWork'])->comment('신청 유형');
|
||||
$table->date('start_date');
|
||||
$table->date('end_date');
|
||||
$table->text('reason')->nullable()->comment('사유');
|
||||
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||
$table->unsignedBigInteger('approved_by')->nullable()->comment('승인자');
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->text('reject_reason')->nullable()->comment('반려 사유');
|
||||
$table->json('json_details')->nullable()->comment('반차 구분 등 추가 정보');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('tenant_id')->references('id')->on('tenants')->cascadeOnDelete();
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
|
||||
$table->index(['tenant_id', 'status']);
|
||||
$table->index(['tenant_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('attendance_requests');
|
||||
}
|
||||
};
|
||||
@@ -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::table('tenant_user_profiles', function (Blueprint $table) {
|
||||
$table->string('worker_type', 20)->default('employee')->after('employee_status')
|
||||
->comment('근로자유형: employee(사원), business_income(사업소득자)');
|
||||
$table->index(['tenant_id', 'worker_type'], 'idx_tenant_worker_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tenant_user_profiles', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_tenant_worker_type');
|
||||
$table->dropColumn('worker_type');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->foreignId('line_id')->nullable()->after('form_id')
|
||||
->constrained('approval_lines')->nullOnDelete()
|
||||
->comment('결재선 템플릿 ID');
|
||||
$table->longText('body')->nullable()->after('content')
|
||||
->comment('본문 내용');
|
||||
$table->boolean('is_urgent')->default(false)->after('status')
|
||||
->comment('긴급 여부');
|
||||
$table->unsignedBigInteger('department_id')->nullable()->after('drafter_id')
|
||||
->comment('기안 부서 ID');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->dropForeign(['line_id']);
|
||||
$table->dropColumn(['line_id', 'body', 'is_urgent', 'department_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('payrolls', function (Blueprint $table) {
|
||||
$table->decimal('long_term_care', 12, 0)->default(0)->after('health_insurance');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payrolls', function (Blueprint $table) {
|
||||
$table->dropColumn('long_term_care');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('approval_steps', function (Blueprint $table) {
|
||||
$table->string('approver_name', 50)->nullable()->after('approver_id')
|
||||
->comment('결재자명 스냅샷');
|
||||
$table->string('approver_department', 100)->nullable()->after('approver_name')
|
||||
->comment('결재자 부서 스냅샷');
|
||||
$table->string('approver_position', 50)->nullable()->after('approver_department')
|
||||
->comment('결재자 직급 스냅샷');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approval_steps', function (Blueprint $table) {
|
||||
$table->dropColumn(['approver_name', 'approver_department', 'approver_position']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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('approval_steps', function (Blueprint $table) {
|
||||
$table->integer('parallel_group')->nullable()->after('step_type')
|
||||
->comment('병렬 그룹 번호');
|
||||
$table->foreignId('acted_by')->nullable()->after('approver_id')
|
||||
->comment('실제 처리자 (대결)');
|
||||
$table->string('approval_type', 20)->default('normal')->after('status')
|
||||
->comment('결재 유형: normal/pre_decided/delegated');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approval_steps', function (Blueprint $table) {
|
||||
$table->dropColumn(['parallel_group', 'acted_by', 'approval_type']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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::table('approvals', function (Blueprint $table) {
|
||||
$table->text('recall_reason')->nullable()->after('completed_at')
|
||||
->comment('회수 사유');
|
||||
$table->foreignId('parent_doc_id')->nullable()->after('recall_reason')
|
||||
->comment('재기안 원본 문서');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->dropColumn(['recall_reason', 'parent_doc_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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('approval_delegations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('delegator_id')->constrained('users')->comment('위임자');
|
||||
$table->foreignId('delegate_id')->constrained('users')->comment('대리인');
|
||||
$table->date('start_date')->comment('시작일');
|
||||
$table->date('end_date')->comment('종료일');
|
||||
$table->json('form_ids')->nullable()->comment('위임 대상 양식 (NULL=전체)');
|
||||
$table->boolean('notify_delegator')->default(true)->comment('대결 시 보고');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->string('reason', 200)->nullable()->comment('위임 사유');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['tenant_id', 'delegator_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('approval_delegations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('esign_contracts', function (Blueprint $table) {
|
||||
$table->string('completion_template_name', 100)->nullable()->after('sms_fallback')
|
||||
->comment('완료 알림톡 템플릿명');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('esign_contracts', function (Blueprint $table) {
|
||||
$table->dropColumn('completion_template_name');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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::create('income_tax_brackets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedSmallInteger('tax_year')->comment('적용 연도 (예: 2024)');
|
||||
$table->unsignedInteger('salary_from')->comment('구간 하한 (천원 단위)');
|
||||
$table->unsignedInteger('salary_to')->comment('구간 상한 (천원 단위)');
|
||||
$table->unsignedTinyInteger('family_count')->comment('공제대상가족수 (1~11)');
|
||||
$table->unsignedInteger('tax_amount')->comment('세액 (원)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['tax_year', 'salary_from', 'salary_to', 'family_count'], 'itb_lookup_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('income_tax_brackets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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('business_income_payments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained('tenants')->comment('테넌트');
|
||||
$table->foreignId('user_id')->constrained('users')->comment('사업소득자 사용자');
|
||||
$table->unsignedSmallInteger('pay_year')->comment('급여 연도');
|
||||
$table->unsignedTinyInteger('pay_month')->comment('급여 월 (1-12)');
|
||||
$table->string('service_content', 200)->nullable()->comment('용역내용');
|
||||
$table->decimal('gross_amount', 15, 0)->default(0)->comment('지급총액');
|
||||
$table->decimal('income_tax', 15, 0)->default(0)->comment('소득세 (3%)');
|
||||
$table->decimal('local_income_tax', 15, 0)->default(0)->comment('지방소득세 (0.3%)');
|
||||
$table->decimal('total_deductions', 15, 0)->default(0)->comment('공제합계');
|
||||
$table->decimal('net_amount', 15, 0)->default(0)->comment('실지급액');
|
||||
$table->date('payment_date')->nullable()->comment('지급일자');
|
||||
$table->text('note')->nullable()->comment('비고');
|
||||
$table->string('status', 20)->default('draft')->comment('상태: draft/confirmed/paid');
|
||||
$table->timestamp('confirmed_at')->nullable()->comment('확정일시');
|
||||
$table->foreignId('confirmed_by')->nullable()->constrained('users')->comment('확정자');
|
||||
$table->timestamp('paid_at')->nullable()->comment('지급일시');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->comment('수정자');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['tenant_id', 'user_id', 'pay_year', 'pay_month'], 'bip_tenant_user_period_unique');
|
||||
$table->index(['tenant_id', 'pay_year', 'pay_month'], 'bip_tenant_period_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('business_income_payments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('document_templates', function (Blueprint $table) {
|
||||
$table->string('builder_type', 20)->default('legacy')->after('category')
|
||||
->comment('빌더 유형 (legacy: 기존 EAV, block: 블록 빌더)');
|
||||
$table->json('schema')->nullable()->after('footer_judgement_options')
|
||||
->comment('블록 빌더 JSON 스키마');
|
||||
$table->json('page_config')->nullable()->after('schema')
|
||||
->comment('페이지 설정 (용지 크기, 방향, 여백 등)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('document_templates', function (Blueprint $table) {
|
||||
$table->dropColumn(['builder_type', 'schema', 'page_config']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('interview_projects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('company_name', 200)->comment('대상 회사명');
|
||||
$table->string('company_type', 100)->nullable()->comment('업종 (방화셔터, 블라인드 등)');
|
||||
$table->string('contact_person', 100)->nullable()->comment('담당자');
|
||||
$table->string('contact_info', 200)->nullable()->comment('연락처');
|
||||
$table->enum('status', ['draft', 'interviewing', 'analyzing', 'code_generated', 'deployed'])
|
||||
->default('draft')->comment('상태');
|
||||
$table->unsignedBigInteger('target_tenant_id')->nullable()->comment('생성될 테넌트 ID');
|
||||
$table->json('product_categories')->nullable()->comment('제품 카테고리 목록');
|
||||
$table->text('summary')->nullable()->comment('AI 생성 요약');
|
||||
$table->unsignedTinyInteger('progress_percent')->default(0)->comment('전체 진행률');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('tenant_id', 'idx_interview_projects_tenant');
|
||||
$table->index('status', 'idx_interview_projects_status');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('interview_projects');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->json('data')->nullable()->after('status')
|
||||
->comment('블록 빌더 문서 데이터 (JSON)');
|
||||
$table->longText('rendered_html')->nullable()->after('data')
|
||||
->comment('렌더링된 HTML (PDF 생성용 캐시)');
|
||||
$table->string('pdf_path', 500)->nullable()->after('rendered_html')
|
||||
->comment('생성된 PDF 파일 경로');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->dropColumn(['data', 'rendered_html', 'pdf_path']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('interview_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('interview_project_id')->comment('인터뷰 프로젝트 ID');
|
||||
$table->enum('file_type', ['excel_template', 'pdf_quote', 'sample_bom', 'price_list', 'photo', 'voice', 'other'])
|
||||
->default('other')->comment('파일 유형');
|
||||
$table->string('file_name', 255)->comment('파일명');
|
||||
$table->string('file_path', 500)->comment('저장 경로');
|
||||
$table->unsignedInteger('file_size')->default(0)->comment('파일 크기 (bytes)');
|
||||
$table->string('mime_type', 100)->nullable()->comment('MIME 타입');
|
||||
$table->json('ai_analysis')->nullable()->comment('AI 분석 결과');
|
||||
$table->enum('ai_analysis_status', ['pending', 'processing', 'completed', 'failed'])
|
||||
->default('pending')->comment('AI 분석 상태');
|
||||
$table->text('description')->nullable()->comment('설명');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('tenant_id', 'idx_interview_attachments_tenant');
|
||||
$table->index('interview_project_id', 'idx_interview_attachments_project');
|
||||
$table->index('file_type', 'idx_interview_attachments_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('interview_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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('interview_knowledge', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('interview_project_id')->comment('인터뷰 프로젝트 ID');
|
||||
$table->enum('domain', [
|
||||
'product_classification', 'bom_structure', 'dimension_formula',
|
||||
'component_config', 'pricing_structure', 'quantity_formula',
|
||||
'conditional_logic', 'quote_format',
|
||||
])->comment('도메인 영역');
|
||||
$table->enum('knowledge_type', ['fact', 'rule', 'formula', 'mapping', 'range', 'table'])
|
||||
->comment('지식 유형');
|
||||
$table->string('title', 300)->comment('제목');
|
||||
$table->json('content')->comment('구조화된 지식 데이터');
|
||||
$table->enum('source_type', ['interview_answer', 'voice_recording', 'document', 'manual'])
|
||||
->comment('출처 유형');
|
||||
$table->unsignedBigInteger('source_id')->nullable()->comment('출처 레코드 ID');
|
||||
$table->decimal('confidence', 3, 2)->default(0.00)->comment('AI 신뢰도 (0.00~1.00)');
|
||||
$table->boolean('is_verified')->default(false)->comment('사용자 검증 여부');
|
||||
$table->unsignedBigInteger('verified_by')->nullable()->comment('검증자');
|
||||
$table->timestamp('verified_at')->nullable()->comment('검증 일시');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('tenant_id', 'idx_interview_knowledge_tenant');
|
||||
$table->index('interview_project_id', 'idx_interview_knowledge_project');
|
||||
$table->index('domain', 'idx_interview_knowledge_domain');
|
||||
$table->index('is_verified', 'idx_interview_knowledge_verified');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('interview_knowledge');
|
||||
}
|
||||
};
|
||||
@@ -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('interview_categories', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('interview_project_id')->nullable()
|
||||
->after('tenant_id')->comment('프로젝트 연결');
|
||||
$table->string('domain', 50)->nullable()
|
||||
->after('description')->comment('도메인 영역 태그');
|
||||
|
||||
$table->index('interview_project_id', 'idx_interview_categories_project');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('interview_categories', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_interview_categories_project');
|
||||
$table->dropColumn(['interview_project_id', 'domain']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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
|
||||
{
|
||||
// interview_questions 확장
|
||||
Schema::table('interview_questions', function (Blueprint $table) {
|
||||
$table->text('ai_hint')->nullable()->after('options')->comment('AI 분석 힌트/가이드');
|
||||
$table->string('expected_format', 100)->nullable()->after('ai_hint')->comment('예상 답변 형식 (mm, 원/kg 등)');
|
||||
$table->json('depends_on')->nullable()->after('expected_format')->comment('조건부 표시 조건');
|
||||
$table->string('domain', 50)->nullable()->after('depends_on')->comment('도메인 영역');
|
||||
});
|
||||
|
||||
// question_type 컬럼 확장 (varchar(20) → varchar(50))
|
||||
Schema::table('interview_questions', function (Blueprint $table) {
|
||||
$table->string('question_type', 50)->default('checkbox')
|
||||
->comment('질문유형: checkbox|text|number|select|multi_select|file_upload|formula_input|table_input|bom_tree|price_table|dimension_diagram')
|
||||
->change();
|
||||
});
|
||||
|
||||
// interview_answers 확장
|
||||
Schema::table('interview_answers', function (Blueprint $table) {
|
||||
$table->json('answer_data')->nullable()->after('answer_text')->comment('구조화 답변 (테이블, 수식, BOM 등)');
|
||||
$table->json('attachments')->nullable()->after('answer_data')->comment('첨부 파일 경로 목록');
|
||||
});
|
||||
|
||||
// interview_sessions 확장
|
||||
Schema::table('interview_sessions', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('interview_project_id')->nullable()
|
||||
->after('tenant_id')->comment('프로젝트 ID');
|
||||
$table->string('session_type', 20)->default('checklist')
|
||||
->after('status')->comment('세션 유형: checklist|structured|voice|review');
|
||||
$table->unsignedBigInteger('voice_recording_id')->nullable()
|
||||
->after('session_type')->comment('음성 녹음 ID');
|
||||
|
||||
$table->index('interview_project_id', 'idx_interview_sessions_project');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('interview_sessions', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_interview_sessions_project');
|
||||
$table->dropColumn(['interview_project_id', 'session_type', 'voice_recording_id']);
|
||||
});
|
||||
|
||||
Schema::table('interview_answers', function (Blueprint $table) {
|
||||
$table->dropColumn(['answer_data', 'attachments']);
|
||||
});
|
||||
|
||||
Schema::table('interview_questions', function (Blueprint $table) {
|
||||
$table->string('question_type', 20)->default('checkbox')
|
||||
->comment('질문유형: checkbox/text')->change();
|
||||
$table->dropColumn(['ai_hint', 'expected_format', 'depends_on', 'domain']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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('interview_categories', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->after('interview_project_id')->comment('부모 카테고리 ID (대분류→중분류)');
|
||||
$table->index('parent_id', 'idx_interview_categories_parent');
|
||||
$table->foreign('parent_id')->references('id')->on('interview_categories')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('interview_categories', function (Blueprint $table) {
|
||||
$table->dropForeign(['parent_id']);
|
||||
$table->dropIndex('idx_interview_categories_parent');
|
||||
$table->dropColumn('parent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 이미 마스터 도메인 카테고리가 있으면 스킵 (중복 방지)
|
||||
$existing = DB::table('interview_categories')
|
||||
->whereNull('interview_project_id')
|
||||
->whereNotNull('domain')
|
||||
->count();
|
||||
|
||||
if ($existing > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::unprepared("
|
||||
SET @tenant_id = 1;
|
||||
SET @user_id = 1;
|
||||
SET @now = NOW();
|
||||
|
||||
-- 대분류: 제조업-방화셔터
|
||||
SET @root_manufacturing = (
|
||||
SELECT id FROM interview_categories
|
||||
WHERE name = '제조업-방화셔터' AND parent_id IS NULL AND interview_project_id IS NULL AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 루트가 없으면 생성
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
SELECT @tenant_id, NULL, NULL, '제조업-방화셔터', '방화셔터 제조업 인터뷰', NULL, 1, 1, @user_id, @user_id, @now, @now
|
||||
FROM DUAL WHERE @root_manufacturing IS NULL;
|
||||
|
||||
SET @root_manufacturing = COALESCE(@root_manufacturing, LAST_INSERT_ID());
|
||||
|
||||
-- Domain 1: 제품 분류 체계
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '제품 분류 체계', '제품 카테고리, 모델 코드, 분류 기준 파악', 'product_classification', 3, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_1, '제품 카테고리 구조', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_1_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_1_1, '귀사의 주요 제품군을 모두 나열해주세요', 'text', NULL, '쉼표 구분으로 제품군 나열', NULL, NULL, 'product_classification', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '각 제품군의 하위 모델명과 코드 체계를 알려주세요', 'table_input', '{\"columns\":[\"모델코드\",\"모델명\",\"비고\"]}', '코드-이름 매핑 테이블', NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '제품을 분류하는 기준은 무엇인가요? (소재, 용도, 크기 등)', 'multi_select', '{\"choices\":[\"소재별\",\"용도별\",\"크기별\",\"설치방식별\",\"인증여부별\"]}', NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '인증(인정) 제품과 비인증 제품의 구분이 있나요?', 'select', '{\"choices\":[\"있음\",\"없음\"]}', NULL, NULL, NULL, 'product_classification', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '인증 제품의 경우 구성이 고정되나요?', 'checkbox', NULL, NULL, NULL, '{\"question_index\":3,\"value\":\"있음\"}', 'product_classification', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '카테고리별 제품 수는 대략 몇 개인가요?', 'number', NULL, NULL, '개', NULL, 'product_classification', 0, 6, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '제품 코드 명명 규칙을 설명해주세요 (예: KSS01의 의미)', 'text', NULL, '코드 체계의 각 자릿수 의미', NULL, NULL, 'product_classification', 0, 7, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_1, '기존 시스템(ERP/엑셀)에서 사용하는 제품 분류 방식을 캡처하여 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'product_classification', 0, 8, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_1, '설치 유형별 분류', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_1_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_1_2, '설치 유형(벽면형, 측면형, 혼합형 등)에 따라 견적이 달라지나요?', 'select', '{\"choices\":[\"예, 크게 달라짐\",\"약간 달라짐\",\"달라지지 않음\"]}', NULL, NULL, NULL, 'product_classification', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_2, '각 설치 유형별로 어떤 부품이 달라지나요?', 'table_input', '{\"columns\":[\"설치유형\",\"추가부품\",\"제외부품\",\"비고\"]}', NULL, NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_1_2, '설치 유형에 따른 추가 비용 항목이 있나요?', 'text', NULL, NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 2: BOM 구조
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, 'BOM 구조', '완제품-부품 관계, 부품 카테고리, BOM 레벨', 'bom_structure', 4, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_2, '완제품-부품 관계', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_2_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_2_1, '대표 제품 1개의 완제품→부품 구성을 트리로 그려주세요', 'bom_tree', NULL, '최상위 제품부터 하위 부품까지 트리 구조', NULL, NULL, 'bom_structure', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_1, '모든 제품에 공통으로 들어가는 부품은 무엇인가요?', 'multi_select', '{\"choices\":[\"가이드레일\",\"케이스\",\"모터\",\"제어기\",\"브라켓\",\"볼트/너트\"]}', '직접 입력 가능', NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_1, '제품별로 선택적(옵션)인 부품은 무엇인가요?', 'table_input', '{\"columns\":[\"제품명\",\"옵션부품\",\"적용조건\"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_1, 'BOM이 현재 엑셀로 관리되고 있나요? 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_1, '하위 부품의 단계(레벨)는 최대 몇 단계인가요?', 'number', NULL, NULL, '단계', NULL, 'bom_structure', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_1, '부품 수량이 고정인 것과 계산이 필요한 것을 구분해주세요', 'table_input', '{\"columns\":[\"부품명\",\"고정/계산\",\"고정수량 또는 계산식\"]}', NULL, NULL, NULL, 'bom_structure', 0, 6, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_2, '부품 카테고리', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_2_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_2_2, '부품을 카테고리로 분류하면 어떻게 나눠지나요? (본체, 절곡품, 전동부, 부자재 등)', 'text', NULL, '부품 분류 체계', NULL, NULL, 'bom_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_2, '각 카테고리에 속하는 부품 목록을 정리해주세요', 'table_input', '{\"columns\":[\"카테고리\",\"부품명\",\"규격\"]}', NULL, NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_2, '외주 구매 부품과 자체 제작 부품의 구분이 있나요?', 'select', '{\"choices\":[\"있음\",\"없음\"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_2_2, '부자재(볼트, 너트, 패킹 등)는 별도 관리하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 3: 치수/변수 계산
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '치수/변수 계산', '오픈 사이즈→제작 사이즈 변환, 파생 변수 계산', 'dimension_formula', 5, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_3 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_3, '오픈 사이즈 → 제작 사이즈', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_3_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_3_1, '고객이 입력하는 기본 치수 항목은 무엇인가요? (폭, 높이, 깊이 등)', 'multi_select', '{\"choices\":[\"폭(W)\",\"높이(H)\",\"깊이(D)\",\"두께(T)\",\"지름(Ø)\"]}', NULL, NULL, NULL, 'dimension_formula', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '오픈 사이즈에서 제작 사이즈로 변환할 때 더하는 마진값은?', 'formula_input', NULL, '예: W1 = W0 + 120, H1 = H0 + 50', 'mm', NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '제품 카테고리별로 마진값이 다른가요?', 'table_input', '{\"columns\":[\"제품카테고리\",\"W 마진(mm)\",\"H 마진(mm)\",\"비고\"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '면적(㎡) 계산 공식을 알려주세요', 'formula_input', NULL, '예: area = W1 * H1 / 1000000', '㎡', NULL, 'dimension_formula', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '중량(kg) 계산 공식을 알려주세요', 'formula_input', NULL, '예: weight = area * 단위중량(kg/㎡)', 'kg', NULL, 'dimension_formula', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '기타 파생 변수가 있나요? (예: 분할 개수, 절곡 길이 등)', 'table_input', '{\"columns\":[\"변수명\",\"계산식\",\"단위\",\"비고\"]}', NULL, NULL, NULL, 'dimension_formula', 0, 6, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_1, '치수 계산에 사용하는 엑셀 수식을 캡처해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 7, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_3, '변수 의존 관계', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_3_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_3_2, '변수 간 의존 관계를 설명해주세요 (A는 B와 C로 계산)', 'text', NULL, '계산 순서와 변수 의존성', NULL, NULL, 'dimension_formula', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_2, '계산 순서가 중요한 변수가 있나요?', 'text', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_3_2, '단위는 mm, m, kg 중 어떤 것을 기본으로 사용하나요?', 'select', '{\"choices\":[\"mm\",\"m\",\"cm\",\"혼용\"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 4: 부품 구성 상세
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '부품 구성 상세', '주요 부품별 규격, 선택 기준, 특수 구성', 'component_config', 6, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_4 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_4, '주요 부품별 상세', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_4_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_4_1, '가이드레일의 표준 길이 규격은? (예: 1219, 2438, 3305mm)', 'table_input', '{\"columns\":[\"규격코드\",\"길이(mm)\",\"비고\"]}', NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '가이드레일 길이 조합 규칙은? (어떤 길이를 몇 개 사용?)', 'text', NULL, '높이에 따른 가이드레일 조합 로직', NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '케이스(하우징) 크기별 규격과 부속품 차이를 설명해주세요', 'table_input', '{\"columns\":[\"케이스규격\",\"적용조건\",\"부속품\"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '모터 용량 종류와 선택 기준은? (무게별? 면적별?)', 'table_input', '{\"columns\":[\"모터용량\",\"적용범위(최소)\",\"적용범위(최대)\",\"단위\"]}', '무게/면적 범위별 모터 매핑', NULL, NULL, 'component_config', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '모터 전압 옵션은? (380V, 220V 등)', 'multi_select', '{\"choices\":[\"380V\",\"220V\",\"110V\",\"DC 24V\"]}', NULL, NULL, NULL, 'component_config', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '제어기 종류와 선택 기준은? (노출형/매립형 등)', 'table_input', '{\"columns\":[\"제어기유형\",\"적용조건\",\"비고\"]}', NULL, NULL, NULL, 'component_config', 0, 6, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '절곡품(판재 가공) 목록과 각각의 치수 결정 방식은?', 'table_input', '{\"columns\":[\"절곡품명\",\"치수결정방식\",\"재질\",\"두께(mm)\"]}', NULL, NULL, NULL, 'component_config', 0, 7, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_1, '부자재(볼트, 너트, 패킹 등) 목록과 수량 결정 방식은?', 'table_input', '{\"columns\":[\"부자재명\",\"규격\",\"수량결정방식\",\"기본수량\"]}', NULL, NULL, NULL, 'component_config', 0, 8, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_4, '특수 구성', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_4_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_4_2, '연기차단재 등 특수 부품이 있나요? 적용 조건은?', 'text', NULL, NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_2, '보강재(샤프트, 파이프, 앵글 등) 사용 조건은?', 'table_input', '{\"columns\":[\"보강재명\",\"규격\",\"적용조건\",\"수량\"]}', NULL, NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_4_2, '고객 요청에 따라 추가/제외되는 옵션 부품은?', 'table_input', '{\"columns\":[\"옵션부품\",\"추가/제외\",\"추가비용\",\"비고\"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 5: 단가 체계
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '단가 체계', '단가 관리 방식, 계산 방식, 마진/LOSS율', 'pricing_structure', 7, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_5 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_5, '단가 관리 방식', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_5_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_5_1, '부품별 단가를 어디서 관리하나요? (엑셀, ERP, 구두 등)', 'select', '{\"choices\":[\"엑셀\",\"ERP 시스템\",\"구두/경험\",\"기타\"]}', NULL, NULL, NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, '단가표 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, '단가 변경 주기는? (월/분기/연 등)', 'select', '{\"choices\":[\"수시\",\"월 단위\",\"분기 단위\",\"반기 단위\",\"연 단위\"]}', NULL, NULL, NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, '단가에 포함되는 항목은? (재료비만? 가공비 포함?)', 'multi_select', '{\"choices\":[\"재료비\",\"가공비\",\"운송비\",\"설치비\",\"마진\"]}', NULL, NULL, NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, '고객별/거래처별 차등 단가가 있나요?', 'select', '{\"choices\":[\"있음 (등급별)\",\"있음 (거래처별)\",\"없음 (일괄 동일)\"]}', NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, 'LOSS율(손실률)을 적용하나요? 적용 방식은?', 'formula_input', NULL, '예: 실제수량 = 계산수량 × (1 + LOSS율)', '%', NULL, 'pricing_structure', 0, 6, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_1, '마진율 설정 방식은? (일괄? 품목별?)', 'select', '{\"choices\":[\"일괄 마진율\",\"품목별 마진율\",\"카테고리별 마진율\",\"고객별 마진율\"]}', NULL, NULL, NULL, 'pricing_structure', 0, 7, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_5, '단가 계산 방식', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_5_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_5_2, '면적 기반 단가 품목은? (원/㎡)', 'table_input', '{\"columns\":[\"품목명\",\"단가(원/㎡)\",\"비고\"]}', NULL, '원/㎡', NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_2, '중량 기반 단가 품목은? (원/kg)', 'table_input', '{\"columns\":[\"품목명\",\"단가(원/kg)\",\"비고\"]}', NULL, '원/kg', NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_2, '수량 기반 단가 품목은? (원/EA)', 'table_input', '{\"columns\":[\"품목명\",\"단가(원/EA)\",\"비고\"]}', NULL, '원/EA', NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_2, '길이 기반 단가 품목은? (원/m)', 'table_input', '{\"columns\":[\"품목명\",\"단가(원/m)\",\"비고\"]}', NULL, '원/m', NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_5_2, '기타 특수 단가 계산 방식이 있나요?', 'text', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 6: 수량 수식
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '수량 수식', '부품별 수량 결정 규칙, 계산식, 검증', 'quantity_formula', 8, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_6 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_6, '수량 결정 규칙', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_6_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_6_1, '고정 수량 부품 목록 (항상 1개, 2개 등)', 'table_input', '{\"columns\":[\"부품명\",\"고정수량\",\"비고\"]}', NULL, NULL, NULL, 'quantity_formula', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_1, '치수 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 슬랫수량 = CEIL(H1 / 슬랫피치)', NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_1, '면적 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 스크린수량 = area / 기준면적', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_1, '중량 기반 수량 계산 부품과 수식', 'formula_input', NULL, NULL, NULL, NULL, 'quantity_formula', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_1, '올림/내림/반올림 규칙이 있는 계산은?', 'table_input', '{\"columns\":[\"계산항목\",\"올림/내림/반올림\",\"소수점자릿수\"]}', NULL, NULL, NULL, 'quantity_formula', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_1, '여유 수량(LOSS) 적용 품목과 비율은?', 'table_input', '{\"columns\":[\"품목명\",\"LOSS율(%)\",\"비고\"]}', NULL, NULL, NULL, 'quantity_formula', 0, 6, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_6, '수식 검증', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_6_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_6_2, '실제 견적서에서 수량 계산 예시를 보여주세요 (W=3000, H=2500일 때)', 'table_input', '{\"columns\":[\"부품명\",\"수식\",\"계산결과\",\"단위\"]}', NULL, NULL, NULL, 'quantity_formula', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_2, '수식에 사용하는 함수가 있나요? (SUM, CEIL, ROUND 등)', 'multi_select', '{\"choices\":[\"CEIL (올림)\",\"FLOOR (내림)\",\"ROUND (반올림)\",\"MAX\",\"MIN\",\"IF 조건문\",\"SUM\"]}', NULL, NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_6_2, '조건에 따라 수식이 달라지는 경우가 있나요?', 'text', NULL, '예: 폭이 3000 초과이면 분할 계산', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 7: 조건부 로직
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '조건부 로직', '범위/매핑 기반 부품 자동 선택 규칙', 'conditional_logic', 9, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_7 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_7, '범위 기반 선택', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_7_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_7_1, '무게 범위별 모터 용량 선택표를 작성해주세요', 'price_table', '{\"columns\":[\"범위 시작(kg)\",\"범위 끝(kg)\",\"모터용량\",\"비고\"]}', NULL, NULL, NULL, 'conditional_logic', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_7_1, '크기 범위별 부품 자동 선택 규칙이 있나요?', 'table_input', '{\"columns\":[\"조건(변수)\",\"범위\",\"선택부품\",\"비고\"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_7_1, '브라켓 크기 결정 기준은?', 'table_input', '{\"columns\":[\"조건\",\"범위\",\"브라켓 규격\"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_7, '매핑 기반 선택', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_7_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_7_2, '제품 모델 → 기본 부품 세트 매핑표', 'table_input', '{\"columns\":[\"제품모델\",\"기본부품1\",\"기본부품2\",\"기본부품3\"]}', NULL, NULL, NULL, 'conditional_logic', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_7_2, '설치 유형 → 추가 부품 매핑표', 'table_input', '{\"columns\":[\"설치유형\",\"추가부품\",\"수량\",\"비고\"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_7_2, '제어기 유형 → 부속품 매핑표', 'table_input', '{\"columns\":[\"제어기유형\",\"부속품1\",\"부속품2\",\"부속품3\"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_7_2, '기타 조건부 자동 선택 규칙', 'text', NULL, '위 항목에 해당하지 않는 조건-결과 매핑', NULL, NULL, 'conditional_logic', 0, 4, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
-- Domain 8: 견적서 양식
|
||||
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, NULL, @root_manufacturing, '견적서 양식', '출력 양식, 항목 그룹, 소계/합계 구조', 'quote_format', 10, 1, @user_id, @user_id, @now, @now);
|
||||
SET @cat_8 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_8, '출력 양식', 1, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_8_1 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_8_1, '현재 사용 중인 견적서 양식을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'quote_format', 1, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '견적서에 표시되는 항목 그룹은? (재료비, 노무비, 설치비 등)', 'multi_select', '{\"choices\":[\"재료비\",\"노무비\",\"경비\",\"설치비\",\"운반비\",\"이윤\",\"부가세\"]}', NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '소계/합계 계산 구조를 설명해주세요', 'text', NULL, '항목 그룹별 소계와 최종 합계의 관계', NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '할인 적용 방식은? (일괄? 항목별?)', 'select', '{\"choices\":[\"일괄 할인\",\"항목별 할인\",\"할인 없음\",\"협의 할인\"]}', NULL, NULL, NULL, 'quote_format', 0, 4, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '부가세 표시 방식은? (별도? 포함?)', 'select', '{\"choices\":[\"별도 표시\",\"포함 표시\",\"선택 가능\"]}', NULL, NULL, NULL, 'quote_format', 0, 5, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '견적서에 표시하지 않는 내부 관리 항목은?', 'text', NULL, NULL, NULL, NULL, 'quote_format', 0, 6, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_1, '견적 번호 체계를 알려주세요', 'text', NULL, '예: Q-2026-001 형식', NULL, NULL, 'quote_format', 0, 7, 1, @user_id, @user_id, @now, @now);
|
||||
|
||||
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
|
||||
VALUES (@tenant_id, @cat_8, '특수 요구사항', 2, 1, @user_id, @user_id, @now, @now);
|
||||
SET @tpl_8_2 = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
|
||||
(@tenant_id, @tpl_8_2, '산출내역서(세부 내역)를 별도로 제공하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 1, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_2, '위치별(층/부호) 개별 산출이 필요한가요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now),
|
||||
(@tenant_id, @tpl_8_2, '일괄 산출(여러 위치 합산)을 사용하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now);
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 마스터 데이터 삭제 (프로젝트 복제 데이터는 유지)
|
||||
$masterCategoryIds = DB::table('interview_categories')
|
||||
->whereNull('interview_project_id')
|
||||
->whereNotNull('domain')
|
||||
->pluck('id');
|
||||
|
||||
if ($masterCategoryIds->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$templateIds = DB::table('interview_templates')
|
||||
->whereIn('interview_category_id', $masterCategoryIds)
|
||||
->pluck('id');
|
||||
|
||||
DB::table('interview_questions')->whereIn('interview_template_id', $templateIds)->delete();
|
||||
DB::table('interview_templates')->whereIn('id', $templateIds)->delete();
|
||||
DB::table('interview_categories')->whereIn('id', $masterCategoryIds)->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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('leaves', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('approval_id')->nullable()->after('status')->comment('연결된 결재 문서 ID');
|
||||
$table->index('approval_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('leaves', function (Blueprint $table) {
|
||||
$table->dropIndex(['approval_id']);
|
||||
$table->dropColumn('approval_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// tenant_id=1 에 휴가신청 결재 양식 등록 (이미 존재하면 무시)
|
||||
DB::table('approval_forms')->insertOrIgnore([
|
||||
'tenant_id' => 1,
|
||||
'name' => '휴가신청',
|
||||
'code' => 'leave',
|
||||
'category' => 'request',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'user_name', 'type' => 'text', 'label' => '신청자'],
|
||||
['name' => 'leave_type', 'type' => 'text', 'label' => '휴가유형'],
|
||||
['name' => 'period', 'type' => 'text', 'label' => '기간'],
|
||||
['name' => 'days', 'type' => 'number', 'label' => '일수'],
|
||||
['name' => 'reason', 'type' => 'text', 'label' => '사유'],
|
||||
['name' => 'remaining_days', 'type' => 'text', 'label' => '잔여연차'],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')
|
||||
->where('tenant_id', 1)
|
||||
->where('code', 'leave')
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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('admin_roadmap_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title', 200);
|
||||
$table->text('description')->nullable();
|
||||
$table->longText('content')->nullable();
|
||||
$table->string('category', 30)->default('general');
|
||||
$table->string('status', 20)->default('planned');
|
||||
$table->string('priority', 10)->default('medium');
|
||||
$table->string('phase', 30)->default('phase_1');
|
||||
$table->date('start_date')->nullable();
|
||||
$table->date('end_date')->nullable();
|
||||
$table->tinyInteger('progress')->unsigned()->default(0);
|
||||
$table->string('color', 7)->default('#3B82F6');
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('category');
|
||||
$table->index('status');
|
||||
$table->index('phase');
|
||||
$table->index('priority');
|
||||
});
|
||||
|
||||
Schema::create('admin_roadmap_milestones', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('plan_id');
|
||||
$table->string('title', 255);
|
||||
$table->text('description')->nullable();
|
||||
$table->string('status', 20)->default('pending');
|
||||
$table->date('due_date')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->unsignedBigInteger('assignee_id')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('plan_id')
|
||||
->references('id')
|
||||
->on('admin_roadmap_plans')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->index('status');
|
||||
$table->index('due_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admin_roadmap_milestones');
|
||||
Schema::dropIfExists('admin_roadmap_plans');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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
|
||||
{
|
||||
// SAM 모듈 카탈로그
|
||||
Schema::create('ai_quotation_modules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('module_code', 50);
|
||||
$table->string('module_name', 100);
|
||||
$table->enum('category', ['basic', 'individual', 'addon']);
|
||||
$table->text('description')->nullable();
|
||||
$table->json('keywords')->nullable();
|
||||
$table->decimal('dev_cost', 12, 0)->default(0);
|
||||
$table->decimal('monthly_fee', 10, 0)->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'module_code'], 'uk_tenant_module');
|
||||
$table->index('tenant_id');
|
||||
$table->index('category');
|
||||
});
|
||||
|
||||
// AI 견적 요청/결과
|
||||
Schema::create('ai_quotations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('title', 200);
|
||||
$table->enum('input_type', ['text', 'voice', 'document'])->default('text');
|
||||
$table->longText('input_text')->nullable();
|
||||
$table->string('input_file_path', 500)->nullable();
|
||||
$table->string('ai_provider', 20)->default('gemini');
|
||||
$table->string('ai_model', 50)->nullable();
|
||||
$table->json('analysis_result')->nullable();
|
||||
$table->json('quotation_result')->nullable();
|
||||
$table->string('status', 20)->default('pending');
|
||||
$table->unsignedBigInteger('linked_quote_id')->nullable();
|
||||
$table->decimal('total_dev_cost', 12, 0)->default(0);
|
||||
$table->decimal('total_monthly_fee', 10, 0)->default(0);
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('tenant_id');
|
||||
$table->index('status');
|
||||
$table->index('created_by');
|
||||
});
|
||||
|
||||
// AI 추천 모듈 목록
|
||||
Schema::create('ai_quotation_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ai_quotation_id');
|
||||
$table->unsignedBigInteger('module_id')->nullable();
|
||||
$table->string('module_code', 50);
|
||||
$table->string('module_name', 100);
|
||||
$table->boolean('is_required')->default(false);
|
||||
$table->text('reason')->nullable();
|
||||
$table->decimal('dev_cost', 12, 0)->default(0);
|
||||
$table->decimal('monthly_fee', 10, 0)->default(0);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('ai_quotation_id')
|
||||
->references('id')
|
||||
->on('ai_quotations')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->index('ai_quotation_id');
|
||||
$table->index('module_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ai_quotation_items');
|
||||
Schema::dropIfExists('ai_quotations');
|
||||
Schema::dropIfExists('ai_quotation_modules');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('business_income_payments', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->nullable()->change();
|
||||
$table->string('display_name', 100)->nullable()->after('user_id')->comment('상호/성명 (표시용)');
|
||||
$table->string('business_reg_number', 20)->nullable()->after('display_name')->comment('사업자등록번호');
|
||||
});
|
||||
|
||||
// 기존 데이터에 display_name/business_reg_number 채우기
|
||||
DB::statement("
|
||||
UPDATE business_income_payments bip
|
||||
JOIN tenant_user_profiles tup ON tup.user_id = bip.user_id
|
||||
AND tup.tenant_id = bip.tenant_id AND tup.worker_type = 'business_income'
|
||||
SET bip.display_name = COALESCE(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(tup.json_extra, '$.business_name')),
|
||||
(SELECT name FROM users WHERE id = bip.user_id)
|
||||
),
|
||||
bip.business_reg_number = JSON_UNQUOTE(JSON_EXTRACT(tup.json_extra, '$.business_registration_number'))
|
||||
WHERE bip.display_name IS NULL
|
||||
");
|
||||
|
||||
// earner 프로필 없는 경우 users 테이블에서 이름만이라도 채우기
|
||||
DB::statement("
|
||||
UPDATE business_income_payments bip
|
||||
JOIN users u ON u.id = bip.user_id
|
||||
SET bip.display_name = u.name
|
||||
WHERE bip.display_name IS NULL AND bip.user_id IS NOT NULL
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('business_income_payments', function (Blueprint $table) {
|
||||
$table->dropColumn(['display_name', 'business_reg_number']);
|
||||
$table->unsignedBigInteger('user_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 근태신청 결재 양식 (출장/재택/외근/조퇴)
|
||||
DB::table('approval_forms')->insertOrIgnore([
|
||||
'tenant_id' => 1,
|
||||
'name' => '근태신청',
|
||||
'code' => 'attendance_request',
|
||||
'category' => 'request',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'user_name', 'type' => 'text', 'label' => '신청자'],
|
||||
['name' => 'request_type', 'type' => 'text', 'label' => '신청유형'],
|
||||
['name' => 'period', 'type' => 'text', 'label' => '기간'],
|
||||
['name' => 'days', 'type' => 'number', 'label' => '일수'],
|
||||
['name' => 'reason', 'type' => 'text', 'label' => '사유'],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 사유서 결재 양식 (지각사유서/결근사유서)
|
||||
DB::table('approval_forms')->insertOrIgnore([
|
||||
'tenant_id' => 1,
|
||||
'name' => '사유서',
|
||||
'code' => 'reason_report',
|
||||
'category' => 'request',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'user_name', 'type' => 'text', 'label' => '신청자'],
|
||||
['name' => 'report_type', 'type' => 'text', 'label' => '사유서유형'],
|
||||
['name' => 'target_date', 'type' => 'date', 'label' => '대상일'],
|
||||
['name' => 'reason', 'type' => 'text', 'label' => '사유'],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')
|
||||
->where('tenant_id', 1)
|
||||
->whereIn('code', ['attendance_request', 'reason_report'])
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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
|
||||
{
|
||||
// ai_quotations 테이블: 제조 견적 필드 추가
|
||||
Schema::table('ai_quotations', function (Blueprint $table) {
|
||||
$table->string('quote_mode', 20)->default('module')->after('tenant_id')
|
||||
->comment('견적 모드: module(모듈추천), manufacture(제조견적)');
|
||||
$table->string('quote_number', 50)->nullable()->after('quote_mode')
|
||||
->comment('견적번호: AQ-SC-260303-01');
|
||||
$table->string('product_category', 50)->nullable()->after('quote_number')
|
||||
->comment('제품 카테고리: SCREEN, STEEL');
|
||||
|
||||
$table->index('quote_mode');
|
||||
$table->index('quote_number');
|
||||
});
|
||||
|
||||
// ai_quotation_items 테이블: 제조 견적 품목 필드 추가
|
||||
Schema::table('ai_quotation_items', function (Blueprint $table) {
|
||||
$table->string('specification', 200)->nullable()->after('module_name')
|
||||
->comment('규격: 3000×2500');
|
||||
$table->string('unit', 20)->nullable()->after('specification')
|
||||
->comment('단위: SET, EA, ㎡');
|
||||
$table->decimal('quantity', 10, 2)->default(1)->after('unit')
|
||||
->comment('수량');
|
||||
$table->decimal('unit_price', 15, 2)->default(0)->after('quantity')
|
||||
->comment('단가');
|
||||
$table->decimal('total_price', 15, 2)->default(0)->after('unit_price')
|
||||
->comment('금액 (수량×단가)');
|
||||
$table->string('item_category', 50)->nullable()->after('total_price')
|
||||
->comment('품목 분류: material, labor, install, etc.');
|
||||
$table->string('floor_code', 50)->nullable()->after('item_category')
|
||||
->comment('위치 코드: B1-A01, 1F-C01');
|
||||
});
|
||||
|
||||
// ai_quote_price_tables: AI 견적용 단가표 (신규)
|
||||
Schema::create('ai_quote_price_tables', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('product_category', 50)->comment('SCREEN, STEEL');
|
||||
$table->string('price_type', 50)->comment('area_based, weight_based, fixed');
|
||||
$table->decimal('min_value', 12, 4)->default(0)->comment('면적/중량 최소');
|
||||
$table->decimal('max_value', 12, 4)->default(0)->comment('면적/중량 최대');
|
||||
$table->decimal('unit_price', 15, 2)->default(0)->comment('해당 구간 단가');
|
||||
$table->decimal('labor_rate', 5, 2)->default(0)->comment('노무비율 (%)');
|
||||
$table->decimal('install_rate', 5, 2)->default(0)->comment('설치비율 (%)');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('tenant_id');
|
||||
$table->index(['product_category', 'price_type']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ai_quote_price_tables');
|
||||
|
||||
Schema::table('ai_quotation_items', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'specification', 'unit', 'quantity', 'unit_price',
|
||||
'total_price', 'item_category', 'floor_code',
|
||||
]);
|
||||
});
|
||||
|
||||
Schema::table('ai_quotations', function (Blueprint $table) {
|
||||
$table->dropIndex(['quote_mode']);
|
||||
$table->dropIndex(['quote_number']);
|
||||
$table->dropColumn(['quote_mode', 'quote_number', 'product_category']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('approval_forms', function (Blueprint $table) {
|
||||
$table->text('body_template')->nullable()->after('template')->comment('본문 HTML 템플릿');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approval_forms', function (Blueprint $table) {
|
||||
$table->dropColumn('body_template');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$bodyTemplate = <<<'HTML'
|
||||
<table style="width:100%; border-collapse:collapse; font-size:14px; border:2px solid #333;">
|
||||
<colgroup>
|
||||
<col style="width:15%;">
|
||||
<col style="width:35%;">
|
||||
<col style="width:15%;">
|
||||
<col style="width:35%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">지출일자</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">부서</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">거래처</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;" colspan="3"></td>
|
||||
</tr>
|
||||
<tr style="background:#e8e8e8;">
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">계정과목</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">적요</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">금액</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">비고</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr style="background:#f5f5f5;">
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;" colspan="2">합계</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">지출 사유</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;" colspan="3" style="min-height:60px;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
HTML;
|
||||
|
||||
DB::table('approval_forms')->insertOrIgnore([
|
||||
'tenant_id' => 1,
|
||||
'name' => '지출결의서',
|
||||
'code' => 'expense',
|
||||
'category' => 'expense',
|
||||
'template' => json_encode([
|
||||
'fields' => [
|
||||
['name' => 'expense_date', 'type' => 'date', 'label' => '지출일자'],
|
||||
['name' => 'vendor', 'type' => 'text', 'label' => '거래처'],
|
||||
['name' => 'account', 'type' => 'text', 'label' => '계정과목'],
|
||||
['name' => 'amount', 'type' => 'number', 'label' => '금액'],
|
||||
['name' => 'description', 'type' => 'text', 'label' => '적요'],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'body_template' => $bodyTemplate,
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')
|
||||
->where('tenant_id', 1)
|
||||
->where('code', 'expense')
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$bodyTemplate = <<<'HTML'
|
||||
<h2 style="text-align:center; margin-bottom:16px; font-size:20px; font-weight:bold; letter-spacing:2px;">지 출 결 의 서</h2>
|
||||
<table style="width:100%; border-collapse:collapse; margin-bottom:12px; font-size:13px;">
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold; width:110px;">지출형식</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px;" colspan="5">
|
||||
☐ 법인카드 ☐ 송금 ☐ 현금/가지급정산 ☐ 복지카드
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold;">세금계산서</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px;" colspan="5">
|
||||
☐ 일반 ☐ 이월발행
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="width:100%; border-collapse:collapse; margin-bottom:12px; font-size:13px;">
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold; width:110px;">작성일자</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px; width:22%;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold; width:80px;">지출부서</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px; width:22%;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold; width:60px;">이름</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 10px; background:#f8f9fa; font-weight:bold;">제목</td>
|
||||
<td style="border:1px solid #999; padding:6px 10px;" colspan="5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="width:100%; border-collapse:collapse; margin-bottom:12px; font-size:13px;">
|
||||
<thead>
|
||||
<tr style="background:#e9ecef;">
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:90px;">년/월/일</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center;">내용</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:90px;">금액</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:90px;">업체명</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:70px;">지급은행</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:100px;">계좌번호</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:60px;">예금주</th>
|
||||
<th style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center; width:60px;">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="background:#f8f9fa;">
|
||||
<td style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:center;" colspan="2">합 계</td>
|
||||
<td style="border:1px solid #999; padding:6px 8px; font-weight:bold; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:6px 8px;" colspan="5"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<p style="font-size:13px; color:#666;">첨부서류:</p>
|
||||
HTML;
|
||||
|
||||
DB::table('approval_forms')
|
||||
->where('tenant_id', 1)
|
||||
->where('code', 'expense')
|
||||
->update([
|
||||
'body_template' => $bodyTemplate,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 이전 body_template으로 복원
|
||||
$oldBodyTemplate = <<<'HTML'
|
||||
<table style="width:100%; border-collapse:collapse; font-size:14px; border:2px solid #333;">
|
||||
<colgroup>
|
||||
<col style="width:15%;">
|
||||
<col style="width:35%;">
|
||||
<col style="width:15%;">
|
||||
<col style="width:35%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">지출일자</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">부서</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">거래처</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;" colspan="3"></td>
|
||||
</tr>
|
||||
<tr style="background:#e8e8e8;">
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">계정과목</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">적요</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">금액</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;">비고</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr style="background:#f5f5f5;">
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:center;" colspan="2">합계</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; font-weight:bold; text-align:right;"></td>
|
||||
<td style="border:1px solid #999; padding:8px 12px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #999; padding:8px 12px; background:#f5f5f5; font-weight:bold;">지출 사유</td>
|
||||
<td style="border:1px solid #999; padding:8px 12px; min-height:60px;" colspan="3"></td>
|
||||
</tr>
|
||||
</table>
|
||||
HTML;
|
||||
|
||||
DB::table('approval_forms')
|
||||
->where('tenant_id', 1)
|
||||
->where('code', 'expense')
|
||||
->update([
|
||||
'body_template' => $oldBodyTemplate,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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
|
||||
{
|
||||
$tables = ['clients', 'tenants', 'site_briefings', 'sites'];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
if (Schema::hasColumn($table, 'address')) {
|
||||
Schema::table($table, function (Blueprint $t) {
|
||||
$t->string('address', 500)->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$tables = ['clients', 'tenants', 'site_briefings', 'sites'];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
if (Schema::hasColumn($table, 'address')) {
|
||||
Schema::table($table, function (Blueprint $t) {
|
||||
$t->string('address', 255)->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('approvals', function (Blueprint $table) {
|
||||
$table->timestamp('drafter_read_at')->nullable()->after('completed_at')
|
||||
->comment('기안자가 완료 결과를 확인한 시각');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->dropColumn('drafter_read_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('approvals', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('resubmit_count')->default(0)->after('current_step');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->dropColumn('resubmit_count');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('approvals', function (Blueprint $table) {
|
||||
$table->json('rejection_history')->nullable()->after('resubmit_count');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approvals', function (Blueprint $table) {
|
||||
$table->dropColumn('rejection_history');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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('cm_songs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('company_name', 100);
|
||||
$table->string('industry', 200);
|
||||
$table->text('lyrics');
|
||||
$table->string('audio_path')->nullable();
|
||||
$table->json('options')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cm_songs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'employment_cert')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '재직증명서',
|
||||
'code' => 'employment_cert',
|
||||
'category' => 'certificate',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'employment_cert')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'career_cert')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '경력증명서',
|
||||
'code' => 'career_cert',
|
||||
'category' => 'certificate',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'career_cert')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'appointment_cert')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '위촉증명서',
|
||||
'code' => 'appointment_cert',
|
||||
'category' => 'certificate',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'appointment_cert')->delete();
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('menu_favorites', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('user_id')->comment('사용자 ID');
|
||||
$table->unsignedBigInteger('menu_id')->comment('메뉴 ID');
|
||||
$table->integer('sort_order')->default(0)->comment('표시 순서');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'user_id', 'menu_id']);
|
||||
$table->index(['tenant_id', 'user_id', 'sort_order']);
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
$table->foreign('menu_id')->references('id')->on('menus')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('menu_favorites');
|
||||
}
|
||||
};
|
||||
@@ -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('departments', function (Blueprint $table) {
|
||||
$table->json('options')->nullable()->after('sort_order');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('departments', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'seal_usage')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '사용인감계',
|
||||
'code' => 'seal_usage',
|
||||
'category' => 'certificate',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'seal_usage')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('condolence_expenses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->index();
|
||||
$table->date('event_date')->nullable()->comment('경조사일자');
|
||||
$table->date('expense_date')->nullable()->comment('지출일자');
|
||||
$table->string('partner_name', 100)->comment('거래처명/대상자');
|
||||
$table->string('description', 200)->nullable()->comment('내역');
|
||||
$table->string('category', 20)->default('congratulation')->comment('구분: congratulation(축의), condolence(부조)');
|
||||
$table->boolean('has_cash')->default(false)->comment('부조금 여부');
|
||||
$table->string('cash_method', 30)->nullable()->comment('지출방법: cash, transfer, card');
|
||||
$table->integer('cash_amount')->default(0)->comment('부조금 금액');
|
||||
$table->boolean('has_gift')->default(false)->comment('선물 여부');
|
||||
$table->string('gift_type', 50)->nullable()->comment('선물 종류');
|
||||
$table->integer('gift_amount')->default(0)->comment('선물 금액');
|
||||
$table->integer('total_amount')->default(0)->comment('총금액');
|
||||
$table->json('options')->nullable();
|
||||
$table->text('memo')->nullable()->comment('비고');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['tenant_id', 'event_date']);
|
||||
$table->index(['tenant_id', 'category']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('condolence_expenses');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 각 테넌트의 부가세관리 메뉴를 찾아서 같은 parent_id 아래에 경조사비관리 추가
|
||||
$vatMenus = DB::table('menus')
|
||||
->where('url', '/finance/vat')
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
foreach ($vatMenus as $vatMenu) {
|
||||
// 이미 존재하면 skip
|
||||
$exists = DB::table('menus')
|
||||
->where('tenant_id', $vatMenu->tenant_id)
|
||||
->where('url', '/finance/condolence-expenses')
|
||||
->whereNull('deleted_at')
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('menus')->insert([
|
||||
'tenant_id' => $vatMenu->tenant_id,
|
||||
'parent_id' => $vatMenu->parent_id,
|
||||
'name' => '경조사비관리',
|
||||
'url' => '/finance/condolence-expenses',
|
||||
'icon' => 'gift',
|
||||
'sort_order' => $vatMenu->sort_order + 1,
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('menus')
|
||||
->where('url', '/finance/condolence-expenses')
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'delegation')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '위임장',
|
||||
'code' => 'delegation',
|
||||
'category' => 'request',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'delegation')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'board_minutes')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '이사회의사록',
|
||||
'code' => 'board_minutes',
|
||||
'category' => 'request',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'board_minutes')->delete();
|
||||
}
|
||||
};
|
||||
38
database/migrations/2026_03_06_235000_add_quotation_form.php
Normal file
38
database/migrations/2026_03_06_235000_add_quotation_form.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'quotation')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '견적서',
|
||||
'code' => 'quotation',
|
||||
'category' => 'expense',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'quotation')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
$exists = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'official_letter')
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '공문서',
|
||||
'code' => 'official_letter',
|
||||
'category' => 'general',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'official_letter')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$tenants = DB::table('tenants')->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
foreach ($tenants as $tenantId) {
|
||||
// 1차 통지서
|
||||
$exists1st = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'leave_promotion_1st')
|
||||
->exists();
|
||||
|
||||
if (! $exists1st) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '연차사용촉진 통지서 (1차)',
|
||||
'code' => 'leave_promotion_1st',
|
||||
'category' => 'hr',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 2차 통지서
|
||||
$exists2nd = DB::table('approval_forms')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', 'leave_promotion_2nd')
|
||||
->exists();
|
||||
|
||||
if (! $exists2nd) {
|
||||
DB::table('approval_forms')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => '연차사용촉진 통지서 (2차)',
|
||||
'code' => 'leave_promotion_2nd',
|
||||
'category' => 'hr',
|
||||
'template' => '[]',
|
||||
'body_template' => '',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('approval_forms')->where('code', 'leave_promotion_1st')->delete();
|
||||
DB::table('approval_forms')->where('code', 'leave_promotion_2nd')->delete();
|
||||
}
|
||||
};
|
||||
281
database/seeders/AiQuotationModuleSeeder.php
Normal file
281
database/seeders/AiQuotationModuleSeeder.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AiQuotationModuleSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1;
|
||||
|
||||
$modules = [
|
||||
[
|
||||
'module_code' => 'BASIC_PKG',
|
||||
'module_name' => '기본 패키지 (인사+근태+급여+게시판)',
|
||||
'category' => 'basic',
|
||||
'description' => '인사관리, 근태관리, 급여관리, 게시판/공지사항을 포함하는 기본 패키지. 모든 기업에 필수적인 기본 관리 모듈을 통합 제공합니다.',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['기본', '인사', '근태', '급여', '게시판', '공지사항', '직원관리'],
|
||||
'pain_points' => ['직원 관리 시스템 없음', '출퇴근 기록 수기', '급여 엑셀 관리'],
|
||||
'business_needs' => ['직원 기본 관리', '출퇴근 관리', '급여 자동화', '사내 공지'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 5000000,
|
||||
'monthly_fee' => 200000,
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'module_code' => 'HR',
|
||||
'module_name' => '인사관리',
|
||||
'category' => 'individual',
|
||||
'description' => '직원 정보, 조직도, 부서 관리, 입퇴사 처리, 인사발령 관리',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['직원', '사원', '인사', '조직도', '부서', '입퇴사', '인력', '인사발령'],
|
||||
'pain_points' => ['엑셀로 직원 관리', '입퇴사 관리가 번거로움', '조직도 없음'],
|
||||
'business_needs' => ['직원 정보 통합', '조직 구조 관리', '인력 현황 파악'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 2000000,
|
||||
'monthly_fee' => 80000,
|
||||
'sort_order' => 10,
|
||||
],
|
||||
[
|
||||
'module_code' => 'ATTENDANCE',
|
||||
'module_name' => '근태관리',
|
||||
'category' => 'individual',
|
||||
'description' => '출퇴근 기록, 연차/휴가 관리, 초과근무, 근태 통계',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['출퇴근', '근태', '연차', '휴가', '초과근무', '야근', '출근부'],
|
||||
'pain_points' => ['수기 출퇴근 기록', '연차 잔여 파악 어려움', '초과근무 관리 부재'],
|
||||
'business_needs' => ['자동 출퇴근 기록', '연차 관리', '근태 현황 파악'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 1500000,
|
||||
'monthly_fee' => 60000,
|
||||
'sort_order' => 11,
|
||||
],
|
||||
[
|
||||
'module_code' => 'PAYROLL',
|
||||
'module_name' => '급여관리',
|
||||
'category' => 'individual',
|
||||
'description' => '급여 계산, 4대보험, 원천징수, 급여명세서, 퇴직금 산출',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['급여', '월급', '4대보험', '원천징수', '급여명세서', '퇴직금', '세금'],
|
||||
'pain_points' => ['엑셀 급여 계산', '4대보험 수동 계산', '급여명세서 수동 발급'],
|
||||
'business_needs' => ['급여 자동 계산', '세금/보험 자동 처리', '급여명세서 자동 발급'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 2500000,
|
||||
'monthly_fee' => 100000,
|
||||
'sort_order' => 12,
|
||||
],
|
||||
[
|
||||
'module_code' => 'BOARD',
|
||||
'module_name' => '게시판/공지사항',
|
||||
'category' => 'individual',
|
||||
'description' => '사내 공지사항, 자유게시판, 부서별 게시판, 파일 첨부',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['게시판', '공지사항', '사내공지', '알림', '커뮤니케이션'],
|
||||
'pain_points' => ['공지 전달 어려움', '사내 소통 부재'],
|
||||
'business_needs' => ['사내 공지 시스템', '부서간 소통'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 500000,
|
||||
'monthly_fee' => 20000,
|
||||
'sort_order' => 13,
|
||||
],
|
||||
[
|
||||
'module_code' => 'SALES',
|
||||
'module_name' => '영업관리 (CRM+견적+수주)',
|
||||
'category' => 'individual',
|
||||
'description' => '고객관리(CRM), 견적서 작성/관리, 수주 관리, 영업 현황 대시보드',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['영업', 'CRM', '고객', '견적', '수주', '거래처', '매출', '영업사원'],
|
||||
'pain_points' => ['견적서 수동 작성', '고객 이력 관리 안 됨', '영업 현황 파악 어려움'],
|
||||
'business_needs' => ['고객 DB 관리', '견적 자동화', '수주 관리', '영업 실적 분석'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 5000000,
|
||||
'monthly_fee' => 150000,
|
||||
'sort_order' => 20,
|
||||
],
|
||||
[
|
||||
'module_code' => 'PURCHASE',
|
||||
'module_name' => '구매/자재관리',
|
||||
'category' => 'individual',
|
||||
'description' => '발주, 입고, 자재관리, 재고현황, 거래처 관리',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['구매', '자재', '발주', '입고', '재고', '거래처', '원자재', '부품'],
|
||||
'pain_points' => ['재고 파악 불가', '발주 관리 수동', '자재 이력 추적 안 됨'],
|
||||
'business_needs' => ['실시간 재고 관리', '자동 발주', '자재 이력 추적'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 3000000,
|
||||
'monthly_fee' => 100000,
|
||||
'sort_order' => 30,
|
||||
],
|
||||
[
|
||||
'module_code' => 'PRODUCTION',
|
||||
'module_name' => '생산관리 (MES)',
|
||||
'category' => 'individual',
|
||||
'description' => '작업지시, 공정관리, LOT 추적, 생산실적, 불량관리',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['생산', '제조', '작업지시', '공정', 'LOT', '불량', 'MES', '작업일보'],
|
||||
'pain_points' => ['생산 현황 수기 기록', '불량 추적 불가', '납기 관리 어려움', '작업일보 종이'],
|
||||
'business_needs' => ['실시간 생산현황', '불량률 관리', '작업지시 자동화', 'LOT 추적'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 8000000,
|
||||
'monthly_fee' => 250000,
|
||||
'sort_order' => 40,
|
||||
],
|
||||
[
|
||||
'module_code' => 'QUALITY',
|
||||
'module_name' => '품질관리',
|
||||
'category' => 'individual',
|
||||
'description' => '수입검사, 공정검사, 출하검사, 불량분석, 검사성적서',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['품질', '검사', '불량', '수입검사', '출하검사', '품질인증', 'ISO'],
|
||||
'pain_points' => ['검사 기록 수기', '불량 원인 분석 어려움', '검사성적서 수동 발행'],
|
||||
'business_needs' => ['검사 이력 관리', '불량 분석 자동화', '검사성적서 자동 발행'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 4000000,
|
||||
'monthly_fee' => 120000,
|
||||
'sort_order' => 41,
|
||||
],
|
||||
[
|
||||
'module_code' => 'FINANCE',
|
||||
'module_name' => '재무/회계관리',
|
||||
'category' => 'individual',
|
||||
'description' => '매출/매입 관리, 세금계산서, 미수금/미지급금, 재무제표',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['재무', '회계', '매출', '매입', '세금계산서', '미수금', '미지급금', '장부'],
|
||||
'pain_points' => ['엑셀 장부 관리', '세금계산서 수동 발행', '미수금 추적 어려움'],
|
||||
'business_needs' => ['자동 장부 관리', '세금계산서 자동 발행', '미수금 알림'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 5000000,
|
||||
'monthly_fee' => 150000,
|
||||
'sort_order' => 50,
|
||||
],
|
||||
[
|
||||
'module_code' => 'LOGISTICS',
|
||||
'module_name' => '물류/출하관리',
|
||||
'category' => 'individual',
|
||||
'description' => '출하 계획, 배송 관리, 운송장 발행, 출하 이력',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['물류', '출하', '배송', '운송', '택배', '납품', '출고'],
|
||||
'pain_points' => ['출하 현황 파악 어려움', '배송 추적 불가'],
|
||||
'business_needs' => ['출하 계획 관리', '배송 추적', '납품서 자동 발행'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 3000000,
|
||||
'monthly_fee' => 100000,
|
||||
'sort_order' => 31,
|
||||
],
|
||||
[
|
||||
'module_code' => 'APPROVAL',
|
||||
'module_name' => '전자결재',
|
||||
'category' => 'individual',
|
||||
'description' => '결재 라인 설정, 기안/승인/반려, 결재 양식 관리',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['결재', '기안', '승인', '품의서', '지출결의', '전자결재', '워크플로우'],
|
||||
'pain_points' => ['종이 결재', '결재 지연', '결재 이력 관리 안 됨'],
|
||||
'business_needs' => ['전자결재 도입', '결재 프로세스 자동화', '모바일 결재'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 3000000,
|
||||
'monthly_fee' => 80000,
|
||||
'sort_order' => 60,
|
||||
],
|
||||
[
|
||||
'module_code' => 'DOCUMENT',
|
||||
'module_name' => '문서관리 (전자서명)',
|
||||
'category' => 'individual',
|
||||
'description' => '문서 생성/관리, 전자서명, 문서 버전 관리, 계약서 관리',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['문서', '계약서', '전자서명', '서명', '파일관리', '문서보관'],
|
||||
'pain_points' => ['종이 계약서', '문서 분실', '서명을 위한 방문'],
|
||||
'business_needs' => ['전자서명', '문서 중앙관리', '계약서 자동 생성'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 2000000,
|
||||
'monthly_fee' => 60000,
|
||||
'sort_order' => 61,
|
||||
],
|
||||
[
|
||||
'module_code' => 'EQUIPMENT',
|
||||
'module_name' => '설비관리',
|
||||
'category' => 'individual',
|
||||
'description' => '설비 대장, 점검/보전 관리, 고장 이력, 예방보전',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['설비', '기계', '점검', '보전', '고장', '수리', '유지보수'],
|
||||
'pain_points' => ['설비 이력 관리 안 됨', '고장 대응 늦음', '점검 일정 수동 관리'],
|
||||
'business_needs' => ['설비 대장 관리', '점검 일정 자동화', '고장 이력 추적'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 3000000,
|
||||
'monthly_fee' => 100000,
|
||||
'sort_order' => 42,
|
||||
],
|
||||
[
|
||||
'module_code' => 'INTEGRATED',
|
||||
'module_name' => '통합 패키지',
|
||||
'category' => 'basic',
|
||||
'description' => '모든 개별 모듈을 포함하는 통합 패키지. 개별 구매 대비 할인 적용.',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['통합', '전사', 'ERP', '올인원', '풀패키지'],
|
||||
'pain_points' => ['전체 업무 디지털화 필요', '시스템 분산으로 인한 비효율'],
|
||||
'business_needs' => ['전사 통합 관리', '데이터 일원화', '부서간 연계'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 30000000,
|
||||
'monthly_fee' => 800000,
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'module_code' => 'AI_TOKEN',
|
||||
'module_name' => 'AI 토큰 추가',
|
||||
'category' => 'addon',
|
||||
'description' => 'AI 기능 사용을 위한 추가 토큰. 기본 월 100만 토큰 포함, 초과분 별도.',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['AI', '인공지능', '자동화', '챗봇', '음성인식'],
|
||||
'pain_points' => [],
|
||||
'business_needs' => ['AI 분석', '자동화 강화'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 0,
|
||||
'monthly_fee' => 0,
|
||||
'sort_order' => 90,
|
||||
],
|
||||
[
|
||||
'module_code' => 'STORAGE',
|
||||
'module_name' => '파일 저장공간 추가',
|
||||
'category' => 'addon',
|
||||
'description' => '기본 10GB 포함, 추가 저장공간. 문서, 이미지, 첨부파일 등.',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['저장공간', '파일', '클라우드', '백업', '스토리지'],
|
||||
'pain_points' => [],
|
||||
'business_needs' => ['대용량 파일 저장', '클라우드 백업'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 0,
|
||||
'monthly_fee' => 0,
|
||||
'sort_order' => 91,
|
||||
],
|
||||
[
|
||||
'module_code' => 'CUSTOM_DEV',
|
||||
'module_name' => '커스텀 개발',
|
||||
'category' => 'addon',
|
||||
'description' => '고객 맞춤형 기능 개발. 별도 협의 필요.',
|
||||
'keywords' => json_encode([
|
||||
'keywords' => ['맞춤', '커스텀', '추가개발', '특수기능'],
|
||||
'pain_points' => [],
|
||||
'business_needs' => ['업종별 특수 기능', '기존 시스템 연동'],
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'dev_cost' => 0,
|
||||
'monthly_fee' => 0,
|
||||
'sort_order' => 92,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
DB::table('ai_quotation_modules')->updateOrInsert(
|
||||
['tenant_id' => $tenantId, 'module_code' => $module['module_code']],
|
||||
array_merge($module, [
|
||||
'tenant_id' => $tenantId,
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
database/seeders/IncomeTaxBracketSeeder.php
Normal file
47
database/seeders/IncomeTaxBracketSeeder.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class IncomeTaxBracketSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$taxYear = 2024;
|
||||
|
||||
// 기존 해당 연도 데이터 삭제 후 재생성
|
||||
DB::table('income_tax_brackets')->where('tax_year', $taxYear)->delete();
|
||||
|
||||
// JSON 데이터 로드 (format: [[salary_from, salary_to, [tax1..tax11]], ...])
|
||||
$jsonPath = database_path('seeders/data/income_tax_2024.json');
|
||||
$data = json_decode(file_get_contents($jsonPath), true);
|
||||
|
||||
$records = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($data as $row) {
|
||||
[$salaryFrom, $salaryTo, $taxes] = $row;
|
||||
|
||||
for ($fc = 1; $fc <= 11; $fc++) {
|
||||
$records[] = [
|
||||
'tax_year' => $taxYear,
|
||||
'salary_from' => $salaryFrom,
|
||||
'salary_to' => $salaryTo,
|
||||
'family_count' => $fc,
|
||||
'tax_amount' => $taxes[$fc - 1],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 500건씩 청크 삽입
|
||||
foreach (array_chunk($records, 500) as $chunk) {
|
||||
DB::table('income_tax_brackets')->insert($chunk);
|
||||
}
|
||||
|
||||
$this->command->info("Income tax brackets seeded: {$taxYear}년 ".count($records).'건');
|
||||
}
|
||||
}
|
||||
1
database/seeders/data/income_tax_2024.json
Normal file
1
database/seeders/data/income_tax_2024.json
Normal file
File diff suppressed because one or more lines are too long
@@ -50,6 +50,7 @@
|
||||
Route::put('/{id}', [CardController::class, 'update'])->whereNumber('id')->name('v1.cards.update');
|
||||
Route::delete('/{id}', [CardController::class, 'destroy'])->whereNumber('id')->name('v1.cards.destroy');
|
||||
Route::patch('/{id}/toggle', [CardController::class, 'toggle'])->whereNumber('id')->name('v1.cards.toggle');
|
||||
Route::get('/stats', fn () => redirect()->route('v1.card-transactions.dashboard', request()->query()));
|
||||
});
|
||||
|
||||
// BankAccount API (계좌 관리)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
use App\Http\Controllers\Api\V1\ApprovalFormController;
|
||||
use App\Http\Controllers\Api\V1\ApprovalLineController;
|
||||
use App\Http\Controllers\Api\V1\AttendanceController;
|
||||
use App\Http\Controllers\Api\V1\CalendarScheduleController;
|
||||
use App\Http\Controllers\Api\V1\Construction\ContractController;
|
||||
use App\Http\Controllers\Api\V1\Construction\HandoverReportController;
|
||||
use App\Http\Controllers\Api\V1\Construction\StructureReviewController;
|
||||
@@ -213,3 +214,14 @@
|
||||
Route::delete('/{id}', [StructureReviewController::class, 'destroy'])->whereNumber('id')->name('v1.construction.structure-reviews.destroy');
|
||||
});
|
||||
});
|
||||
|
||||
// Calendar Schedule API (달력 일정 관리)
|
||||
Route::prefix('calendar-schedules')->group(function () {
|
||||
Route::get('', [CalendarScheduleController::class, 'index'])->name('v1.calendar-schedules.index');
|
||||
Route::get('/stats', [CalendarScheduleController::class, 'stats'])->name('v1.calendar-schedules.stats');
|
||||
Route::post('', [CalendarScheduleController::class, 'store'])->name('v1.calendar-schedules.store');
|
||||
Route::post('/bulk', [CalendarScheduleController::class, 'bulkStore'])->name('v1.calendar-schedules.bulk');
|
||||
Route::get('/{id}', [CalendarScheduleController::class, 'show'])->whereNumber('id')->name('v1.calendar-schedules.show');
|
||||
Route::put('/{id}', [CalendarScheduleController::class, 'update'])->whereNumber('id')->name('v1.calendar-schedules.update');
|
||||
Route::delete('/{id}', [CalendarScheduleController::class, 'destroy'])->whereNumber('id')->name('v1.calendar-schedules.destroy');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user