운영 기능 및 GA4 대시보드 반영

This commit is contained in:
박성필
2026-08-18 10:53:58 +09:00
parent 612e87847c
commit b492abd3b4
24 changed files with 1521 additions and 77 deletions

View File

@@ -64,13 +64,40 @@ final class AdminRepository
{
$statement = $this->db->prepare(
'SELECT posts.*,
(SELECT file_path FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_alt
(SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE \'@gtsit:inline:%\' ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE \'@gtsit:inline:%\' ORDER BY sort_order, id LIMIT 1) AS image_alt
FROM posts WHERE posts.id = :id AND posts.deleted_at IS NULL LIMIT 1'
);
$statement->execute(['id' => $id]);
$post = $statement->fetch();
return $post ?: null;
if (!$post) {
return null;
}
$media = $this->postMedia($id);
$post['gallery_images'] = $media['gallery'];
$post['inline_images'] = $media['inline'];
return $post;
}
public function postMedia(int $postId): array
{
$statement = $this->db->prepare(
'SELECT id, file_path, alt_text, caption, sort_order
FROM post_images WHERE post_id = :post_id ORDER BY sort_order, id'
);
$statement->execute(['post_id' => $postId]);
$gallery = [];
$inline = [];
foreach ($statement->fetchAll() as $image) {
if (preg_match('/^@gtsit:inline:(\d+)$/', (string) $image['caption'], $matches) === 1) {
$image['token'] = (int) $matches[1];
$inline[] = $image;
continue;
}
$gallery[] = $image;
}
return ['gallery' => $gallery, 'inline' => $inline];
}
public function savePost(array $data, ?int $id = null): int
@@ -80,16 +107,20 @@ final class AdminRepository
: null;
if ($id === null) {
$temporarySlug = 'pending-' . bin2hex(random_bytes(12));
$statement = $this->db->prepare(
'INSERT INTO posts (title, slug, category, excerpt, body, author, status, published_at)
VALUES (:title, :slug, :category, :excerpt, :body, :author, :status, :published_at)'
);
$statement->execute([
'title'=>$data['title'],'slug'=>$data['slug'],'category'=>$data['category'],
'title'=>$data['title'],'slug'=>$temporarySlug,'category'=>$data['category'],
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
'status'=>$data['status'],'published_at'=>$publishedAt,
]);
return (int) $this->db->lastInsertId();
$postId = (int) $this->db->lastInsertId();
$updateSlug = $this->db->prepare('UPDATE posts SET slug = :slug WHERE id = :id');
$updateSlug->execute(['slug' => 'post-' . $postId, 'id' => $postId]);
return $postId;
}
$statement = $this->db->prepare(
@@ -97,7 +128,7 @@ final class AdminRepository
body=:body, author=:author, status=:status, published_at=:published_at WHERE id=:id AND deleted_at IS NULL'
);
$statement->execute([
'title'=>$data['title'],'slug'=>$data['slug'],'category'=>$data['category'],
'title'=>$data['title'],'slug'=>'post-' . $id,'category'=>$data['category'],
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
'status'=>$data['status'],'published_at'=>$publishedAt,'id'=>$id,
]);
@@ -138,17 +169,84 @@ final class AdminRepository
}
}
public function slugExists(string $slug, ?int $exceptId = null): bool
{
$sql = 'SELECT COUNT(*) FROM posts WHERE slug = :slug AND deleted_at IS NULL';
$params = ['slug' => $slug];
if ($exceptId !== null) {
$sql .= ' AND id <> :id';
$params['id'] = $exceptId;
public function savePostWithMedia(
array $data,
?int $id,
array $galleryImages,
array $inlineImages,
array $deleteImageIds,
array $imageAlts
): array {
$oldPaths = [];
$this->db->beginTransaction();
try {
$postId = $this->savePost($data, $id);
$existing = $this->db->prepare(
'SELECT id, file_path, caption FROM post_images WHERE post_id = :post_id FOR UPDATE'
);
$existing->execute(['post_id' => $postId]);
$existingImages = $existing->fetchAll();
$existingIds = array_map('intval', array_column($existingImages, 'id'));
foreach ($imageAlts as $imageId => $altText) {
$imageId = (int) $imageId;
if (!in_array($imageId, $existingIds, true)) continue;
$update = $this->db->prepare(
'UPDATE post_images SET alt_text = :alt_text WHERE id = :id AND post_id = :post_id'
);
$update->execute(['alt_text' => $altText, 'id' => $imageId, 'post_id' => $postId]);
}
$deleteIds = array_values(array_intersect($existingIds, array_map('intval', $deleteImageIds)));
if ($deleteIds !== []) {
foreach ($existingImages as $existingImage) {
if (in_array((int) $existingImage['id'], $deleteIds, true)) {
$oldPaths[] = (string) $existingImage['file_path'];
}
}
$placeholders = implode(',', array_fill(0, count($deleteIds), '?'));
$delete = $this->db->prepare("DELETE FROM post_images WHERE post_id = ? AND id IN ({$placeholders})");
$delete->execute([$postId, ...$deleteIds]);
}
$galleryOrder = 0;
foreach ($existingImages as $existingImage) {
if (!in_array((int) $existingImage['id'], $deleteIds, true)
&& !str_starts_with((string) $existingImage['caption'], '@gtsit:inline:')) {
$galleryOrder++;
}
}
$insert = $this->db->prepare(
'INSERT INTO post_images (post_id, file_path, alt_text, caption, sort_order)
VALUES (:post_id, :file_path, :alt_text, :caption, :sort_order)'
);
foreach ($galleryImages as $image) {
$insert->execute([
'post_id' => $postId,
'file_path' => $image['path'],
'alt_text' => $image['alt'],
'caption' => '@gtsit:gallery',
'sort_order' => $galleryOrder++,
]);
}
foreach ($inlineImages as $image) {
$insert->execute([
'post_id' => $postId,
'file_path' => $image['path'],
'alt_text' => $image['alt'],
'caption' => '@gtsit:inline:' . $image['token'],
'sort_order' => 1000 + (int) $image['token'],
]);
}
$this->db->commit();
return ['id' => $postId, 'old_paths' => $oldPaths];
} catch (\Throwable $exception) {
if ($this->db->inTransaction()) {
$this->db->rollBack();
}
throw $exception;
}
$statement = $this->db->prepare($sql);
$statement->execute($params);
return (int) $statement->fetchColumn() > 0;
}
public function deletePost(int $id): void

View File

@@ -20,8 +20,8 @@ final class PostRepository
{
$statement = $this->db->prepare(
"SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.published_at,
(SELECT file_path FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_alt
(SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt
FROM posts
WHERE posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW()
ORDER BY published_at DESC, id DESC
@@ -48,8 +48,8 @@ final class PostRepository
$offset = max(0, ($page - 1) * $perPage);
$query = $this->db->prepare(
"SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.published_at,
(SELECT file_path FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_alt
(SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt
FROM posts WHERE {$where}
ORDER BY published_at DESC, id DESC
LIMIT :limit OFFSET :offset"
@@ -68,14 +68,34 @@ final class PostRepository
{
$statement = $this->db->prepare(
"SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.body, posts.author, posts.published_at,
(SELECT file_path FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id ORDER BY sort_order, id LIMIT 1) AS image_alt
(SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path,
(SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt
FROM posts
WHERE posts.slug = :slug AND posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW()
LIMIT 1"
);
$statement->execute(['slug' => $slug]);
$post = $statement->fetch();
return $post ?: null;
return $post ? $this->withMedia($post) : null;
}
public function withMedia(array $post): array
{
$statement = $this->db->prepare(
'SELECT id, file_path, alt_text, caption, sort_order
FROM post_images WHERE post_id = :post_id ORDER BY sort_order, id'
);
$statement->execute(['post_id' => $post['id']]);
$post['gallery_images'] = [];
$post['inline_images'] = [];
foreach ($statement->fetchAll() as $image) {
if (preg_match('/^@gtsit:inline:(\d+)$/', (string) $image['caption'], $matches) === 1) {
$image['token'] = (int) $matches[1];
$post['inline_images'][(int) $matches[1]] = $image;
continue;
}
$post['gallery_images'][] = $image;
}
return $post;
}
}