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:
@@ -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(/ /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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user