글작성 미리보기시 이미지 호출로인한 페이지 리로드 이슈 개선, css적용하여 이미지지 div넘어가는 이슈 개선
All checks were successful
Build And Test / build-and-push (push) Successful in 4m22s
All checks were successful
Build And Test / build-and-push (push) Successful in 4m22s
This commit is contained in:
8
_static/components/css/style.57aeb17465e1.css
Normal file
8
_static/components/css/style.57aeb17465e1.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/* 이미지가 div 크기 넘지 않도록 */
|
||||||
|
#post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
/*margin: 0 auto; (선택) 가운데 정렬 */
|
||||||
|
}
|
||||||
|
|
8
_static/components/css/style.css
Normal file
8
_static/components/css/style.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/* 이미지가 div 크기 넘지 않도록 */
|
||||||
|
#post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
/*margin: 0 auto; (선택) 가운데 정렬 */
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
@ -32,17 +32,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
<!-- 마크다운 파서 및 하이라이트 -->
|
||||||
<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">
|
<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)) {
|
||||||
@ -52,49 +53,34 @@
|
|||||||
.value;
|
.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");
|
||||||
|
|
||||||
// 미리보기 업데이트 함수
|
let presignedUrlMap = new Map(); // object_name => presigned_url 저장
|
||||||
async function updatePreview() {
|
|
||||||
|
// 미리보기 업데이트 (textarea 내용은 그대로, 렌더링할 때만 presigned 치환)
|
||||||
|
function updatePreview() {
|
||||||
const markdownContent = textarea.value;
|
const markdownContent = textarea.value;
|
||||||
const lines = markdownContent.split("\n");
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
const match = previewLines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const objectName = match[1];
|
const objectName = match[1];
|
||||||
try {
|
if (presignedUrlMap.has(objectName)) {
|
||||||
// Presigned URL 가져오기
|
const presignedUrl = presignedUrlMap.get(objectName);
|
||||||
const response = await fetch("/obs_minio/get_presigned_url/", {
|
previewLines[i] = ``;
|
||||||
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] = ``; // Presigned URL로 업데이트
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching presigned URL:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마크다운 렌더링 및 미리보기 업데이트
|
preview.innerHTML = md.render(previewLines.join("\n"));
|
||||||
const renderedContent = md.render(lines.join("\n"));
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
document
|
||||||
.querySelectorAll("#preview pre code")
|
.querySelectorAll("#preview pre code")
|
||||||
.forEach((block) => {
|
.forEach((block) => {
|
||||||
@ -102,10 +88,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
// 이미지 붙여넣기 처리
|
||||||
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;
|
||||||
|
|
||||||
@ -119,7 +102,6 @@
|
|||||||
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 +109,39 @@
|
|||||||
|
|
||||||
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("/")
|
.split("/")
|
||||||
.slice(-2)
|
.slice(-2)
|
||||||
.join("/");
|
.join("/");
|
||||||
|
|
||||||
// 마크다운 에디터에 이미지 삽입
|
// textarea에 object_name 삽입
|
||||||
const markdownImage = `\n`;
|
const markdownImage = `\n`;
|
||||||
textarea.value += markdownImage;
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const textBefore = textarea
|
||||||
|
.value
|
||||||
|
.substring(0, cursorPos);
|
||||||
|
const textAfter = textarea
|
||||||
|
.value
|
||||||
|
.substring(cursorPos);
|
||||||
|
textarea.value = textBefore + markdownImage + textAfter;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// presigned URL 받아서 map에 저장
|
||||||
|
const presignedResponse = await fetch("/obs_minio/get_presigned_url/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({object_name: objectName})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (presignedResponse.ok) {
|
||||||
|
const presignedData = await presignedResponse.json();
|
||||||
|
presignedUrlMap.set(objectName, presignedData.presigned_url);
|
||||||
|
}
|
||||||
|
|
||||||
// 미리보기 업데이트
|
|
||||||
updatePreview();
|
updatePreview();
|
||||||
} else {
|
} else {
|
||||||
alert("Image upload failed. Please try again.");
|
alert("Image upload failed. Please try again.");
|
||||||
@ -149,6 +153,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 입력할 때는 부드럽게 presigned 적용해서 렌더링
|
||||||
|
textarea.addEventListener("input", function () {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -43,22 +44,27 @@
|
|||||||
<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">
|
<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.highlight(str, { language: lang }).value;
|
return hljs
|
||||||
|
.highlight(str, {language: lang})
|
||||||
|
.value;
|
||||||
} catch (__) {}
|
} catch (__) {}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const textarea = document.getElementById("markdown-editor");
|
const textarea = document.getElementById("markdown-editor"); // ✅ form.contents에 id="markdown-editor" 필수
|
||||||
const preview = document.getElementById("preview");
|
const preview = document.getElementById("preview");
|
||||||
|
|
||||||
async function updatePreview() {
|
let presignedUrlMap = new Map(); // object_name => presigned_url 저장
|
||||||
|
|
||||||
|
async function generatePresignedUrls() {
|
||||||
const markdownContent = textarea.value;
|
const markdownContent = textarea.value;
|
||||||
const lines = markdownContent.split("\n");
|
const lines = markdownContent.split("\n");
|
||||||
|
|
||||||
@ -66,48 +72,71 @@
|
|||||||
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const objectName = match[1];
|
const objectName = match[1];
|
||||||
|
|
||||||
|
if (!presignedUrlMap.has(objectName)) {
|
||||||
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: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
body: JSON.stringify({object_name: objectName})
|
body: JSON.stringify({object_name: objectName})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const presignedUrl = data.presigned_url;
|
presignedUrlMap.set(objectName, data.presigned_url);
|
||||||
lines[i] = ``;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching presigned URL:", error);
|
console.error("Error fetching presigned URL:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preview.innerHTML = md.render(lines.join("\n"));
|
function updatePreview() {
|
||||||
|
const markdownContent = textarea.value;
|
||||||
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
// 코드 블록 하이라이트 적용
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
document.querySelectorAll("#preview pre code").forEach((block) => {
|
const match = previewLines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const objectName = match[1];
|
||||||
|
if (presignedUrlMap.has(objectName)) {
|
||||||
|
const presignedUrl = presignedUrlMap.get(objectName);
|
||||||
|
previewLines[i] = ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.innerHTML = md.render(previewLines.join("\n"));
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("#preview pre code")
|
||||||
|
.forEach((block) => {
|
||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지 로드되자마자 초기 미리보기 렌더링
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
await generatePresignedUrls();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 입력할 때마다 미리보기 업데이트
|
textarea.addEventListener("input", function () {
|
||||||
textarea.addEventListener("input", updatePreview);
|
updatePreview(); // 입력 시에도 presigned 치환 적용
|
||||||
|
});
|
||||||
|
|
||||||
// 이미지 붙여넣기
|
|
||||||
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) return;
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("image", file);
|
formData.append("image", file);
|
||||||
@ -121,11 +150,28 @@
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const fullImageUrl = data.url;
|
const fullImageUrl = data.url;
|
||||||
const objectName = fullImageUrl.split("/").slice(-2).join("/");
|
const objectName = fullImageUrl
|
||||||
|
.split("/")
|
||||||
|
.slice(-2)
|
||||||
|
.join("/");
|
||||||
|
|
||||||
const markdownImage = `\n`;
|
const markdownImage = `\n`;
|
||||||
textarea.value += markdownImage;
|
|
||||||
|
|
||||||
|
// 현재 커서 위치에 이미지 삽입
|
||||||
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const textBefore = textarea
|
||||||
|
.value
|
||||||
|
.substring(0, cursorPos);
|
||||||
|
const textAfter = textarea
|
||||||
|
.value
|
||||||
|
.substring(cursorPos);
|
||||||
|
textarea.value = textBefore + markdownImage + textAfter;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// 새 이미지니까 presigned 새로 받아야 함
|
||||||
|
presignedUrlMap.delete(objectName);
|
||||||
|
await generatePresignedUrls();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
} else {
|
} else {
|
||||||
alert("Image upload failed. Please try again.");
|
alert("Image upload failed. Please try again.");
|
||||||
@ -138,5 +184,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
<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">Create Notice</button>
|
<button type="submit" class="btn btn-primary me-2">Create Notice</button>
|
||||||
@ -26,17 +27,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
<!-- 마크다운 파서 및 하이라이트 -->
|
||||||
<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">
|
<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)) {
|
||||||
@ -46,49 +48,34 @@
|
|||||||
.value;
|
.value;
|
||||||
} catch (__) {}
|
} catch (__) {}
|
||||||
}
|
}
|
||||||
return ''; // 기본 HTML 이스케이프 처리
|
return '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
const textarea = document.getElementById("markdown-editor"); // form.contents에서 id="markdown-editor" 꼭 설정돼 있어야 함
|
||||||
const textarea = document.getElementById("markdown-editor");
|
|
||||||
const preview = document.getElementById("preview");
|
const preview = document.getElementById("preview");
|
||||||
|
|
||||||
// 미리보기 업데이트 함수
|
let presignedUrlMap = new Map(); // object_name => presigned_url 저장
|
||||||
async function updatePreview() {
|
|
||||||
|
// 미리보기 렌더링 (fetch 없이 빠르게)
|
||||||
|
function updatePreview() {
|
||||||
const markdownContent = textarea.value;
|
const markdownContent = textarea.value;
|
||||||
const lines = markdownContent.split("\n");
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
const match = previewLines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const objectName = match[1];
|
const objectName = match[1];
|
||||||
try {
|
if (presignedUrlMap.has(objectName)) {
|
||||||
// Presigned URL 가져오기
|
const presignedUrl = presignedUrlMap.get(objectName);
|
||||||
const response = await fetch("/obs_minio/get_presigned_url/", {
|
previewLines[i] = ``;
|
||||||
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] = ``; // Presigned URL로 업데이트
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching presigned URL:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마크다운 렌더링 및 미리보기 업데이트
|
preview.innerHTML = md.render(previewLines.join("\n"));
|
||||||
const renderedContent = md.render(lines.join("\n"));
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
document
|
||||||
.querySelectorAll("#preview pre code")
|
.querySelectorAll("#preview pre code")
|
||||||
.forEach((block) => {
|
.forEach((block) => {
|
||||||
@ -96,10 +83,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
// 이미지 붙여넣기 처리
|
||||||
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;
|
||||||
|
|
||||||
@ -113,7 +97,6 @@
|
|||||||
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
|
||||||
@ -121,17 +104,39 @@
|
|||||||
|
|
||||||
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("/")
|
.split("/")
|
||||||
.slice(-2)
|
.slice(-2)
|
||||||
.join("/");
|
.join("/");
|
||||||
|
|
||||||
// 마크다운 에디터에 이미지 삽입
|
// textarea에 object_name 삽입
|
||||||
const markdownImage = `\n`;
|
const markdownImage = `\n`;
|
||||||
textarea.value += markdownImage;
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const textBefore = textarea
|
||||||
|
.value
|
||||||
|
.substring(0, cursorPos);
|
||||||
|
const textAfter = textarea
|
||||||
|
.value
|
||||||
|
.substring(cursorPos);
|
||||||
|
textarea.value = textBefore + markdownImage + textAfter;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// presigned URL 받아서 map에 저장
|
||||||
|
const presignedResponse = await fetch("/obs_minio/get_presigned_url/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({object_name: objectName})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (presignedResponse.ok) {
|
||||||
|
const presignedData = await presignedResponse.json();
|
||||||
|
presignedUrlMap.set(objectName, presignedData.presigned_url);
|
||||||
|
}
|
||||||
|
|
||||||
// 미리보기 업데이트
|
|
||||||
updatePreview();
|
updatePreview();
|
||||||
} else {
|
} else {
|
||||||
alert("Image upload failed. Please try again.");
|
alert("Image upload failed. Please try again.");
|
||||||
@ -143,6 +148,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 입력할 때는 부드럽게 미리보기만 갱신
|
||||||
|
textarea.addEventListener("input", function () {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -43,22 +44,28 @@
|
|||||||
<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">
|
<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.highlight(str, { language: lang }).value;
|
return hljs
|
||||||
|
.highlight(str, {language: lang})
|
||||||
|
.value;
|
||||||
} catch (__) {}
|
} catch (__) {}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const textarea = document.getElementById("markdown-editor");
|
const textarea = document.getElementById("markdown-editor"); // ✅ 반드시 id="markdown-editor" 되어야 함
|
||||||
const preview = document.getElementById("preview");
|
const preview = document.getElementById("preview");
|
||||||
|
|
||||||
async function updatePreview() {
|
let presignedUrlMap = new Map(); // object_name => presigned_url 매핑
|
||||||
|
|
||||||
|
// textarea 내용에서 presigned URL을 받아오기
|
||||||
|
async function generatePresignedUrls() {
|
||||||
const markdownContent = textarea.value;
|
const markdownContent = textarea.value;
|
||||||
const lines = markdownContent.split("\n");
|
const lines = markdownContent.split("\n");
|
||||||
|
|
||||||
@ -66,39 +73,65 @@
|
|||||||
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const objectName = match[1];
|
const objectName = match[1];
|
||||||
|
|
||||||
|
if (!presignedUrlMap.has(objectName)) { // 이미 있으면 다시 안 불러옴
|
||||||
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: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
body: JSON.stringify({object_name: objectName})
|
body: JSON.stringify({object_name: objectName})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const presignedUrl = data.presigned_url;
|
presignedUrlMap.set(objectName, data.presigned_url);
|
||||||
lines[i] = ``;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching presigned URL:", error);
|
console.error("Error fetching presigned URL:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preview.innerHTML = md.render(lines.join("\n"));
|
// textarea의 내용을 presigned URL 적용해서 렌더링
|
||||||
|
function updatePreview() {
|
||||||
|
const markdownContent = textarea.value;
|
||||||
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
// 코드 블록 하이라이트 적용
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
document.querySelectorAll("#preview pre code").forEach((block) => {
|
const match = previewLines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const objectName = match[1];
|
||||||
|
if (presignedUrlMap.has(objectName)) {
|
||||||
|
const presignedUrl = presignedUrlMap.get(objectName);
|
||||||
|
previewLines[i] = ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.innerHTML = md.render(previewLines.join("\n"));
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("#preview pre code")
|
||||||
|
.forEach((block) => {
|
||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지 로드되자마자 초기 미리보기 렌더링
|
// 페이지 처음 열릴 때
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
updatePreview();
|
await generatePresignedUrls(); // presigned URL 가져오기
|
||||||
|
updatePreview(); // 그리고 미리보기 렌더링
|
||||||
});
|
});
|
||||||
|
|
||||||
// 입력할 때마다 미리보기 업데이트
|
// 키 입력할 때
|
||||||
textarea.addEventListener("input", updatePreview);
|
textarea.addEventListener("input", function () {
|
||||||
|
updatePreview(); // 항상 최신 입력을 presigned 매핑으로 렌더링
|
||||||
|
});
|
||||||
|
|
||||||
// 이미지 붙여넣기
|
// 이미지 붙여넣기
|
||||||
textarea.addEventListener("paste", async function (event) {
|
textarea.addEventListener("paste", async function (event) {
|
||||||
@ -107,7 +140,8 @@
|
|||||||
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) return;
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("image", file);
|
formData.append("image", file);
|
||||||
@ -121,12 +155,29 @@
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const fullImageUrl = data.url;
|
const fullImageUrl = data.url;
|
||||||
const objectName = fullImageUrl.split("/").slice(-2).join("/");
|
const objectName = fullImageUrl
|
||||||
|
.split("/")
|
||||||
|
.slice(-2)
|
||||||
|
.join("/");
|
||||||
|
|
||||||
const markdownImage = `\n`;
|
const markdownImage = `\n`;
|
||||||
textarea.value += markdownImage;
|
|
||||||
|
|
||||||
updatePreview();
|
// 현재 커서 위치에 붙여넣기
|
||||||
|
const cursorPos = textarea.selectionStart;
|
||||||
|
const textBefore = textarea
|
||||||
|
.value
|
||||||
|
.substring(0, cursorPos);
|
||||||
|
const textAfter = textarea
|
||||||
|
.value
|
||||||
|
.substring(cursorPos);
|
||||||
|
textarea.value = textBefore + markdownImage + textAfter;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// 새 이미지니까 presigned map 갱신 필요
|
||||||
|
presignedUrlMap.delete(objectName);
|
||||||
|
await generatePresignedUrls(); // 새로 presigned URL 받아오기
|
||||||
|
updatePreview(); // 그리고 렌더링
|
||||||
} else {
|
} else {
|
||||||
alert("Image upload failed. Please try again.");
|
alert("Image upload failed. Please try again.");
|
||||||
}
|
}
|
||||||
|
8
components/static/components/css/style.css
Normal file
8
components/static/components/css/style.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/* 이미지가 div 크기 넘지 않도록 */
|
||||||
|
#post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
/*margin: 0 auto; (선택) 가운데 정렬 */
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
@ -10,6 +11,8 @@
|
|||||||
{% include "components/_favicon.html" %}
|
{% include "components/_favicon.html" %}
|
||||||
<!-- Bootstrap CSS 추가 -->
|
<!-- Bootstrap CSS 추가 -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<!-- custom css 추가 -->
|
||||||
|
<link rel="stylesheet" href="{% static 'components/css/style.css' %}">
|
||||||
<!-- fontawesome 추가 -->
|
<!-- fontawesome 추가 -->
|
||||||
<script src="https://kit.fontawesome.com/77d81e5d17.js" crossorigin="anonymous"></script>
|
<script src="https://kit.fontawesome.com/77d81e5d17.js" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
|
Reference in New Issue
Block a user