diff --git a/app/Http/Controllers/Finance/DailyWorkLogController.php b/app/Http/Controllers/Finance/DailyWorkLogController.php index 3da3e13e..2afdfd2d 100644 --- a/app/Http/Controllers/Finance/DailyWorkLogController.php +++ b/app/Http/Controllers/Finance/DailyWorkLogController.php @@ -72,6 +72,55 @@ public function show(Request $request): JsonResponse ]); } + /** + * 기간별 업무일지 조회 (데이터 있는 날짜만) + */ + public function range(Request $request): JsonResponse + { + $request->validate([ + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + ]); + + $startDate = $request->input('start_date'); + $endDate = $request->input('end_date'); + + $logs = DailyWorkLog::whereBetween('log_date', [$startDate, $endDate]) + ->orderBy('log_date') + ->get(); + + $result = $logs->map(function (DailyWorkLog $log) { + $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 [ + '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, + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $result->values(), + 'count' => $result->count(), + ]); + } + /** * 업무일지 저장 (일지 + 항목 일괄) */ diff --git a/resources/views/finance/daily-work-log.blade.php b/resources/views/finance/daily-work-log.blade.php index 7e603619..ab528f53 100644 --- a/resources/views/finance/daily-work-log.blade.php +++ b/resources/views/finance/daily-work-log.blade.php @@ -71,6 +71,16 @@ function DailyWorkLog() { const [dirty, setDirty] = useState(false); const [message, setMessage] = useState(null); + // 기간 보기 모드 + const [viewMode, setViewMode] = useState('single'); // 'single' | 'range' + const [rangeStart, setRangeStart] = useState(() => { + const d = new Date(); d.setDate(d.getDate() - 6); + return toLocalDateString(d); + }); + const [rangeEnd, setRangeEnd] = useState(toLocalDateString(new Date())); + const [rangeLogs, setRangeLogs] = useState([]); + const [rangeLoading, setRangeLoading] = useState(false); + const dateObj = parseDate(currentDate); const dayName = DAYS[dateObj.getDay()]; @@ -271,6 +281,32 @@ function DailyWorkLog() { const completed = items.filter(it => it.is_completed).length; const rate = total > 0 ? ((completed / total) * 100).toFixed(2) : '0.00'; + // 기간 조회 + const fetchRange = useCallback(async () => { + setRangeLoading(true); + try { + const res = await fetch('/finance/daily-work-log/range?start_date=' + rangeStart + '&end_date=' + rangeEnd); + const json = await res.json(); + if (json.success) { + setRangeLogs(json.data || []); + if (json.data.length === 0) { + showMessage('해당 기간에 작성된 업무일지가 없습니다.', 'error'); + } + } + } catch (e) { + console.error('기간 조회 실패:', e); + } finally { + setRangeLoading(false); + } + }, [rangeStart, rangeEnd]); + + // 모드 전환 + const toggleViewMode = () => { + const next = viewMode === 'single' ? 'range' : 'single'; + setViewMode(next); + if (next === 'range') fetchRange(); + }; + // 인쇄 const handlePrint = () => window.print(); @@ -307,6 +343,15 @@ function DailyWorkLog() { const rateContainerStyle = { flex: '1', maxWidth: '400px', marginLeft: '20px' }; const rateTextStyle = { color: rateColor, minWidth: '80px', textAlign: 'right' }; const tableStyle = { width: '100%', borderCollapse: 'collapse' }; + const btnActive = { padding: '8px 16px', background: '#4f46e5', color: 'white', border: '1px solid #4f46e5', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: '600' }; + const rangeDateInput = { padding: '6px 12px', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '14px', fontWeight: '600', color: '#374151' }; + const rangeCardStyle = { marginBottom: '24px', background: 'white', borderRadius: '12px', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', border: '1px solid #e5e7eb', overflow: 'hidden' }; + const rangeDateHeader = { padding: '12px 16px', background: '#f8fafc', borderBottom: '2px solid #4a5568', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }; + const rangeDateText = { fontSize: '16px', fontWeight: '700', color: '#1e293b' }; + const rangeDayBadge = { fontSize: '13px', fontWeight: '600', color: '#6366f1', background: '#eef2ff', padding: '2px 10px', borderRadius: '12px' }; + const rangeMemoBlock = { padding: '10px 16px', fontSize: '13px', color: '#374151', background: '#fefce8', borderTop: '1px solid #e5e7eb', whiteSpace: 'pre-wrap' }; + const rangeReflBlock = { padding: '10px 16px', fontSize: '13px', color: '#374151', background: '#f0fdf4', borderTop: '1px solid #e5e7eb', whiteSpace: 'pre-wrap' }; + const rangeRateBox = { padding: '10px 16px', borderTop: '2px solid #4a5568', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }; const fileFlexContent = { flex: 1, minWidth: 0 }; const fileLinkStyle = { display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px' }; const fileSizeStyle = { fontSize: '11px', color: '#9ca3af' }; @@ -374,31 +419,50 @@ function DailyWorkLog() { {/* 날짜 네비게이션 */}
+ {viewMode === 'single' ? ( +
+ + setCurrentDate(e.target.value)} + className="text-lg font-bold text-gray-800 border border-gray-300 rounded-lg px-3 py-1" /> + {dayName} + +
+ ) : ( +
+ setRangeStart(e.target.value)} style={rangeDateInput} /> + ~ + setRangeEnd(e.target.value)} style={rangeDateInput} /> + + + {rangeLogs.length > 0 ? rangeLogs.length + '건' : ''} + +
+ )}
- - setCurrentDate(e.target.value)} - className="text-lg font-bold text-gray-800 border border-gray-300 rounded-lg px-3 py-1" /> - {dayName} - -
-
- - + {viewMode === 'single' && ( + <> + + + + )}
- {loading ? ( + {viewMode === 'single' && loading ? (
로딩 중...
- ) : ( + ) : viewMode === 'single' ? (
{/* 날짜/요일 인쇄 헤더 */} @@ -528,9 +592,105 @@ className="text-xs border border-gray-200 rounded" style={colorSelectStyle} titl + ) : null} + + {/* 기간 보기 모드 */} + {viewMode === 'range' && ( + rangeLoading ? ( +
+ + 기간 조회 중... +
+ ) : rangeLogs.length > 0 && ( +
+ {rangeLogs.map(log => { + const logDate = parseDate(log.log_date); + const logDayName = DAYS[logDate.getDay()]; + const logTotal = log.items.length; + const logCompleted = log.items.filter(it => it.is_completed).length; + const logRate = logTotal > 0 ? ((logCompleted / logTotal) * 100).toFixed(0) : '0'; + const logRateColor = logRate >= 80 ? '#48bb78' : (logRate >= 50 ? '#ecc94b' : '#f56565'); + const logRateFill = { height: '100%', borderRadius: '4px', background: logRateColor, width: logRate + '%', transition: 'width 0.3s' }; + return ( +
+
+
+ {log.log_date} + {logDayName} +
+ +
+
+ + + + + + + + + + + + + + + + + + + + {log.items.map((item, idx) => { + const bgColor = getHighlightColor(item.highlight); + const rowStyle = bgColor ? { background: bgColor } : {}; + return ( + + + + + + + + + ); + })} + {log.items.length === 0 && ( + + )} + +
No.구분업무우선순위완료비고
{idx + 1}{item.category}{item.task}{item.priority || '-'}{item.is_completed ? '\u2705' : '\u2B1C'}{item.note}
항목 없음
+ {log.memo && ( +
+ 메모 + {log.memo} +
+ )} + {log.reflection && ( +
+ 회고 + {log.reflection} +
+ )} +
+ 달성률 +
+
+
+
+ {logRate}% +
+
+
+ ); + })} + + ) )} - {/* 하단 액션 */} + {/* 하단 액션 (단건 모드에서만) */} + {viewMode === 'single' && (
{logId && ( @@ -544,6 +704,7 @@ className="text-xs border border-gray-200 rounded" style={colorSelectStyle} titl
+ ) ); } diff --git a/routes/web.php b/routes/web.php index 813f443d..08052e11 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1199,6 +1199,7 @@ Route::prefix('daily-work-log')->name('daily-work-log.')->group(function () { Route::get('/show', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'show'])->name('show'); + Route::get('/range', [\App\Http\Controllers\Finance\DailyWorkLogController::class, 'range'])->name('range'); 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');