운영 기능 및 GA4 대시보드 반영
This commit is contained in:
@@ -1 +1,38 @@
|
||||
<header class="admin-page-heading"><div><span>OVERVIEW</span><h1>대시보드</h1><p>사이트 콘텐츠와 문의 현황입니다.</p></div></header><section class="metric-grid" aria-label="운영 현황"><a href="/admin/posts"><span>전체 글</span><strong><?= e($counts['posts']) ?></strong></a><a href="/admin/posts"><span>공개 글</span><strong><?= e($counts['published']) ?></strong></a><a href="/admin/inquiries"><span>전체 문의</span><strong><?= e($counts['inquiries']) ?></strong></a><a href="/admin/inquiries"><span>처리 대기</span><strong><?= e($counts['pending']) ?></strong></a></section>
|
||||
<?php
|
||||
$summary = $analytics['summary'] ?? [];
|
||||
$daily = $analytics['daily'] ?? [];
|
||||
$maxViews = max(1, ...array_map(static fn(array $item): int => (int) ($item['views'] ?? 0), $daily ?: [['views' => 0]]));
|
||||
$channelLabels = [
|
||||
'Direct' => '직접 유입',
|
||||
'Organic Search' => '검색 유입',
|
||||
'Organic Social' => '소셜 유입',
|
||||
'Referral' => '외부 링크',
|
||||
'Unassigned' => '미분류',
|
||||
];
|
||||
?>
|
||||
<header class="admin-page-heading"><div><span>OVERVIEW</span><h1>대시보드</h1><p>사이트 콘텐츠, 문의, 방문 현황입니다.</p></div></header>
|
||||
<section class="metric-grid" aria-label="운영 현황"><a href="/admin/posts"><span>전체 글</span><strong><?= e($counts['posts']) ?></strong></a><a href="/admin/posts"><span>공개 글</span><strong><?= e($counts['published']) ?></strong></a><a href="/admin/inquiries"><span>전체 문의</span><strong><?= e($counts['inquiries']) ?></strong></a><a href="/admin/inquiries"><span>처리 대기</span><strong><?= e($counts['pending']) ?></strong></a></section>
|
||||
|
||||
<section class="analytics-section" aria-labelledby="analytics-heading">
|
||||
<header class="analytics-heading"><div><span>GOOGLE ANALYTICS</span><h2 id="analytics-heading">방문 현황</h2><p>운영 사이트의 최근 30일 통계입니다. 관리자와 테스트 페이지는 제외됩니다.</p></div><?php if (!empty($analytics['cached_at'])): ?><small><?= e(date('Y.m.d H:i', (int) $analytics['cached_at'])) ?> 기준 · 15분 간격 갱신<?= !empty($analytics['stale']) ? ' · 마지막 정상 데이터' : '' ?></small><?php endif; ?></header>
|
||||
<?php if (empty($analytics['available'])): ?>
|
||||
<div class="analytics-unavailable" role="status"><strong>방문 통계를 표시할 수 없습니다.</strong><p><?= e($analytics['message'] ?? '잠시 후 다시 확인해 주세요.') ?></p></div>
|
||||
<?php else: ?>
|
||||
<div class="analytics-metrics" aria-label="방문 핵심 지표">
|
||||
<article><span>최근 접속자</span><strong><?= e(number_format((int) ($analytics['realtime'] ?? 0))) ?></strong><small>최근 30분 내 활동</small></article>
|
||||
<article><span>전체 방문자</span><strong><?= e(number_format((int) ($analytics['all_time_users'] ?? 0))) ?></strong><small>2026.08.12 측정 시작 이후</small></article>
|
||||
<article><span>30일 방문자</span><strong><?= e(number_format((int) ($summary['active_users'] ?? 0))) ?></strong><small>최근 30일 내 활동</small></article>
|
||||
<article><span>처음 온 방문자</span><strong><?= e(number_format((int) ($summary['new_users'] ?? 0))) ?></strong><small>최근 30일 첫 방문</small></article>
|
||||
<article><span>페이지 열람 수</span><strong><?= e(number_format((int) ($summary['page_views'] ?? 0))) ?></strong><small>같은 페이지 반복 포함</small></article>
|
||||
<article><span>사이트 방문 횟수</span><strong><?= e(number_format((int) ($summary['sessions'] ?? 0))) ?></strong><small>방문을 시작한 횟수</small></article>
|
||||
</div>
|
||||
<div class="analytics-panel analytics-chart-panel">
|
||||
<div class="analytics-panel-heading"><h3>14일 페이지 열람 추이</h3><span><i></i> 열람 수</span></div>
|
||||
<?php if ($daily): ?><div class="analytics-chart" role="img" aria-label="최근 14일 일별 페이지 열람 수 막대 차트"><?php foreach ($daily as $item): ?><div class="analytics-bar" title="<?= e($item['date']) ?> · 페이지 열람 <?= e($item['views']) ?> · 방문자 <?= e($item['users']) ?>"><span><?= e($item['views']) ?></span><i style="height:<?= e(max(3, (int) round(((int) $item['views'] / $maxViews) * 100))) ?>%"></i><small><?= e($item['date']) ?></small></div><?php endforeach; ?></div><?php else: ?><p class="analytics-empty">아직 집계된 방문 데이터가 없습니다.</p><?php endif; ?>
|
||||
</div>
|
||||
<div class="analytics-detail-grid">
|
||||
<section class="analytics-panel"><div class="analytics-panel-heading"><h3>많이 본 페이지</h3><span>열람 수</span></div><?php if (!empty($analytics['pages'])): ?><ol class="analytics-ranking"><?php foreach ($analytics['pages'] as $page): ?><li><div><strong><?= e($page['title']) ?></strong><small><?= e($page['path']) ?></small></div><b><?= e(number_format((int) $page['views'])) ?></b></li><?php endforeach; ?></ol><?php else: ?><p class="analytics-empty">아직 집계된 페이지가 없습니다.</p><?php endif; ?></section>
|
||||
<section class="analytics-panel"><div class="analytics-panel-heading"><h3>방문한 경로</h3><span>방문 횟수 / 방문자</span></div><?php if (!empty($analytics['channels'])): ?><ol class="analytics-ranking"><?php foreach ($analytics['channels'] as $channel): ?><li><div><strong><?= e($channelLabels[$channel['channel']] ?? $channel['channel']) ?></strong><small><?= e($channel['channel']) ?></small></div><b><?= e(number_format((int) $channel['sessions'])) ?> / <?= e(number_format((int) $channel['users'])) ?></b></li><?php endforeach; ?></ol><?php else: ?><p class="analytics-empty">아직 집계된 유입 데이터가 없습니다.</p><?php endif; ?></section>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
@@ -1,23 +1,61 @@
|
||||
<header class="admin-page-heading">
|
||||
<div><span>CONTENTS</span><h1><?= $post ? '글 수정' : '새 글 작성' ?></h1><p>초안으로 저장하거나 공개 상태로 게시합니다.</p></div>
|
||||
</header>
|
||||
<?php if (!empty($mediaExperiment)): ?><div class="admin-experiment-notice"><strong>테스트 기능</strong><span>여러 대표 이미지와 본문 이미지는 테스트 경로의 초안 미리보기에서만 확인합니다. 확인 전 운영 공개는 제한됩니다.</span></div><?php endif; ?>
|
||||
<?php if ($errors): ?><div class="admin-error" role="alert"><strong>입력 내용을 확인해 주세요.</strong><ul><?php foreach ($errors as $error): ?><li><?= e($error) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
|
||||
<form class="admin-form" method="post" enctype="multipart/form-data" action="<?= $post ? '/admin/posts/' . e($post['id']) . '/edit' : '/admin/posts/new' ?>">
|
||||
<?= csrf_field() ?>
|
||||
<label>제목 <span>*</span><input name="title" value="<?= old('title', $post['title'] ?? '') ?>" required maxlength="200"></label>
|
||||
<div class="admin-field-grid">
|
||||
<label>URL slug <span>*</span><input name="slug" value="<?= old('slug', $post['slug'] ?? '') ?>" required maxlength="220" pattern="[a-z0-9-]+" placeholder="office-network-checklist"><small>영문 소문자, 숫자, 하이픈만 사용</small></label>
|
||||
<label>게시글 URL<?php if ($post): ?><input value="/blog/post-<?= e($post['id']) ?>" readonly aria-describedby="post-url-help"><?php else: ?><input value="저장 시 자동 생성" readonly aria-describedby="post-url-help"><?php endif; ?><small id="post-url-help"><?= $post ? '게시글 번호를 기준으로 자동 관리됩니다.' : '저장 후 post-글번호 형식으로 생성됩니다.' ?></small></label>
|
||||
<label>카테고리<select name="category"><?php $currentCategory=$_POST['category']??$post['category']??'네트워크'; foreach(['네트워크','키폰시스템','CCTV','시공사례'] as $option): ?><option<?= $currentCategory===$option?' selected':'' ?>><?= e($option) ?></option><?php endforeach; ?></select></label>
|
||||
</div>
|
||||
<label>목록 요약<textarea name="excerpt" rows="3" maxlength="500"><?= old('excerpt', $post['excerpt'] ?? '') ?></textarea></label>
|
||||
<fieldset class="admin-upload-field">
|
||||
<legend>대표 이미지</legend>
|
||||
<?php if (!empty($post['image_path'])): ?><img src="<?= e($post['image_path']) ?>" alt="<?= e($post['image_alt']) ?>"><p>새 파일을 선택하면 현재 대표 이미지가 교체됩니다.</p><?php endif; ?>
|
||||
<label>이미지 파일<input type="file" name="image" accept="image/jpeg,image/png,image/webp"><small>JPG, PNG, WebP · 최대 8MB · 최대 6000×6000px</small></label>
|
||||
<label>대체 텍스트<input name="image_alt" value="<?= old('image_alt', $post['image_alt'] ?? '') ?>" maxlength="255"><small>이미지 내용을 간단히 설명합니다. 이미지를 등록할 때 필수입니다.</small></label>
|
||||
</fieldset>
|
||||
<label>본문 <span>*</span><textarea name="body" rows="16" required><?= old('body', $post['body'] ?? '') ?></textarea><small>빈 줄을 기준으로 문단이 구분됩니다.</small></label>
|
||||
<?php if (!empty($mediaExperiment)): ?>
|
||||
<?php
|
||||
$galleryImages = $post['gallery_images'] ?? [];
|
||||
$inlineImages = $post['inline_images'] ?? [];
|
||||
$nextInlineToken = 1;
|
||||
foreach ($inlineImages as $inlineImage) $nextInlineToken = max($nextInlineToken, (int) $inlineImage['token'] + 1);
|
||||
?>
|
||||
<fieldset class="admin-upload-field" data-media-upload="gallery">
|
||||
<legend>대표 이미지 슬라이드</legend>
|
||||
<p>최대 6장 · 2장 이상이면 글 상단에서 5초 간격으로 자동 슬라이드됩니다.</p>
|
||||
<?php if ($galleryImages): ?><div class="admin-media-list"><?php foreach ($galleryImages as $image): ?><article class="admin-media-item"><img src="<?= e($image['file_path']) ?>" alt=""><div><label>대체 텍스트<input name="media_alt[<?= e($image['id']) ?>]" value="<?= e($image['alt_text']) ?>" maxlength="255" required></label><label class="admin-media-delete"><input type="checkbox" name="delete_images[]" value="<?= e($image['id']) ?>"> 이 이미지 삭제</label></div></article><?php endforeach; ?></div><?php endif; ?>
|
||||
<label>이미지 파일<input type="file" name="gallery_images[]" accept="image/jpeg,image/png,image/webp" multiple data-media-files><small>JPG, PNG, WebP · 파일당 최대 8MB · 최대 6000×6000px</small></label>
|
||||
<div class="admin-new-media-list" data-media-list aria-live="polite"></div>
|
||||
</fieldset>
|
||||
<div class="admin-body-source" data-body-source><label>본문 <span>*</span><textarea name="body" rows="16" required data-post-body><?= old('body', $post['body'] ?? '') ?></textarea><small>JavaScript를 사용할 수 없을 때는 빈 줄로 문단을 나누고 이미지 위치 토큰을 사용합니다.</small></label></div>
|
||||
<section class="admin-block-editor" data-block-editor data-next-token="<?= e($nextInlineToken) ?>" aria-labelledby="block-editor-title">
|
||||
<header><div><h2 id="block-editor-title">본문 편집</h2><p>텍스트 문단과 이미지만 사용해 글의 구성을 정리합니다.</p></div><span>본문 이미지 최대 10장</span></header>
|
||||
<div class="admin-block-list" data-block-list></div>
|
||||
<div class="admin-block-editor-actions"><button class="button button-secondary" type="button" data-add-text>문단 추가</button><button class="button button-primary" type="button" data-add-image>이미지 추가</button></div>
|
||||
<p class="admin-block-help">이미지는 JPG, PNG, WebP 형식으로 파일당 8MB·6000×6000px까지 등록할 수 있습니다.</p>
|
||||
<div data-delete-bin></div>
|
||||
<div data-inline-media-inventory hidden><?php foreach ($inlineImages as $image): ?><span data-existing-inline data-id="<?= e($image['id']) ?>" data-token="<?= e($image['token']) ?>" data-path="<?= e(url($image['file_path'])) ?>" data-alt="<?= e($image['alt_text']) ?>"></span><?php endforeach; ?></div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<fieldset class="admin-upload-field">
|
||||
<legend>대표 이미지</legend>
|
||||
<?php if (!empty($post['image_path'])): ?><img src="<?= e($post['image_path']) ?>" alt="<?= e($post['image_alt']) ?>"><p>새 파일을 선택하면 현재 대표 이미지가 교체됩니다.</p><?php endif; ?>
|
||||
<label>이미지 파일<input type="file" name="image" accept="image/jpeg,image/png,image/webp"><small>JPG, PNG, WebP · 최대 8MB · 최대 6000×6000px</small></label>
|
||||
<label>대체 텍스트<input name="image_alt" value="<?= old('image_alt', $post['image_alt'] ?? '') ?>" maxlength="255"><small>이미지 내용을 간단히 설명합니다. 이미지를 등록할 때 필수입니다.</small></label>
|
||||
</fieldset>
|
||||
<label>본문 <span>*</span><textarea name="body" rows="16" required><?= old('body', $post['body'] ?? '') ?></textarea><small>빈 줄을 기준으로 문단이 구분됩니다.</small></label>
|
||||
<?php endif; ?>
|
||||
<div class="admin-field-grid"><label>작성자<input name="author" value="<?= old('author', $post['author'] ?? config('app.legal_name')) ?>" maxlength="100"></label><label>공개 상태<select name="status"><option value="draft"<?= ($_POST['status']??$post['status']??'draft')==='draft'?' selected':'' ?>>초안</option><option value="published"<?= ($_POST['status']??$post['status']??'draft')==='published'?' selected':'' ?>>공개</option></select></label></div>
|
||||
<div class="admin-form-actions"><button class="button button-primary" type="submit">저장하기</button><a class="button button-secondary" href="/admin/posts">취소</a></div>
|
||||
<div class="admin-form-actions"><button class="button button-primary" type="submit">저장하기</button><?php if (!empty($mediaExperiment)): ?><button class="button button-secondary" type="button" data-open-live-preview>작성 내용 미리보기</button><?php endif; ?><a class="button button-secondary" href="/admin/posts">취소</a></div>
|
||||
</form>
|
||||
<?php if (!empty($mediaExperiment)): ?>
|
||||
<dialog class="admin-live-preview" data-live-preview aria-labelledby="live-preview-title">
|
||||
<div class="admin-live-preview-shell">
|
||||
<header><div><span>LIVE PREVIEW</span><h2 id="live-preview-title">작성 내용 미리보기</h2><p>현재 입력 상태를 저장하지 않고 확인합니다.</p></div><button type="button" data-close-live-preview aria-label="미리보기 닫기">×</button></header>
|
||||
<article class="admin-live-preview-article">
|
||||
<div class="admin-live-preview-meta"><span data-preview-category></span><strong data-preview-title></strong><small data-preview-author></small></div>
|
||||
<section class="admin-live-preview-gallery" data-preview-gallery hidden><div data-preview-gallery-main></div><div data-preview-gallery-thumbs></div><p data-preview-gallery-count></p></section>
|
||||
<div class="admin-live-preview-body" data-preview-body></div>
|
||||
</article>
|
||||
</div>
|
||||
</dialog>
|
||||
<?php endif; ?>
|
||||
<?php if ($post): ?><form class="danger-zone" method="post" action="/admin/posts/<?= e($post['id']) ?>/delete" onsubmit="return confirm('이 글을 삭제할까요? 삭제 후 목록에 표시되지 않습니다.');"><?= csrf_field() ?><div><strong>글 삭제</strong><p>게시글을 복구 가능한 삭제 상태로 전환합니다.</p></div><button type="submit">삭제</button></form><?php endif; ?>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<header class="admin-page-heading"><div><span>CONTENTS</span><h1>글 관리</h1><p>블로그 글을 작성하고 공개 상태를 관리합니다.</p></div><a class="button button-primary" href="/admin/posts/new">새 글 작성</a></header><?php if ($posts): ?><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>제목</th><th>분류</th><th>상태</th><th>공개일</th><th>관리</th></tr></thead><tbody><?php foreach ($posts as $post): ?><tr><td data-label="제목"><strong><?= e($post['title']) ?></strong><small>/blog/<?= e($post['slug']) ?></small></td><td data-label="분류"><?= e($post['category']) ?></td><td data-label="상태"><span class="status status-<?= e($post['status']) ?>"><?= $post['status'] === 'published' ? '공개' : '초안' ?></span></td><td data-label="공개일"><?= $post['published_at'] ? e(date('Y.m.d', strtotime($post['published_at']))) : '-' ?></td><td data-label="관리"><a href="/admin/posts/<?= e($post['id']) ?>/edit">수정</a></td></tr><?php endforeach; ?></tbody></table></div><?php else: ?><div class="empty-state"><h2>등록된 글이 없습니다</h2><p>새 글을 작성해 콘텐츠를 추가해 주세요.</p></div><?php endif; ?>
|
||||
<header class="admin-page-heading"><div><span>CONTENTS</span><h1>글 관리</h1><p>블로그 글을 작성하고 공개 상태를 관리합니다.</p></div><a class="button button-primary" href="/admin/posts/new">새 글 작성</a></header><?php if ($posts): ?><div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>제목</th><th>분류</th><th>상태</th><th>공개일</th><th>관리</th></tr></thead><tbody><?php foreach ($posts as $post): ?><tr><td data-label="제목"><strong><?= e($post['title']) ?></strong><small>/blog/<?= e($post['slug']) ?></small></td><td data-label="분류"><?= e($post['category']) ?></td><td data-label="상태"><span class="status status-<?= e($post['status']) ?>"><?= $post['status'] === 'published' ? '공개' : '초안' ?></span></td><td data-label="공개일"><?= $post['published_at'] ? e(date('Y.m.d', strtotime($post['published_at']))) : '-' ?></td><td data-label="관리"><span class="admin-row-actions"><a href="/admin/posts/<?= e($post['id']) ?>/preview" target="_blank" rel="noopener">미리보기</a><a href="/admin/posts/<?= e($post['id']) ?>/edit">수정</a></span></td></tr><?php endforeach; ?></tbody></table></div><?php else: ?><div class="empty-state"><h2>등록된 글이 없습니다</h2><p>새 글을 작성해 콘텐츠를 추가해 주세요.</p></div><?php endif; ?>
|
||||
|
||||
@@ -1 +1,30 @@
|
||||
<article class="article"><a class="back-link" href="/blog">← 블로그 목록</a><header><span class="badge"><?= e($post['category']) ?></span><time datetime="<?= e(date('Y-m-d', strtotime($post['published_at']))) ?>"><?= e(date('Y.m.d', strtotime($post['published_at']))) ?></time><h1><?= e($post['title']) ?></h1><p><?= e($post['author']) ?> 기술팀</p></header><?php if (!empty($post['image_path'])): ?><figure class="article-image"><img src="<?= e($post['image_path']) ?>" alt="<?= e($post['image_alt']) ?>"></figure><?php endif; ?><div class="article-body"><?php foreach (preg_split('/\R{2,}/', trim($post['body'])) ?: [] as $paragraph): ?><p><?= nl2br(e($paragraph)) ?></p><?php endforeach; ?></div><aside class="article-cta"><div><strong>비슷한 시공이 필요하신가요?</strong><p>현장 조건에 맞는 구성을 상담해 드립니다.</p></div><a class="button button-primary" href="/contact">상담 신청</a></aside></article>
|
||||
<?php
|
||||
$previewMode = !empty($preview);
|
||||
$displayDate = $post['published_at'] ?: ($post['updated_at'] ?? $post['created_at'] ?? date('Y-m-d H:i:s'));
|
||||
$galleryImages = $post['gallery_images'] ?? [];
|
||||
if ($galleryImages === [] && !empty($post['image_path'])) {
|
||||
$galleryImages[] = ['file_path' => $post['image_path'], 'alt_text' => $post['image_alt'] ?? ''];
|
||||
}
|
||||
$inlineImages = [];
|
||||
foreach (($post['inline_images'] ?? []) as $image) $inlineImages[(int) $image['token']] = $image;
|
||||
$paragraphs = preg_split('/\R{2,}/', trim($post['body'])) ?: [];
|
||||
?>
|
||||
<?php if ($previewMode): ?><aside class="preview-toolbar" role="status"><div><strong><?= $post['status'] === 'published' ? '공개 글 미리보기' : '초안 미리보기' ?></strong><span>저장된 내용을 실제 공개 페이지 형식으로 표시합니다.</span></div><a class="button button-secondary button-small" href="/admin/posts/<?= e($post['id']) ?>/edit">글 수정</a></aside><?php endif; ?>
|
||||
<article class="article">
|
||||
<a class="back-link" href="<?= $previewMode ? '/admin/posts' : '/blog' ?>">← <?= $previewMode ? '글 관리' : '블로그 목록' ?></a>
|
||||
<header><span class="badge"><?= e($post['category']) ?></span><time datetime="<?= e(date('Y-m-d', strtotime($displayDate))) ?>"><?= e(date('Y.m.d', strtotime($displayDate))) ?></time><h1><?= e($post['title']) ?></h1><p><?= e($post['author']) ?> 기술팀</p></header>
|
||||
<?php if (count($galleryImages) > 1): ?>
|
||||
<section class="article-gallery" data-article-slider aria-label="대표 이미지 슬라이드">
|
||||
<div class="article-gallery-slides"><?php foreach ($galleryImages as $index => $image): ?><figure class="article-gallery-slide<?= $index === 0 ? ' is-active' : '' ?>" aria-hidden="<?= $index === 0 ? 'false' : 'true' ?>"><img src="<?= e($image['file_path']) ?>" alt="<?= e($image['alt_text']) ?>"></figure><?php endforeach; ?></div>
|
||||
<div class="article-gallery-controls"><button type="button" data-article-prev aria-label="이전 이미지">‹</button><div class="article-gallery-dots"><?php foreach ($galleryImages as $index => $_): ?><button type="button" data-article-dot="<?= e($index) ?>" class="<?= $index === 0 ? 'is-active' : '' ?>"<?= $index === 0 ? ' aria-current="true"' : '' ?> aria-label="<?= e($index + 1) ?>번째 이미지"></button><?php endforeach; ?></div><button type="button" data-article-next aria-label="다음 이미지">›</button><button class="article-gallery-pause" type="button" data-article-pause aria-pressed="false">자동재생 일시정지</button></div>
|
||||
</section>
|
||||
<?php elseif ($galleryImages): ?><figure class="article-image"><img src="<?= e($galleryImages[0]['file_path']) ?>" alt="<?= e($galleryImages[0]['alt_text']) ?>"></figure><?php endif; ?>
|
||||
<div class="article-body">
|
||||
<?php foreach ($paragraphs as $paragraph): ?>
|
||||
<?php if (preg_match('/^\[\[image:(\d+)\]\]$/', trim($paragraph), $tokenMatch) === 1 && isset($inlineImages[(int) $tokenMatch[1]])): $image = $inlineImages[(int) $tokenMatch[1]]; ?>
|
||||
<figure class="article-inline-image"><img src="<?= e($image['file_path']) ?>" alt="<?= e($image['alt_text']) ?>"></figure>
|
||||
<?php else: ?><p><?= nl2br(e($paragraph)) ?></p><?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<aside class="article-cta"><div><strong>비슷한 시공이 필요하신가요?</strong><p>현장 조건에 맞는 구성을 상담해 드립니다.</p></div><a class="button button-primary" href="/contact">상담 신청</a></aside>
|
||||
</article>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<p><?= e($address) ?></p>
|
||||
<div class="naver-map-frame" data-naver-map data-client-id="<?= e(config('app.naver_maps.client_id')) ?>" data-latitude="<?= e(config('app.naver_maps.latitude')) ?>" data-longitude="<?= e(config('app.naver_maps.longitude')) ?>" data-label="<?= e(config('app.legal_name')) ?>">
|
||||
<div class="naver-map" aria-label="<?= e(config('app.legal_name')) ?> 위치 지도"></div>
|
||||
<div class="naver-map-zoom" data-naver-map-zoom hidden><button type="button" aria-label="지도 확대" data-naver-map-zoom-in>+</button><button type="button" aria-label="지도 축소" data-naver-map-zoom-out>−</button></div>
|
||||
<p class="naver-map-status" role="status" data-naver-map-status>지도를 불러오는 중입니다.</p>
|
||||
<button class="naver-map-fullscreen" type="button" aria-label="지도를 전체 화면으로 보기" title="전체 화면" data-naver-map-fullscreen hidden><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="robots" content="noindex,nofollow,noarchive">
|
||||
<title><?= e($title ?? '관리자') ?> | <?= e(config('app.legal_name')) ?> 관리자</title>
|
||||
<link rel="stylesheet" href="<?= e(asset('css/site.css')) ?>">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/content.css')) ?>">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/admin.css')) ?>">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/site.css')) ?>?v=20260810-12">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/content.css')) ?>?v=20260810-13">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/admin.css')) ?>?v=20260813-2">
|
||||
<script src="<?= e(asset('js/admin.js')) ?>?v=20260810-4" defer></script>
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<a class="skip-link" href="#admin-main">본문 바로가기</a>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
$pageTitle = isset($title) ? $title . ' | ' . config('app.name') : config('app.name');
|
||||
$pageDescription = $description ?? '네트워크, 키폰, CCTV 설계·시공·유지보수 전문 기업';
|
||||
$canonicalPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$canonical = rtrim((string) config('app.base_url'), '/') . $canonicalPath;
|
||||
$canonical = isset($canonicalUrl) ? (string) $canonicalUrl : rtrim((string) config('app.base_url'), '/') . $canonicalPath;
|
||||
$robotsNoindex = config('app.noindex', false) || !empty($noindex);
|
||||
$legalName = site_setting('legal_name', config('app.legal_name'));
|
||||
$phone = site_setting('phone', config('app.contact.phone'));
|
||||
$phoneSub = site_setting('phone_sub', config('app.contact.phone_sub'));
|
||||
@@ -10,6 +11,10 @@ $fax = site_setting('fax', config('app.contact.fax'));
|
||||
$email = site_setting('email', config('app.contact.email'));
|
||||
$address = site_setting('address', config('app.contact.address'));
|
||||
$bizno = site_setting('bizno', '206-86-70582');
|
||||
$analyticsMeasurementId = (string) config('app.analytics.measurement_id', '');
|
||||
$analyticsHost = strtolower((string) preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST'] ?? ''));
|
||||
$analyticsEnabled = config('app.base_path') === '' && in_array($analyticsHost, ['gtsit.co.kr', 'www.gtsit.co.kr'], true);
|
||||
$analyticsConsentKey = 'gtsit_analytics_consent_v' . config('app.analytics.consent_version', '1') . ($analyticsEnabled ? '' : '_preview');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
@@ -18,7 +23,7 @@ $bizno = site_setting('bizno', '206-86-70582');
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e($pageTitle) ?></title>
|
||||
<meta name="description" content="<?= e($pageDescription) ?>">
|
||||
<?php if (config('app.noindex', false)): ?><meta name="robots" content="noindex,nofollow,noarchive"><?php endif; ?>
|
||||
<?php if ($robotsNoindex): ?><meta name="robots" content="noindex,nofollow,noarchive"><?php endif; ?>
|
||||
<link rel="canonical" href="<?= e($canonical) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="ko_KR">
|
||||
@@ -26,11 +31,11 @@ $bizno = site_setting('bizno', '206-86-70582');
|
||||
<meta property="og:title" content="<?= e($pageTitle) ?>">
|
||||
<meta property="og:description" content="<?= e($pageDescription) ?>">
|
||||
<meta property="og:url" content="<?= e($canonical) ?>">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/site.css')) ?>?v=20260805-11">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/content.css')) ?>?v=20260805-9">
|
||||
<script src="<?= e(asset('js/site.js')) ?>?v=20260805-9" defer></script>
|
||||
<link rel="stylesheet" href="<?= e(asset('css/site.css')) ?>?v=20260812-01">
|
||||
<link rel="stylesheet" href="<?= e(asset('css/content.css')) ?>?v=20260810-13">
|
||||
<script src="<?= e(asset('js/site.js')) ?>?v=20260812-01" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<body data-analytics-consent data-analytics-enabled="<?= $analyticsEnabled ? 'true' : 'false' ?>" data-analytics-measurement-id="<?= e($analyticsMeasurementId) ?>" data-analytics-consent-key="<?= e($analyticsConsentKey) ?>">
|
||||
<a class="skip-link" href="#main-content">본문 바로가기</a>
|
||||
<header class="site-header">
|
||||
<div class="header-inner">
|
||||
@@ -69,8 +74,18 @@ $bizno = site_setting('bizno', '206-86-70582');
|
||||
<div><h2>연락처</h2><p>대표번호 <?= e($phone) ?><br>경기지점 <?= e($phoneSub) ?><br>FAX <?= e($fax) ?><br>이메일 <?= e($email) ?></p></div>
|
||||
<div><h2>사업자 정보</h2><p><?= e($legalName) ?><br>사업자등록번호 <?= e($bizno) ?><br><?= e($address) ?></p></div>
|
||||
</div>
|
||||
<div class="container footer-bottom">© 2026 <?= e($legalName) ?>. All rights reserved.</div>
|
||||
<div class="container footer-bottom"><span>© 2026 <?= e($legalName) ?>. All rights reserved.</span><button type="button" data-cookie-settings>통계 쿠키 설정</button></div>
|
||||
</footer>
|
||||
<aside class="cookie-consent" data-cookie-consent hidden aria-labelledby="cookie-consent-title" aria-describedby="cookie-consent-description">
|
||||
<div>
|
||||
<strong id="cookie-consent-title">방문 통계 쿠키 안내</strong>
|
||||
<p id="cookie-consent-description">사이트 개선을 위해 Google Analytics 통계 쿠키와 이에 따른 국외 이전을 사용합니다. 거부해도 모든 기능을 이용할 수 있습니다. <a href="/privacy#policy-cookie">자세히 보기</a></p>
|
||||
</div>
|
||||
<div class="cookie-consent-actions">
|
||||
<button type="button" class="button button-secondary" data-cookie-decline>거부</button>
|
||||
<button type="button" class="button button-primary" data-cookie-accept>허용</button>
|
||||
</div>
|
||||
</aside>
|
||||
<a class="floating-call" href="tel:<?= e($phone) ?>" aria-label="전화상담 <?= e($phone) ?>">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
<span>전화상담</span>
|
||||
|
||||
@@ -15,6 +15,7 @@ $email = site_setting('email', config('app.contact.email'));
|
||||
<li><a href="#policy-purpose">처리 목적·항목·보유기간</a></li>
|
||||
<li><a href="#policy-third-party">제3자 제공</a></li>
|
||||
<li><a href="#policy-outsourcing">처리 위탁</a></li>
|
||||
<li><a href="#policy-overseas">국외 이전</a></li>
|
||||
<li><a href="#policy-destruction">파기</a></li>
|
||||
<li><a href="#policy-rights">정보주체의 권리</a></li>
|
||||
<li><a href="#policy-security">안전성 확보 조치</a></li>
|
||||
@@ -29,7 +30,10 @@ $email = site_setting('email', config('app.contact.email'));
|
||||
<div class="policy-table-wrap">
|
||||
<table>
|
||||
<thead><tr><th scope="col">구분</th><th scope="col">처리 목적</th><th scope="col">처리 항목</th><th scope="col">법적 근거</th><th scope="col">보유기간</th></tr></thead>
|
||||
<tbody><tr><th scope="row">온라인 상담 문의</th><td>상담 접수, 문의자 확인, 답변 제공 및 처리 이력 관리</td><td><strong>필수</strong>: 이름 또는 회사명, 연락처, 문의 유형, 제목, 내용, 조회 PIN의 일방향 암호화값<br><strong>선택</strong>: 이메일<br><strong>자동 생성</strong>: 접수번호, 접수·동의·답변·처리 시각</td><td>정보주체의 동의<br>「개인정보 보호법」 제15조 제1항 제1호</td><td>문의 처리 종료 후 1년</td></tr></tbody>
|
||||
<tbody>
|
||||
<tr><th scope="row">온라인 상담 문의</th><td>상담 접수, 문의자 확인, 답변 제공 및 처리 이력 관리</td><td><strong>필수</strong>: 이름 또는 회사명, 연락처, 문의 유형, 제목, 내용, 조회 PIN의 일방향 암호화값<br><strong>선택</strong>: 이메일<br><strong>자동 생성</strong>: 접수번호, 접수·동의·답변·처리 시각</td><td>정보주체의 동의<br>「개인정보 보호법」 제15조 제1항 제1호</td><td>문의 처리 종료 후 1년</td></tr>
|
||||
<tr><th scope="row">웹사이트 이용 통계</th><td>방문 현황, 이용 경로, 콘텐츠 성과 분석 및 사이트 개선</td><td>쿠키 식별자, 방문·세션 정보, 페이지 URL·제목, 유입 경로, 브라우저·기기 정보, 대략적인 지역, 상호작용 시각</td><td>통계 쿠키와 국외 이전에 대한 정보주체의 선택 동의</td><td>Google Analytics 사용자·이벤트 데이터 2개월<br>집계 보고서는 서비스 정책에 따른 기간</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>관계 법령에 따라 개인정보를 별도로 보존해야 하는 경우에는 해당 법령에서 정한 기간 동안 분리하여 보관합니다.</p>
|
||||
@@ -43,43 +47,57 @@ $email = site_setting('email', config('app.contact.email'));
|
||||
<section id="policy-outsourcing">
|
||||
<h2>3. 개인정보 처리업무의 위탁</h2>
|
||||
<p>회사는 원활한 사이트 운영을 위해 다음 업무를 위탁하고 있으며, 위탁계약 등을 통해 개인정보가 안전하게 관리되도록 필요한 사항을 규정하고 있습니다.</p>
|
||||
<div class="policy-table-wrap"><table><thead><tr><th scope="col">수탁업체</th><th scope="col">위탁업무</th></tr></thead><tbody><tr><td>카페24(주)</td><td>웹·데이터베이스 호스팅, 데이터 저장 및 백업을 위한 인프라 제공</td></tr></tbody></table></div>
|
||||
<div class="policy-table-wrap"><table><thead><tr><th scope="col">수탁업체</th><th scope="col">위탁업무</th></tr></thead><tbody><tr><td>카페24(주)</td><td>웹·데이터베이스 호스팅, 데이터 저장 및 백업을 위한 인프라 제공</td></tr><tr><td>Google LLC</td><td>동의한 이용자의 Google Analytics 방문 통계 처리</td></tr></tbody></table></div>
|
||||
<p>수탁업체 또는 위탁업무가 변경되면 본 처리방침을 통해 공개합니다.</p>
|
||||
</section>
|
||||
|
||||
<section id="policy-overseas">
|
||||
<h2>4. 개인정보의 국외 이전</h2>
|
||||
<p>회사는 이용자가 통계 쿠키를 허용한 경우에만 다음과 같이 이용 정보를 국외로 이전합니다. 동의하지 않거나 이후 동의를 철회해도 웹사이트의 일반 기능 이용에는 제한이 없습니다.</p>
|
||||
<div class="policy-table-wrap">
|
||||
<table>
|
||||
<thead><tr><th scope="col">이전받는 자</th><th scope="col">이전 국가</th><th scope="col">이전 항목·목적</th><th scope="col">이전 시기·방법</th><th scope="col">보유·이용기간</th></tr></thead>
|
||||
<tbody><tr><td>Google LLC<br><a href="https://support.google.com/policies/troubleshooter/7575787" target="_blank" rel="noopener">문의처</a></td><td>미국 및 Google 데이터센터가 위치한 국가</td><td>쿠키 식별자, 방문·세션 정보, 페이지 URL·제목, 유입 경로, 브라우저·기기 정보, 대략적인 지역, 상호작용 시각<br>방문 통계 분석 및 사이트 개선</td><td>동의 후 사이트 이용 시 암호화된 네트워크를 통해 이전</td><td>사용자·이벤트 데이터 2개월. 관계 법령이나 Google 서비스 정책에 따른 예외가 있는 경우 해당 기간</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>이용자는 최초 안내에서 “거부”를 선택하거나, 페이지 하단의 “통계 쿠키 설정”에서 동의를 철회할 수 있습니다.</p>
|
||||
</section>
|
||||
|
||||
<section id="policy-destruction">
|
||||
<h2>4. 개인정보의 파기 절차 및 방법</h2>
|
||||
<h2>5. 개인정보의 파기 절차 및 방법</h2>
|
||||
<p>회사는 보유기간이 지나거나 처리 목적을 달성해 개인정보가 불필요하게 된 경우 지체 없이 파기합니다. 온라인 문의는 관리자가 처리 상태를 “종료”로 변경한 시점부터 1년간 보관한 뒤 데이터베이스에서 복구할 수 없도록 삭제합니다. 관계 법령에 따라 보존해야 하는 정보는 다른 개인정보와 분리하여 보관한 후 기간이 끝나면 파기합니다.</p>
|
||||
<ul><li>전자적 파일: 복구 또는 재생할 수 없는 방법으로 영구 삭제</li><li>종이 문서가 발생한 경우: 분쇄 또는 소각</li></ul>
|
||||
</section>
|
||||
|
||||
<section id="policy-rights">
|
||||
<h2>5. 정보주체와 법정대리인의 권리·의무 및 행사방법</h2>
|
||||
<h2>6. 정보주체와 법정대리인의 권리·의무 및 행사방법</h2>
|
||||
<p>정보주체는 회사에 개인정보 열람, 정정·삭제, 처리정지 및 동의 철회를 요구할 수 있습니다. 아래 담당부서에 전화 또는 이메일로 요청하면 본인 확인 후 관련 법령에서 정한 절차에 따라 처리합니다. 법정대리인이나 위임받은 사람을 통해서도 권리를 행사할 수 있으며, 이 경우 위임장 등 정당한 대리권을 확인할 수 있는 자료를 요청할 수 있습니다.</p>
|
||||
</section>
|
||||
|
||||
<section id="policy-security">
|
||||
<h2>6. 개인정보의 안전성 확보 조치</h2>
|
||||
<h2>7. 개인정보의 안전성 확보 조치</h2>
|
||||
<p>회사는 개인정보의 분실·도난·유출·위조·변조 또는 훼손을 방지하기 위해 다음 조치를 시행합니다.</p>
|
||||
<ul><li>개인정보 취급자 및 관리자 접근 권한 제한</li><li>조회 PIN과 관리자 비밀번호의 일방향 암호화 저장</li><li>HTTPS 암호화 통신, 접근 통제 및 조회 시도 제한</li><li>보안 프로그램과 서버 소프트웨어의 점검 및 갱신</li><li>개인정보 처리시스템 접속기록의 보호와 정기 점검</li></ul>
|
||||
</section>
|
||||
|
||||
<section id="policy-cookie">
|
||||
<h2>7. 자동으로 수집하는 장치의 설치·운영 및 거부</h2>
|
||||
<h2>8. 자동으로 수집하는 장치의 설치·운영 및 거부</h2>
|
||||
<p>회사는 로그인 상태 유지, 위조 요청 방지 및 문의 중복 접수 방지를 위해 필수 세션 쿠키를 사용합니다. 이 쿠키에는 임의의 세션 식별자만 저장되며 브라우저를 닫으면 만료됩니다. 브라우저 설정에서 쿠키를 차단할 수 있으나, 차단하면 문의 접수와 관리자 기능을 이용하기 어려울 수 있습니다.</p>
|
||||
<p>이용자가 통계 쿠키를 허용한 경우 Google Analytics가 이용자를 구분하기 위한 자사 쿠키 <code>_ga</code> 등을 저장합니다. 회사는 광고 개인 최적화와 Google 신호 기능을 사용하지 않으며, 문의 양식에 입력한 이름·연락처·이메일·내용·접수번호를 Analytics로 전송하지 않습니다.</p>
|
||||
<p>통계 쿠키는 선택 사항입니다. 최초 안내에서 허용 또는 거부할 수 있고, 언제든 페이지 하단의 “통계 쿠키 설정”을 눌러 선택을 변경할 수 있습니다. 거부하거나 철회하면 통계 스크립트를 불러오지 않거나 Analytics 저장 권한을 철회하고 회사 도메인의 관련 쿠키 삭제를 시도합니다.</p>
|
||||
<p>오시는 길의 네이버 지도를 불러올 때 이용자의 브라우저가 네이버 지도 서비스에 직접 연결되며, 해당 서비스에서 접속 정보 등이 처리될 수 있습니다. 자세한 내용은 <a href="https://www.navercorp.com/policy/privacy" target="_blank" rel="noopener">네이버 개인정보처리방침</a>에서 확인할 수 있습니다.</p>
|
||||
</section>
|
||||
|
||||
<section id="policy-contact">
|
||||
<h2>8. 개인정보 보호 담당부서 및 권익침해 구제</h2>
|
||||
<h2>9. 개인정보 보호 담당부서 및 권익침해 구제</h2>
|
||||
<div class="policy-contact"><h3>개인정보 보호 담당부서</h3><dl><div><dt>담당</dt><dd><?= e($legalName) ?> 개인정보 보호 담당부서</dd></div><div><dt>전화</dt><dd><a href="tel:<?= e($phone) ?>"><?= e($phone) ?></a></dd></div><div><dt>이메일</dt><dd><a href="mailto:<?= e($email) ?>"><?= e($email) ?></a></dd></div></dl></div>
|
||||
<p>개인정보 침해에 관한 상담이나 피해 구제가 필요한 경우 다음 기관에 문의할 수 있습니다.</p>
|
||||
<ul><li>개인정보침해신고센터: 국번 없이 118 · <a href="https://privacy.kisa.or.kr" target="_blank" rel="noopener">privacy.kisa.or.kr</a></li><li>개인정보분쟁조정위원회: 1833-6972 · <a href="https://www.kopico.go.kr" target="_blank" rel="noopener">www.kopico.go.kr</a></li><li>대검찰청: 국번 없이 1301 · <a href="https://www.spo.go.kr" target="_blank" rel="noopener">www.spo.go.kr</a></li><li>경찰청: 국번 없이 182 · <a href="https://ecrm.police.go.kr" target="_blank" rel="noopener">ecrm.police.go.kr</a></li></ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. 개인정보처리방침의 변경</h2>
|
||||
<p>이 개인정보처리방침은 2026년 8월 5일부터 적용됩니다. 내용이 변경되는 경우 시행 전에 사이트를 통해 안내합니다.</p>
|
||||
<p class="policy-effective">공고일자: 2026년 8월 5일<br>시행일자: 2026년 8월 5일</p>
|
||||
<h2>10. 개인정보처리방침의 변경</h2>
|
||||
<p>이 개인정보처리방침은 2026년 8월 12일부터 적용됩니다. 내용이 변경되는 경우 시행 전에 사이트를 통해 안내합니다.</p>
|
||||
<p class="policy-effective">공고일자: 2026년 8월 12일<br>시행일자: 2026년 8월 12일</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user