본문 바로가기

-

라공 에디션으로 웹앱 만들기

오랜 사투 끝에...... 푸시 알림 출력에 성공하여 백업 겸 작성하는 글입니다.

 

순서가 뒤죽박죽입니다. 파일 수정, 생성, 다운 > 업로드 > 실행 순서이므로, 파일 준비를 꼭 작성된 순서대로 하실 필요는 없습니다. 덧붙여 ios는 작동을 보장하기 힘듭니다. (안 할 확률이 높습니다.)

 

이건 이렇고 저건 저거고 불필요한데 왜 이렇게 하냐 > 그럼 쉽게하는 법 써서 배포해주세요...... 코딩 잘 모릅니다. 모든 과정에 AI의 도움이 있었습니다. 아직도 뭐가 뭔지 잘 모르겠지만 일단 동작하니까 괜찮다고 생각합니다...... 

 

 

준비물

라공 에디션이 설치된 홈페이지 (1.8.2 버전을 기준으로 작성하였습니다.) 

Notepad++ 등의 텍스트 편집기

 

 

(1) VAPID 키를 생성합니다. 

VAPID 키란? 웹 푸시(Web Push)에서 애플리케이션 서버를 식별하고 인증하기 위한 공개키/비공개키 쌍(키 페어)을 뜻합니다. (AI 답변) 

저는 아래 사이트를 사용했습니다. 이 키는 분실 및 유출되지 않게 보관합니다.

https://vapidkeys.com/

 

 

(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) 아래 파일을 다운로드합니다. 

vendor.zip
1.00MB

 

더보기
더보기

(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 를 직접 입력해서 확인하시면 됩니다. 오류가 있다면, 테스트 페이지에서 돌려보고 에러 코드를 복사하여 제미나이에게 물어봐주세요.  

test_push.php
0.01MB

 

 

(10) 웹 푸시 알림이 온다면, 모바일 기기로 사이트에 접속한 후 앱을 설치합니다. 크롬 기준, 우측 상단 더보기 버튼을 눌러주세요. '설치 및 바로가기 만들기'를 눌러 앱을 설치합니다. 앱으로 로그인한 후 마이페이지에서 알림 설정을 켜주시면 됩니다. (크롬 자체의 알림 또한 허용되어야 합니다.) 수고하셨습니다.

 

 

알람 필요없고 앱만 있으면 된다 > sw.js / head.sub.php 파일을 적당히 수정하시면 됩니다. (manifest.json 파일 수정과 아이콘 이미지는 여전히 필요합니다.) 제미나이에게 물어보세요.

 

라공 에디션의 기존 코드를 활용하였으나 굉장히 야매입니다. 오류 문의는 대응이 어렵습니다. 다시 말하지만 저도 잘 모릅니다...... (그렇다고 라공홈에 관련 문의 남기진 마시고요......) 하지만 누군가에게는 이런 글도 도움이 되겠지요. 모두 즐거운 갠홈 생활하세요. 감사합니다. 

'-' 카테고리의 다른 글

자동봇 html 코드 배포  (0) 2026.06.17
자동봇 예시  (0) 2026.06.17
의지 커미션 자료  (0) 2024.10.13