- 경로: {tenant_id}/daily-work-log/{year}/{month}/{hex}.ext 형식으로 변경
- 파일명: UUID → 64bit 난수 hex (bin2hex(random_bytes(8)))
- 최대 크기: 10MB → 20MB (file-storage-guide.md 기준)
293 lines
9.3 KiB
PHP
293 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Finance;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Boards\File;
|
|
use App\Models\Finance\DailyWorkLog;
|
|
use App\Models\Finance\DailyWorkLogItem;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class DailyWorkLogController extends Controller
|
|
{
|
|
/**
|
|
* 특정 날짜의 업무일지 조회
|
|
*/
|
|
public function show(Request $request): JsonResponse
|
|
{
|
|
$date = $request->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();
|
|
|
|
$files = File::where('document_type', 'daily_work_log')
|
|
->where('document_id', $log->id)
|
|
->whereNull('deleted_at')
|
|
->get()
|
|
->map(fn (File $f) => [
|
|
'id' => $f->id,
|
|
'field_key' => $f->field_key,
|
|
'original_name' => $f->original_name,
|
|
'file_size' => $f->file_size,
|
|
'formatted_size' => $f->getFormattedSize(),
|
|
'mime_type' => $f->mime_type,
|
|
'is_image' => $f->isImage(),
|
|
'url' => $f->getUrl(),
|
|
]);
|
|
|
|
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,
|
|
'files' => $files->values(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 업무일지 저장 (일지 + 항목 일괄)
|
|
*/
|
|
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(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 파일 업로드 (메모/회고 첨부)
|
|
*/
|
|
public function uploadFile(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'log_date' => 'required|date',
|
|
'field_key' => 'required|in:memo,reflection',
|
|
'file' => 'required|file|max:20480',
|
|
]);
|
|
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$log = DailyWorkLog::updateOrCreate(
|
|
['tenant_id' => $tenantId, 'log_date' => $request->input('log_date')],
|
|
['created_by' => auth()->id()]
|
|
);
|
|
|
|
$file = $request->file('file');
|
|
$storedName = bin2hex(random_bytes(8)).'.'.$file->getClientOriginalExtension();
|
|
$year = date('Y');
|
|
$month = date('m');
|
|
$filePath = "{$tenantId}/daily-work-log/{$year}/{$month}/{$storedName}";
|
|
|
|
Storage::disk('tenant')->put($filePath, file_get_contents($file));
|
|
|
|
$record = File::create([
|
|
'tenant_id' => $tenantId,
|
|
'is_temp' => false,
|
|
'file_path' => $filePath,
|
|
'display_name' => $file->getClientOriginalName(),
|
|
'stored_name' => $storedName,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'file_name' => $file->getClientOriginalName(),
|
|
'file_size' => $file->getSize(),
|
|
'mime_type' => $file->getMimeType(),
|
|
'file_type' => $this->detectFileType($file->getMimeType()),
|
|
'field_key' => $request->input('field_key'),
|
|
'document_type' => 'daily_work_log',
|
|
'document_id' => $log->id,
|
|
'uploaded_by' => auth()->id(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'file' => [
|
|
'id' => $record->id,
|
|
'field_key' => $record->field_key,
|
|
'original_name' => $record->original_name,
|
|
'file_size' => $record->file_size,
|
|
'formatted_size' => $record->getFormattedSize(),
|
|
'mime_type' => $record->mime_type,
|
|
'is_image' => $record->isImage(),
|
|
'url' => $record->getUrl(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 파일 삭제
|
|
*/
|
|
public function deleteFile(int $fileId): JsonResponse
|
|
{
|
|
$file = File::where('document_type', 'daily_work_log')->findOrFail($fileId);
|
|
$file->softDeleteFile(auth()->id());
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 파일 다운로드
|
|
*/
|
|
public function downloadFile(int $fileId)
|
|
{
|
|
$file = File::where('document_type', 'daily_work_log')->findOrFail($fileId);
|
|
|
|
return $file->download();
|
|
}
|
|
|
|
private function detectFileType(?string $mimeType): string
|
|
{
|
|
if (! $mimeType) {
|
|
return 'other';
|
|
}
|
|
if (str_starts_with($mimeType, 'image/')) {
|
|
return 'image';
|
|
}
|
|
if ($mimeType === 'application/pdf') {
|
|
return 'pdf';
|
|
}
|
|
if (str_contains($mimeType, 'spreadsheet') || str_contains($mimeType, 'excel')) {
|
|
return 'excel';
|
|
}
|
|
|
|
return 'document';
|
|
}
|
|
}
|