게시글 저장 방식 선택: HTML(WYSIWYG) vs Markdown
WYSIWYG(Cheditor4) → 에디터가 주는 HTML을 서버에서 XSS 방지 하여 그대로 저장/렌더.
Markdown → 서버(또는 클라이언트)에서 Markdown → HTML 변환 후 sanitize 하여 렌더.
보안: 절대 원본 HTML을 그대로 페이지에 출력하지 마세요 — bleach 같은 라이브러리로 허용 태그/속성만 필터링.
이미지/첨부: 로컬/객체저장소(S3/GCS)로 업로드하고 URL만 DB에 저장. (IPFS 같은 탈중앙 저장소도 옵션)
프론트: 좋은 타이포그래피, 가독성(폰트 크기, 행간), 표/테이블 스타일, 코드 블록/키포인트 박스 스타일을 CSS로 준비.
편의: 미리보기(Preview), 모바일 반응형, SEO 메타(og:title/desc), 캐싱(LRU 또는 CDN) 권장.
클라이언트: 에디터(Cheditor4) 또는 Markdown 에디터 → 작성된 HTML 또는 Markdown 전송
서버(FastAPI): 업로드/저장/변환/필터링 → DB(예: PostgreSQL)
정적파일/이미지: S3/GCS 또는 로컬(권장: S3)
프론트(게시글 보기): 서버에서 안전한 HTML 반환 혹은 클라이언트에서 렌더
아래 코드는 Markdown 또는 HTML(에디터) 모두 처리 가능한 예시입니다. 저장 전에 sanitize(bleach) 처리, 마크다운 변환(mistune), 이미지 업로드 핸들링 포함.
# app/main.py from fastapi import FastAPI, UploadFile, File, Form, HTTPException from pydantic import BaseModel from typing import Optional import uuid, os import bleach import markdown as md # pip install markdown from sqlalchemy.orm import Session # DB 모델/세션은 생략(사용중인 SQLAlchemy 스타일에 맞게 구현) app = FastAPI() # bleach allow lists (필요에 따라 조정) ALLOWED_TAGS = bleach.sanitizer.ALLOWED_TAGS + [ "p","pre","code","img","h1","h2","h3","h4","table","thead","tbody","tr","th","td","blockquote","ul","ol","li","strong","em","a" ] ALLOWED_ATTRIBUTES = { **bleach.sanitizer.ALLOWED_ATTRIBUTES, "img": ["src","alt","width","height"], "a": ["href","title","target","rel"] } def sanitize_html(raw_html: str) -> str: return bleach.clean(raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, strip=True) @app.post("/upload-image/") async def upload_image(file: UploadFile = File(...)): # 예: 로컬 저장 (실서비스는 S3/GCS 사용 권장) ext = os.path.splitext(file.filename)[1] filename = f"{uuid.uuid4().hex}{ext}" save_path = f"./static/uploads/{filename}" os.makedirs(os.path.dirname(save_path), exist_ok=True) with open(save_path, "wb") as f: content = await file.read() f.write(content) return {"url": f"/static/uploads/{filename}"} class PostCreate(BaseModel): title: str content: str # 에디터가 넘긴 HTML 또는 Markdown is_markdown: Optional[bool] = False @app.post("/posts/") def create_post(payload: PostCreate): raw = payload.content if payload.is_markdown: # Markdown -> HTML html = md.markdown(raw, extensions=["fenced_code","tables","codehilite"]) else: html = raw # 이미 에디터에서 생성된 HTML safe_html = sanitize_html(html) # DB에 저장 (예시, 실제는 ORM 모델 저장) # post = Post(title=payload.title, content=safe_html, ...) # db.add(post); db.commit() return {"status":"ok", "sanitized_preview": safe_html[:200]} # 게시글 조회는 DB에서 content를 꺼내 템플릿으로 렌더
보안 포인트: sanitize_html()은 꼭 적용하세요. WYSIWYG HTML에는 <script> 등 악성 스크립트가 섞일 수 있습니다.
에디터에서 작성 → HTML을 /posts/로 POST
이미지 업로드 버튼 → /upload-image/ 호출하여 URL을 받아 에디터에 삽입
작성 전 ‘미리보기(Preview)’ 기능: 클라이언트에서 미리 렌더(HTML이면 innerHTML, Markdown이면 marked.js) 또는 서버로 sanitized_preview 요청
간단한 JS 예시(에디터가 HTML 문자열을 반환한다 가정):
<form id="postForm"> <input name="title" id="title"> <!-- Cheditor 영역, 예: <div id="editor"></div> --> <button type="button" id="btnSave">저장</button> </form> <script> document.getElementById("btnSave").addEventListener("click", async ()=>{ const title = document.getElementById("title").value; const html = window.CHEditor.getHTML(); // 가상의 API const res = await fetch("/posts/", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({title, content: html, is_markdown:false}) }); const data = await res.json(); alert("저장됨"); }); </script>
클라이언트 렌더: marked.js로 Markdown → HTML 후 sanitize(DOMPurify)로 필터링 → DOM에 삽입
서버 렌더: 서버에서 Markdown → HTML → bleach로 sanitize → 템플릿에 삽입
→ 장점: 일관된 렌더링, SEO(서버사이드 렌더) 유리
<!-- templates/post_detail.html --> <!doctype html> <html> <head> <meta charset="utf-8"> <meta property="og:title" content="{{ post.title }}"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/static/css/article.css"> <title>{{ post.title }}</title> </head> <body> <div class="container article"> <h1 class="article-title">{{ post.title }}</h1> <div class="meta">작성일: {{ post.created_at }}</div> <article class="article-content"> {{ post.content|safe }} <!-- content는 이미 sanitize되어야 함 --> </article> </div> </body> </html>
{{ post.content|safe }} 를 쓸 때는 DB에 저장된 content가 서버에서 sanitize 되어 있어야 안전합니다.
아래 CSS는 가독성 중심의 심플한 스타일입니다. (폰트, 색상 등 취향에 맞게 수정)
/* static/css/article.css */ :root{ --max-width: 900px; --body-font: "Noto Sans KR", Arial, sans-serif; --accent: #2E7D32; } body{ font-family: var(--body-font); background:#fafafa; color:#111; line-height:1.75; margin:0; padding:32px; display:flex; justify-content:center; } .container.article{ width:100%; max-width:var(--max-width); background:#fff; padding:36px; border-radius:10px; box-shadow:0 6px 18px rgba(0,0,0,0.08); } .article-title{font-size:28px; margin:0 0 8px; font-weight:700;} .meta{color:#666; font-size:13px; margin-bottom:20px;} .article-content p{margin:16px 0; font-size:16px;} .article-content h2{font-size:20px; margin-top:28px;} .article-content img{max-width:100%; height:auto; display:block; margin:12px 0; border-radius:6px;} .article-content pre{ background:#0f1724; color:#e6edf3; padding:16px; overflow:auto; border-radius:8px; } /* table 스타일 (Okada vs Solaire 비교표 같은 것에 유용) */ .article-content table{ width:100%; border-collapse:collapse; margin:18px 0; } .article-content th, .article-content td{ border:1px solid #e6e6e6; padding:10px; text-align:left; } .article-content th{background:#f7f7f7; font-weight:700;} /* 강조 박스 */ .tip-box{ border-left:4px solid var(--accent); background:#f3fff5; padding:12px 16px; margin:18px 0; border-radius:6px; }
이 스타일로 테이블과 강조 박스(팁), 코드 블록이 깔끔하게 보입니다. 사용자가 올려준 긴 글(제목, 섹션, 비교표 등)에 잘 맞습니다.
타이포그래피: 본문 폰트 1618px, 행간 1.61.8 권장. 모바일 폰트 작게 설정.
요약 박스: 글 상단에 한 줄 요약과 핵심 키워드 배치.
비주얼: 대표 이미지(og:image)와 섬네일을 메타로 지정 → SNS 공유 시 예쁘게 보임.
표/비교: 사용자 요청의 Okada vs Solaire 비교표는 <table>로 표준화 → CSS로 꾸미기.
목차(TOC): 긴 글엔 자동 생성된 목차(섹션 anchor) 추가.
모바일 퍼스트: 반응형 테스트 필수.
<meta name="description" content="...">, og:title, og:description, og:image 를 동적으로 채우면 공유·검색에서 유리.
버전관리: 게시글 수정 이력(수정 전 버전 백업).
컨텐츠 경고/면책: 도박 관련 내용이면 국가 규정/면책문구 필요(사이트 정책 페이지).
자동 요약/키워드 추출: 댓글/검색 최적화에 도움.
콘텐츠 검수 워크플로우: 에디터 → 임시저장 → 검수 → 공개(Release) 방식 권장.
보안: CSRF 토큰, 파일 확장자 검사, 업로드 파일 스캔(바이러스), rate-limit 등.
에디터(Cheditor4)에서 작성 → 이미지 업로드 시 /upload-image로 파일 전송(응답은 URL)
작성 완료 → HTML(또는 Markdown)과 메타(타이틀, 썸네일, 카테고리) 서버 /posts/로 전송
서버: (Markdown이면 변환) → bleach로 sanitize → DB 저장 → 캐시(혹은 CDN) 갱신
유저가 글 열람 → 서버가 템플릿으로 렌더(서버사이드) 혹은 정적페이지(SSG) 제공
에디터에서 이미지를 업로드할 수 있도록 서버 엔드포인트 구현
서버에서 HTML/Markdown → HTML 변환 로직 구현
bleach로 sanitize 구현 (허용 태그/속성 정책 테스트)
게시글 템플릿(Jinja) + CSS(article.css) 적용 → 미리보기 확인
이미지 업로드를 S3/GCS로 변경(배포 전)
SEO 메타/OG 태그 적용
도박 관련 법적 문구 및 콘텐츠 경고/면책 추가
원하시면, 지금 제공하신 “2025 Macau Baccarat Jaetech Guide…” 글을 실제로 FastAPI에 올렸을 때 어떻게 보일지 —
서버에 저장되는 최종 sanitized HTML 예시,
Jinja 템플릿에 넣은 완성된 화면 미리보기(HTML),
또는 Cheditor4 연동용 프론트 코드 중에서 하나를 직접 만들어 드릴게요.
어떤 걸 먼저 만들어 드릴까요? (나는 확인 질문 대신 바로 샘플 HTML 미리보기를 만들어 드리는 쪽으로 진행할게요 — 원하시면 실행 파일/코드까지 바로 드립니다.)
댓글목록
등록된 댓글이 없습니다.