repo->paginate($filters, $perPage)->withQueryString(); } public function get(int $id): GcBoard { return $this->repo->findOrFailForEdit($id); } public function create(array $data, ?UploadedFile $file1, ?UploadedFile $file2, int $adminId): int { // 파일은 먼저 저장(성공 시 DB 반영), 예외 시 롤백+파일 삭제 $new1 = $file1 ? $this->storeFile($file1) : null; $new2 = $file2 ? $this->storeFile($file2) : null; try { return $this->repo->transaction(function () use ($data, $adminId, $new1, $new2) { $wantPinned = !empty($data['first_sign']); $firstSign = 0; if ($wantPinned) { $max = $this->repo->maxFirstSignForUpdate(); // lock 포함 $firstSign = $max + 1; // ✅ 상단공지 순번(큰 값이 먼저) } $payload = [ 'gubun' => self::GUBUN, 'subject' => (string)($data['subject'] ?? ''), 'content' => (string)($data['content'] ?? ''), 'admin_num' => $adminId, 'regdate' => now()->format('Y-m-d H:i:s'), 'hiding' => 'N', // 기본 노출 'first_sign' => $firstSign, 'link_01' => $this->nullIfBlank($data['link_01'] ?? null), 'link_02' => $this->nullIfBlank($data['link_02'] ?? null), 'hit' => 0, ]; if ($new1) $payload['file_01'] = $new1; if ($new2) $payload['file_02'] = $new2; $row = $this->repo->create($payload); return (int) $row->getKey(); }); } catch (\Throwable $e) { // DB 실패 시 새로 저장한 파일 제거 $this->deletePhysicalFiles([$new1, $new2]); throw $e; } } public function update(int $id, array $data, ?UploadedFile $file1, ?UploadedFile $file2): void { // 새 파일 먼저 저장 $new1 = $file1 ? $this->storeFile($file1) : null; $new2 = $file2 ? $this->storeFile($file2) : null; $oldToDelete = []; try { $oldToDelete = (array) $this->repo->transaction(function () use ($id, $data, $new1, $new2) { $row = $this->repo->lockForUpdate($id); $hiding = !empty($data['hiding']) ? 'Y' : 'N'; // ✅ first_sign(상단공지) 정리된 로직 // - 체크 해제: 0 // - 체크 + 기존이 0: max+1 부여 // - 체크 + 기존이 >0: 기존 값 유지 $wantPinned = !empty($data['first_sign']); $firstSign = (int)($row->first_sign ?? 0); if ($wantPinned) { if ($firstSign <= 0) { $firstSign = $this->repo->maxFirstSignForUpdate() + 1; } } else { $firstSign = 0; } $payload = [ 'subject' => (string)($data['subject'] ?? ''), 'content' => (string)($data['content'] ?? ''), 'link_01' => $this->nullIfBlank($data['link_01'] ?? null), 'link_02' => $this->nullIfBlank($data['link_02'] ?? null), 'hiding' => $hiding, 'first_sign' => $firstSign, ]; $oldFiles = []; if ($new1) { $oldFiles[] = (string)($row->file_01 ?? ''); $payload['file_01'] = $new1; } if ($new2) { $oldFiles[] = (string)($row->file_02 ?? ''); $payload['file_02'] = $new2; } $this->repo->update($row, $payload); // 트랜잭션 밖에서 삭제할 이전 파일명 반환 return $oldFiles; }); } catch (\Throwable $e) { // DB 실패 시 새로 저장한 파일 제거 $this->deletePhysicalFiles([$new1, $new2]); throw $e; } // ✅ 커밋 이후에만 기존 파일 삭제 $this->deletePhysicalFiles($oldToDelete); } public function delete(int $id): void { $files = (array) $this->repo->transaction(function () use ($id) { $row = $this->repo->lockForUpdate($id); $files = [ (string)($row->file_01 ?? ''), (string)($row->file_02 ?? ''), ]; $this->repo->delete($row); return $files; }); $this->deletePhysicalFiles($files); } public function download(int $id, int $slot): array { $row = $this->repo->findOrFailForEdit($id); $file = $slot === 1 ? (string)($row->file_01 ?? '') : (string)($row->file_02 ?? ''); $file = trim($file); if ($file === '') return ['ok' => false, 'path' => '', 'name' => '']; $path = self::DIR.'/'.basename($file); if (!Storage::disk(self::DISK)->exists($path)) { return ['ok' => false, 'path' => '', 'name' => '']; } return ['ok' => true, 'path' => $path, 'name' => $file]; } private function storeFile(UploadedFile $file): string { $orig = $file->getClientOriginalName(); $safe = $this->safeFilename($orig); $base = pathinfo($safe, PATHINFO_FILENAME); $ext = pathinfo($safe, PATHINFO_EXTENSION); $name = $safe; // 충돌 방지 $i = 0; while (Storage::disk(self::DISK)->exists(self::DIR.'/'.$name)) { $i++; $suffix = date('YmdHis').'_'.mt_rand(1000, 9999).'_'.$i; $name = $base.'_'.$suffix.($ext ? '.'.$ext : ''); } Storage::disk(self::DISK)->putFileAs(self::DIR, $file, $name); return $name; } private function deletePhysicalFiles(array $files): void { foreach ($files as $f) { $f = trim((string)$f); if ($f === '') continue; $path = self::DIR.'/'.$f; if (Storage::disk(self::DISK)->exists($path)) { Storage::disk(self::DISK)->delete($path); } } } private function safeFilename(string $name): string { $name = trim($name); $name = preg_replace('/[^\pL\pN\.\-\_\s]/u', '', $name) ?: 'file'; $name = preg_replace('/\s+/', '_', $name); $name = preg_replace('/_+/', '_', $name); return mb_substr($name, 0, 180); } private function nullIfBlank($v): ?string { $v = trim((string)($v ?? '')); return $v === '' ? null : $v; } }