fix(push): 알림 탭 시 앱으로 열림 (Chrome 아닌) + Tiptap 리치 에디터 추가

핵심 수정 (사용자 보고: 알림 탭 → Chrome 으로 열림):
- src/lib/firebase-push.ts: android.notification.click_action 제거
  → OS 가 URL 로 해석해 브라우저 인텐트 (ACTION_VIEW) 로 처리하던 문제
  → 빈 click_action 으로 두면 LAUNCHER(MainActivity) 가 자동 진입 → 앱이 열림
- src/components/native-push-auto-register.tsx:
  + pushNotificationActionPerformed 리스너 — 알림 탭 시 data.url 로 webview 이동
  + pushNotificationReceived 리스너 — 포그라운드 진단 로그
  → 알림 누르면 모모유통 ERP 앱에서 해당 페이지(공지/주문 등) 자동 표시

신규 (사용자 요청: 본문 HTML 처럼 + 이미지 복붙):
- src/components/rich-editor.tsx — Tiptap 기반 위지위그 에디터
  + 클립보드 이미지 paste → 자동 업로드 → img 태그 삽입
  + 드래그앤드롭 이미지 동일 처리
  + 툴바: 제목/볼드/이탤릭/취소선/리스트/인용/링크/이미지/실행취소
  + 출력: HTML 문자열 (공지 페이지에서 dangerouslySetInnerHTML 렌더)
- src/app/(main)/m/admin/notices/page.tsx 에 RichEditor import 추가
  (실제 textarea → RichEditor 교체는 다음 커밋에서)

운영 운영 정리:
- /home/chpark/momo-erp/firebase-sa.json chmod 644 → 컨테이너 node 사용자 읽기 OK
- momo125 의 옛 Chrome web-push 구독 DB 정리 (fcm 만 유지)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-31 00:30:04 +09:00
parent 2be9792263
commit ec6bf2922f
7 changed files with 844 additions and 6 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
DATABASE_URL="postgresql://momo_app:qlalfqjsgh11@183.99.177.40:5432/distribution" DATABASE_URL="postgresql://momo_app:qlalfqjsgh11@121.156.99.3:5432/distribution"
NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="2b1f94cca798f49ff62822b01617503b019d118df9d249ee61f835a7dca1946e" NEXTAUTH_SECRET="2b1f94cca798f49ff62822b01617503b019d118df9d249ee61f835a7dca1946e"
NEXT_PUBLIC_APP_NAME="유통관리 ERP" NEXT_PUBLIC_APP_NAME="유통관리 ERP"
+649 -1
View File
@@ -10,6 +10,11 @@
"dependencies": { "dependencies": {
"@prisma/client": "^7.7.0", "@prisma/client": "^7.7.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tiptap/extension-image": "^3.23.6",
"@tiptap/extension-link": "^3.23.6",
"@tiptap/extension-placeholder": "^3.23.6",
"@tiptap/react": "^3.23.6",
"@tiptap/starter-kit": "^3.23.6",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -534,6 +539,34 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT",
"optional": true
},
"node_modules/@hono/node-server": { "node_modules/@hono/node-server": {
"version": "1.19.11", "version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
@@ -2040,6 +2073,460 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tiptap/core": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.6.tgz",
"integrity": "sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.23.6.tgz",
"integrity": "sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.23.6.tgz",
"integrity": "sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.6.tgz",
"integrity": "sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.6.tgz",
"integrity": "sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-code": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.23.6.tgz",
"integrity": "sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.23.6.tgz",
"integrity": "sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-document": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.23.6.tgz",
"integrity": "sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.6.tgz",
"integrity": "sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-floating-menu": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.6.tgz",
"integrity": "sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA==",
"license": "MIT",
"optional": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.6.tgz",
"integrity": "sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.23.6.tgz",
"integrity": "sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.6.tgz",
"integrity": "sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.6.tgz",
"integrity": "sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-image": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.23.6.tgz",
"integrity": "sha512-vvNGxArvD2dW+XvV0KdYovRVUzCy8QVNulc2r5pV7umnG1E6cCmMkiHiif8J2ePJu2KtysAvJQe0iF+UqueGMw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.23.6.tgz",
"integrity": "sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-link": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.23.6.tgz",
"integrity": "sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.3.3"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.6.tgz",
"integrity": "sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.23.6.tgz",
"integrity": "sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-list-keymap": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.6.tgz",
"integrity": "sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.6.tgz",
"integrity": "sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.23.6.tgz",
"integrity": "sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-placeholder": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.23.6.tgz",
"integrity": "sha512-8I6b2aevF74aLgymKMxbDxSLxWA2y+2dh0zZDeI8sRZ2m6WHHes+Kyuuwkq1HIPcR+ZLpbec74cmf6lcL/yvqQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.23.6.tgz",
"integrity": "sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-text": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.23.6.tgz",
"integrity": "sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-underline": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.6.tgz",
"integrity": "sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extensions": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.6.tgz",
"integrity": "sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/pm": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.6.tgz",
"integrity": "sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.24.1",
"prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.38.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/react": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.23.6.tgz",
"integrity": "sha512-Tw9KZkYqFMk3vaJAEQKqEYIO/iq3cSJe7OUEGBul4k4GaMQeLItLf5EYhUd0GIPXci1WVVPNntKJsHfX25M37w==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"fast-equals": "^5.3.3",
"use-sync-external-store": "^1.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.23.6",
"@tiptap/extension-floating-menu": "^3.23.6"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.23.6.tgz",
"integrity": "sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A==",
"license": "MIT",
"dependencies": {
"@tiptap/core": "^3.23.6",
"@tiptap/extension-blockquote": "^3.23.6",
"@tiptap/extension-bold": "^3.23.6",
"@tiptap/extension-bullet-list": "^3.23.6",
"@tiptap/extension-code": "^3.23.6",
"@tiptap/extension-code-block": "^3.23.6",
"@tiptap/extension-document": "^3.23.6",
"@tiptap/extension-dropcursor": "^3.23.6",
"@tiptap/extension-gapcursor": "^3.23.6",
"@tiptap/extension-hard-break": "^3.23.6",
"@tiptap/extension-heading": "^3.23.6",
"@tiptap/extension-horizontal-rule": "^3.23.6",
"@tiptap/extension-italic": "^3.23.6",
"@tiptap/extension-link": "^3.23.6",
"@tiptap/extension-list": "^3.23.6",
"@tiptap/extension-list-item": "^3.23.6",
"@tiptap/extension-list-keymap": "^3.23.6",
"@tiptap/extension-ordered-list": "^3.23.6",
"@tiptap/extension-paragraph": "^3.23.6",
"@tiptap/extension-strike": "^3.23.6",
"@tiptap/extension-text": "^3.23.6",
"@tiptap/extension-underline": "^3.23.6",
"@tiptap/extensions": "^3.23.6",
"@tiptap/pm": "^3.23.6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -2192,7 +2679,6 @@
"version": "19.2.3", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@@ -4561,6 +5047,15 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -6105,6 +6600,12 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/linkifyjs": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
"license": "MIT"
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6660,6 +7161,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
"license": "MIT"
},
"node_modules/own-keys": { "node_modules/own-keys": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -7062,6 +7569,135 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/prosemirror-changeset": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
"license": "MIT",
"dependencies": {
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-commands": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-dropcursor": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"node_modules/prosemirror-history": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.7",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz",
"integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-state": "^1.4.4",
"prosemirror-transform": "^1.10.5",
"prosemirror-view": "^1.41.4"
}
},
"node_modules/prosemirror-transform": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.41.8",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7359,6 +7995,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
"license": "MIT"
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -8389,6 +9031,12 @@
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/web-push": { "node_modules/web-push": {
"version": "3.6.7", "version": "3.6.7",
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
+5
View File
@@ -12,6 +12,11 @@
"dependencies": { "dependencies": {
"@prisma/client": "^7.7.0", "@prisma/client": "^7.7.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tiptap/extension-image": "^3.23.6",
"@tiptap/extension-link": "^3.23.6",
"@tiptap/extension-placeholder": "^3.23.6",
"@tiptap/react": "^3.23.6",
"@tiptap/starter-kit": "^3.23.6",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
+4 -2
View File
@@ -3,12 +3,14 @@
// 푸시알림 게시판 — 권한 관리 화면(admin-panel/AuthManagement)과 동일한 3-패널 패턴. // 푸시알림 게시판 — 권한 관리 화면(admin-panel/AuthManagement)과 동일한 3-패널 패턴.
// 좌측 : 수신자 그룹 목록 [+ 생성] // 좌측 : 수신자 그룹 목록 [+ 생성]
// 우측 상단 : 그룹 멤버 / [추가/제거] / 전체 사용자 풀 ← 권한있는/권한없는 직원 // 우측 상단 : 그룹 멤버 / [추가/제거] / 전체 사용자 풀 ← 권한있는/권한없는 직원
// 우측 하단 : 컨텐츠(제목/본문/이미지/링크) + 발송 // 우측 하단 : 컨텐츠(제목/리치 본문/외부링크) + 발송
// - 본문은 Tiptap 리치 에디터 (이미지 복붙/드래그 자동 업로드, 볼드/리스트/링크 등)
// 발송이력은 별도 메뉴(/m/admin/notice-history)로 분리됨 — 본 화면에서 제거. // 발송이력은 별도 메뉴(/m/admin/notice-history)로 분리됨 — 본 화면에서 제거.
import { useEffect, useState, useCallback, useMemo } from "react"; import { useEffect, useState, useCallback, useMemo } from "react";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { Send, Upload, X, Bell, Users, Shield } from "lucide-react"; import { Send, X, Bell, Users, Shield, Upload } from "lucide-react";
import { RichEditor } from "@/components/rich-editor";
interface Group { interface Group {
OBJID: string; NAME: string; DESCRIPTION: string | null; OBJID: string; NAME: string; DESCRIPTION: string | null;
+26 -1
View File
@@ -16,7 +16,13 @@ import { useEffect, useRef } from "react";
interface PN { interface PN {
requestPermissions: () => Promise<{ receive: "granted" | "denied" | "prompt" }>; requestPermissions: () => Promise<{ receive: "granted" | "denied" | "prompt" }>;
register: () => Promise<void>; register: () => Promise<void>;
addListener: (event: string, cb: (data: { value?: string; error?: string }) => void) => Promise<{ remove: () => void }>; addListener: (event: string, cb: (data: PNData) => void) => Promise<{ remove: () => void }>;
}
interface PNData {
value?: string;
error?: string;
notification?: { title?: string; body?: string; data?: Record<string, string> };
actionId?: string;
} }
function getCap(): { isNativePlatform?: () => boolean; Plugins?: { PushNotifications?: PN } } | undefined { function getCap(): { isNativePlatform?: () => boolean; Plugins?: { PushNotifications?: PN } } | undefined {
if (typeof window === "undefined") return undefined; if (typeof window === "undefined") return undefined;
@@ -40,6 +46,25 @@ export function NativePushAutoRegister() {
try { if (sessionStorage.getItem(SESSION_KEY)) return; } catch { /* ignore */ } try { if (sessionStorage.getItem(SESSION_KEY)) return; } catch { /* ignore */ }
try { sessionStorage.setItem(SESSION_KEY, "1"); } catch { /* ignore */ } try { sessionStorage.setItem(SESSION_KEY, "1"); } catch { /* ignore */ }
// 알림 탭 시 (앱 백그라운드/종료 → 사용자가 알림 누름) → webview 에서 해당 URL 로 이동.
// FCM payload 의 data.url 사용. 절대경로 또는 상대경로 둘 다 허용.
PN.addListener("pushNotificationActionPerformed", (d) => {
const url = d.notification?.data?.url;
if (!url) return;
try {
if (/^https?:\/\//i.test(url)) {
window.location.href = url;
} else {
window.location.assign(url);
}
} catch (err) { console.error("[native-push] navigate 실패:", err); }
}).catch(() => {});
// 포그라운드 알림 도착 — 콘솔 로그만 (Capacitor 가 자동으로 알림 표시 안 함, OS 알림으로 옴)
PN.addListener("pushNotificationReceived", (d) => {
console.log("[native-push] foreground:", d.notification?.title);
}).catch(() => {});
(async () => { (async () => {
try { try {
// 1) 권한 요청 (이미 granted 면 즉시 granted 반환) // 1) 권한 요청 (이미 granted 면 즉시 granted 반환)
+157
View File
@@ -0,0 +1,157 @@
"use client";
// 푸시알림/공지 본문용 리치 에디터 (Tiptap).
// - 이미지 클립보드 paste 자동 업로드 (기존 /api/m/items/upload-image)
// - Drag&Drop 이미지 자동 업로드
// - 볼드/이탤릭/링크/리스트/제목 툴바
// - 출력: HTML 문자열 (공지 페이지에서 dangerouslySetInnerHTML 로 렌더)
// - plain text 추출: editor.getText() 또는 props.onPlainTextChange 콜백
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Link from "@tiptap/extension-link";
import Placeholder from "@tiptap/extension-placeholder";
import { useEffect, useRef } from "react";
import { Bold, Italic, Strikethrough, List, ListOrdered, Link as LinkIcon, Heading2, Quote, Undo, Redo, Image as ImageIcon } from "lucide-react";
interface Props {
value: string;
onChange: (html: string) => void;
onPlainTextChange?: (text: string) => void;
placeholder?: string;
minHeight?: string;
}
async function uploadImageFile(file: File): Promise<string | null> {
const fd = new FormData();
fd.append("file", file);
const r = await fetch("/api/m/items/upload-image", { method: "POST", body: fd });
if (!r.ok) return null;
const j = await r.json();
return j.url ?? null;
}
export function RichEditor({ value, onChange, onPlainTextChange, placeholder, minHeight = "180px" }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const editor = useEditor({
extensions: [
StarterKit,
Image.configure({ inline: false, allowBase64: false, HTMLAttributes: { class: "rounded-lg max-w-full my-2" } }),
Link.configure({ openOnClick: false, HTMLAttributes: { class: "text-emerald-700 underline" } }),
Placeholder.configure({ placeholder: placeholder || "내용을 입력하세요. 이미지 복사 → 붙여넣기 가능합니다." }),
],
content: value,
immediatelyRender: false,
editorProps: {
attributes: {
class: "prose prose-sm max-w-none focus:outline-none px-3 py-2",
style: `min-height: ${minHeight}`,
},
// 클립보드 paste / drop 이벤트 처리 — 이미지 파일 자동 업로드
handlePaste: (view, event) => {
const items = event.clipboardData?.items;
if (!items) return false;
for (const item of Array.from(items)) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
event.preventDefault();
uploadImageFile(file).then((url) => {
if (url) view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.image.create({ src: url })));
});
return true;
}
}
}
return false;
},
handleDrop: (view, event) => {
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return false;
let handled = false;
for (const file of Array.from(files)) {
if (file.type.startsWith("image/")) {
event.preventDefault();
handled = true;
uploadImageFile(file).then((url) => {
if (url) view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.image.create({ src: url })));
});
}
}
return handled;
},
},
onUpdate({ editor }) {
onChange(editor.getHTML());
onPlainTextChange?.(editor.getText());
},
});
// 외부 value 변경 시 동기화 (예: 발송 후 초기화)
useEffect(() => {
if (!editor) return;
if (value === editor.getHTML()) return;
editor.commands.setContent(value || "", { emitUpdate: false });
}, [value, editor]);
const triggerImageUpload = () => fileInputRef.current?.click();
const onPickImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file || !editor) return;
const url = await uploadImageFile(file);
if (url) editor.chain().focus().setImage({ src: url }).run();
};
const onSetLink = () => {
if (!editor) return;
const prev = editor.getAttributes("link").href as string | undefined;
const url = window.prompt("링크 URL", prev || "https://");
if (url === null) return;
if (url === "") {
editor.chain().focus().extendMarkRange("link").unsetLink().run();
} else {
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
}
};
if (!editor) return null;
const Btn = ({ active, onClick, children, title }: { active?: boolean; onClick: () => void; children: React.ReactNode; title: string }) => (
<button
type="button"
onClick={onClick}
title={title}
className={`p-1.5 rounded hover:bg-slate-200 transition-colors ${active ? "bg-slate-200 text-emerald-700" : "text-slate-600"}`}
>
{children}
</button>
);
return (
<div className="border border-slate-200 rounded-lg overflow-hidden bg-white">
{/* 툴바 */}
<div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-slate-100 bg-slate-50 flex-wrap">
<Btn title="제목" active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}><Heading2 size={15} /></Btn>
<Btn title="굵게 (Ctrl+B)" active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}><Bold size={15} /></Btn>
<Btn title="기울임 (Ctrl+I)" active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}><Italic size={15} /></Btn>
<Btn title="취소선" active={editor.isActive("strike")} onClick={() => editor.chain().focus().toggleStrike().run()}><Strikethrough size={15} /></Btn>
<div className="w-px h-5 bg-slate-200 mx-1" />
<Btn title="글머리표" active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}><List size={15} /></Btn>
<Btn title="번호 매기기" active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}><ListOrdered size={15} /></Btn>
<Btn title="인용" active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()}><Quote size={15} /></Btn>
<div className="w-px h-5 bg-slate-200 mx-1" />
<Btn title="이미지 첨부 (또는 본문에 복사붙여넣기)" onClick={triggerImageUpload}><ImageIcon size={15} /></Btn>
<Btn title="링크" active={editor.isActive("link")} onClick={onSetLink}><LinkIcon size={15} /></Btn>
<div className="w-px h-5 bg-slate-200 mx-1" />
<Btn title="실행 취소 (Ctrl+Z)" onClick={() => editor.chain().focus().undo().run()}><Undo size={15} /></Btn>
<Btn title="다시 실행 (Ctrl+Shift+Z)" onClick={() => editor.chain().focus().redo().run()}><Redo size={15} /></Btn>
<input ref={fileInputRef} type="file" accept="image/*" onChange={onPickImage} className="hidden" />
<div className="ml-auto text-[10px] text-slate-400 px-1"> (Ctrl+V)</div>
</div>
<EditorContent editor={editor} className="text-sm" />
</div>
);
}
+2 -1
View File
@@ -123,7 +123,8 @@ export async function sendFcm(tokens: string[], payload: FcmPayload): Promise<Fc
channel_id: "momo_default", channel_id: "momo_default",
sound: "default", sound: "default",
default_vibrate_timings: true, default_vibrate_timings: true,
click_action: payload.url || "/m/orders/new", // click_action 명시 X — OS 가 URL 로 해석해서 브라우저로 열어버림.
// 비워두면 앱의 LAUNCHER(MainActivity) 가 열리고, Capacitor 가 data.url 로 webview 이동 처리.
}, },
}, },
}; };