운영 기능 및 GA4 대시보드 반영
This commit is contained in:
185
public/index.php
185
public/index.php
@@ -10,9 +10,11 @@ use App\Repositories\InquiryRepository;
|
||||
use App\Repositories\AdminRepository;
|
||||
use App\Auth\AdminAuth;
|
||||
use App\Repositories\SiteSettingRepository;
|
||||
use App\Services\AnalyticsService;
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$basePath = (string) config('app.base_path', '');
|
||||
$mediaExperiment = $basePath !== '';
|
||||
if ($basePath !== '') {
|
||||
if ($path !== $basePath && !str_starts_with($path, $basePath . '/')) {
|
||||
http_response_code(404);
|
||||
@@ -68,7 +70,12 @@ if ($path === '/blog' && $method === 'GET') {
|
||||
}
|
||||
|
||||
if (preg_match('#^/blog/([a-z0-9-]+)$#', $path, $matches) === 1 && $method === 'GET') {
|
||||
$post = $postRepository?->findPublishedBySlug($matches[1]);
|
||||
$requestedSlug = $matches[1];
|
||||
$slugRedirects = config('post_slug_redirects', []);
|
||||
if (is_array($slugRedirects) && isset($slugRedirects[$requestedSlug])) {
|
||||
redirect('/blog/' . $slugRedirects[$requestedSlug], 301);
|
||||
}
|
||||
$post = $postRepository?->findPublishedBySlug($requestedSlug);
|
||||
if ($post === null) {
|
||||
http_response_code(404);
|
||||
render('errors/404', ['title' => '게시글을 찾을 수 없습니다']);
|
||||
@@ -346,20 +353,48 @@ if (str_starts_with($path, '/admin')) {
|
||||
}
|
||||
|
||||
if ($path === '/admin' && $method === 'GET') {
|
||||
render('admin/dashboard', ['title'=>'대시보드','counts'=>$adminRepository->dashboardCounts()], 'layouts/admin');
|
||||
$analytics = (new AnalyticsService())->dashboard();
|
||||
render('admin/dashboard', ['title'=>'대시보드','counts'=>$adminRepository->dashboardCounts(),'analytics'=>$analytics], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
if ($path === '/admin/posts' && $method === 'GET') {
|
||||
render('admin/posts/index', ['title'=>'글 관리','posts'=>$adminRepository->posts()], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
if (preg_match('#^/admin/posts/(\d+)/preview$#', $path, $adminPreviewMatch) === 1 && $method === 'GET') {
|
||||
$post = $adminRepository->findPost((int) $adminPreviewMatch[1]);
|
||||
if (!$post) {
|
||||
http_response_code(404);
|
||||
render('errors/404', ['title'=>'게시글을 찾을 수 없습니다'], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
render('blog/show', [
|
||||
'title'=>'미리보기 · ' . $post['title'],
|
||||
'description'=>$post['excerpt'] ?: '관리자 게시글 미리보기',
|
||||
'post'=>$post,
|
||||
'preview'=>true,
|
||||
'mediaExperiment'=>$mediaExperiment,
|
||||
'noindex'=>true,
|
||||
'canonicalUrl'=>rtrim((string) config('app.base_url'), '/') . '/blog/' . $post['slug'],
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
if ($path === '/admin/posts/new' && $method === 'GET') {
|
||||
render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>[]], 'layouts/admin');
|
||||
render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>[],'mediaExperiment'=>$mediaExperiment], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
if ($path === '/admin/posts/new' && $method === 'POST') {
|
||||
verify_csrf();
|
||||
$postData = collect_post_input();
|
||||
if ($mediaExperiment) {
|
||||
$result = save_media_post_request($adminRepository, $postData, null, null);
|
||||
if ($result['errors']) {
|
||||
http_response_code(422);
|
||||
render('admin/posts/form', ['title'=>'새 글 작성','post'=>null,'errors'=>$result['errors'],'mediaExperiment'=>true], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
redirect('/admin/posts/' . $result['id'] . '/preview');
|
||||
}
|
||||
$errors = validate_post_input($postData, $adminRepository);
|
||||
[$image, $imageError] = prepare_post_image($_FILES['image'] ?? null);
|
||||
if ($imageError !== null) $errors[] = $imageError;
|
||||
@@ -390,12 +425,22 @@ if (str_starts_with($path, '/admin')) {
|
||||
exit;
|
||||
}
|
||||
if ($method === 'GET') {
|
||||
render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>[]], 'layouts/admin');
|
||||
render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>[],'mediaExperiment'=>$mediaExperiment], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
verify_csrf();
|
||||
$postData = collect_post_input();
|
||||
if ($mediaExperiment) {
|
||||
$result = save_media_post_request($adminRepository, $postData, $post, $postId);
|
||||
if ($result['errors']) {
|
||||
$post = $adminRepository->findPost($postId) ?? $post;
|
||||
http_response_code(422);
|
||||
render('admin/posts/form', ['title'=>'글 수정','post'=>$post,'errors'=>$result['errors'],'mediaExperiment'=>true], 'layouts/admin');
|
||||
exit;
|
||||
}
|
||||
redirect('/admin/posts/' . $postId . '/preview');
|
||||
}
|
||||
$errors = validate_post_input($postData, $adminRepository, $postId);
|
||||
[$image, $imageError] = prepare_post_image($_FILES['image'] ?? null);
|
||||
if ($imageError !== null) $errors[] = $imageError;
|
||||
@@ -515,8 +560,6 @@ function validate_post_input(array $data, AdminRepository $repository, ?int $exc
|
||||
{
|
||||
$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[]='본문을 입력해 주세요.';
|
||||
@@ -526,14 +569,140 @@ function validate_post_input(array $data, AdminRepository $repository, ?int $exc
|
||||
return $errors;
|
||||
}
|
||||
|
||||
function prepare_post_image(?array $file): array
|
||||
function save_media_post_request(AdminRepository $repository, array $postData, ?array $post, ?int $postId): array
|
||||
{
|
||||
$deleteIds = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['delete_images'] ?? [])))));
|
||||
$existingGallery = $post['gallery_images'] ?? [];
|
||||
$existingInline = $post['inline_images'] ?? [];
|
||||
$existingById = [];
|
||||
foreach ([...$existingGallery, ...$existingInline] as $image) {
|
||||
$existingById[(int) $image['id']] = $image;
|
||||
}
|
||||
$deleteIds = array_values(array_filter($deleteIds, static fn (int $id): bool => isset($existingById[$id])));
|
||||
|
||||
foreach ($existingInline as $image) {
|
||||
if (in_array((int) $image['id'], $deleteIds, true)) {
|
||||
$postData['body'] = preg_replace(
|
||||
'/(?:\R{2,})?\[\[image:' . preg_quote((string) $image['token'], '/') . '\]\](?:\R{2,})?/',
|
||||
"\n\n",
|
||||
$postData['body']
|
||||
) ?? $postData['body'];
|
||||
}
|
||||
}
|
||||
$postData['body'] = trim($postData['body']);
|
||||
$_POST['body'] = $postData['body'];
|
||||
|
||||
$errors = validate_post_input($postData, $repository, $postId);
|
||||
[$galleryUploads, $galleryErrors] = prepare_post_images($_FILES['gallery_images'] ?? null, '대표 이미지');
|
||||
[$inlineUploads, $inlineErrors] = prepare_post_images($_FILES['inline_images'] ?? null, '본문 이미지');
|
||||
$errors = [...$errors, ...$galleryErrors, ...$inlineErrors];
|
||||
|
||||
$newGalleryAlts = array_map(static fn ($value): string => trim((string) $value), (array) ($_POST['new_gallery_alts'] ?? []));
|
||||
$newInlineAlts = array_map(static fn ($value): string => trim((string) $value), (array) ($_POST['new_inline_alts'] ?? []));
|
||||
$submittedInlineTokens = array_map(static fn ($value): string => trim((string) $value), (array) ($_POST['new_inline_tokens'] ?? []));
|
||||
$newInlineTokens = [];
|
||||
foreach ($submittedInlineTokens as $token) {
|
||||
if (preg_match('/^[1-9]\d{0,2}$/D', $token) !== 1) {
|
||||
$errors[] = '본문 이미지 위치 정보를 다시 확인해 주세요.';
|
||||
continue;
|
||||
}
|
||||
$newInlineTokens[] = (int) $token;
|
||||
}
|
||||
if (count($newGalleryAlts) !== count($galleryUploads)) $errors[] = '선택한 대표 이미지 정보를 다시 확인해 주세요.';
|
||||
if (count($newInlineAlts) !== count($inlineUploads)) $errors[] = '선택한 본문 이미지 정보를 다시 확인해 주세요.';
|
||||
if (count($newInlineTokens) !== count($inlineUploads)) $errors[] = '선택한 본문 이미지의 위치 정보를 다시 확인해 주세요.';
|
||||
foreach ($newGalleryAlts as $alt) if ($alt === '' || mb_strlen($alt) > 255) $errors[] = '새 대표 이미지의 대체 텍스트를 255자 이내로 입력해 주세요.';
|
||||
foreach ($newInlineAlts as $alt) if ($alt === '' || mb_strlen($alt) > 255) $errors[] = '새 본문 이미지의 대체 텍스트를 255자 이내로 입력해 주세요.';
|
||||
|
||||
$imageAlts = [];
|
||||
foreach ((array) ($_POST['media_alt'] ?? []) as $imageId => $alt) {
|
||||
$imageId = (int) $imageId;
|
||||
if (!isset($existingById[$imageId]) || in_array($imageId, $deleteIds, true)) continue;
|
||||
$alt = trim((string) $alt);
|
||||
if ($alt === '' || mb_strlen($alt) > 255) $errors[] = '등록된 이미지의 대체 텍스트를 255자 이내로 입력해 주세요.';
|
||||
$imageAlts[$imageId] = $alt;
|
||||
}
|
||||
|
||||
$remainingGallery = count(array_filter($existingGallery, static fn (array $image): bool => !in_array((int) $image['id'], $deleteIds, true)));
|
||||
$remainingInlineImages = array_values(array_filter($existingInline, static fn (array $image): bool => !in_array((int) $image['id'], $deleteIds, true)));
|
||||
$remainingInline = count($remainingInlineImages);
|
||||
$totalGallery = $remainingGallery + count($galleryUploads);
|
||||
$totalInline = $remainingInline + count($inlineUploads);
|
||||
if ($totalGallery > 6) $errors[] = '대표 이미지는 최대 6장까지 등록할 수 있습니다.';
|
||||
if ($totalInline > 10) $errors[] = '본문 이미지는 최대 10장까지 등록할 수 있습니다.';
|
||||
if (($totalGallery > 1 || $totalInline > 0) && $postData['status'] !== 'draft') {
|
||||
$errors[] = '다중·본문 이미지 기능은 테스트 중이므로 초안으로만 저장할 수 있습니다.';
|
||||
}
|
||||
$activeTokens = array_map(static fn (array $image): int => (int) $image['token'], $remainingInlineImages);
|
||||
foreach ($newInlineTokens as $token) {
|
||||
if (in_array($token, $activeTokens, true)) $errors[] = '본문 이미지 위치 정보가 중복되었습니다.';
|
||||
$activeTokens[] = $token;
|
||||
}
|
||||
if (count($activeTokens) !== count(array_unique($activeTokens))) $errors[] = '본문 이미지 위치 정보가 중복되었습니다.';
|
||||
preg_match_all('/\[\[image:(\d+)\]\]/', $postData['body'], $bodyTokenMatches);
|
||||
$bodyTokens = array_map('intval', $bodyTokenMatches[1] ?? []);
|
||||
foreach ($activeTokens as $token) {
|
||||
if (count(array_keys($bodyTokens, $token, true)) !== 1) $errors[] = '각 본문 이미지는 글 안에 한 번씩 배치해 주세요.';
|
||||
}
|
||||
foreach ($bodyTokens as $token) {
|
||||
if (!in_array($token, $activeTokens, true)) $errors[] = '연결되지 않은 본문 이미지 위치가 있습니다.';
|
||||
}
|
||||
if ($errors !== []) return ['id' => $postId, 'errors' => array_values(array_unique($errors))];
|
||||
|
||||
$storedPaths = [];
|
||||
$galleryImages = [];
|
||||
$inlineImages = [];
|
||||
try {
|
||||
foreach ($galleryUploads as $index => $image) {
|
||||
$path = store_post_image($image);
|
||||
$storedPaths[] = $path;
|
||||
$galleryImages[] = ['path' => $path, 'alt' => $newGalleryAlts[$index]];
|
||||
}
|
||||
foreach ($inlineUploads as $index => $image) {
|
||||
$path = store_post_image($image);
|
||||
$storedPaths[] = $path;
|
||||
$inlineImages[] = ['path' => $path, 'alt' => $newInlineAlts[$index], 'token' => $newInlineTokens[$index]];
|
||||
}
|
||||
$result = $repository->savePostWithMedia($postData, $postId, $galleryImages, $inlineImages, $deleteIds, $imageAlts);
|
||||
foreach ($result['old_paths'] as $oldPath) remove_stored_image((string) $oldPath);
|
||||
return ['id' => $result['id'], 'errors' => []];
|
||||
} catch (Throwable) {
|
||||
foreach ($storedPaths as $storedPath) remove_stored_image($storedPath);
|
||||
return ['id' => $postId, 'errors' => ['저장하지 못했습니다. 잠시 후 다시 시도해 주세요.']];
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_post_images(?array $files, string $label): array
|
||||
{
|
||||
if ($files === null || !isset($files['name'], $files['error'], $files['size'], $files['tmp_name'])) return [[], []];
|
||||
$names = is_array($files['name']) ? $files['name'] : [$files['name']];
|
||||
$errors = is_array($files['error']) ? $files['error'] : [$files['error']];
|
||||
$sizes = is_array($files['size']) ? $files['size'] : [$files['size']];
|
||||
$temporaryNames = is_array($files['tmp_name']) ? $files['tmp_name'] : [$files['tmp_name']];
|
||||
$prepared = [];
|
||||
$messages = [];
|
||||
foreach ($names as $index => $name) {
|
||||
if (($errors[$index] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) continue;
|
||||
[$image, $error] = prepare_post_image([
|
||||
'name' => $name,
|
||||
'error' => $errors[$index] ?? UPLOAD_ERR_NO_FILE,
|
||||
'size' => $sizes[$index] ?? 0,
|
||||
'tmp_name' => $temporaryNames[$index] ?? '',
|
||||
], $label);
|
||||
if ($error !== null) $messages[] = $error;
|
||||
if ($image !== null) $prepared[] = $image;
|
||||
}
|
||||
return [$prepared, $messages];
|
||||
}
|
||||
|
||||
function prepare_post_image(?array $file, string $label = '대표 이미지'): 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 이하만 등록할 수 있습니다.'];
|
||||
if ((int) $file['size'] <= 0 || (int) $file['size'] > 8 * 1024 * 1024) return [null, $label . '는 파일당 8MB 이하만 등록할 수 있습니다.'];
|
||||
|
||||
$tmpName = (string) $file['tmp_name'];
|
||||
if (!is_uploaded_file($tmpName)) return [null, '업로드된 이미지 파일을 확인할 수 없습니다.'];
|
||||
|
||||
Reference in New Issue
Block a user