markdown 수정 페이지 호출 방식 변경
All checks were successful
Build And Test / build-and-push (push) Successful in 4m20s

This commit is contained in:
icurfer 2025-04-09 13:47:35 +09:00
parent fd3ab674c3
commit 06d6010c19
6 changed files with 275 additions and 87 deletions

View File

@ -42,13 +42,14 @@
<!-- Highlight.js Script --> <!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script> <script>
// Initialize Highlight.js // 문서 로드 후 동작
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
document // 코드 블럭 하이라이트
.querySelectorAll("pre code") document.querySelectorAll("pre code").forEach((block) => {
.forEach((block) => {
hljs.highlightElement(block); hljs.highlightElement(block);
}); });
await updateImagesWithPresignedUrls();
}); });
// Function to update images with presigned URLs // Function to update images with presigned URLs
@ -90,9 +91,6 @@
}); });
} }
// Call the function when the page loads
document.addEventListener("DOMContentLoaded", updateImagesWithPresignedUrls);
// Delete post // Delete post
document document
.getElementById("delete-button") .getElementById("delete-button")

View File

@ -1,10 +1,9 @@
{% extends "components/base.html" %} {% extends "components/base.html" %}
{% block title %}Update Post{% endblock %} {% block title %}post edit{% endblock %}
{% block main_area %} {% block main_area %}
<h1 class="pt-3">Update Post</h1> <h1 class="pt-3">post edit</h1>
<div class="container mt-3"> <div class="container">
<h5 class="text-danger">&#9671;마크다운 미리보기는 Contents 편집 내용 입력시 동작합니다.</h5>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-md-6"> <div class="col-md-6">
<form method="POST" id="post-form"> <form method="POST" id="post-form">
@ -27,9 +26,10 @@
<div class="col-md-12"> <div class="col-md-12">
{{ form.contents }} {{ form.contents }}
</div> </div>
<!-- 버튼 --> <!-- 버튼 -->
<div class="d-flex justify-content-end mt-4"> <div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Save Changes</button> <button type="submit" class="btn btn-primary me-2">Save</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a> <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div> </div>
</form> </form>
@ -44,25 +44,20 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script> <script>
// 마크다운 파서 초기화
const md = window.markdownit({ const md = window.markdownit({
highlight: function (str, lang) { highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs return hljs.highlight(str, { language: lang }).value;
.highlight(str, {language: lang})
.value;
} catch (__) {} } catch (__) {}
} }
return ''; // 기본 HTML 이스케이프 처리 return '';
} }
}); });
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor"); const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview"); const preview = document.getElementById("preview");
// 미리보기 업데이트 함수
async function updatePreview() { async function updatePreview() {
const markdownContent = textarea.value; const markdownContent = textarea.value;
const lines = markdownContent.split("\n"); const lines = markdownContent.split("\n");
@ -74,9 +69,7 @@
try { try {
const response = await fetch("/obs_minio/get_presigned_url/", { const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json"
},
body: JSON.stringify({ object_name: objectName }) body: JSON.stringify({ object_name: objectName })
}); });
@ -91,35 +84,35 @@
} }
} }
const renderedContent = md.render(lines.join("\n")); preview.innerHTML = md.render(lines.join("\n"));
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조 // 코드 블록 하이라이트 적용
document document.querySelectorAll("#preview pre code").forEach((block) => {
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block); hljs.highlightElement(block);
}); });
} }
// 실시간 미리보기 업데이트 // 페이지 로드되자마자 초기 미리보기 렌더링
document.addEventListener("DOMContentLoaded", function () {
updatePreview();
});
// 입력할 때마다 미리보기 업데이트
textarea.addEventListener("input", updatePreview); textarea.addEventListener("input", updatePreview);
// Ctrl+V로 이미지 붙여넣기 처리 // 이미지 붙여넣기
textarea.addEventListener("paste", async function (event) { textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items; const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) { for (const item of items) {
if (item.type.startsWith("image/")) { if (item.type.startsWith("image/")) {
const file = item.getAsFile(); const file = item.getAsFile();
if (!file) if (!file) return;
return;
const formData = new FormData(); const formData = new FormData();
formData.append("image", file); formData.append("image", file);
try { try {
// 이미지 업로드 API 호출
const response = await fetch("/obs_minio/upload/", { const response = await fetch("/obs_minio/upload/", {
method: "POST", method: "POST",
body: formData body: formData
@ -127,17 +120,12 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const fullImageUrl = data.url; // 전체 URL const fullImageUrl = data.url;
const objectName = fullImageUrl const objectName = fullImageUrl.split("/").slice(-2).join("/");
.split("/")
.slice(-2)
.join("/");
// 마크다운 에디터에 이미지 삽입
const markdownImage = `![Image](${objectName})\n`; const markdownImage = `![Image](${objectName})\n`;
textarea.value += markdownImage; textarea.value += markdownImage;
// 미리보기 업데이트
updatePreview(); updatePreview();
} else { } else {
alert("Image upload failed. Please try again."); alert("Image upload failed. Please try again.");

View File

@ -54,10 +54,38 @@
const textarea = document.getElementById("markdown-editor"); const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview"); const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트 // 미리보기 업데이트 함수
textarea.addEventListener("input", function () { async function updatePreview() {
const markdownContent = textarea.value; // textarea 내용 가져오기 const markdownContent = textarea.value;
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환 const lines = markdownContent.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/!\[Image\]\((.+)\)/);
if (match) {
const objectName = match[1];
try {
// Presigned URL 가져오기
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({object_name: objectName})
});
if (response.ok) {
const data = await response.json();
const presignedUrl = data.presigned_url;
lines[i] = `![Image](${presignedUrl})`; // Presigned URL로 업데이트
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
}
// 마크다운 렌더링 및 미리보기 업데이트
const renderedContent = md.render(lines.join("\n"));
preview.innerHTML = renderedContent; preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조 // Highlight.js로 코드 블록 강조
@ -66,6 +94,54 @@
.forEach((block) => { .forEach((block) => {
hljs.highlightElement(block); hljs.highlightElement(block);
}); });
}
// 실시간 미리보기 업데이트
textarea.addEventListener("input", updatePreview);
// Ctrl+V로 이미지 붙여넣기 처리
textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file)
return;
const formData = new FormData();
formData.append("image", file);
try {
// 이미지 업로드 API 호출
const response = await fetch("/obs_minio/upload/", {
method: "POST",
body: formData
});
if (response.ok) {
const data = await response.json();
const fullImageUrl = data.url; // 전체 URL
const objectName = fullImageUrl
.split("/")
.slice(-2)
.join("/");
// 마크다운 에디터에 이미지 삽입
const markdownImage = `![Image](${objectName})\n`;
textarea.value += markdownImage;
// 미리보기 업데이트
updatePreview();
} else {
alert("Image upload failed. Please try again.");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("An error occurred during image upload.");
}
}
}
}); });
</script> </script>
</div> </div>

View File

@ -4,15 +4,18 @@
{% block main_area %} {% block main_area %}
<!-- Title --> <!-- Title -->
<h1 class="mt-4">{{ notice.title }}</h1> <h1 class="mt-4">{{ notice.title }}</h1>
<!-- Author --> <!-- Author -->
<div class="text-muted fst-italic mb-2">{{ notice.created_at }} <div class="text-muted fst-italic mb-2">
<br> {{ notice.created_at }}<br>
by. by. {{ notice.author | upper }}
{{ notice.author | upper }}</div> </div>
<!-- Rendered Markdown Content --> <!-- Rendered Markdown Content -->
<div> <div id="post-content">
{{ notice.render_markdown|safe }} {{ notice.render_markdown|safe }}
</div> </div>
<hr> <hr>
{% if request.user == notice.author %} {% if request.user == notice.author %}
@ -28,20 +31,59 @@
<!-- Highlight.js Styles --> <!-- Highlight.js Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- Highlight.js Script --> <!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script> <script>
// Initialize Highlight.js // 문서 로드 후 동작
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
document // 코드 블럭 하이라이트
.querySelectorAll("pre code") document.querySelectorAll("pre code").forEach((block) => {
.forEach((block) => {
hljs.highlightElement(block); hljs.highlightElement(block);
}); });
await updateImagesWithPresignedUrls();
}); });
</script>
<!-- Delete Notice --> // 이미지 Presigned URL로 변환
<script> async function updateImagesWithPresignedUrls() {
const contentDiv = document.getElementById("post-content");
const contentHtml = contentDiv.innerHTML;
const parser = new DOMParser();
const doc = parser.parseFromString(contentHtml, "text/html");
const images = doc.querySelectorAll("img");
for (const img of images) {
const objectName = img.getAttribute("src");
try {
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ object_name: objectName })
});
if (response.ok) {
const data = await response.json();
img.setAttribute("src", data.presigned_url);
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
// 변경된 HTML 다시 적용
contentDiv.innerHTML = doc.body.innerHTML;
// 다시 하이라이트 적용
document.querySelectorAll("pre code").forEach((block) => {
hljs.highlightElement(block);
});
}
// Delete 버튼 클릭 이벤트
document document
.getElementById("delete-button") .getElementById("delete-button")
.addEventListener("click", function () { .addEventListener("click", function () {

View File

@ -1,10 +1,10 @@
{% extends "components/base.html" %} {% extends "components/base.html" %}
{% block title %}Update Post{% endblock %} {% block title %}notice edit{% endblock %}
{% block main_area %} {% block main_area %}
<h1 class="pt-3">Update Post</h1> <h1 class="pt-3">notice edit</h1>
<div class="container"> <div class="container">
<div class="row"> <div class="row mt-3">
<div class="col-md-6"> <div class="col-md-6">
<form method="POST" id="post-form"> <form method="POST" id="post-form">
{% csrf_token %} {% csrf_token %}
@ -26,10 +26,11 @@
<div class="col-md-12"> <div class="col-md-12">
{{ form.contents }} {{ form.contents }}
</div> </div>
<!-- 버튼 --> <!-- 버튼 -->
<div class="d-flex justify-content-end mt-4"> <div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Save Changes</button> <button type="submit" class="btn btn-primary me-2">Save</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a> <a href="{% url 'board_notice:notice_list' %}" class="btn btn-secondary">Cancel</a>
</div> </div>
</form> </form>
</div> </div>
@ -40,18 +41,101 @@
<!-- 마크다운 파서 및 스크립트 --> <!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script> <script>
// 마크다운 파서 초기화 const md = window.markdownit({
const md = window.markdownit(); highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (__) {}
}
return '';
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor"); const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview"); const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트 async function updatePreview() {
textarea.addEventListener("input", function () { const markdownContent = textarea.value;
const markdownContent = textarea.value; // textarea 내용 가져오기 const lines = markdownContent.split("\n");
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/!\[Image\]\((.+)\)/);
if (match) {
const objectName = match[1];
try {
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ object_name: objectName })
});
if (response.ok) {
const data = await response.json();
const presignedUrl = data.presigned_url;
lines[i] = `![Image](${presignedUrl})`;
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
}
preview.innerHTML = md.render(lines.join("\n"));
// 코드 블록 하이라이트 적용
document.querySelectorAll("#preview pre code").forEach((block) => {
hljs.highlightElement(block);
});
}
// 페이지 로드되자마자 초기 미리보기 렌더링
document.addEventListener("DOMContentLoaded", function () {
updatePreview();
});
// 입력할 때마다 미리보기 업데이트
textarea.addEventListener("input", updatePreview);
// 이미지 붙여넣기
textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file) return;
const formData = new FormData();
formData.append("image", file);
try {
const response = await fetch("/obs_minio/upload/", {
method: "POST",
body: formData
});
if (response.ok) {
const data = await response.json();
const fullImageUrl = data.url;
const objectName = fullImageUrl.split("/").slice(-2).join("/");
const markdownImage = `![Image](${objectName})\n`;
textarea.value += markdownImage;
updatePreview();
} else {
alert("Image upload failed. Please try again.");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("An error occurred during image upload.");
}
}
}
}); });
</script> </script>
</div> </div>

View File

@ -1 +1 @@
dev_0.0.34 dev_0.0.35