feat: GTSIT 사이트 구축
This commit is contained in:
189
app/Repositories/AdminRepository.php
Normal file
189
app/Repositories/AdminRepository.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Database;
|
||||
use PDO;
|
||||
|
||||
final class AdminRepository
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connection();
|
||||
}
|
||||
|
||||
public function dashboardCounts(): array
|
||||
{
|
||||
return [
|
||||
'posts' => (int) $this->db->query("SELECT COUNT(*) FROM posts WHERE deleted_at IS NULL")->fetchColumn(),
|
||||
'published' => (int) $this->db->query("SELECT COUNT(*) FROM posts WHERE status = 'published' AND deleted_at IS NULL")->fetchColumn(),
|
||||
'inquiries' => (int) $this->db->query("SELECT COUNT(*) FROM inquiries WHERE deleted_at IS NULL")->fetchColumn(),
|
||||
'pending' => (int) $this->db->query("SELECT COUNT(*) FROM inquiries WHERE status IN ('pending','in_progress') AND deleted_at IS NULL")->fetchColumn(),
|
||||
];
|
||||
}
|
||||
|
||||
public function adminCount(): int
|
||||
{
|
||||
return (int) $this->db->query('SELECT COUNT(*) FROM admins')->fetchColumn();
|
||||
}
|
||||
|
||||
public function createAdmin(string $username, string $displayName, string $password): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'INSERT INTO admins (username, password_hash, display_name)
|
||||
VALUES (:username, :password_hash, :display_name)'
|
||||
);
|
||||
$statement->execute([
|
||||
'username' => $username,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'display_name' => $displayName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function posts(): array
|
||||
{
|
||||
return $this->db->query(
|
||||
"SELECT id, title, slug, category, status, published_at, updated_at
|
||||
FROM posts WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 100"
|
||||
)->fetchAll();
|
||||
}
|
||||
|
||||
public function inquiries(): array
|
||||
{
|
||||
return $this->db->query(
|
||||
"SELECT id, ticket, name, phone, inquiry_type, title, status, created_at
|
||||
FROM inquiries WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 100"
|
||||
)->fetchAll();
|
||||
}
|
||||
|
||||
public function findPost(int $id): ?array
|
||||
{
|
||||
$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
|
||||
FROM posts WHERE posts.id = :id AND posts.deleted_at IS NULL LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
$post = $statement->fetch();
|
||||
return $post ?: null;
|
||||
}
|
||||
|
||||
public function savePost(array $data, ?int $id = null): int
|
||||
{
|
||||
$publishedAt = $data['status'] === 'published'
|
||||
? ($data['published_at'] ?: date('Y-m-d H:i:s'))
|
||||
: null;
|
||||
|
||||
if ($id === null) {
|
||||
$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'],
|
||||
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
|
||||
'status'=>$data['status'],'published_at'=>$publishedAt,
|
||||
]);
|
||||
return (int) $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
'UPDATE posts SET title=:title, slug=:slug, category=:category, excerpt=:excerpt,
|
||||
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'],
|
||||
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
|
||||
'status'=>$data['status'],'published_at'=>$publishedAt,'id'=>$id,
|
||||
]);
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function savePostWithImage(array $data, ?int $id, ?string $imagePath, string $imageAlt): array
|
||||
{
|
||||
$oldPaths = [];
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
$postId = $this->savePost($data, $id);
|
||||
if ($imagePath !== null) {
|
||||
$select = $this->db->prepare('SELECT file_path FROM post_images WHERE post_id = :post_id FOR UPDATE');
|
||||
$select->execute(['post_id' => $postId]);
|
||||
$oldPaths = array_column($select->fetchAll(), 'file_path');
|
||||
|
||||
$delete = $this->db->prepare('DELETE FROM post_images WHERE post_id = :post_id');
|
||||
$delete->execute(['post_id' => $postId]);
|
||||
$insert = $this->db->prepare(
|
||||
'INSERT INTO post_images (post_id, file_path, alt_text, sort_order)
|
||||
VALUES (:post_id, :file_path, :alt_text, 0)'
|
||||
);
|
||||
$insert->execute(['post_id'=>$postId, 'file_path'=>$imagePath, 'alt_text'=>$imageAlt]);
|
||||
} elseif ($id !== null) {
|
||||
$update = $this->db->prepare(
|
||||
'UPDATE post_images SET alt_text = :alt_text WHERE post_id = :post_id AND sort_order = 0'
|
||||
);
|
||||
$update->execute(['post_id'=>$postId, 'alt_text'=>$imageAlt]);
|
||||
}
|
||||
$this->db->commit();
|
||||
return ['id' => $postId, 'old_paths' => $oldPaths];
|
||||
} catch (\Throwable $exception) {
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
$statement = $this->db->prepare($sql);
|
||||
$statement->execute($params);
|
||||
return (int) $statement->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
public function deletePost(int $id): void
|
||||
{
|
||||
$statement = $this->db->prepare('UPDATE posts SET deleted_at = NOW() WHERE id = :id');
|
||||
$statement->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
public function findInquiry(int $id): ?array
|
||||
{
|
||||
$statement = $this->db->prepare('SELECT * FROM inquiries WHERE id = :id AND deleted_at IS NULL LIMIT 1');
|
||||
$statement->execute(['id' => $id]);
|
||||
$inquiry = $statement->fetch();
|
||||
return $inquiry ?: null;
|
||||
}
|
||||
|
||||
public function updateInquiry(int $id, string $status, string $reply): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'UPDATE inquiries SET status = :new_status, reply = :reply,
|
||||
replied_at = CASE WHEN :has_reply = 1 THEN NOW() ELSE replied_at END,
|
||||
closed_at = CASE
|
||||
WHEN :closed_status = \'closed\' AND closed_at IS NULL THEN NOW()
|
||||
WHEN :open_status <> \'closed\' THEN NULL
|
||||
ELSE closed_at
|
||||
END
|
||||
WHERE id = :id AND deleted_at IS NULL'
|
||||
);
|
||||
$statement->execute([
|
||||
'new_status' => $status,
|
||||
'closed_status' => $status,
|
||||
'open_status' => $status,
|
||||
'reply' => $reply === '' ? null : $reply,
|
||||
'has_reply' => $reply === '' ? 0 : 1,
|
||||
'id' => $id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
111
app/Repositories/InquiryRepository.php
Normal file
111
app/Repositories/InquiryRepository.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Database;
|
||||
use PDO;
|
||||
|
||||
final class InquiryRepository
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connection();
|
||||
}
|
||||
|
||||
public function create(array $data): string
|
||||
{
|
||||
do {
|
||||
$ticket = 'GTS-' . date('Ymd') . '-' . strtoupper(bin2hex(random_bytes(3)));
|
||||
$check = $this->db->prepare('SELECT COUNT(*) FROM inquiries WHERE ticket = :ticket');
|
||||
$check->execute(['ticket' => $ticket]);
|
||||
} while ((int) $check->fetchColumn() > 0);
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
"INSERT INTO inquiries
|
||||
(ticket, name, phone, email, inquiry_type, title, body, password_hash, status, consent_at)
|
||||
VALUES (:ticket, :name, :phone, :email, :inquiry_type, :title, :body, :password_hash, 'pending', NOW())"
|
||||
);
|
||||
$statement->execute([
|
||||
'ticket' => $ticket,
|
||||
'name' => $data['name'],
|
||||
'phone' => $data['phone'],
|
||||
'email' => $data['email'] ?: null,
|
||||
'inquiry_type' => $data['type'],
|
||||
'title' => $data['title'],
|
||||
'body' => $data['body'],
|
||||
'password_hash' => password_hash($data['password'], PASSWORD_DEFAULT),
|
||||
]);
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
public function findForLookup(string $ticket): ?array
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'SELECT id, ticket, inquiry_type, title, body, status, reply, replied_at, created_at,
|
||||
password_hash, lookup_failed_attempts, lookup_locked_until
|
||||
FROM inquiries WHERE ticket = :ticket AND deleted_at IS NULL LIMIT 1'
|
||||
);
|
||||
$statement->execute(['ticket' => strtoupper($ticket)]);
|
||||
$inquiry = $statement->fetch();
|
||||
return $inquiry ?: null;
|
||||
}
|
||||
|
||||
public function resetLookupLimit(int $id): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'UPDATE inquiries
|
||||
SET lookup_failed_attempts = 0, lookup_locked_until = NULL
|
||||
WHERE id = :id AND deleted_at IS NULL'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
public function recordLookupFailure(int $id): bool
|
||||
{
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
$select = $this->db->prepare(
|
||||
'SELECT lookup_failed_attempts, lookup_locked_until
|
||||
FROM inquiries WHERE id = :id AND deleted_at IS NULL FOR UPDATE'
|
||||
);
|
||||
$select->execute(['id' => $id]);
|
||||
$limit = $select->fetch();
|
||||
if (!$limit) {
|
||||
$this->db->rollBack();
|
||||
return false;
|
||||
}
|
||||
|
||||
$attempts = (int) $limit['lookup_failed_attempts'];
|
||||
$lockedUntil = $limit['lookup_locked_until'];
|
||||
if (is_string($lockedUntil) && strtotime($lockedUntil) <= time()) {
|
||||
$attempts = 0;
|
||||
}
|
||||
$attempts++;
|
||||
$locked = $attempts >= 5;
|
||||
|
||||
$update = $this->db->prepare(
|
||||
'UPDATE inquiries
|
||||
SET lookup_failed_attempts = :attempts,
|
||||
lookup_locked_until = CASE WHEN :locked = 1 THEN DATE_ADD(NOW(), INTERVAL 15 MINUTE) ELSE NULL END
|
||||
WHERE id = :id'
|
||||
);
|
||||
$update->execute([
|
||||
'attempts' => $attempts,
|
||||
'locked' => $locked ? 1 : 0,
|
||||
'id' => $id,
|
||||
]);
|
||||
$this->db->commit();
|
||||
return $locked;
|
||||
} catch (\Throwable $error) {
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
app/Repositories/PostRepository.php
Normal file
81
app/Repositories/PostRepository.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Database;
|
||||
use PDO;
|
||||
|
||||
final class PostRepository
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connection();
|
||||
}
|
||||
|
||||
public function latest(int $limit = 3): array
|
||||
{
|
||||
$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
|
||||
FROM posts
|
||||
WHERE posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW()
|
||||
ORDER BY published_at DESC, id DESC
|
||||
LIMIT :limit"
|
||||
);
|
||||
$statement->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$statement->execute();
|
||||
return $statement->fetchAll();
|
||||
}
|
||||
|
||||
public function paginate(int $page, int $perPage, ?string $category = null): array
|
||||
{
|
||||
$where = "posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW()";
|
||||
$params = [];
|
||||
if ($category !== null && $category !== '') {
|
||||
$where .= ' AND posts.category = :category';
|
||||
$params['category'] = $category;
|
||||
}
|
||||
|
||||
$count = $this->db->prepare("SELECT COUNT(*) FROM posts WHERE {$where}");
|
||||
$count->execute($params);
|
||||
$total = (int) $count->fetchColumn();
|
||||
|
||||
$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
|
||||
FROM posts WHERE {$where}
|
||||
ORDER BY published_at DESC, id DESC
|
||||
LIMIT :limit OFFSET :offset"
|
||||
);
|
||||
foreach ($params as $key => $value) {
|
||||
$query->bindValue(':' . $key, $value);
|
||||
}
|
||||
$query->bindValue(':limit', $perPage, PDO::PARAM_INT);
|
||||
$query->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
return ['items' => $query->fetchAll(), 'total' => $total];
|
||||
}
|
||||
|
||||
public function findPublishedBySlug(string $slug): ?array
|
||||
{
|
||||
$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
|
||||
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;
|
||||
}
|
||||
}
|
||||
42
app/Repositories/SiteSettingRepository.php
Normal file
42
app/Repositories/SiteSettingRepository.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Database;
|
||||
use PDO;
|
||||
|
||||
final class SiteSettingRepository
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connection();
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
$rows = $this->db->query('SELECT setting_key, setting_value FROM site_settings')->fetchAll();
|
||||
$settings = [];
|
||||
foreach ($rows as $row) $settings[$row['setting_key']] = $row['setting_value'];
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function save(array $settings): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
|
||||
);
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
foreach ($settings as $key => $value) $statement->execute(['key'=>$key, 'value'=>$value]);
|
||||
$this->db->commit();
|
||||
} catch (\Throwable $exception) {
|
||||
$this->db->rollBack();
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user