Files
sam-manage/resources/views/daily-logs/show.blade.php
hskwon a2477837d0 feat: [daily-logs] 일일 스크럼 기능 구현
주요 기능:
- 일일 로그 CRUD (생성, 조회, 수정, 삭제, 복원, 영구삭제)
- 로그 항목(Entry) 관리 (추가, 상태변경, 삭제, 순서변경)
- 주간 타임라인 (최근 7일 진행률 표시)
- 테이블 리스트 아코디언 상세보기
- 담당자 자동완성 (일반 사용자는 슈퍼관리자 목록 제외)
- HTMX 기반 동적 테이블 로딩 및 필터링
- Soft Delete 지원

파일 추가:
- Models: AdminPmDailyLog, AdminPmDailyLogEntry
- Controllers: DailyLogController (Web, API)
- Service: DailyLogService
- Requests: StoreDailyLogRequest, UpdateDailyLogRequest
- Views: index, show, table partial, modal-form partial

라우트 추가:
- Web: /daily-logs, /daily-logs/today, /daily-logs/{id}
- API: /api/admin/daily-logs/* (CRUD + 항목관리)
2025-12-01 14:07:55 +09:00

312 lines
14 KiB
PHP

@extends('layouts.app')
@section('title', '일일 스크럼 - ' . $log->log_date->format('Y-m-d'))
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<div>
<a href="{{ route('daily-logs.index') }}" class="text-blue-600 hover:text-blue-800 text-sm mb-2 inline-block">
목록으로
</a>
<h1 class="text-2xl font-bold text-gray-800">📅 {{ $log->log_date->format('Y-m-d (l)') }}</h1>
@if($log->project)
<span class="px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
{{ $log->project->name }}
</span>
@endif
</div>
<div class="flex gap-2">
<button type="button"
onclick="openEditModal()"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
수정
</button>
</div>
</div>
<!-- 요약 카드 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-3">일일 요약</h2>
@if($log->summary)
<p class="text-gray-700 whitespace-pre-wrap">{{ $log->summary }}</p>
@else
<p class="text-gray-400">요약이 작성되지 않았습니다.</p>
@endif
<div class="mt-4 pt-4 border-t flex items-center text-sm text-gray-500">
<span>작성자: {{ $log->creator?->name ?? '-' }}</span>
<span class="mx-2"></span>
<span>작성일: {{ $log->created_at->format('Y-m-d H:i') }}</span>
@if($log->updater)
<span class="mx-2"></span>
<span>수정자: {{ $log->updater->name }} ({{ $log->updated_at->format('Y-m-d H:i') }})</span>
@endif
</div>
</div>
<!-- 항목 통계 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
@php $stats = $log->entry_stats; @endphp
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">전체</div>
<div class="text-2xl font-bold text-gray-800">{{ $stats['total'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">예정</div>
<div class="text-2xl font-bold text-gray-600">{{ $stats['todo'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">진행중</div>
<div class="text-2xl font-bold text-yellow-600">{{ $stats['in_progress'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">완료</div>
<div class="text-2xl font-bold text-green-600">{{ $stats['done'] }}</div>
</div>
</div>
<!-- 항목 목록 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">업무 항목</h2>
<button type="button"
onclick="openAddEntryModal()"
class="text-sm text-blue-600 hover:text-blue-800">
+ 항목 추가
</button>
</div>
<div id="entriesList" class="divide-y divide-gray-200">
@forelse($log->entries as $entry)
<div class="p-4 hover:bg-gray-50 entry-row" data-entry-id="{{ $entry->id }}">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<!-- 담당자 유형 배지 -->
<span class="px-2 py-0.5 text-xs font-medium rounded-full {{ $entry->assignee_type_color }}">
{{ $entry->assignee_type_label }}
</span>
<!-- 담당자 이름 -->
<span class="font-medium text-gray-900">{{ $entry->assignee_name }}</span>
<!-- 상태 배지 -->
<span class="px-2 py-0.5 text-xs font-medium rounded-full {{ $entry->status_color }}">
{{ $entry->status_label }}
</span>
</div>
<p class="text-gray-700 whitespace-pre-wrap">{{ $entry->content }}</p>
</div>
<div class="flex items-center gap-2 ml-4">
<!-- 상태 변경 버튼들 -->
<div class="flex gap-1">
@if($entry->status !== 'todo')
<button onclick="updateStatus({{ $entry->id }}, 'todo')"
class="p-1 text-gray-400 hover:text-gray-600" title="예정으로 변경">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</button>
@endif
@if($entry->status !== 'in_progress')
<button onclick="updateStatus({{ $entry->id }}, 'in_progress')"
class="p-1 text-yellow-400 hover:text-yellow-600" title="진행중으로 변경">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"/>
</svg>
</button>
@endif
@if($entry->status !== 'done')
<button onclick="updateStatus({{ $entry->id }}, 'done')"
class="p-1 text-green-400 hover:text-green-600" title="완료로 변경">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
@endif
</div>
<!-- 삭제 버튼 -->
<button onclick="confirmDeleteEntry({{ $entry->id }})"
class="p-1 text-red-400 hover:text-red-600" title="삭제">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</div>
</div>
@empty
<div class="p-12 text-center text-gray-500">
등록된 항목이 없습니다.
</div>
@endforelse
</div>
</div>
<!-- 항목 추가 모달 -->
<div id="addEntryModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closeAddEntryModal()"></div>
<div class="flex min-h-full items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-lg">
<div class="bg-gray-50 px-6 py-4 border-b">
<h3 class="text-lg font-semibold text-gray-900">항목 추가</h3>
<button type="button" onclick="closeAddEntryModal()" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<form id="addEntryForm" class="p-6">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">담당자 유형</label>
<select name="assignee_type" id="newAssigneeType" onchange="updateNewAssigneeOptions()"
class="w-full px-3 py-2 border border-gray-300 rounded-lg">
@foreach($assigneeTypes as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select name="status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg">
@foreach($entryStatuses as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">담당자 <span class="text-red-500">*</span></label>
<input type="text"
name="assignee_name"
id="newAssigneeName"
list="newAssigneeList"
placeholder="담당자 (직접입력 또는 선택)"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg">
<datalist id="newAssigneeList">
@foreach($assignees['users'] as $user)
<option value="{{ $user['name'] }}" data-id="{{ $user['id'] }}">
@endforeach
</datalist>
<input type="hidden" name="assignee_id" id="newAssigneeId">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">업무 내용 <span class="text-red-500">*</span></label>
<textarea name="content"
rows="3"
required
placeholder="업무 내용을 입력하세요..."
class="w-full px-3 py-2 border border-gray-300 rounded-lg"></textarea>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeAddEntryModal()"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300">
취소
</button>
<button type="submit"
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
추가
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
const logId = {{ $log->id }};
const assignees = @json($assignees);
// 상태 업데이트
function updateStatus(entryId, status) {
fetch(`/api/admin/daily-logs/entries/${entryId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ status })
})
.then(res => res.json())
.then(result => {
if (result.success) {
location.reload();
}
});
}
// 항목 삭제
function confirmDeleteEntry(entryId) {
if (confirm('이 항목을 삭제하시겠습니까?')) {
fetch(`/api/admin/daily-logs/entries/${entryId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(result => {
if (result.success) {
location.reload();
}
});
}
}
// 항목 추가 모달
function openAddEntryModal() {
document.getElementById('addEntryForm').reset();
document.getElementById('addEntryModal').classList.remove('hidden');
}
function closeAddEntryModal() {
document.getElementById('addEntryModal').classList.add('hidden');
}
// 담당자 옵션 업데이트
function updateNewAssigneeOptions() {
const type = document.getElementById('newAssigneeType').value;
const datalist = document.getElementById('newAssigneeList');
const list = type === 'team' ? assignees.teams : assignees.users;
datalist.innerHTML = list.map(item =>
`<option value="${item.name}" data-id="${item.id}">`
).join('');
}
// 항목 추가 폼 제출
document.getElementById('addEntryForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
fetch(`/api/admin/daily-logs/${logId}/entries`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert(result.message || '오류가 발생했습니다.');
}
});
});
// 로그 수정 모달 열기 (index.blade.php의 모달 재사용)
function openEditModal() {
// index 페이지로 리다이렉트하면서 수정 모달 열기
window.location.href = '{{ route('daily-logs.index') }}?edit={{ $log->id }}';
}
</script>
@endpush