diff --git a/app/Http/Controllers/Api/Admin/DailyLogController.php b/app/Http/Controllers/Api/Admin/DailyLogController.php
index b2937048..16f3fbf8 100644
--- a/app/Http/Controllers/Api/Admin/DailyLogController.php
+++ b/app/Http/Controllers/Api/Admin/DailyLogController.php
@@ -178,6 +178,28 @@ public function addEntry(Request $request, int $logId): JsonResponse
]);
}
+ /**
+ * 항목 수정
+ */
+ public function updateEntry(Request $request, int $entryId): JsonResponse
+ {
+ $validated = $request->validate([
+ 'assignee_type' => 'sometimes|in:team,user',
+ 'assignee_id' => 'nullable|integer',
+ 'assignee_name' => 'sometimes|string|max:100',
+ 'content' => 'sometimes|string|max:2000',
+ 'status' => 'sometimes|in:todo,in_progress,done',
+ ]);
+
+ $entry = $this->dailyLogService->updateEntry($entryId, $validated);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => '항목이 수정되었습니다.',
+ 'data' => $entry,
+ ]);
+ }
+
/**
* 항목 상태 변경
*/
diff --git a/app/Models/Admin/AdminPmProject.php b/app/Models/Admin/AdminPmProject.php
index bd9c9737..a57922a8 100644
--- a/app/Models/Admin/AdminPmProject.php
+++ b/app/Models/Admin/AdminPmProject.php
@@ -94,6 +94,25 @@ public function issues(): HasMany
return $this->hasMany(AdminPmIssue::class, 'project_id');
}
+ /**
+ * 관계: 일일 로그
+ */
+ public function dailyLogs(): HasMany
+ {
+ return $this->hasMany(AdminPmDailyLog::class, 'project_id');
+ }
+
+ /**
+ * 오늘의 스크럼 항목
+ */
+ public function getTodayScrumAttribute(): ?AdminPmDailyLog
+ {
+ return $this->dailyLogs()
+ ->where('log_date', now()->format('Y-m-d'))
+ ->with('entries')
+ ->first();
+ }
+
/**
* 관계: 생성자
*/
diff --git a/app/Services/ProjectManagement/DailyLogService.php b/app/Services/ProjectManagement/DailyLogService.php
index 66ea27c1..c1c7453e 100644
--- a/app/Services/ProjectManagement/DailyLogService.php
+++ b/app/Services/ProjectManagement/DailyLogService.php
@@ -224,6 +224,17 @@ public function forceDeleteDailyLog(int $id): bool
return $log->forceDelete();
}
+ /**
+ * 항목 수정
+ */
+ public function updateEntry(int $entryId, array $data): AdminPmDailyLogEntry
+ {
+ $entry = AdminPmDailyLogEntry::findOrFail($entryId);
+ $entry->update($data);
+
+ return $entry->fresh();
+ }
+
/**
* 항목 상태 변경
*/
diff --git a/app/Services/ProjectManagement/ProjectService.php b/app/Services/ProjectManagement/ProjectService.php
index 52cfc9e2..55378d26 100644
--- a/app/Services/ProjectManagement/ProjectService.php
+++ b/app/Services/ProjectManagement/ProjectService.php
@@ -158,12 +158,16 @@ public function getProjectStats(): array
*/
public function getDashboardSummary(): array
{
+ $today = now()->format('Y-m-d');
+
$activeProjects = AdminPmProject::active()
->withCount(['tasks', 'issues'])
->with(['tasks' => function ($q) {
$q->select('id', 'project_id', 'status');
}, 'issues' => function ($q) {
$q->whereIn('status', [AdminPmIssue::STATUS_OPEN, AdminPmIssue::STATUS_IN_PROGRESS]);
+ }, 'dailyLogs' => function ($q) use ($today) {
+ $q->where('log_date', $today)->with('entries');
}])
->get();
diff --git a/resources/views/project-management/index.blade.php b/resources/views/project-management/index.blade.php
index 872ba24a..45b8754e 100644
--- a/resources/views/project-management/index.blade.php
+++ b/resources/views/project-management/index.blade.php
@@ -187,6 +187,204 @@
{{ $issueStats['resolved'] ?? 0 }}
+ @php
+ $todayScrum = $project->dailyLogs->first();
+ $scrumEntries = $todayScrum?->entries ?? collect([]);
+ @endphp
+
+ 스크럼:
+
+ {{ $scrumEntries->count() }}개
+
+
+
+
+
+ @php
+ $todoEntries = $scrumEntries->where('status', 'todo');
+ $inProgressEntries = $scrumEntries->where('status', 'in_progress');
+ $doneEntries = $scrumEntries->where('status', 'done');
+ @endphp
+
+
+
+
+ 오늘의 활동
+ @if($scrumEntries->count() > 0)
+ ({{ $scrumEntries->count() }})
+ @endif
+
+
+
+ @if($todayScrum)
+
+ 더보기 →
+
+ @endif
+
+
+
+ @if($scrumEntries->count() > 0)
+
+
+
+
+
+
+ 예정
+ ({{ $todoEntries->count() }})
+
+
+ @forelse($todoEntries->groupBy('assignee_name') as $assigneeName => $groupedEntries)
+ @php
+ $entriesJson = $groupedEntries->map(fn($e) => [
+ 'id' => $e->id,
+ 'daily_log_id' => $e->daily_log_id,
+ 'content' => $e->content,
+ 'status' => $e->status
+ ])->values()->toJson();
+ @endphp
+
+
{{ $assigneeName }}
+
+ @foreach($groupedEntries as $entry)
+
+
{{ Str::limit($entry->content, 25) }}
+
+
+ @endforeach
+
+
+ @empty
+
-
+ @endforelse
+
+
+
+
+
+
+
+ 진행중
+ ({{ $inProgressEntries->count() }})
+
+
+ @forelse($inProgressEntries->groupBy('assignee_name') as $assigneeName => $groupedEntries)
+ @php
+ $entriesJson = $groupedEntries->map(fn($e) => [
+ 'id' => $e->id,
+ 'daily_log_id' => $e->daily_log_id,
+ 'content' => $e->content,
+ 'status' => $e->status
+ ])->values()->toJson();
+ @endphp
+
+
{{ $assigneeName }}
+
+ @foreach($groupedEntries as $entry)
+
+
{{ Str::limit($entry->content, 25) }}
+
+
+ @endforeach
+
+
+ @empty
+
-
+ @endforelse
+
+
+
+
+
+
+
+
완료
+
({{ $doneEntries->count() }})
+
+
+ @forelse($doneEntries->groupBy('assignee_name') as $assigneeName => $groupedEntries)
+ @php
+ $entriesJson = $groupedEntries->map(fn($e) => [
+ 'id' => $e->id,
+ 'daily_log_id' => $e->daily_log_id,
+ 'content' => $e->content,
+ 'status' => $e->status
+ ])->values()->toJson();
+ @endphp
+
+
{{ $assigneeName }}
+
+ @foreach($groupedEntries as $entry)
+
+
{{ Str::limit($entry->content, 25) }}
+
+
+ @endforeach
+
+
+ @empty
+
-
+ @endforelse
+
+
+
+ @else
+
+
+
오늘의 활동이 없습니다
+
+
+ @endif
@empty
@@ -244,6 +442,180 @@ class="p-6">
+
+
+
+
+
+
+
+
+
+
+
+ 스크럼 항목 수정
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 스크럼 항목 추가
+
+
+
+
+
+
+
+
+
+
+
+
@endsection
@push('scripts')
@@ -358,5 +730,375 @@ function renderOpenIssues(container, issues) {
container.innerHTML = html;
}
+
+ // 빠른 스크럼 모달 관련 함수
+ function openQuickScrumModal(projectId, projectName, logId) {
+ document.getElementById('quickScrumProjectId').value = projectId;
+ document.getElementById('quickScrumProjectName').textContent = projectName;
+ document.getElementById('quickScrumLogId').value = logId || '';
+ document.getElementById('quickScrumModal').classList.remove('hidden');
+
+ // 폼 초기화
+ document.getElementById('quickScrumAssignee').value = '';
+ document.getElementById('quickScrumContent').value = '';
+ document.querySelector('input[name="status"][value="todo"]').checked = true;
+
+ // 담당자 입력창에 포커스
+ setTimeout(() => {
+ document.getElementById('quickScrumAssignee').focus();
+ }, 100);
+ }
+
+ function closeQuickScrumModal() {
+ document.getElementById('quickScrumModal').classList.add('hidden');
+ }
+
+ async function submitQuickScrum(event) {
+ event.preventDefault();
+
+ const form = document.getElementById('quickScrumForm');
+ const submitBtn = document.getElementById('quickScrumSubmitBtn');
+ const projectId = document.getElementById('quickScrumProjectId').value;
+ const logId = document.getElementById('quickScrumLogId').value;
+
+ const formData = {
+ project_id: parseInt(projectId),
+ log_date: new Date().toISOString().split('T')[0],
+ assignee_name: document.getElementById('quickScrumAssignee').value,
+ content: document.getElementById('quickScrumContent').value,
+ status: document.querySelector('input[name="status"]:checked').value,
+ assignee_type: 'user'
+ };
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = '추가 중...';
+
+ try {
+ let response;
+
+ if (logId) {
+ // 기존 로그에 항목 추가
+ response = await fetch(`/api/admin/pm/daily-logs/${logId}/entries`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ },
+ body: JSON.stringify(formData)
+ });
+ } else {
+ // 새 로그 생성과 함께 항목 추가
+ response = await fetch('/api/admin/pm/daily-logs', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ },
+ body: JSON.stringify({
+ project_id: formData.project_id,
+ log_date: formData.log_date,
+ entries: [{
+ assignee_type: formData.assignee_type,
+ assignee_name: formData.assignee_name,
+ content: formData.content,
+ status: formData.status
+ }]
+ })
+ });
+ }
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ closeQuickScrumModal();
+ // 페이지 새로고침으로 데이터 반영
+ window.location.reload();
+ } else {
+ alert(data.message || '스크럼 항목 추가에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ alert('오류가 발생했습니다. 다시 시도해주세요.');
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.textContent = '추가';
+ }
+ }
+
+ // ESC 키로 모달 닫기
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ if (!document.getElementById('quickScrumModal').classList.contains('hidden')) {
+ closeQuickScrumModal();
+ }
+ if (!document.getElementById('editEntryModal').classList.contains('hidden')) {
+ closeEditEntryModal();
+ }
+ }
+ });
+
+ // 스크럼 항목 수정 모달 관련 함수 (담당자별 그룹 편집)
+ let currentEditEntries = []; // 현재 편집 중인 항목들
+ let entriesToDelete = []; // 삭제할 항목 ID 목록
+
+ function openEditEntryModal(entriesJson, assigneeName, projectId, projectName) {
+ const entries = JSON.parse(entriesJson);
+ currentEditEntries = entries;
+ entriesToDelete = [];
+
+ document.getElementById('editEntryProjectId').value = projectId;
+ document.getElementById('editEntryProjectName').textContent = projectName;
+ document.getElementById('editEntryAssignee').value = assigneeName;
+
+ // 항목 컨테이너 초기화 및 렌더링
+ renderEntryRows(entries);
+
+ document.getElementById('editEntryModal').classList.remove('hidden');
+ }
+
+ function renderEntryRows(entries) {
+ const container = document.getElementById('editEntriesContainer');
+ container.innerHTML = '';
+
+ entries.forEach((entry, index) => {
+ container.appendChild(createEntryRow(entry, index));
+ });
+ }
+
+ function createEntryRow(entry, index) {
+ const row = document.createElement('div');
+ row.className = 'entry-row flex items-start gap-2 p-2 bg-gray-50 rounded-lg';
+ row.dataset.entryId = entry.id || '';
+ row.dataset.index = index;
+
+ row.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ return row;
+ }
+
+ function setEntryStatus(btn, status) {
+ const row = btn.closest('.entry-row');
+ const statusInput = row.querySelector('input[type="hidden"]');
+ statusInput.value = status;
+
+ // 버튼 스타일 업데이트
+ row.querySelectorAll('.status-btn').forEach(b => {
+ b.classList.remove('bg-gray-200', 'bg-blue-100', 'bg-green-100');
+ b.classList.add('hover:bg-gray-100');
+ });
+ btn.classList.remove('hover:bg-gray-100');
+ if (status === 'todo') btn.classList.add('bg-gray-200');
+ else if (status === 'in_progress') btn.classList.add('bg-blue-100');
+ else if (status === 'done') btn.classList.add('bg-green-100');
+ }
+
+ function addNewEntryRow() {
+ const container = document.getElementById('editEntriesContainer');
+ const index = container.children.length;
+ container.appendChild(createEntryRow({ id: '', content: '', status: 'todo' }, index));
+
+ // 새로 추가된 행의 textarea에 포커스
+ const newRow = container.lastChild;
+ newRow.querySelector('textarea').focus();
+ }
+
+ function removeEntryRow(btn) {
+ const row = btn.closest('.entry-row');
+ const entryId = row.dataset.entryId;
+
+ // 기존 항목이면 삭제 목록에 추가
+ if (entryId) {
+ entriesToDelete.push(parseInt(entryId));
+ }
+
+ row.remove();
+
+ // 마지막 항목은 삭제 불가 (최소 1개 유지)
+ const container = document.getElementById('editEntriesContainer');
+ if (container.children.length === 0) {
+ addNewEntryRow();
+ }
+ }
+
+ function closeEditEntryModal() {
+ document.getElementById('editEntryModal').classList.add('hidden');
+ currentEditEntries = [];
+ entriesToDelete = [];
+ }
+
+ async function submitEditEntries(event) {
+ event.preventDefault();
+
+ const submitBtn = document.getElementById('editEntrySubmitBtn');
+ const assigneeName = document.getElementById('editEntryAssignee').value;
+ const container = document.getElementById('editEntriesContainer');
+ const rows = container.querySelectorAll('.entry-row');
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = '저장 중...';
+
+ try {
+ const promises = [];
+
+ // 삭제 처리
+ for (const entryId of entriesToDelete) {
+ promises.push(
+ fetch(`/api/admin/daily-logs/entries/${entryId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ }
+ })
+ );
+ }
+
+ // 수정/생성 처리
+ rows.forEach((row, index) => {
+ const entryId = row.dataset.entryId;
+ const content = row.querySelector('textarea').value.trim();
+ const status = row.querySelector('input[type="hidden"]').value;
+
+ if (!content) return; // 빈 내용은 스킵
+
+ const data = {
+ assignee_name: assigneeName,
+ assignee_type: 'user',
+ content: content,
+ status: status
+ };
+
+ if (entryId) {
+ // 기존 항목 수정
+ promises.push(
+ fetch(`/api/admin/daily-logs/entries/${entryId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ },
+ body: JSON.stringify(data)
+ })
+ );
+ } else {
+ // 새 항목 - 기존 항목의 daily_log_id를 사용
+ const existingEntry = currentEditEntries.find(e => e.id);
+ if (existingEntry && existingEntry.daily_log_id) {
+ promises.push(
+ fetch(`/api/admin/pm/daily-logs/${existingEntry.daily_log_id}/entries`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ },
+ body: JSON.stringify(data)
+ })
+ );
+ }
+ }
+ });
+
+ await Promise.all(promises);
+ closeEditEntryModal();
+ window.location.reload();
+
+ } catch (error) {
+ console.error('Error:', error);
+ alert('오류가 발생했습니다. 다시 시도해주세요.');
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.textContent = '저장';
+ }
+ }
+
+ // 인라인 상태 변경 (카드에서 바로 상태 변경)
+ async function changeEntryStatus(entryId, status) {
+ try {
+ const response = await fetch(`/api/admin/daily-logs/entries/${entryId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ },
+ body: JSON.stringify({ status })
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ window.location.reload();
+ } else {
+ alert(data.message || '상태 변경에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ alert('오류가 발생했습니다.');
+ }
+ }
+
+ // 빠른 삭제 (모달 없이 바로 삭제)
+ async function quickDeleteEntry(entryId) {
+ if (!confirm('이 항목을 삭제하시겠습니까?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/admin/daily-logs/entries/${entryId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ }
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ window.location.reload();
+ } else {
+ alert(data.message || '항목 삭제에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ alert('오류가 발생했습니다.');
+ }
+ }
@endpush
\ No newline at end of file
diff --git a/routes/api.php b/routes/api.php
index 1ec682d1..4105d12c 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -359,6 +359,7 @@
// 항목 개별 API (로그 ID 없이 직접 접근)
Route::prefix('daily-logs/entries')->name('daily-logs.entries.')->group(function () {
+ Route::put('/{entryId}', [DailyLogController::class, 'updateEntry'])->name('update');
Route::put('/{entryId}/status', [DailyLogController::class, 'updateEntryStatus'])->name('updateStatus');
Route::delete('/{entryId}', [DailyLogController::class, 'deleteEntry'])->name('delete');
});