Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions apps/web/public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const CACHE = `haribo-shell-${BUILD}`;
const SHELL = new URL('./', self.registration.scope).href;
// 화면(pwa.ts)과 같은 이름이에요
const OPEN_CHECK = 'haribo:open-check';
const DISPLAY_MODE = 'haribo:display-mode';
const DISPLAY_MODE_TIMEOUT_MS = 500;

self.addEventListener('install', (event) => {
// 'reload'로 받아요. 브라우저에 남은 옛 HTML을 새 저장 이름에 담지 않으려고요.
Expand Down Expand Up @@ -80,6 +82,7 @@ self.addEventListener('fetch', (event) => {

// 알림을 누르면 나에게 온 변경 화면에서 그 변경을 열어요 (app.notify.click-to-inbox).
// 앱 창이 열려 있으면 그 창을 앞으로 가져와 어떤 변경인지 알려 주고, 없으면 ?check=<ID> 주소로 새 창을 열어요.
// 설치한 앱(PWA) 창과 브라우저 탭이 둘 다 열려 있으면 앱 창을 먼저 골라요. 결정 사항이 "앱의" 화면을 여는 거라서예요.
// 알림은 pwa.ts의 showChangeNotification이 data.checkId를 넣어 띄워요.
self.addEventListener('notificationclick', (event) => {
event.notification.close();
Expand All @@ -90,11 +93,27 @@ self.addEventListener('notificationclick', (event) => {
event.waitUntil(
self.clients
.matchAll({ type: 'window', includeUncontrolled: true })
.then((windows) => {
const open = windows.find((client) => client.url.startsWith(SHELL));
if (!open) return self.clients.openWindow(target.href);
.then(async (windows) => {
const ours = windows.filter((client) => client.url.startsWith(SHELL));
if (ours.length === 0) return self.clients.openWindow(target.href);
const isStandalone = await Promise.all(ours.map(askIsStandalone));
const open = ours[isStandalone.indexOf(true)] ?? ours[0];
if (checkId) open.postMessage({ type: OPEN_CHECK, checkId });
return open.focus();
}),
);
});

// 창이 앱(PWA) 창인지는 서비스 워커가 알 수 없어서 창에 물어봐요 (pwa.ts의 registerServiceWorker가 답해요).
// 답이 없으면(옛 화면, 멈춘 창) 브라우저 탭으로 여겨요.
function askIsStandalone(client) {
return new Promise((resolve) => {
const channel = new MessageChannel();
const timer = setTimeout(() => resolve(false), DISPLAY_MODE_TIMEOUT_MS);
channel.port1.onmessage = (event) => {
clearTimeout(timer);
resolve(event.data?.isStandalone === true);
};
client.postMessage({ type: DISPLAY_MODE }, [channel.port2]);
});
}
10 changes: 10 additions & 0 deletions apps/web/src/pwa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ import { useEffect, useRef, useState } from 'react';
const OPEN_CHECK = 'haribo:open-check';
/** 알림을 눌러 새 창이 열릴 때 주소에 붙는 이름이에요 (?check=<확인할 변경 ID>) */
const CHECK_PARAM = 'check';
/** 서비스 워커(sw.js)와 같은 이름이에요. 알림을 누를 때 "이 창이 앱 창이야?"를 물어요 */
const DISPLAY_MODE = 'haribo:display-mode';

export function registerServiceWorker(): void {
if (!import.meta.env.PROD || !('serviceWorker' in navigator)) return;
// 알림을 누르면 서비스 워커가 앱(PWA) 창을 먼저 고를 수 있게 이 창이 앱 창인지 답해요 (app.notify.click-to-inbox)
navigator.serviceWorker.addEventListener('message', (event) => {
const data = event.data as { type?: unknown } | null;
if (data?.type !== DISPLAY_MODE) return;
event.ports[0]?.postMessage({
isStandalone: currentPlatform() === 'installed',
});
});
window.addEventListener('load', () => {
navigator.serviceWorker
.register(`${import.meta.env.BASE_URL}sw.js`)
Expand Down
Loading