giftcon_dev/app/Services/Admin/Notice/AdminNoticeService.php
2026-02-09 19:47:58 +09:00

225 lines
7.3 KiB
PHP

<?php
namespace App\Services\Admin\Notice;
use App\Models\GcBoard;
use App\Repositories\Admin\Notice\AdminNoticeRepository;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
final class AdminNoticeService
{
private const GUBUN = 'notice';
private const DISK = 'public';
private const DIR = 'bbs';
public function __construct(
private readonly AdminNoticeRepository $repo,
) {}
public function paginate(array $filters, int $perPage = 15)
{
// paginator 링크 쿼리스트링 유지
return $this->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;
}
}