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
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\ProjectManagement;
|
|
|
|
|
|
|
|
|
|
use App\Models\Admin\AdminPmDailyLog;
|
|
|
|
|
use App\Models\Admin\AdminPmDailyLogEntry;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class DailyLogService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 목록 조회 (페이지네이션)
|
|
|
|
|
*/
|
|
|
|
|
public function getDailyLogs(int $tenantId, array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
|
|
|
{
|
|
|
|
|
$query = AdminPmDailyLog::query()
|
|
|
|
|
->tenant($tenantId)
|
|
|
|
|
->with(['project:id,name', 'creator:id,name', 'entries'])
|
|
|
|
|
->withCount('entries')
|
|
|
|
|
->withTrashed();
|
|
|
|
|
|
|
|
|
|
// 날짜 범위 필터
|
|
|
|
|
if (! empty($filters['start_date']) || ! empty($filters['end_date'])) {
|
|
|
|
|
$query->dateRange($filters['start_date'] ?? null, $filters['end_date'] ?? null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 프로젝트 필터
|
|
|
|
|
if (! empty($filters['project_id'])) {
|
|
|
|
|
$query->project($filters['project_id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 검색 필터 (요약 내용)
|
|
|
|
|
if (! empty($filters['search'])) {
|
|
|
|
|
$search = $filters['search'];
|
|
|
|
|
$query->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('summary', 'like', "%{$search}%")
|
|
|
|
|
->orWhereHas('entries', function ($eq) use ($search) {
|
|
|
|
|
$eq->where('content', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('assignee_name', 'like', "%{$search}%");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Soft Delete 필터
|
|
|
|
|
if (isset($filters['trashed'])) {
|
|
|
|
|
if ($filters['trashed'] === 'only') {
|
|
|
|
|
$query->onlyTrashed();
|
|
|
|
|
} elseif ($filters['trashed'] === 'with') {
|
|
|
|
|
$query->withTrashed();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 정렬 (기본: 날짜 내림차순)
|
|
|
|
|
$sortBy = $filters['sort_by'] ?? 'log_date';
|
|
|
|
|
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
|
|
|
|
$query->orderBy($sortBy, $sortDirection);
|
|
|
|
|
|
|
|
|
|
return $query->paginate($perPage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 일일 로그 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getDailyLogById(int $id, bool $withTrashed = false): ?AdminPmDailyLog
|
|
|
|
|
{
|
|
|
|
|
$query = AdminPmDailyLog::query()
|
|
|
|
|
->with(['project:id,name', 'creator:id,name', 'updater:id,name', 'entries']);
|
|
|
|
|
|
|
|
|
|
if ($withTrashed) {
|
|
|
|
|
$query->withTrashed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query->find($id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 날짜의 로그 조회 (또는 생성)
|
|
|
|
|
*/
|
|
|
|
|
public function getOrCreateByDate(int $tenantId, string $date, ?int $projectId = null): AdminPmDailyLog
|
|
|
|
|
{
|
|
|
|
|
$log = AdminPmDailyLog::query()
|
|
|
|
|
->tenant($tenantId)
|
|
|
|
|
->where('log_date', $date)
|
|
|
|
|
->where('project_id', $projectId)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (! $log) {
|
|
|
|
|
$log = AdminPmDailyLog::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'project_id' => $projectId,
|
|
|
|
|
'log_date' => $date,
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $log->load('entries');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 생성
|
|
|
|
|
*/
|
|
|
|
|
public function createDailyLog(int $tenantId, array $data): AdminPmDailyLog
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($tenantId, $data) {
|
|
|
|
|
$log = AdminPmDailyLog::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'project_id' => $data['project_id'] ?? null,
|
|
|
|
|
'log_date' => $data['log_date'],
|
|
|
|
|
'summary' => $data['summary'] ?? null,
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 항목이 있으면 함께 생성
|
|
|
|
|
if (! empty($data['entries'])) {
|
|
|
|
|
$this->syncEntries($log, $data['entries']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $log->load('entries');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 수정
|
|
|
|
|
*/
|
|
|
|
|
public function updateDailyLog(int $id, array $data): AdminPmDailyLog
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($id, $data) {
|
|
|
|
|
$log = AdminPmDailyLog::findOrFail($id);
|
|
|
|
|
|
|
|
|
|
$log->update([
|
2025-12-02 13:03:48 +09:00
|
|
|
'log_date' => $data['log_date'] ?? $log->log_date,
|
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
|
|
|
'project_id' => $data['project_id'] ?? $log->project_id,
|
|
|
|
|
'summary' => $data['summary'] ?? $log->summary,
|
|
|
|
|
'updated_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 항목 동기화
|
|
|
|
|
if (isset($data['entries'])) {
|
|
|
|
|
$this->syncEntries($log, $data['entries']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $log->fresh('entries');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 항목 동기화 (bulk upsert/delete)
|
|
|
|
|
*/
|
|
|
|
|
protected function syncEntries(AdminPmDailyLog $log, array $entries): void
|
|
|
|
|
{
|
|
|
|
|
$existingIds = $log->entries()->pluck('id')->toArray();
|
|
|
|
|
$incomingIds = [];
|
|
|
|
|
|
|
|
|
|
foreach ($entries as $index => $entryData) {
|
|
|
|
|
if (! empty($entryData['id'])) {
|
|
|
|
|
// 기존 항목 업데이트
|
|
|
|
|
$entry = AdminPmDailyLogEntry::find($entryData['id']);
|
|
|
|
|
if ($entry && $entry->daily_log_id === $log->id) {
|
|
|
|
|
$entry->update([
|
|
|
|
|
'assignee_type' => $entryData['assignee_type'] ?? $entry->assignee_type,
|
|
|
|
|
'assignee_id' => $entryData['assignee_id'] ?? null,
|
|
|
|
|
'assignee_name' => $entryData['assignee_name'],
|
|
|
|
|
'content' => $entryData['content'],
|
|
|
|
|
'status' => $entryData['status'] ?? $entry->status,
|
|
|
|
|
'sort_order' => $index,
|
|
|
|
|
]);
|
|
|
|
|
$incomingIds[] = $entry->id;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 새 항목 생성
|
|
|
|
|
$entry = $log->entries()->create([
|
|
|
|
|
'assignee_type' => $entryData['assignee_type'] ?? 'user',
|
|
|
|
|
'assignee_id' => $entryData['assignee_id'] ?? null,
|
|
|
|
|
'assignee_name' => $entryData['assignee_name'],
|
|
|
|
|
'content' => $entryData['content'],
|
|
|
|
|
'status' => $entryData['status'] ?? 'todo',
|
|
|
|
|
'sort_order' => $index,
|
|
|
|
|
]);
|
|
|
|
|
$incomingIds[] = $entry->id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 제거된 항목 삭제
|
|
|
|
|
$toDelete = array_diff($existingIds, $incomingIds);
|
|
|
|
|
if (! empty($toDelete)) {
|
|
|
|
|
AdminPmDailyLogEntry::whereIn('id', $toDelete)->delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 삭제 (Soft Delete)
|
|
|
|
|
*/
|
|
|
|
|
public function deleteDailyLog(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$log = AdminPmDailyLog::findOrFail($id);
|
|
|
|
|
|
|
|
|
|
$log->deleted_by = auth()->id();
|
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
|
|
return $log->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 복원
|
|
|
|
|
*/
|
|
|
|
|
public function restoreDailyLog(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$log = AdminPmDailyLog::onlyTrashed()->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
$log->deleted_by = null;
|
|
|
|
|
|
|
|
|
|
return $log->restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일일 로그 영구 삭제
|
|
|
|
|
*/
|
|
|
|
|
public function forceDeleteDailyLog(int $id): bool
|
|
|
|
|
{
|
|
|
|
|
$log = AdminPmDailyLog::withTrashed()->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
return $log->forceDelete();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 14:07:50 +09:00
|
|
|
/**
|
|
|
|
|
* 항목 수정
|
|
|
|
|
*/
|
|
|
|
|
public function updateEntry(int $entryId, array $data): AdminPmDailyLogEntry
|
|
|
|
|
{
|
|
|
|
|
$entry = AdminPmDailyLogEntry::findOrFail($entryId);
|
|
|
|
|
$entry->update($data);
|
|
|
|
|
|
|
|
|
|
return $entry->fresh();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* 항목 상태 변경
|
|
|
|
|
*/
|
|
|
|
|
public function updateEntryStatus(int $entryId, string $status): AdminPmDailyLogEntry
|
|
|
|
|
{
|
|
|
|
|
$entry = AdminPmDailyLogEntry::findOrFail($entryId);
|
|
|
|
|
$entry->update(['status' => $status]);
|
|
|
|
|
|
|
|
|
|
return $entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 항목 추가
|
|
|
|
|
*/
|
|
|
|
|
public function addEntry(int $logId, array $data): AdminPmDailyLogEntry
|
|
|
|
|
{
|
|
|
|
|
$log = AdminPmDailyLog::findOrFail($logId);
|
|
|
|
|
|
|
|
|
|
// 마지막 정렬 순서 가져오기
|
|
|
|
|
$maxOrder = $log->entries()->max('sort_order') ?? -1;
|
|
|
|
|
|
|
|
|
|
return $log->entries()->create([
|
|
|
|
|
'assignee_type' => $data['assignee_type'] ?? 'user',
|
|
|
|
|
'assignee_id' => $data['assignee_id'] ?? null,
|
|
|
|
|
'assignee_name' => $data['assignee_name'],
|
|
|
|
|
'content' => $data['content'],
|
|
|
|
|
'status' => $data['status'] ?? 'todo',
|
|
|
|
|
'sort_order' => $maxOrder + 1,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 항목 삭제
|
|
|
|
|
*/
|
|
|
|
|
public function deleteEntry(int $entryId): bool
|
|
|
|
|
{
|
|
|
|
|
return AdminPmDailyLogEntry::findOrFail($entryId)->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 항목 순서 변경
|
|
|
|
|
*/
|
|
|
|
|
public function reorderEntries(int $logId, array $entryIds): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($entryIds as $index => $entryId) {
|
|
|
|
|
AdminPmDailyLogEntry::where('id', $entryId)
|
|
|
|
|
->where('daily_log_id', $logId)
|
|
|
|
|
->update(['sort_order' => $index]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 통계 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getStats(int $tenantId, ?int $projectId = null): array
|
|
|
|
|
{
|
|
|
|
|
$query = AdminPmDailyLog::query()->tenant($tenantId);
|
|
|
|
|
|
|
|
|
|
if ($projectId) {
|
|
|
|
|
$query->project($projectId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$totalLogs = $query->count();
|
|
|
|
|
|
|
|
|
|
// 최근 7일 로그 수
|
|
|
|
|
$recentLogs = (clone $query)
|
|
|
|
|
->where('log_date', '>=', now()->subDays(7)->format('Y-m-d'))
|
|
|
|
|
->count();
|
|
|
|
|
|
|
|
|
|
// 전체 항목 통계
|
|
|
|
|
$entryStats = AdminPmDailyLogEntry::query()
|
|
|
|
|
->whereHas('dailyLog', function ($q) use ($tenantId, $projectId) {
|
|
|
|
|
$q->tenant($tenantId);
|
|
|
|
|
if ($projectId) {
|
|
|
|
|
$q->project($projectId);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
->selectRaw("
|
|
|
|
|
COUNT(*) as total,
|
|
|
|
|
SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) as todo,
|
|
|
|
|
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
|
|
|
|
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done
|
|
|
|
|
")
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'total_logs' => $totalLogs,
|
|
|
|
|
'recent_logs' => $recentLogs,
|
|
|
|
|
'entries' => [
|
|
|
|
|
'total' => (int) ($entryStats->total ?? 0),
|
|
|
|
|
'todo' => (int) ($entryStats->todo ?? 0),
|
|
|
|
|
'in_progress' => (int) ($entryStats->in_progress ?? 0),
|
|
|
|
|
'done' => (int) ($entryStats->done ?? 0),
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 주간 타임라인 데이터 조회 (최근 7일)
|
|
|
|
|
*/
|
|
|
|
|
public function getWeeklyTimeline(int $tenantId, ?int $projectId = null): array
|
|
|
|
|
{
|
|
|
|
|
$days = [];
|
|
|
|
|
$today = now();
|
|
|
|
|
|
|
|
|
|
// 최근 7일 날짜 목록 생성
|
|
|
|
|
for ($i = 6; $i >= 0; $i--) {
|
|
|
|
|
$date = $today->copy()->subDays($i);
|
|
|
|
|
$days[$date->format('Y-m-d')] = [
|
|
|
|
|
'date' => $date->format('Y-m-d'),
|
|
|
|
|
'day_name' => $date->locale('ko')->isoFormat('ddd'),
|
|
|
|
|
'day_number' => $date->format('d'),
|
|
|
|
|
'is_today' => $i === 0,
|
|
|
|
|
'is_weekend' => $date->isWeekend(),
|
|
|
|
|
'log' => null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 해당 기간의 로그 조회
|
|
|
|
|
$startDate = $today->copy()->subDays(6)->format('Y-m-d');
|
|
|
|
|
$endDate = $today->format('Y-m-d');
|
|
|
|
|
|
|
|
|
|
$query = AdminPmDailyLog::query()
|
|
|
|
|
->tenant($tenantId)
|
|
|
|
|
->whereBetween('log_date', [$startDate, $endDate])
|
|
|
|
|
->with(['entries' => function ($q) {
|
|
|
|
|
$q->select('id', 'daily_log_id', 'status');
|
|
|
|
|
}]);
|
|
|
|
|
|
|
|
|
|
if ($projectId) {
|
|
|
|
|
$query->project($projectId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$logs = $query->get();
|
|
|
|
|
|
|
|
|
|
// 로그 데이터 매핑
|
|
|
|
|
foreach ($logs as $log) {
|
|
|
|
|
$dateKey = $log->log_date->format('Y-m-d');
|
|
|
|
|
if (isset($days[$dateKey])) {
|
|
|
|
|
$entryStats = [
|
|
|
|
|
'total' => $log->entries->count(),
|
|
|
|
|
'done' => $log->entries->where('status', 'done')->count(),
|
|
|
|
|
'in_progress' => $log->entries->where('status', 'in_progress')->count(),
|
|
|
|
|
'todo' => $log->entries->where('status', 'todo')->count(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$days[$dateKey]['log'] = [
|
|
|
|
|
'id' => $log->id,
|
|
|
|
|
'summary' => $log->summary,
|
|
|
|
|
'entry_stats' => $entryStats,
|
|
|
|
|
'completion_rate' => $entryStats['total'] > 0
|
|
|
|
|
? round(($entryStats['done'] / $entryStats['total']) * 100)
|
|
|
|
|
: 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return array_values($days);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 담당자 목록 조회 (자동완성용)
|
|
|
|
|
* - 일반 사용자는 슈퍼관리자 목록을 볼 수 없음
|
|
|
|
|
*/
|
|
|
|
|
public function getAssigneeList(int $tenantId): array
|
|
|
|
|
{
|
|
|
|
|
// 사용자 목록
|
|
|
|
|
$query = User::query()
|
|
|
|
|
->whereHas('tenants', function ($q) use ($tenantId) {
|
|
|
|
|
$q->where('tenant_id', $tenantId);
|
|
|
|
|
})
|
|
|
|
|
->where('is_active', true);
|
|
|
|
|
|
|
|
|
|
// 슈퍼관리자가 아니면 슈퍼관리자 목록 제외
|
|
|
|
|
if (! auth()->user()?->is_super_admin) {
|
|
|
|
|
$query->where('is_super_admin', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$users = $query
|
|
|
|
|
->orderBy('name')
|
|
|
|
|
->get(['id', 'name'])
|
|
|
|
|
->map(fn ($u) => [
|
|
|
|
|
'type' => 'user',
|
|
|
|
|
'id' => $u->id,
|
|
|
|
|
'name' => $u->name,
|
|
|
|
|
])
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
// 팀 목록 (부서 활용)
|
|
|
|
|
$teams = DB::table('departments')
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
->orderBy('name')
|
|
|
|
|
->get(['id', 'name'])
|
|
|
|
|
->map(fn ($d) => [
|
|
|
|
|
'type' => 'team',
|
|
|
|
|
'id' => $d->id,
|
|
|
|
|
'name' => $d->name,
|
|
|
|
|
])
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'users' => $users,
|
|
|
|
|
'teams' => $teams,
|
|
|
|
|
];
|
|
|
|
|
}
|
2025-12-04 22:25:50 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 미완료 항목(예정, 진행중) 조회 - 담당자별 그룹핑, 날짜 오래된 순 정렬
|
|
|
|
|
*/
|
|
|
|
|
public function getPendingEntries(int $tenantId, ?int $projectId = null, int $limit = 100): array
|
|
|
|
|
{
|
|
|
|
|
$query = AdminPmDailyLogEntry::query()
|
|
|
|
|
->select('admin_pm_daily_log_entries.*')
|
|
|
|
|
->with(['dailyLog:id,log_date,project_id', 'dailyLog.project:id,name'])
|
|
|
|
|
->join('admin_pm_daily_logs', 'admin_pm_daily_log_entries.daily_log_id', '=', 'admin_pm_daily_logs.id')
|
|
|
|
|
->where('admin_pm_daily_logs.tenant_id', $tenantId)
|
|
|
|
|
->when($projectId, fn ($q) => $q->where('admin_pm_daily_logs.project_id', $projectId))
|
|
|
|
|
->whereIn('admin_pm_daily_log_entries.status', ['todo', 'in_progress'])
|
|
|
|
|
->orderBy('admin_pm_daily_logs.log_date', 'asc') // 날짜 오래된 순
|
|
|
|
|
->orderBy('admin_pm_daily_log_entries.id', 'asc')
|
|
|
|
|
->limit($limit);
|
|
|
|
|
|
|
|
|
|
$entries = $query->get();
|
|
|
|
|
|
|
|
|
|
// 담당자별로 그룹핑
|
|
|
|
|
$grouped = $entries->groupBy('assignee_name')->map(function ($items, $assigneeName) {
|
|
|
|
|
$todoItems = $items->where('status', 'todo');
|
|
|
|
|
$inProgressItems = $items->where('status', 'in_progress');
|
|
|
|
|
|
|
|
|
|
// 항목들을 날짜 오래된 순으로 정렬
|
|
|
|
|
$sortedEntries = $items->sortBy(fn ($e) => $e->dailyLog?->log_date)->values();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'assignee_name' => $assigneeName,
|
|
|
|
|
'total_count' => $items->count(),
|
|
|
|
|
'todo_count' => $todoItems->count(),
|
|
|
|
|
'in_progress_count' => $inProgressItems->count(),
|
|
|
|
|
'oldest_date' => $sortedEntries->first()?->dailyLog?->log_date, // 카드 정렬용
|
|
|
|
|
'entries' => $sortedEntries->map(function ($entry) {
|
|
|
|
|
return [
|
|
|
|
|
'id' => $entry->id,
|
|
|
|
|
'daily_log_id' => $entry->daily_log_id,
|
|
|
|
|
'log_date' => $entry->dailyLog?->log_date,
|
|
|
|
|
'project_name' => $entry->dailyLog?->project?->name,
|
|
|
|
|
'content' => $entry->content,
|
|
|
|
|
'status' => $entry->status,
|
|
|
|
|
];
|
|
|
|
|
})->values()->toArray(),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 담당자 카드도 가장 오래된 항목 날짜 기준으로 정렬
|
|
|
|
|
$sorted = $grouped->sortBy('oldest_date')->values()->toArray();
|
|
|
|
|
|
|
|
|
|
return $sorted;
|
|
|
|
|
}
|
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
|
|
|
}
|