latest(3); } render('home', [ 'title' => '사무실 통신 인프라 전문', 'description' => '네트워크, 키폰, CCTV 설계부터 시공과 유지보수까지 책임지는 (주)지티에스정보통신입니다.', 'posts' => $posts, ]); exit; } if ($path === '/blog' && $method === 'GET') { $page = max(1, (int) ($_GET['page'] ?? 1)); $category = trim((string) ($_GET['category'] ?? '')); $result = ['items' => [], 'total' => 0]; if ($postRepository !== null) { $result = $postRepository->paginate($page, 6, $category ?: null); } render('blog/index', [ 'title' => '시공 이야기와 정보', 'description' => '네트워크, 키폰, CCTV 시공 사례와 장비 선택 정보를 확인하세요.', 'posts' => $result['items'], 'total' => $result['total'], 'page' => $page, 'perPage' => 6, 'category' => $category, ]); exit; } if (preg_match('#^/blog/([a-z0-9-]+)$#', $path, $matches) === 1 && $method === 'GET') { $post = $postRepository?->findPublishedBySlug($matches[1]); if ($post === null) { http_response_code(404); render('errors/404', ['title' => '게시글을 찾을 수 없습니다']); exit; } render('blog/show', [ 'title' => $post['title'], 'description' => $post['excerpt'], 'post' => $post, ]); exit; } if ($path === '/contact' && $method === 'GET') { render('contact', [ 'title' => '1:1 비공개 상담 신청', 'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.', 'errors' => [], ]); exit; } if ($path === '/contact' && $method === 'POST') { verify_csrf(); $allowedTypes = ['네트워크 구축', '키폰시스템', 'CCTV 설치', '시공·유지보수', '기타 문의']; $data = [ 'name' => trim((string) ($_POST['name'] ?? '')), 'phone' => trim((string) ($_POST['phone'] ?? '')), 'email' => trim((string) ($_POST['email'] ?? '')), 'type' => trim((string) ($_POST['type'] ?? '')), 'title' => trim((string) ($_POST['title'] ?? '')), 'body' => trim((string) ($_POST['body'] ?? '')), 'password' => (string) ($_POST['password'] ?? ''), ]; $errors = []; if ($data['name'] === '' || mb_strlen($data['name']) > 120) $errors[] = '이름 또는 회사명을 확인해 주세요.'; if (!preg_match('/^(?:02-\d{3,4}-\d{4}|0\d{2}-\d{3,4}-\d{4}|050\d-\d{4}-\d{4}|1\d{3}-\d{4})$/D', $data['phone'])) $errors[] = '연락처 형식을 확인해 주세요.'; if ($data['email'] !== '' && filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) $errors[] = '이메일 형식을 확인해 주세요.'; if (!in_array($data['type'], $allowedTypes, true)) $errors[] = '문의 유형을 선택해 주세요.'; if ($data['title'] === '' || mb_strlen($data['title']) > 200) $errors[] = '제목을 200자 이내로 입력해 주세요.'; if ($data['body'] === '' || mb_strlen($data['body']) > 5000) $errors[] = '문의 내용을 5,000자 이내로 입력해 주세요.'; if (!preg_match('/^[0-9]{4}$/D', $data['password'])) $errors[] = '조회 PIN은 숫자 4자리로 입력해 주세요.'; if (($_POST['consent'] ?? '') !== '1') $errors[] = '개인정보 수집·이용 동의가 필요합니다.'; $lastSubmit = (int) ($_SESSION['last_inquiry_submit'] ?? 0); if (time() - $lastSubmit < 20) $errors[] = '중복 접수를 방지하기 위해 잠시 후 다시 시도해 주세요.'; if ($errors) { http_response_code(422); render('contact', [ 'title' => '1:1 비공개 상담 신청', 'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.', 'errors' => $errors, ]); exit; } try { $ticket = (new InquiryRepository())->create($data); $_SESSION['last_inquiry_submit'] = time(); $_SESSION['submitted_ticket'] = $ticket; redirect('/contact/success'); } catch (Throwable) { http_response_code(503); render('contact', [ 'title' => '1:1 비공개 상담 신청', 'description' => '네트워크, 키폰, CCTV 구축과 유지보수 상담을 신청하세요.', 'errors' => ['현재 온라인 접수가 원활하지 않습니다. 대표번호로 문의해 주세요.'], ]); exit; } } if ($path === '/contact/success' && $method === 'GET') { $ticket = $_SESSION['submitted_ticket'] ?? null; if (!is_string($ticket) || $ticket === '') redirect('/contact'); unset($_SESSION['submitted_ticket']); render('inquiries/success', ['title' => '문의 접수 완료', 'ticket' => $ticket]); exit; } if ($path === '/inquiry/lookup' && $method === 'GET') { render('inquiries/lookup', [ 'title' => '문의 접수 확인', 'description' => '접수번호와 숫자 4자리 조회 PIN으로 상담 처리 상태를 확인하세요.', 'ticket' => trim((string) ($_GET['ticket'] ?? '')), 'error' => '', ]); exit; } if ($path === '/inquiry/lookup' && $method === 'POST') { verify_csrf(); $window = $_SESSION['lookup_rate'] ?? ['started' => time(), 'attempts' => 0]; if (time() - (int) $window['started'] > 900) $window = ['started' => time(), 'attempts' => 0]; if ((int) $window['attempts'] >= 5) { http_response_code(429); render('inquiries/lookup', ['title'=>'문의 접수 확인','ticket'=>'','error'=>'잠시 후 다시 시도해 주세요.']); exit; } $ticket = strtoupper(trim((string) ($_POST['ticket'] ?? ''))); $password = (string) ($_POST['password'] ?? ''); $inquiry = null; $inquiryRepository = null; try { $inquiryRepository = new InquiryRepository(); $inquiry = $inquiryRepository->findForLookup($ticket); } catch (Throwable) {} if ($inquiry !== null && !empty($inquiry['lookup_locked_until'])) { if (strtotime((string) $inquiry['lookup_locked_until']) > time()) { http_response_code(429); render('inquiries/lookup', ['title'=>'문의 접수 확인','ticket'=>$ticket,'error'=>'조회 시도가 제한되었습니다. 15분 후 다시 시도해 주세요.']); exit; } try { $inquiryRepository?->resetLookupLimit((int) $inquiry['id']); } catch (Throwable) {} } $validPin = preg_match('/^[0-9]{4}$/D', $password) === 1; if ($inquiry === null || !$validPin || !password_verify($password, $inquiry['password_hash'])) { $window['attempts']++; $_SESSION['lookup_rate'] = $window; $locked = false; if ($inquiry !== null && $inquiryRepository instanceof InquiryRepository) { try { $locked = $inquiryRepository->recordLookupFailure((int) $inquiry['id']); } catch (Throwable) {} } http_response_code($locked ? 429 : 422); render('inquiries/lookup', [ 'title'=>'문의 접수 확인', 'ticket'=>$ticket, 'error'=>$locked ? '조회 시도가 제한되었습니다. 15분 후 다시 시도해 주세요.' : '접수번호 또는 조회 PIN이 올바르지 않습니다.', ]); exit; } try { $inquiryRepository?->resetLookupLimit((int) $inquiry['id']); } catch (Throwable) {} unset($_SESSION['lookup_rate'], $inquiry['id'], $inquiry['password_hash'], $inquiry['lookup_failed_attempts'], $inquiry['lookup_locked_until']); render('inquiries/show', ['title'=>'문의 처리 상태','description'=>'비공개 상담 처리 상태입니다.','inquiry'=>$inquiry]); exit; } if ($path === '/privacy' && $method === 'GET') { render('privacy', [ 'title' => '개인정보처리방침', 'description' => '(주)지티에스정보통신 개인정보처리방침입니다.', ]); exit; } if ($path === '/admin/setup' && $method === 'GET') { try { $adminRepository = new AdminRepository(); if ($adminRepository->adminCount() > 0) redirect('/admin/login'); } catch (Throwable) { http_response_code(503); render('errors/404', ['title'=>'관리자 등록을 준비할 수 없습니다'], 'layouts/admin'); exit; } $token = (string) ($_GET['token'] ?? ''); $tokenFile = (string) (getenv('GTSIT_ADMIN_SETUP_TOKEN_FILE') ?: ''); if ($token !== '' && $tokenFile !== '' && is_readable($tokenFile) && time() - (int) filemtime($tokenFile) <= 86400) { $storedHash = trim((string) file_get_contents($tokenFile)); if ($storedHash !== '' && hash_equals($storedHash, hash('sha256', $token))) { session_regenerate_id(true); $_SESSION['admin_setup_verified'] = time(); redirect('/admin/setup'); } } if (time() - (int) ($_SESSION['admin_setup_verified'] ?? 0) > 900) { unset($_SESSION['admin_setup_verified']); http_response_code(404); render('errors/404', ['title'=>'페이지를 찾을 수 없습니다'], 'layouts/admin'); exit; } render('admin/setup', [ 'title'=>'관리자 계정 등록', 'username'=>(string) (getenv('GTSIT_INITIAL_ADMIN_USERNAME') ?: 'gts0201'), 'displayName'=>'관리자', 'errors'=>[], ], 'layouts/admin'); exit; } if ($path === '/admin/setup' && $method === 'POST') { verify_csrf(); if (time() - (int) ($_SESSION['admin_setup_verified'] ?? 0) > 900) { unset($_SESSION['admin_setup_verified']); http_response_code(404); render('errors/404', ['title'=>'페이지를 찾을 수 없습니다'], 'layouts/admin'); exit; } $username = (string) (getenv('GTSIT_INITIAL_ADMIN_USERNAME') ?: 'gts0201'); $displayName = trim((string) ($_POST['display_name'] ?? '')); $password = (string) ($_POST['password'] ?? ''); $passwordConfirmation = (string) ($_POST['password_confirmation'] ?? ''); $errors = []; if ($displayName === '' || mb_strlen($displayName) > 100) $errors[] = '표시 이름을 100자 이내로 입력해 주세요.'; if (strlen($password) < 12 || strlen($password) > 72 || !preg_match('/[A-Za-z]/', $password) || !preg_match('/[0-9]/', $password)) { $errors[] = '비밀번호는 영문과 숫자를 포함해 12자 이상 입력해 주세요.'; } if (!hash_equals($password, $passwordConfirmation)) $errors[] = '비밀번호 확인이 일치하지 않습니다.'; if ($errors) { http_response_code(422); render('admin/setup', compact('username', 'displayName', 'errors') + ['title'=>'관리자 계정 등록'], 'layouts/admin'); exit; } try { $adminRepository = new AdminRepository(); if ($adminRepository->adminCount() > 0) redirect('/admin/login'); $adminRepository->createAdmin($username, $displayName, $password); $tokenFile = (string) (getenv('GTSIT_ADMIN_SETUP_TOKEN_FILE') ?: ''); if ($tokenFile !== '' && is_file($tokenFile)) unlink($tokenFile); unset($_SESSION['admin_setup_verified']); $_SESSION['admin_created'] = true; redirect('/admin/login'); } catch (Throwable) { http_response_code(503); render('admin/setup', [ 'title'=>'관리자 계정 등록','username'=>$username,'displayName'=>$displayName, 'errors'=>['계정을 생성하지 못했습니다. 잠시 후 다시 시도해 주세요.'], ], 'layouts/admin'); exit; } } if ($path === '/admin/login' && $method === 'GET') { if (AdminAuth::check()) redirect('/admin'); $notice = !empty($_SESSION['admin_created']) ? '관리자 계정이 생성되었습니다. 설정한 비밀번호로 로그인해 주세요.' : ''; unset($_SESSION['admin_created']); render('admin/login', ['title'=>'관리자 로그인','error'=>'','notice'=>$notice,'username'=>'gts0201'], 'layouts/admin'); exit; } if ($path === '/admin/login' && $method === 'POST') { verify_csrf(); $username = trim((string) ($_POST['username'] ?? '')); $password = (string) ($_POST['password'] ?? ''); $loginRate = $_SESSION['admin_login_rate'] ?? ['started'=>time(),'attempts'=>0]; if (time() - (int)$loginRate['started'] > 900) $loginRate = ['started'=>time(),'attempts'=>0]; if ((int)$loginRate['attempts'] >= 10) { http_response_code(429); render('admin/login', ['title'=>'관리자 로그인','error'=>'로그인 시도가 많습니다. 잠시 후 다시 시도해 주세요.','notice'=>'','username'=>$username], 'layouts/admin'); exit; } $authenticated = false; try { $authenticated = (new AdminAuth())->attempt($username, $password); } catch (Throwable) {} if ($authenticated) { unset($_SESSION['admin_login_rate']); redirect('/admin'); } $loginRate['attempts']++; $_SESSION['admin_login_rate'] = $loginRate; http_response_code(422); render('admin/login', ['title'=>'관리자 로그인','error'=>'아이디 또는 비밀번호를 확인해 주세요.','notice'=>'','username'=>$username], 'layouts/admin'); exit; } if ($path === '/admin/logout' && $method === 'POST') { verify_csrf(); AdminAuth::logout(); redirect('/admin/login'); } if (str_starts_with($path, '/admin')) { AdminAuth::requireLogin(); try { $adminRepository = new AdminRepository(); } catch (Throwable) { http_response_code(503); render('errors/404', ['title'=>'관리 데이터를 불러올 수 없습니다'], 'layouts/admin'); exit; } if ($path === '/admin' && $method === 'GET') { render('admin/dashboard', ['title'=>'대시보드','counts'=>$adminRepository->dashboardCounts()], 'layouts/admin'); exit; } if ($path === '/admin/posts' && $method === 'GET') { render('admin/posts/index', ['title'=>'글 관리','posts'=>$adminRepository->posts()], 'layouts/admin'); exit; } if ($path === '/admin/posts/new' && $method === 'GET') { render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>[]], 'layouts/admin'); exit; } if ($path === '/admin/posts/new' && $method === 'POST') { verify_csrf(); $postData = collect_post_input(); $errors = validate_post_input($postData, $adminRepository); [$image, $imageError] = prepare_post_image($_FILES['image'] ?? null); if ($imageError !== null) $errors[] = $imageError; if ($image !== null && $postData['image_alt'] === '') $errors[] = '대표 이미지의 대체 텍스트를 입력해 주세요.'; if ($errors) { http_response_code(422); render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>$errors], 'layouts/admin'); exit; } $newImagePath = null; try { if ($image !== null) $newImagePath = store_post_image($image); $adminRepository->savePostWithImage($postData, null, $newImagePath, $postData['image_alt']); } catch (Throwable) { if ($newImagePath !== null) remove_stored_image($newImagePath); http_response_code(503); render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>['저장하지 못했습니다. 잠시 후 다시 시도해 주세요.']], 'layouts/admin'); exit; } redirect('/admin/posts'); } if (preg_match('#^/admin/posts/(\d+)/edit$#', $path, $adminPostMatch) === 1) { $postId = (int) $adminPostMatch[1]; $post = $adminRepository->findPost($postId); if (!$post) { http_response_code(404); render('errors/404', ['title'=>'게시글을 찾을 수 없습니다'], 'layouts/admin'); exit; } if ($method === 'GET') { render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>[]], 'layouts/admin'); exit; } if ($method === 'POST') { verify_csrf(); $postData = collect_post_input(); $errors = validate_post_input($postData, $adminRepository, $postId); [$image, $imageError] = prepare_post_image($_FILES['image'] ?? null); if ($imageError !== null) $errors[] = $imageError; if (($image !== null || !empty($post['image_path'])) && $postData['image_alt'] === '') $errors[] = '대표 이미지의 대체 텍스트를 입력해 주세요.'; if ($errors) { http_response_code(422); render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>$errors], 'layouts/admin'); exit; } $newImagePath = null; try { if ($image !== null) $newImagePath = store_post_image($image); $result = $adminRepository->savePostWithImage($postData, $postId, $newImagePath, $postData['image_alt']); foreach ($result['old_paths'] as $oldPath) remove_stored_image((string) $oldPath); } catch (Throwable) { if ($newImagePath !== null) remove_stored_image($newImagePath); http_response_code(503); render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>['저장하지 못했습니다. 잠시 후 다시 시도해 주세요.']], 'layouts/admin'); exit; } redirect('/admin/posts'); } } if (preg_match('#^/admin/posts/(\d+)/delete$#', $path, $adminDeleteMatch) === 1 && $method === 'POST') { verify_csrf(); $adminRepository->deletePost((int) $adminDeleteMatch[1]); redirect('/admin/posts'); } if ($path === '/admin/inquiries' && $method === 'GET') { render('admin/inquiries/index', ['title'=>'문의 관리','inquiries'=>$adminRepository->inquiries()], 'layouts/admin'); exit; } if (preg_match('#^/admin/inquiries/(\d+)$#', $path, $adminInquiryMatch) === 1) { $inquiryId = (int) $adminInquiryMatch[1]; $inquiry = $adminRepository->findInquiry($inquiryId); if (!$inquiry) { http_response_code(404); render('errors/404', ['title'=>'문의를 찾을 수 없습니다'], 'layouts/admin'); exit; } if ($method === 'GET') { render('admin/inquiries/show', ['title'=>'문의 상세','inquiry'=>$inquiry], 'layouts/admin'); exit; } if ($method === 'POST') { verify_csrf(); $allowedStatuses = ['pending','in_progress','replied','closed']; $status = (string) ($_POST['status'] ?? 'pending'); $reply = trim((string) ($_POST['reply'] ?? '')); if (!in_array($status, $allowedStatuses, true) || mb_strlen($reply) > 5000) { http_response_code(422); render('admin/inquiries/show', ['title'=>'문의 상세','inquiry'=>$inquiry], 'layouts/admin'); exit; } if ($reply !== '' && $status === 'pending') $status = 'replied'; $adminRepository->updateInquiry($inquiryId, $status, $reply); redirect('/admin/inquiries/' . $inquiryId); } } if ($path === '/admin/company') { $defaults = [ 'legal_name'=>(string)config('app.legal_name'),'ceo'=>'서선배', 'phone'=>(string)config('app.contact.phone'),'phone_sub'=>(string)config('app.contact.phone_sub'), 'fax'=>(string)config('app.contact.fax'),'email'=>(string)config('app.contact.email'), 'address'=>(string)config('app.contact.address'),'bizno'=>'206-86-70582', 'business_hours'=>(string)config('app.contact.business_hours'), ]; $settingRepository = new SiteSettingRepository(); $settings = array_merge($defaults, $settingRepository->all()); if ($method === 'GET') { render('admin/company', ['title'=>'회사 정보','settings'=>$settings,'errors'=>[]], 'layouts/admin'); exit; } if ($method === 'POST') { verify_csrf(); $submitted = []; foreach (array_keys($defaults) as $key) $submitted[$key] = trim((string)($_POST[$key]??'')); $errors = []; if ($submitted['legal_name']==='' || mb_strlen($submitted['legal_name'])>120) $errors[]='법인명을 확인해 주세요.'; if (!preg_match('/^[0-9+() -]{8,30}$/',$submitted['phone'])) $errors[]='대표번호를 확인해 주세요.'; if ($submitted['email']!=='' && filter_var($submitted['email'],FILTER_VALIDATE_EMAIL)===false) $errors[]='이메일 형식을 확인해 주세요.'; if (mb_strlen($submitted['address'])>255) $errors[]='주소를 255자 이내로 입력해 주세요.'; if ($errors) { http_response_code(422); render('admin/company', ['title'=>'회사 정보','settings'=>$settings,'errors'=>$errors], 'layouts/admin'); exit; } $settingRepository->save($submitted); redirect('/admin/company'); } } http_response_code(404); render('errors/404', ['title'=>'관리자 페이지를 찾을 수 없습니다'], 'layouts/admin'); exit; } http_response_code(404); render('errors/404', ['title' => '페이지를 찾을 수 없습니다']); function collect_post_input(): array { return [ 'title'=>trim((string)($_POST['title']??'')), 'slug'=>trim((string)($_POST['slug']??'')), 'category'=>trim((string)($_POST['category']??'')), 'excerpt'=>trim((string)($_POST['excerpt']??'')), 'body'=>trim((string)($_POST['body']??'')), 'author'=>trim((string)($_POST['author']??'')), 'status'=>(string)($_POST['status']??'draft'), 'published_at'=>'', 'image_alt'=>trim((string)($_POST['image_alt']??'')), ]; } function validate_post_input(array $data, AdminRepository $repository, ?int $exceptId = null): array { $errors = []; if ($data['title']==='' || mb_strlen($data['title'])>200) $errors[]='제목을 200자 이내로 입력해 주세요.'; if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $data['slug'])) $errors[]='slug는 영문 소문자, 숫자, 하이픈으로 입력해 주세요.'; elseif ($repository->slugExists($data['slug'], $exceptId)) $errors[]='이미 사용 중인 slug입니다.'; if (!in_array($data['category'], ['네트워크','키폰시스템','CCTV','시공사례'], true)) $errors[]='카테고리를 확인해 주세요.'; if (mb_strlen($data['excerpt'])>500) $errors[]='목록 요약을 500자 이내로 입력해 주세요.'; if ($data['body']==='') $errors[]='본문을 입력해 주세요.'; if ($data['author']==='' || mb_strlen($data['author'])>100) $errors[]='작성자를 확인해 주세요.'; if (!in_array($data['status'], ['draft','published'], true)) $errors[]='공개 상태를 확인해 주세요.'; if (mb_strlen($data['image_alt'])>255) $errors[]='이미지 대체 텍스트를 255자 이내로 입력해 주세요.'; return $errors; } function prepare_post_image(?array $file): array { if ($file === null || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return [null, null]; if (!isset($file['error'], $file['size'], $file['tmp_name']) || is_array($file['error']) || is_array($file['size']) || is_array($file['tmp_name'])) { return [null, '이미지 업로드 요청이 올바르지 않습니다.']; } if ((int) $file['error'] !== UPLOAD_ERR_OK) return [null, '이미지를 업로드하지 못했습니다. 파일 크기를 확인해 주세요.']; if ((int) $file['size'] <= 0 || (int) $file['size'] > 8 * 1024 * 1024) return [null, '대표 이미지는 8MB 이하만 등록할 수 있습니다.']; $tmpName = (string) $file['tmp_name']; if (!is_uploaded_file($tmpName)) return [null, '업로드된 이미지 파일을 확인할 수 없습니다.']; $mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmpName); $extensions = ['image/jpeg'=>'jpg', 'image/png'=>'png', 'image/webp'=>'webp']; if (!is_string($mime) || !isset($extensions[$mime])) return [null, 'JPG, PNG, WebP 이미지만 등록할 수 있습니다.']; $dimensions = getimagesize($tmpName); if ($dimensions === false || $dimensions[0] < 1 || $dimensions[1] < 1 || $dimensions[0] > 6000 || $dimensions[1] > 6000) { return [null, '이미지 크기는 최대 6000×6000px까지 등록할 수 있습니다.']; } if (($dimensions['mime'] ?? '') !== $mime) return [null, '이미지 파일의 형식이 올바르지 않습니다.']; return [['tmp_name'=>$tmpName, 'extension'=>$extensions[$mime]], null]; } function store_post_image(array $image): string { $relativeDirectory = '/uploads/posts/' . date('Y/m'); $publicRoot = rtrim((string) config('app.public_root', __DIR__), '/'); $absoluteDirectory = $publicRoot . $relativeDirectory; if (!is_dir($absoluteDirectory) && !mkdir($absoluteDirectory, 0750, true) && !is_dir($absoluteDirectory)) { throw new RuntimeException('Upload directory could not be created.'); } $relativePath = $relativeDirectory . '/' . bin2hex(random_bytes(20)) . '.' . $image['extension']; if (!move_uploaded_file($image['tmp_name'], $publicRoot . $relativePath)) { throw new RuntimeException('Uploaded file could not be moved.'); } chmod($publicRoot . $relativePath, 0640); return $relativePath; } function remove_stored_image(string $path): void { if (!str_starts_with($path, '/uploads/posts/')) return; $absolutePath = rtrim((string) config('app.public_root', __DIR__), '/') . $path; if (is_file($absolutePath)) unlink($absolutePath); }