오랜 사투 끝에...... 푸시 알림 출력에 성공하여 백업 겸 작성하는 글입니다.
순서가 뒤죽박죽입니다. 파일 수정, 생성, 다운 > 업로드 > 실행 순서이므로, 파일 준비를 꼭 작성된 순서대로 하실 필요는 없습니다. 덧붙여 ios는 작동을 보장하기 힘듭니다. (안 할 확률이 높습니다.)
이건 이렇고 저건 저거고 불필요한데 왜 이렇게 하냐 > 그럼 쉽게하는 법 써서 배포해주세요...... 코딩 잘 모릅니다. 모든 과정에 AI의 도움이 있었습니다. 아직도 뭐가 뭔지 잘 모르겠지만 일단 동작하니까 괜찮다고 생각합니다......
준비물
라공 에디션이 설치된 홈페이지 (1.8.2 버전을 기준으로 작성하였습니다.)
Notepad++ 등의 텍스트 편집기
(1) VAPID 키를 생성합니다.
VAPID 키란? 웹 푸시(Web Push)에서 애플리케이션 서버를 식별하고 인증하기 위한 공개키/비공개키 쌍(키 페어)을 뜻합니다. (AI 답변)
저는 아래 사이트를 사용했습니다. 이 키는 분실 및 유출되지 않게 보관합니다.
(2) 아래 코드를 복사하여 push_config.php 파일을 신규 생성합니다.
<?php
if (!defined('_GNUBOARD_')) exit;
$push_config = [
'enabled' => true,
// 발급한 VAPID 키 입력 (키 전후로 '따옴표'는 그대로 있어야 합니다)
'public_key' => '공개 키 입력',
'private_key' => '비밀 키 입력',
// 관리자 이메일 주소를 앞에 mailto: 지우지 말고 공백 없이 입력
'subject' => 'mailto:이메일을@입력하세요.com',
'ttl' => 86400, // 알림 유효 시간 (초 단위, 86400 = 24시간)[cite: 1]
'urgency' => 'normal', // 전송 우선순위 (normal, high 등)[cite: 1]
// 알림 유형별 발송 여부 제어
'types' => [
'comment' => true, // 새 댓글 알림[cite: 1]
'reply' => true, // 새 답글 알림[cite: 1]
'mention' => true, // 멘션 알림[cite: 1]
'message' => true, // 채팅 알림[cite: 1]
'like' => true, // 관심/추천 표시 알림[cite: 1]
'gift' => true, // 선물 도착 알림[cite: 1]
'trade' => true, // 거래 알림[cite: 1]
]
];
?>
(3) 아래 코드를 복사하여 WebPush.php 파일을 신규 생성합니다.
<?php
if (!defined('_GNUBOARD_')) exit;
// Composer Autoload 경로 탐색
$autoload_paths = [
__DIR__ . '/vendor/autoload.php', // plugin/WebPush/vendor
G5_PLUGIN_PATH . '/vendor/autoload.php', // plugin/vendor
G5_PATH . '/vendor/autoload.php' // 그누보드 루트/vendor
];
$autoload_loaded = false;
foreach ($autoload_paths as $path) {
if (file_exists($path)) {
require_once($path);
$autoload_loaded = true;
break;
}
}
if (!$autoload_loaded || !class_exists('\Minishlink\WebPush\WebPush')) {
return;
}
use Minishlink\WebPush\WebPush as MinishlinkWebPush;
use Minishlink\WebPush\Subscription;
class WebPush
{
protected $webPush;
protected $ttl;
protected $urgency;
public function __construct(array $config = [])
{
$auth = [
'VAPID' => [
'subject' => $config['subject'] ?? 'mailto:메일@주소입력.com',
'publicKey' => $config['public_key'] ?? $config['publicKey'] ?? '',
'privateKey' => $config['private_key'] ?? $config['privateKey'] ?? '',
]
];
$options = [
'timeout' => $config['timeout'] ?? 10,
];
$timeout = $config['timeout'] ?? 10;
$client = null;
// [핵심 해결 로직] Buzz 1.0+ 버전의 MultiCurl 파라미터 요구 오류(ArgumentCountError) 방지
if (class_exists('\Buzz\Client\MultiCurl')) {
try {
$ref = new \ReflectionClass('\Buzz\Client\MultiCurl');
$constructor = $ref->getConstructor();
if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
// Buzz 1.0 이상: vendor 내부에 있는 PSR-17 ResponseFactory를 자동 탐색하여 안전하게 주입
$responseFactory = null;
if (class_exists('\Http\Discovery\Psr17FactoryDiscovery')) {
try { $responseFactory = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory(); } catch (\Throwable $e) {}
}
if (!$responseFactory && class_exists('\Http\Discovery\MessageFactoryDiscovery')) {
try { $responseFactory = \Http\Discovery\MessageFactoryDiscovery::find(); } catch (\Throwable $e) {}
}
if (!$responseFactory && class_exists('\Nyholm\Psr7\Factory\Psr17Factory')) {
$responseFactory = new \Nyholm\Psr7\Factory\Psr17Factory();
}
if (!$responseFactory && class_exists('\GuzzleHttp\Psr7\HttpFactory')) {
$responseFactory = new \GuzzleHttp\Psr7\HttpFactory();
}
if ($responseFactory) {
$client = new \Buzz\Client\MultiCurl($responseFactory, ['timeout' => $timeout]);
}
} else {
// Buzz 0.15~0.16 구버전인 경우
$client = new \Buzz\Client\MultiCurl();
}
} catch (\Throwable $e) {
// 예외 발생 시 null로 유지하여 라이브러리 기본 로직에 위임
}
}
// 괄호 안의 $timeout과 $client를 지우고 ($auth, $options) 2개만 넣습니다!
$this->webPush = new MinishlinkWebPush($auth, $options);
$this->ttl = $config['ttl'] ?? 86400;
$this->urgency = $config['urgency'] ?? 'normal';
}
/**
* 다수 구독 대상에게 푸시 발송 및 결과 리턴
*/
public function sendToAll(array $subscriptions, string $payload): array
{
$results = [];
$endpointMap = [];
foreach ($subscriptions as $sub) {
if (empty($sub['endpoint']) || empty($sub['ps_id'])) {
continue;
}
$ps_id = (int)$sub['ps_id'];
$endpointMap[$sub['endpoint']] = $ps_id;
try {
$subscription = Subscription::create([
'endpoint' => $sub['endpoint'],
'publicKey' => $sub['p256dh_key'],
'authToken' => $sub['auth_key'],
'keys' => [
'p256dh' => $sub['p256dh_key'],
'auth' => $sub['auth_key'],
]
]);
$this->webPush->queueNotification($subscription, $payload, [
'TTL' => $this->ttl,
'urgency' => $this->urgency,
]);
} catch (\Throwable $e) {
$results[] = [
'ps_id' => $ps_id,
'result' => [
'success' => false,
'statusCode' => 500,
'reason' => $e->getMessage(),
]
];
}
}
try {
foreach ($this->webPush->flush() as $report) {
$endpoint = (string)$report->getEndpoint();
$ps_id = $endpointMap[$endpoint] ?? 0;
if (!$ps_id) {
continue;
}
$statusCode = 0;
if ($report->getResponse() !== null) {
$statusCode = $report->getResponse()->getStatusCode();
}
if ($report->isSubscriptionExpired() && !in_array($statusCode, [404, 410])) {
$statusCode = 410;
}
$results[] = [
'ps_id' => $ps_id,
'result' => [
'success' => $report->isSuccess(),
'statusCode' => $statusCode,
'reason' => $report->isSuccess() ? '' : $report->getReason(),
]
];
}
} catch (\Throwable $e) {
// 발송 도중 예외 발생 시 메인 프로세스 보호
}
return $results;
}
}
?>
(4) manifest.json 파일을 수정합니다. (기존 라공 에디션 최상단에 위치)
또, 아래 파일명과 사이즈에 맞춰 아이콘 이미지(png)도 준비해둡니다.
{
"name": "사이트 이름",
"short_name": "홈 화면에 나타나는 앱 이름",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/img/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
(5) push.js 파일을 수정합니다. (기존 js 폴더에 위치)
상단 applicationServerKey: null, 부분에 (1)의 VAPID 공개 키를 입력합니다. 이때 입력에도 '따옴표'가 필요합니다.
(6) 아래 파일을 다운로드합니다.
(6-1) 위 파일을 다운 받은 경로를 백업합니다. 구글 클라우드 셸 접속 후 아래 명령어를 순서대로 입력했습니다.
sudo apt-get update (패키지 목록 갱신)
sudo apt-get install -y php-curl php-gmp php-mbstring php-xml php-zip (확장 모듈 설치 사전 준비)
mkdir webpush_p (이 이름의 폴더 생성)
cd webpush_p (그 폴더로 이동)
composer require minishlink/web-push php-http/guzzle7-adapter guzzlehttp/guzzle (필요한 라이브러리 설치)
composer config platform.php 8.2.0 (8.2 버전에 호환되도록 설정)
composer update (버전 갱신)
중간에 경고문 하나가 나올 텐데 y 입력하고 진행하면 됩니다. 이후 우측 상단의 더보기 탭에서 다운로드합니다.
(7) notification.lib 파일을 수정합니다. // WebPush 인스턴스 생성 주석을 찾아, private_key_pem 이라고 된 부분을 private_key 로 수정합니다. (1072줄)
(8) 지금까지 수정 및 생성한 파일을 FTP를 통해 업로드합니다. (닷홈 기준이라 최상위 경로가 html 입니다.) 해당하는 경로가 없으면 이름에 맞춰서 폴더를 생성하고 업로드해주세요. 경로가 다르면 작동하지 않습니다.
파일 갱신 (덮어쓰기)
- html/manifest.json
- html/js/push.js
- html/lib/notification.lib
신규 업로드
- html/vendor (압축 해제한 폴더 전체를 업로드, vendor/vendor가 되지 않도록 주의)
- html/img/icon-512x512.png
- html/img/icon-192x192.png
- html/data/push/push_config.php
- html/plugin/WebPush/WebPush.php
(9) 브라우저 캐시를 삭제하고 사이트에 접속합니다. 마이페이지의 알림 탭에서 아래와 같은 설정이 나오면 활성화해주세요. 웹 환경에서 푸시 알림이 제대로 출력되는지 테스트하는 걸 권장합니다.

(9-1) 아래는 제가 사용한 테스트 페이지입니다. 최상단에 업로드 후 사이트주소/test_push.php 를 직접 입력해서 확인하시면 됩니다. 오류가 있다면, 테스트 페이지에서 돌려보고 에러 코드를 복사하여 제미나이에게 물어봐주세요.
(10) 웹 푸시 알림이 온다면, 모바일 기기로 사이트에 접속한 후 앱을 설치합니다. 크롬 기준, 우측 상단 더보기 버튼을 눌러주세요. '설치 및 바로가기 만들기'를 눌러 앱을 설치합니다. 앱으로 로그인한 후 마이페이지에서 알림 설정을 켜주시면 됩니다. (크롬 자체의 알림 또한 허용되어야 합니다.) 수고하셨습니다.
알람 필요없고 앱만 있으면 된다 > sw.js / head.sub.php 파일을 적당히 수정하시면 됩니다. (manifest.json 파일 수정과 아이콘 이미지는 여전히 필요합니다.) 제미나이에게 물어보세요.
라공 에디션의 기존 코드를 활용하였으나 굉장히 야매입니다. 오류 문의는 대응이 어렵습니다. 다시 말하지만 저도 잘 모릅니다...... (그렇다고 라공홈에 관련 문의 남기진 마시고요......) 하지만 누군가에게는 이런 글도 도움이 되겠지요. 모두 즐거운 갠홈 생활하세요. 감사합니다.
'-' 카테고리의 다른 글
| 자동봇 html 코드 배포 (0) | 2026.06.17 |
|---|---|
| 자동봇 예시 (0) | 2026.06.17 |
| 의지 커미션 자료 (0) | 2024.10.13 |