From ec6bf2922fdbfec243878fb040e7d0a932afcf0d Mon Sep 17 00:00:00 2001 From: chpark Date: Sun, 31 May 2026 00:30:04 +0900 Subject: [PATCH] =?UTF-8?q?fix(push):=20=EC=95=8C=EB=A6=BC=20=ED=83=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=95=B1=EC=9C=BC=EB=A1=9C=20=EC=97=B4=EB=A6=BC=20?= =?UTF-8?q?(Chrome=20=EC=95=84=EB=8B=8C)=20+=20Tiptap=20=EB=A6=AC=EC=B9=98?= =?UTF-8?q?=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 핵심 수정 (사용자 보고: 알림 탭 → 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 --- .env.development | 2 +- package-lock.json | 650 ++++++++++++++++++- package.json | 5 + src/app/(main)/m/admin/notices/page.tsx | 6 +- src/components/native-push-auto-register.tsx | 27 +- src/components/rich-editor.tsx | 157 +++++ src/lib/firebase-push.ts | 3 +- 7 files changed, 844 insertions(+), 6 deletions(-) create mode 100644 src/components/rich-editor.tsx diff --git a/.env.development b/.env.development index 7695c8c..f0850de 100644 --- a/.env.development +++ b/.env.development @@ -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_SECRET="2b1f94cca798f49ff62822b01617503b019d118df9d249ee61f835a7dca1946e" NEXT_PUBLIC_APP_NAME="유통관리 ERP" diff --git a/package-lock.json b/package-lock.json index b300540..c4628aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,11 @@ "dependencies": { "@prisma/client": "^7.7.0", "@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", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", @@ -534,6 +539,34 @@ "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": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -2040,6 +2073,460 @@ "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": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2192,7 +2679,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4561,6 +5047,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -6105,6 +6600,12 @@ "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6660,6 +7161,12 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -7062,6 +7569,135 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7359,6 +7995,12 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8389,6 +9031,12 @@ "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": { "version": "3.6.7", "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", diff --git a/package.json b/package.json index 159db5e..2892cf8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "dependencies": { "@prisma/client": "^7.7.0", "@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", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", diff --git a/src/app/(main)/m/admin/notices/page.tsx b/src/app/(main)/m/admin/notices/page.tsx index 2e65b8c..45e6397 100644 --- a/src/app/(main)/m/admin/notices/page.tsx +++ b/src/app/(main)/m/admin/notices/page.tsx @@ -3,12 +3,14 @@ // 푸시알림 게시판 — 권한 관리 화면(admin-panel/AuthManagement)과 동일한 3-패널 패턴. // 좌측 : 수신자 그룹 목록 [+ 생성] // 우측 상단 : 그룹 멤버 / [추가/제거] / 전체 사용자 풀 ← 권한있는/권한없는 직원 -// 우측 하단 : 컨텐츠(제목/본문/이미지/링크) + 발송 +// 우측 하단 : 컨텐츠(제목/리치 본문/외부링크) + 발송 +// - 본문은 Tiptap 리치 에디터 (이미지 복붙/드래그 자동 업로드, 볼드/리스트/링크 등) // 발송이력은 별도 메뉴(/m/admin/notice-history)로 분리됨 — 본 화면에서 제거. import { useEffect, useState, useCallback, useMemo } from "react"; 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 { OBJID: string; NAME: string; DESCRIPTION: string | null; diff --git a/src/components/native-push-auto-register.tsx b/src/components/native-push-auto-register.tsx index 10a9a91..62b0e65 100644 --- a/src/components/native-push-auto-register.tsx +++ b/src/components/native-push-auto-register.tsx @@ -16,7 +16,13 @@ import { useEffect, useRef } from "react"; interface PN { requestPermissions: () => Promise<{ receive: "granted" | "denied" | "prompt" }>; register: () => Promise; - 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 }; + actionId?: string; } function getCap(): { isNativePlatform?: () => boolean; Plugins?: { PushNotifications?: PN } } | undefined { if (typeof window === "undefined") return undefined; @@ -40,6 +46,25 @@ export function NativePushAutoRegister() { try { if (sessionStorage.getItem(SESSION_KEY)) return; } 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 () => { try { // 1) 권한 요청 (이미 granted 면 즉시 granted 반환) diff --git a/src/components/rich-editor.tsx b/src/components/rich-editor.tsx new file mode 100644 index 0000000..f8a2b11 --- /dev/null +++ b/src/components/rich-editor.tsx @@ -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 { + 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(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) => { + 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 }) => ( + + ); + + return ( +
+ {/* 툴바 */} +
+ editor.chain().focus().toggleHeading({ level: 2 }).run()}> + editor.chain().focus().toggleBold().run()}> + editor.chain().focus().toggleItalic().run()}> + editor.chain().focus().toggleStrike().run()}> +
+ editor.chain().focus().toggleBulletList().run()}> + editor.chain().focus().toggleOrderedList().run()}> + editor.chain().focus().toggleBlockquote().run()}> +
+ + +
+ editor.chain().focus().undo().run()}> + editor.chain().focus().redo().run()}> + +
이미지 복사 → 본문 붙여넣기 (Ctrl+V)
+
+ +
+ ); +} diff --git a/src/lib/firebase-push.ts b/src/lib/firebase-push.ts index 200f795..f1ba36c 100644 --- a/src/lib/firebase-push.ts +++ b/src/lib/firebase-push.ts @@ -123,7 +123,8 @@ export async function sendFcm(tokens: string[], payload: FcmPayload): Promise