Files
sam-manage/resources/views/daily-logs/index.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

695 lines
29 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@section('title', '일일 스크럼')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">📅 일일 스크럼</h1>
<div class="flex gap-2">
<a href="{{ route('daily-logs.today') }}" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
오늘 작성
</a>
<button type="button"
onclick="openCreateModal()"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
+ 로그
</button>
</div>
</div>
<!-- 주간 타임라인 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<h2 class="text-sm font-medium text-gray-500 mb-3">최근 7</h2>
<div class="grid grid-cols-7 gap-2">
@foreach($weeklyTimeline as $index => $day)
<div class="relative group">
<button type="button"
onclick="{{ $day['log'] ? 'scrollToTableRow(' . $day['log']['id'] . ')' : 'openCreateModalWithDate(\'' . $day['date'] . '\')' }}"
data-log-id="{{ $day['log']['id'] ?? '' }}"
data-date="{{ $day['date'] }}"
class="day-card w-full text-left p-3 rounded-lg border-2 transition-all hover:shadow-md
{{ $day['is_today'] ? 'border-blue-500 bg-blue-50' : 'border-gray-200' }}
{{ $day['is_weekend'] && !$day['is_today'] ? 'bg-gray-50' : '' }}
{{ $day['log'] ? 'cursor-pointer' : 'cursor-pointer hover:border-blue-300' }}">
<!-- 요일 -->
<div class="text-xs font-medium {{ $day['is_weekend'] ? 'text-red-500' : 'text-gray-500' }} {{ $day['is_today'] ? 'text-blue-600' : '' }}">
{{ $day['day_name'] }}
</div>
<!-- 날짜 -->
<div class="text-lg font-bold {{ $day['is_today'] ? 'text-blue-600' : 'text-gray-800' }}">
{{ $day['day_number'] }}
</div>
@if($day['log'])
<!-- 로그 있음: 진행률 표시 -->
<div class="mt-2">
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div class="bg-green-500 h-1.5 rounded-full transition-all" style="width: {{ $day['log']['completion_rate'] }}%"></div>
</div>
<div class="flex justify-between items-center mt-1">
<span class="text-xs text-gray-500">{{ $day['log']['entry_stats']['total'] }}</span>
<span class="text-xs font-medium {{ $day['log']['completion_rate'] >= 100 ? 'text-green-600' : 'text-blue-600' }}">
{{ $day['log']['completion_rate'] }}%
</span>
</div>
</div>
@else
<!-- 로그 없음 -->
<div class="mt-2 text-center">
<span class="text-xs text-gray-400">미작성</span>
</div>
@endif
<!-- 오늘 표시 -->
@if($day['is_today'])
<div class="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
@endif
</button>
<!-- 툴팁 (호버 표시) -->
@if($day['log'] && $day['log']['entry_stats']['total'] > 0)
<div class="hidden group-hover:block absolute z-10 left-1/2 -translate-x-1/2 top-full mt-2 w-36 bg-gray-800 text-white text-xs rounded-lg p-2 shadow-lg pointer-events-none">
<div class="flex justify-between mb-1">
<span>예정</span>
<span>{{ $day['log']['entry_stats']['todo'] }}</span>
</div>
<div class="flex justify-between mb-1">
<span>진행중</span>
<span>{{ $day['log']['entry_stats']['in_progress'] }}</span>
</div>
<div class="flex justify-between">
<span>완료</span>
<span>{{ $day['log']['entry_stats']['done'] }}</span>
</div>
<div class="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-gray-800 rotate-45"></div>
</div>
@endif
</div>
@endforeach
</div>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<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_logs'] ?? 0 }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">최근 7</div>
<div class="text-2xl font-bold text-blue-600">{{ $stats['recent_logs'] ?? 0 }}</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['entries']['in_progress'] ?? 0 }}</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['entries']['done'] ?? 0 }}</div>
</div>
</div>
<!-- 필터 영역 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form id="filterForm" class="flex gap-4 flex-wrap">
<!-- 검색 -->
<div class="flex-1 min-w-[200px]">
<input type="text"
name="search"
placeholder="요약, 담당자, 내용으로 검색..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<!-- 프로젝트 필터 -->
<div class="w-48">
<select name="project_id" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">전체 프로젝트</option>
@foreach($projects as $project)
<option value="{{ $project->id }}">{{ $project->name }}</option>
@endforeach
</select>
</div>
<!-- 날짜 범위 -->
<div class="w-40">
<input type="date"
name="start_date"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<span class="self-center text-gray-500">~</span>
<div class="w-40">
<input type="date"
name="end_date"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<!-- 삭제된 항목 포함 -->
<div class="w-36">
<select name="trashed" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">활성만</option>
<option value="with">삭제 포함</option>
<option value="only">삭제만</option>
</select>
</div>
<!-- 검색 버튼 -->
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
검색
</button>
</form>
</div>
<!-- 테이블 영역 (HTMX로 로드) -->
<div id="log-table"
hx-get="/api/admin/daily-logs"
hx-trigger="load, filterSubmit from:body"
hx-include="#filterForm"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
class="bg-white rounded-lg shadow-sm overflow-hidden">
<!-- 로딩 스피너 -->
<div class="flex justify-center items-center p-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
</div>
<!-- 생성/수정 모달 -->
@include('daily-logs.partials.modal-form', [
'projects' => $projects,
'assigneeTypes' => $assigneeTypes,
'entryStatuses' => $entryStatuses,
'assignees' => $assignees
])
@endsection
@push('scripts')
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
// 담당자 데이터
const assignees = @json($assignees);
// 폼 제출 시 HTMX 이벤트 트리거
document.getElementById('filterForm').addEventListener('submit', function(e) {
e.preventDefault();
htmx.trigger('#log-table', 'filterSubmit');
});
// 모달 열기
function openCreateModal() {
document.getElementById('modalTitle').textContent = '새 일일 로그';
document.getElementById('logForm').reset();
document.getElementById('logId').value = '';
document.getElementById('logDate').value = new Date().toISOString().split('T')[0];
clearEntries();
document.getElementById('logModal').classList.remove('hidden');
}
// 특정 날짜로 모달 열기 (주간 타임라인에서 사용)
function openCreateModalWithDate(date) {
document.getElementById('modalTitle').textContent = '새 일일 로그';
document.getElementById('logForm').reset();
document.getElementById('logId').value = '';
document.getElementById('logDate').value = date;
clearEntries();
document.getElementById('logModal').classList.remove('hidden');
}
// 모달 닫기
function closeModal() {
document.getElementById('logModal').classList.add('hidden');
}
// 로그 수정 모달 열기
function editLog(id) {
fetch(`/api/admin/daily-logs/${id}`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById('modalTitle').textContent = '일일 로그 수정';
document.getElementById('logId').value = data.data.id;
document.getElementById('logDate').value = data.data.log_date;
document.getElementById('projectId').value = data.data.project_id || '';
document.getElementById('summary').value = data.data.summary || '';
// 항목들 로드
clearEntries();
if (data.data.entries && data.data.entries.length > 0) {
data.data.entries.forEach(entry => addEntry(entry));
}
document.getElementById('logModal').classList.remove('hidden');
}
});
}
// 항목 비우기
function clearEntries() {
document.getElementById('entriesContainer').innerHTML = '';
}
// 항목 추가
function addEntry(entry = null) {
const container = document.getElementById('entriesContainer');
const index = container.children.length;
const html = `
<div class="entry-item border rounded-lg p-4 bg-gray-50" data-index="${index}">
<div class="flex gap-4 mb-2">
<input type="hidden" name="entries[${index}][id]" value="${entry?.id || ''}">
<div class="w-24">
<select name="entries[${index}][assignee_type]" class="w-full px-2 py-1 border rounded text-sm" onchange="updateAssigneeOptions(this, ${index})">
<option value="user" ${(!entry || entry.assignee_type === 'user') ? 'selected' : ''}>개인</option>
<option value="team" ${entry?.assignee_type === 'team' ? 'selected' : ''}>팀</option>
</select>
</div>
<div class="flex-1">
<input type="text"
name="entries[${index}][assignee_name]"
value="${entry?.assignee_name || ''}"
placeholder="담당자 (직접입력 또는 선택)"
list="assigneeList${index}"
class="w-full px-2 py-1 border rounded text-sm"
required>
<datalist id="assigneeList${index}">
${getAssigneeOptions(entry?.assignee_type || 'user')}
</datalist>
<input type="hidden" name="entries[${index}][assignee_id]" value="${entry?.assignee_id || ''}">
</div>
<div class="w-24">
<select name="entries[${index}][status]" class="w-full px-2 py-1 border rounded text-sm">
<option value="todo" ${(!entry || entry.status === 'todo') ? 'selected' : ''}>예정</option>
<option value="in_progress" ${entry?.status === 'in_progress' ? 'selected' : ''}>진행중</option>
<option value="done" ${entry?.status === 'done' ? 'selected' : ''}>완료</option>
</select>
</div>
<button type="button" onclick="removeEntry(this)" class="text-red-500 hover:text-red-700">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<textarea name="entries[${index}][content]"
placeholder="업무 내용"
rows="2"
class="w-full px-2 py-1 border rounded text-sm"
required>${entry?.content || ''}</textarea>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
// 담당자 옵션 생성
function getAssigneeOptions(type) {
const list = type === 'team' ? assignees.teams : assignees.users;
return list.map(item => `<option value="${item.name}" data-id="${item.id}">`).join('');
}
// 담당자 타입 변경 시 옵션 업데이트
function updateAssigneeOptions(select, index) {
const datalist = document.getElementById(`assigneeList${index}`);
datalist.innerHTML = getAssigneeOptions(select.value);
}
// 항목 삭제
function removeEntry(btn) {
btn.closest('.entry-item').remove();
reindexEntries();
}
// 항목 인덱스 재정렬
function reindexEntries() {
const entries = document.querySelectorAll('.entry-item');
entries.forEach((entry, index) => {
entry.dataset.index = index;
entry.querySelectorAll('[name^="entries["]').forEach(input => {
input.name = input.name.replace(/entries\[\d+\]/, `entries[${index}]`);
});
const datalist = entry.querySelector('datalist');
if (datalist) {
datalist.id = `assigneeList${index}`;
}
const nameInput = entry.querySelector('input[list]');
if (nameInput) {
nameInput.setAttribute('list', `assigneeList${index}`);
}
});
}
// 폼 제출
document.getElementById('logForm').addEventListener('submit', function(e) {
e.preventDefault();
const logId = document.getElementById('logId').value;
const url = logId ? `/api/admin/daily-logs/${logId}` : '/api/admin/daily-logs';
const method = logId ? 'PUT' : 'POST';
const formData = new FormData(this);
const data = {};
// FormData를 객체로 변환
for (let [key, value] of formData.entries()) {
const match = key.match(/^entries\[(\d+)\]\[(.+)\]$/);
if (match) {
const idx = match[1];
const field = match[2];
if (!data.entries) data.entries = [];
if (!data.entries[idx]) data.entries[idx] = {};
data.entries[idx][field] = value;
} else {
data[key] = value;
}
}
// 빈 항목 제거
if (data.entries) {
data.entries = data.entries.filter(e => e && e.assignee_name && e.content);
}
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(result => {
if (result.success) {
closeModal();
htmx.trigger('#log-table', 'filterSubmit');
} else {
alert(result.message || '오류가 발생했습니다.');
}
})
.catch(err => {
console.error(err);
alert('오류가 발생했습니다.');
});
});
// 삭제 확인
function confirmDelete(id, date) {
if (confirm(`"${date}" 일일 로그를 삭제하시겠습니까?`)) {
fetch(`/api/admin/daily-logs/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(result => {
if (result.success) {
htmx.trigger('#log-table', 'filterSubmit');
}
});
}
}
// 복원 확인
function confirmRestore(id, date) {
if (confirm(`"${date}" 일일 로그를 복원하시겠습니까?`)) {
fetch(`/api/admin/daily-logs/${id}/restore`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(result => {
if (result.success) {
htmx.trigger('#log-table', 'filterSubmit');
}
});
}
}
// 영구삭제 확인
function confirmForceDelete(id, date) {
if (confirm(`"${date}" 일일 로그를 영구 삭제하시겠습니까?\n\n⚠ 이 작업은 되돌릴 수 없습니다!`)) {
fetch(`/api/admin/daily-logs/${id}/force`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(result => {
if (result.success) {
htmx.trigger('#log-table', 'filterSubmit');
}
});
}
}
// ========================================
// 주간 타임라인 → 테이블 행 스크롤 및 아코디언 열기
// ========================================
function scrollToTableRow(logId) {
// 테이블 행 찾기
const row = document.querySelector(`tr.log-row[data-log-id="${logId}"]`);
if (row) {
// 선택된 카드 하이라이트
document.querySelectorAll('.day-card').forEach(card => {
card.classList.remove('ring-2', 'ring-indigo-500');
});
const selectedCard = document.querySelector(`.day-card[data-log-id="${logId}"]`);
if (selectedCard) {
selectedCard.classList.add('ring-2', 'ring-indigo-500');
}
// 테이블 행으로 스크롤
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 스크롤 완료 후 아코디언 열기 (약간의 딜레이)
setTimeout(() => {
// 가상 이벤트 객체 생성
const fakeEvent = { target: row };
toggleTableAccordion(logId, fakeEvent);
}, 300);
} else {
// 테이블에 해당 로그가 없는 경우 (필터링 등으로 인해)
alert('해당 로그가 현재 목록에 표시되지 않습니다.\n필터를 확인해주세요.');
}
}
// ========================================
// 테이블 리스트 아코디언 기능
// ========================================
let tableOpenAccordionId = null;
// 테이블 아코디언 토글
function toggleTableAccordion(logId, event) {
// 클릭한 요소가 버튼이면 무시 (이벤트 버블링 방지)
if (event.target.closest('button') || event.target.closest('a')) {
return;
}
const row = document.querySelector(`tr.log-row[data-log-id="${logId}"]`);
const accordionRow = document.querySelector(`tr.accordion-row[data-accordion-for="${logId}"]`);
const chevron = row.querySelector('.accordion-chevron');
// 같은 행을 다시 클릭하면 닫기
if (tableOpenAccordionId === logId) {
accordionRow.classList.add('hidden');
chevron.classList.remove('rotate-90');
row.classList.remove('bg-blue-50');
tableOpenAccordionId = null;
return;
}
// 다른 열린 아코디언 닫기
if (tableOpenAccordionId !== null) {
const prevRow = document.querySelector(`tr.log-row[data-log-id="${tableOpenAccordionId}"]`);
const prevAccordion = document.querySelector(`tr.accordion-row[data-accordion-for="${tableOpenAccordionId}"]`);
if (prevRow && prevAccordion) {
prevAccordion.classList.add('hidden');
prevRow.querySelector('.accordion-chevron')?.classList.remove('rotate-90');
prevRow.classList.remove('bg-blue-50');
}
}
// 현재 아코디언 열기
accordionRow.classList.remove('hidden');
chevron.classList.add('rotate-90');
row.classList.add('bg-blue-50');
tableOpenAccordionId = logId;
// 데이터 로드
loadTableAccordionContent(logId);
}
// 테이블 아코디언 콘텐츠 로드
function loadTableAccordionContent(logId) {
const contentDiv = document.getElementById(`accordion-content-${logId}`);
contentDiv.innerHTML = '<div class="text-center py-4 text-gray-500">로딩 중...</div>';
fetch(`/api/admin/daily-logs/${logId}`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
renderTableAccordionContent(logId, data.data);
} else {
contentDiv.innerHTML = '<div class="text-center py-4 text-red-500">데이터 로드 실패</div>';
}
})
.catch(err => {
contentDiv.innerHTML = '<div class="text-center py-4 text-red-500">데이터 로드 실패</div>';
});
}
// 테이블 아코디언 콘텐츠 렌더링
function renderTableAccordionContent(logId, log) {
const contentDiv = document.getElementById(`accordion-content-${logId}`);
const statusColors = {
'todo': 'bg-gray-100 text-gray-700',
'in_progress': 'bg-yellow-100 text-yellow-700',
'done': 'bg-green-100 text-green-700'
};
const statusLabels = {
'todo': '예정',
'in_progress': '진행중',
'done': '완료'
};
let entriesHtml = '';
if (log.entries && log.entries.length > 0) {
entriesHtml = log.entries.map(entry => `
<div class="flex items-start gap-3 p-3 bg-white rounded-lg border" data-entry-id="${entry.id}">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-900">${entry.assignee_name}</span>
<span class="px-2 py-0.5 text-xs rounded-full ${statusColors[entry.status]}">${statusLabels[entry.status]}</span>
</div>
<p class="text-sm text-gray-600">${entry.content}</p>
</div>
<div class="flex items-center gap-1">
${entry.status !== 'todo' ? `
<button onclick="updateTableEntryStatus(${logId}, ${entry.id}, 'todo')" class="p-1 text-gray-400 hover:text-gray-600" title="예정">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2"/></svg>
</button>` : ''}
${entry.status !== 'in_progress' ? `
<button onclick="updateTableEntryStatus(${logId}, ${entry.id}, 'in_progress')" class="p-1 text-yellow-500 hover:text-yellow-700" title="진행중">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg>
</button>` : ''}
${entry.status !== 'done' ? `
<button onclick="updateTableEntryStatus(${logId}, ${entry.id}, 'done')" class="p-1 text-green-500 hover:text-green-700" title="완료">
<svg class="w-4 h-4" 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>` : ''}
<button onclick="deleteTableEntry(${logId}, ${entry.id})" class="p-1 text-red-400 hover:text-red-600" title="삭제">
<svg class="w-4 h-4" 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>
`).join('');
} else {
entriesHtml = '<div class="text-center py-4 text-gray-400">등록된 항목이 없습니다.</div>';
}
contentDiv.innerHTML = `
<div class="space-y-3">
${log.summary ? `<div class="text-sm text-gray-700 mb-3 pb-3 border-b">${log.summary}</div>` : ''}
<div class="space-y-2">
${entriesHtml}
</div>
<div class="pt-3 border-t flex justify-between items-center">
<button onclick="openQuickAddTableEntry(${logId})" class="text-sm text-blue-600 hover:text-blue-800">
+ 항목 추가
</button>
<button onclick="editLog(${logId})" class="text-sm text-indigo-600 hover:text-indigo-800">
전체 수정
</button>
</div>
</div>
`;
}
// 테이블 항목 상태 업데이트
function updateTableEntryStatus(logId, 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) {
loadTableAccordionContent(logId);
}
});
}
// 테이블 항목 삭제
function deleteTableEntry(logId, 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) {
loadTableAccordionContent(logId);
}
});
}
}
// 테이블 빠른 항목 추가
function openQuickAddTableEntry(logId) {
const content = prompt('업무 내용을 입력하세요:');
if (content && content.trim()) {
const assigneeName = prompt('담당자 이름을 입력하세요:');
if (assigneeName && assigneeName.trim()) {
fetch(`/api/admin/daily-logs/${logId}/entries`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
assignee_type: 'user',
assignee_name: assigneeName.trim(),
content: content.trim(),
status: 'todo'
})
})
.then(res => res.json())
.then(result => {
if (result.success) {
loadTableAccordionContent(logId);
} else {
alert(result.message || '오류가 발생했습니다.');
}
});
}
}
}
// HTMX 로드 완료 후 테이블 아코디언 상태 리셋
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'log-table') {
tableOpenAccordionId = null;
}
});
</script>
@endpush