From 91d51a39a9909f2cbe596323eccef3306e5b9e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 13 Mar 2026 18:10:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[finance]=20=EC=9D=BC=EC=9D=BC=EC=97=85?= =?UTF-8?q?=EB=AC=B4=EC=9D=BC=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이그레이션: daily_work_logs, daily_work_log_items 테이블 생성 - 모델: DailyWorkLog, DailyWorkLogItem (멀티테넌트, SoftDeletes) - 컨트롤러: CRUD + 완료토글 + 이전일지 복사 - 뷰: React(Babel) 기반, 날짜 화살표 네비게이션, 달성률 표시 - 라우트: finance/daily-work-log 하위 API 라우트 --- .../Finance/DailyWorkLogController.php | 176 +++++++ app/Models/Finance/DailyWorkLog.php | 49 ++ app/Models/Finance/DailyWorkLogItem.php | 53 +++ ...3_180000_create_daily_work_logs_tables.php | 48 ++ .../views/finance/daily-work-log.blade.php | 437 ++++++++++++++++++ routes/web.php | 17 + 6 files changed, 780 insertions(+) create mode 100644 app/Http/Controllers/Finance/DailyWorkLogController.php create mode 100644 app/Models/Finance/DailyWorkLog.php create mode 100644 app/Models/Finance/DailyWorkLogItem.php create mode 100644 database/migrations/2026_03_13_180000_create_daily_work_logs_tables.php create mode 100644 resources/views/finance/daily-work-log.blade.php diff --git a/app/Http/Controllers/Finance/DailyWorkLogController.php b/app/Http/Controllers/Finance/DailyWorkLogController.php new file mode 100644 index 00000000..6d5d9e57 --- /dev/null +++ b/app/Http/Controllers/Finance/DailyWorkLogController.php @@ -0,0 +1,176 @@ +input('date', date('Y-m-d')); + $tenantId = session('selected_tenant_id', 1); + + $log = DailyWorkLog::where('log_date', $date)->first(); + + if (! $log) { + return response()->json([ + 'success' => true, + 'data' => null, + ]); + } + + $items = $log->items()->get()->map(fn ($item) => [ + 'id' => $item->id, + 'sort_order' => $item->sort_order, + 'category' => $item->category ?? '', + 'task' => $item->task, + 'priority' => $item->priority ?? '', + 'is_completed' => $item->is_completed, + 'note' => $item->note ?? '', + 'highlight' => $item->getOption('highlight', ''), + ]); + + $total = $items->count(); + $completed = $items->where('is_completed', true)->count(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $log->id, + 'log_date' => $log->log_date->format('Y-m-d'), + 'memo' => $log->memo ?? '', + 'reflection' => $log->reflection ?? '', + 'items' => $items->values(), + 'achievement_rate' => $total > 0 ? round(($completed / $total) * 100, 2) : 0, + ], + ]); + } + + /** + * 업무일지 저장 (일지 + 항목 일괄) + */ + public function store(Request $request): JsonResponse + { + $request->validate([ + 'log_date' => 'required|date', + 'memo' => 'nullable|string', + 'reflection' => 'nullable|string', + 'items' => 'nullable|array', + 'items.*.category' => 'nullable|string|max:100', + 'items.*.task' => 'required|string|max:500', + 'items.*.priority' => 'nullable|string|max:50', + 'items.*.is_completed' => 'boolean', + 'items.*.note' => 'nullable|string|max:500', + 'items.*.highlight' => 'nullable|string|max:20', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $log = DailyWorkLog::updateOrCreate( + ['tenant_id' => $tenantId, 'log_date' => $request->input('log_date')], + [ + 'memo' => $request->input('memo', ''), + 'reflection' => $request->input('reflection', ''), + 'created_by' => auth()->id(), + ] + ); + + // 기존 항목 삭제 후 재생성 + $log->items()->forceDelete(); + + $items = $request->input('items', []); + foreach ($items as $i => $itemData) { + DailyWorkLogItem::create([ + 'tenant_id' => $tenantId, + 'daily_work_log_id' => $log->id, + 'sort_order' => $i + 1, + 'category' => $itemData['category'] ?? '', + 'task' => $itemData['task'], + 'priority' => $itemData['priority'] ?? '', + 'is_completed' => $itemData['is_completed'] ?? false, + 'note' => $itemData['note'] ?? '', + 'options' => $itemData['highlight'] ? ['highlight' => $itemData['highlight']] : null, + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '저장되었습니다.', + ]); + } + + /** + * 업무 항목 완료 토글 + */ + public function toggleItem(int $id): JsonResponse + { + $item = DailyWorkLogItem::findOrFail($id); + $item->update(['is_completed' => ! $item->is_completed]); + + return response()->json([ + 'success' => true, + 'is_completed' => $item->is_completed, + ]); + } + + /** + * 업무일지 삭제 + */ + public function destroy(int $id): JsonResponse + { + $log = DailyWorkLog::findOrFail($id); + $log->items()->delete(); + $log->delete(); + + return response()->json([ + 'success' => true, + 'message' => '삭제되었습니다.', + ]); + } + + /** + * 이전 날짜의 항목 복사 + */ + public function copyFromPrevious(Request $request): JsonResponse + { + $request->validate(['date' => 'required|date']); + $tenantId = session('selected_tenant_id', 1); + $date = $request->input('date'); + + $prevLog = DailyWorkLog::where('log_date', '<', $date) + ->orderByDesc('log_date') + ->first(); + + if (! $prevLog) { + return response()->json([ + 'success' => false, + 'message' => '이전 업무일지가 없습니다.', + ]); + } + + $items = $prevLog->items()->get()->map(fn ($item) => [ + 'category' => $item->category ?? '', + 'task' => $item->task, + 'priority' => $item->priority ?? '', + 'is_completed' => false, + 'note' => '', + 'highlight' => $item->getOption('highlight', ''), + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'source_date' => $prevLog->log_date->format('Y-m-d'), + 'items' => $items->values(), + ], + ]); + } +} diff --git a/app/Models/Finance/DailyWorkLog.php b/app/Models/Finance/DailyWorkLog.php new file mode 100644 index 00000000..62d3aba2 --- /dev/null +++ b/app/Models/Finance/DailyWorkLog.php @@ -0,0 +1,49 @@ + 'date', + 'options' => 'array', + ]; + + public function items(): HasMany + { + return $this->hasMany(DailyWorkLogItem::class)->orderBy('sort_order'); + } + + public function getOption(string $key, mixed $default = null): mixed + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, mixed $value): static + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + + return $this; + } +} diff --git a/app/Models/Finance/DailyWorkLogItem.php b/app/Models/Finance/DailyWorkLogItem.php new file mode 100644 index 00000000..662c1546 --- /dev/null +++ b/app/Models/Finance/DailyWorkLogItem.php @@ -0,0 +1,53 @@ + 'boolean', + 'sort_order' => 'integer', + 'options' => 'array', + ]; + + public function dailyWorkLog(): BelongsTo + { + return $this->belongsTo(DailyWorkLog::class); + } + + public function getOption(string $key, mixed $default = null): mixed + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, mixed $value): static + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + + return $this; + } +} diff --git a/database/migrations/2026_03_13_180000_create_daily_work_logs_tables.php b/database/migrations/2026_03_13_180000_create_daily_work_logs_tables.php new file mode 100644 index 00000000..dddb58d1 --- /dev/null +++ b/database/migrations/2026_03_13_180000_create_daily_work_logs_tables.php @@ -0,0 +1,48 @@ +id(); + $table->unsignedBigInteger('tenant_id'); + $table->date('log_date'); + $table->text('memo')->nullable(); + $table->text('reflection')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + $table->unique(['tenant_id', 'log_date']); + $table->index('tenant_id'); + }); + + Schema::create('daily_work_log_items', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('tenant_id'); + $table->foreignId('daily_work_log_id')->constrained('daily_work_logs')->cascadeOnDelete(); + $table->unsignedSmallInteger('sort_order')->default(0); + $table->string('category', 100)->nullable(); + $table->string('task', 500); + $table->string('priority', 50)->nullable(); + $table->boolean('is_completed')->default(false); + $table->string('note', 500)->nullable(); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + $table->index('tenant_id'); + $table->index('daily_work_log_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('daily_work_log_items'); + Schema::dropIfExists('daily_work_logs'); + } +}; diff --git a/resources/views/finance/daily-work-log.blade.php b/resources/views/finance/daily-work-log.blade.php new file mode 100644 index 00000000..d22731fe --- /dev/null +++ b/resources/views/finance/daily-work-log.blade.php @@ -0,0 +1,437 @@ +@extends('layouts.app') + +@section('title', '일일업무일지') + +@push('styles') + +@endpush + +@section('content') + +
+@endsection + +@push('scripts') +@include('partials.react-cdn') + +@endpush diff --git a/routes/web.php b/routes/web.php index 9476d620..366c522e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1186,6 +1186,23 @@ Route::post('/memo', [\App\Http\Controllers\Finance\DailyFundController::class, 'saveMemo'])->name('memo'); }); + // 일일업무일지 + Route::get('/daily-work-log', function () { + if (request()->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('finance.daily-work-log')); + } + + return view('finance.daily-work-log'); + })->name('daily-work-log'); + + Route::prefix('daily-work-log')->name('daily-work-log.')->group(function () { + Route::get('/show', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'show'])->name('show'); + Route::post('/store', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'store'])->name('store'); + Route::post('/toggle-item/{id}', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'toggleItem'])->name('toggle-item'); + Route::delete('/{id}', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'destroy'])->name('destroy'); + Route::post('/copy-previous', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'copyFromPrevious'])->name('copy-previous'); + }); + // 일반전표입력 Route::get('/journal-entries', function () { if (request()->header('HX-Request')) {