feat(notices): 본문 textarea → Tiptap 리치 에디터 + 공지 페이지 HTML 렌더

푸시알림 게시판:
- 본문 입력을 Tiptap 위지위그로 교체
- 이미지 복붙(Ctrl+V) / 드래그앤드롭 → 자동 업로드 → img 태그 삽입
- 푸시 메시지는 HTML 태그 제거 후 plain text 로 전송

공지 상세(/m/notices/[id]):
- BODY 를 dangerouslySetInnerHTML 로 HTML 렌더링
- 관리자만 작성 가능(requireMomoAdmin) 이라 XSS 위험 낮음

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-31 00:34:33 +09:00
parent ec6bf2922f
commit b11d0df704
2 changed files with 18 additions and 8 deletions
+12 -5
View File
@@ -197,10 +197,12 @@ export default function AdminNoticesPage() {
if (!svj.success) { Swal.fire({ icon: "error", title: "저장 실패", text: svj.message }); return; }
const noticeObjid = svj.objId;
const groupNames = activeGroup && !sendAll ? [activeGroup.NAME] : [];
// 푸시 본문은 plain text — HTML 태그 제거 + 공백 압축
const plainBody = bodyText.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
const sendRes = await fetch("/api/m/admin/notices/send-push", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
noticeObjid, title, message: bodyText,
noticeObjid, title, message: plainBody,
url: linkUrl || `/m/notices/${noticeObjid}`,
userIds: sendAll ? undefined : targetUserIds,
sendAll,
@@ -390,10 +392,15 @@ export default function AdminNoticesPage() {
</div>
<div>
<label className="text-xs font-semibold text-slate-600"></label>
<textarea value={bodyText} onChange={(e) => setBodyText(e.target.value)}
placeholder="알림 내용 + 공지 페이지 본문이 됩니다." rows={6}
className="w-full px-3 py-2 rounded-lg border border-slate-200 text-sm mt-1 resize-y" />
<div className="text-[10px] text-slate-400 mt-0.5"> 240 . .</div>
<div className="mt-1">
<RichEditor
value={bodyText}
onChange={setBodyText}
placeholder="알림 내용 + 공지 페이지 본문이 됩니다. 이미지는 그냥 복사해서 붙여넣으세요 (Ctrl+V)."
minHeight="200px"
/>
</div>
<div className="text-[10px] text-slate-400 mt-0.5"> 240() . · .</div>
</div>
</div>
<div className="space-y-3">
+6 -3
View File
@@ -57,9 +57,12 @@ export default function NoticeViewPage({ params }: { params: Promise<{ id: strin
<h1 className="text-xl sm:text-2xl font-bold text-slate-900">{notice.TITLE}</h1>
<div className="text-[11px] text-slate-400">{notice.REGDATE}</div>
{notice.BODY && (
<div className="text-sm sm:text-base text-slate-700 whitespace-pre-wrap leading-relaxed pt-2 border-t border-slate-100">
{notice.BODY}
</div>
// 본문은 Tiptap 에디터의 HTML — 관리자만 작성 가능(requireMomoAdmin)이므로 XSS 위험 낮음.
// prose 클래스로 기본 위지위그 스타일 적용 (Tailwind Typography 가 없으면 일반 텍스트 렌더).
<div
className="text-sm sm:text-base text-slate-700 leading-relaxed pt-2 border-t border-slate-100 notice-body whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: notice.BODY }}
/>
)}
{notice.URL && (
<a href={notice.URL} target="_blank" rel="noreferrer"