Compare commits
No commits in common. "1d38fe26bdaa807059353122cdcaf7e9874b2243" and "af57b56e6983a6fd8c21532eb2e0585e43b295ee" have entirely different histories.
1d38fe26bd
...
af57b56e69
@ -48,10 +48,3 @@ docker 또는 kubernetes를 이용하여 배포 합니다.
|
|||||||
* CSRF_TRUSTED_ORIGINS=https://www.example.com,https://butler.example.com,https://butler.example.com:8000
|
* CSRF_TRUSTED_ORIGINS=https://www.example.com,https://butler.example.com,https://butler.example.com:8000
|
||||||
* MM_URL=https://mm.example.com/hooks/${hash}
|
* MM_URL=https://mm.example.com/hooks/${hash}
|
||||||
* (기능 미구현)MINIO_URL=https://minio.example.com/
|
* (기능 미구현)MINIO_URL=https://minio.example.com/
|
||||||
|
|
||||||
## 특이사항
|
|
||||||
### markdown에디터에 삽입된 이미지 렌더링할때 404에러가 발생하지만, 실제 동작은 잘 되는 이유.
|
|
||||||
* MinIO와 연결하여 이미지 저장 기능을 사용 할 수 있도록 구현되어 있습니다.
|
|
||||||
* Bucket을 private로 설정하는 것을 선호하므로 Presigned URL로 호출해 오도록 설정되어 있습니다.
|
|
||||||
* 게시물에는 Presigned URL이 아닌 버킷에 저장된 이미지의 이름만 들어있으므로 get_presigned_url을 이용하여 Presigned URL을 호출하기전에 렌더링이 발생하는 에러 메세지입니다.
|
|
||||||
* 에러가 아니니 기능상 문제 없이 사용 가능합니다.
|
|
@ -23,8 +23,10 @@
|
|||||||
|
|
||||||
<!-- 마크다운 에디터 -->
|
<!-- 마크다운 에디터 -->
|
||||||
<h2>Contents</h2>
|
<h2>Contents</h2>
|
||||||
<textarea id="markdown-editor" name="contents" class="form-control" rows="10"></textarea>
|
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
{{ form.contents }}
|
||||||
|
</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 Post</button>
|
<button type="submit" class="btn btn-primary me-2">Create Post</button>
|
||||||
@ -60,38 +62,10 @@
|
|||||||
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");
|
const renderedContent = 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 {
|
|
||||||
// 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] = ``; // 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로 코드 블록 강조
|
||||||
@ -100,54 +74,6 @@
|
|||||||
.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 = `\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>
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
{% extends "components/base.html" %}
|
|
||||||
{% block title %}Create Post{% endblock %}
|
|
||||||
|
|
||||||
{% block main_area %}
|
|
||||||
<h1 class="pt-3">Create New Post</h1>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<form method="POST" id="post-form">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.title.label_tag }}
|
|
||||||
{{ form.title }}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.summary.label_tag }}
|
|
||||||
{{ form.summary }}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.tags.label_tag }}
|
|
||||||
{{ form.tags }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 마크다운 에디터 -->
|
|
||||||
<h2>Contents</h2>
|
|
||||||
|
|
||||||
<div class="col-md-12">
|
|
||||||
{{ form.contents }}
|
|
||||||
</div>
|
|
||||||
<!-- 버튼 -->
|
|
||||||
<div class="d-flex justify-content-end mt-4">
|
|
||||||
<button type="submit" class="btn btn-primary me-2">Create Post</button>
|
|
||||||
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
|
||||||
<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>
|
|
||||||
// 마크다운 파서 초기화
|
|
||||||
const md = window.markdownit({
|
|
||||||
highlight: function (str, lang) {
|
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
|
||||||
try {
|
|
||||||
return hljs
|
|
||||||
.highlight(str, {language: lang})
|
|
||||||
.value;
|
|
||||||
} catch (__) {}
|
|
||||||
}
|
|
||||||
return ''; // 기본 HTML 이스케이프 처리
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
|
||||||
const textarea = document.getElementById("markdown-editor");
|
|
||||||
const preview = document.getElementById("preview");
|
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
|
||||||
textarea.addEventListener("input", function () {
|
|
||||||
const markdownContent = textarea.value; // textarea 내용 가져오기
|
|
||||||
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
|
||||||
.querySelectorAll("#preview pre code")
|
|
||||||
.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,128 +0,0 @@
|
|||||||
{% extends "components/base.html" %}
|
|
||||||
{% block title %}Create Post{% endblock %}
|
|
||||||
|
|
||||||
{% block main_area %}
|
|
||||||
<h1 class="pt-3">Create New Post</h1>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<form method="POST" id="post-form">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.title.label_tag }}
|
|
||||||
{{ form.title }}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.summary.label_tag }}
|
|
||||||
{{ form.summary }}
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.tags.label_tag }}
|
|
||||||
{{ form.tags }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 마크다운 에디터 -->
|
|
||||||
<h2>Contents</h2>
|
|
||||||
<textarea id="markdown-editor" name="contents" class="form-control" rows="10"></textarea>
|
|
||||||
|
|
||||||
<!-- 버튼 -->
|
|
||||||
<div class="d-flex justify-content-end mt-4">
|
|
||||||
<button type="submit" class="btn btn-primary me-2">Create Post</button>
|
|
||||||
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
|
||||||
<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>
|
|
||||||
// 마크다운 파서 초기화
|
|
||||||
const md = window.markdownit({
|
|
||||||
highlight: function (str, lang) {
|
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
|
||||||
try {
|
|
||||||
return hljs
|
|
||||||
.highlight(str, {language: lang})
|
|
||||||
.value;
|
|
||||||
} catch (__) {}
|
|
||||||
}
|
|
||||||
return ''; // 기본 HTML 이스케이프 처리
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
|
||||||
const textarea = document.getElementById("markdown-editor");
|
|
||||||
const preview = document.getElementById("preview");
|
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
|
||||||
textarea.addEventListener("input", function () {
|
|
||||||
const markdownContent = textarea.value; // textarea 내용 가져오기
|
|
||||||
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
|
||||||
.querySelectorAll("#preview pre code")
|
|
||||||
.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 imageUrl = data.url; // 업로드된 이미지 URL
|
|
||||||
|
|
||||||
// 마크다운 에디터에 이미지 삽입
|
|
||||||
const markdownImage = `\n`;
|
|
||||||
textarea.value += markdownImage;
|
|
||||||
|
|
||||||
// 미리보기 업데이트
|
|
||||||
const markdownContent = textarea.value;
|
|
||||||
const renderedContent = md.render(markdownContent);
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
|
||||||
.querySelectorAll("#preview pre code")
|
|
||||||
.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert("Image upload failed. Please try again.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error uploading image:", error);
|
|
||||||
alert("An error occurred during image upload.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -13,7 +13,7 @@
|
|||||||
by.
|
by.
|
||||||
{{ post.author | upper }}</div>
|
{{ post.author | upper }}</div>
|
||||||
<!-- Rendered Markdown Content -->
|
<!-- Rendered Markdown Content -->
|
||||||
<div id="post-content">
|
<div>
|
||||||
{{ post.render_markdown|safe }}
|
{{ post.render_markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -50,50 +50,9 @@
|
|||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
// Function to update images with presigned URLs
|
<!-- Delete post -->
|
||||||
async function updateImagesWithPresignedUrls() {
|
<script>
|
||||||
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"); // src에 저장된 object_name
|
|
||||||
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); // Presigned URL로 src 업데이트
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching presigned URL:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 업데이트된 HTML을 다시 삽입
|
|
||||||
contentDiv.innerHTML = doc.body.innerHTML;
|
|
||||||
|
|
||||||
// Reapply syntax highlighting
|
|
||||||
document
|
|
||||||
.querySelectorAll("pre code")
|
|
||||||
.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the function when the page loads
|
|
||||||
document.addEventListener("DOMContentLoaded", updateImagesWithPresignedUrls);
|
|
||||||
|
|
||||||
// Delete post
|
|
||||||
document
|
document
|
||||||
.getElementById("delete-button")
|
.getElementById("delete-button")
|
||||||
.addEventListener("click", function () {
|
.addEventListener("click", function () {
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
|
|
||||||
{% block main_area %}
|
{% block main_area %}
|
||||||
<h1 class="pt-3">Update Post</h1>
|
<h1 class="pt-3">Update Post</h1>
|
||||||
<div class="container mt-3">
|
<div class="container">
|
||||||
<h5 class="text-danger">◇마크다운 미리보기는 Contents 편집 내용 입력시 동작합니다.</h5>
|
<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 %}
|
||||||
@ -41,113 +40,18 @@
|
|||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
<!-- 마크다운 파서 및 스크립트 -->
|
||||||
<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 ''; // 기본 HTML 이스케이프 처리
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
// 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() {
|
|
||||||
const markdownContent = textarea.value;
|
|
||||||
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 {
|
|
||||||
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] = ``;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching presigned URL:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedContent = md.render(lines.join("\n"));
|
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
|
||||||
.querySelectorAll("#preview pre code")
|
|
||||||
.forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
// 실시간 미리보기 업데이트
|
||||||
textarea.addEventListener("input", updatePreview);
|
textarea.addEventListener("input", function () {
|
||||||
|
const markdownContent = textarea.value; // textarea 내용 가져오기
|
||||||
// Ctrl+V로 이미지 붙여넣기 처리
|
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
|
||||||
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 = `\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>
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 4.2.14 on 2025-01-25 19:23
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('butler', '0003_rename_desc_ipmanagementrecord_contents'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='NoticeBoard',
|
|
||||||
),
|
|
||||||
]
|
|
@ -51,7 +51,6 @@ INSTALLED_APPS = [
|
|||||||
'nhnc_mgmt',
|
'nhnc_mgmt',
|
||||||
'mm_msg',
|
'mm_msg',
|
||||||
'ansible_manager',
|
'ansible_manager',
|
||||||
'obs_minio',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -168,7 +167,6 @@ LOGIN_REDIRECT_URL = '/'
|
|||||||
LOGOUT_REDIRECT_URL = '/'
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
# MinIO 설정
|
# MinIO 설정
|
||||||
MINIO_ENDPOINT_URL = os.environ.get('MINIO_ENDPOINT_URL', '')
|
# MINIO_STORAGE_MEDIA_URL = os.environ.get('MINIO_STORAGE_MEDIA_URL', '')
|
||||||
MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY', '')
|
# MINIO_STORAGE_ACCESS_KEY = os.environ.get('MINIO_STORAGE_ACCESS_KEY', '')
|
||||||
MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY', '')
|
# MINIO_STORAGE_SECRET_KEY = os.environ.get('MINIO_STORAGE_SECRET_KEY', '')
|
||||||
MINIO_DEFAULT_BUCKET=os.environ.get('MINIO_DEFAULT_BUCKET', '')
|
|
@ -30,7 +30,6 @@ urlpatterns = [
|
|||||||
path('nhnc_mgmt/', include('nhnc_mgmt.urls')),
|
path('nhnc_mgmt/', include('nhnc_mgmt.urls')),
|
||||||
path('mm_msg/', include('mm_msg.urls')),
|
path('mm_msg/', include('mm_msg.urls')),
|
||||||
path('ansible_manager/', include('ansible_manager.urls')),
|
path('ansible_manager/', include('ansible_manager.urls')),
|
||||||
path('obs_minio/', include('obs_minio.urls')),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ObsMinioConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'obs_minio'
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,9 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = 'obs_minio'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('upload/', views.upload_image, name='upload_image'),
|
|
||||||
path('get_presigned_url/', views.get_presigned_url, name='get_presigned_url'),
|
|
||||||
]
|
|
@ -1,76 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from minio import Minio
|
|
||||||
from minio.error import S3Error
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
import urllib3
|
|
||||||
|
|
||||||
# MinIO 설정
|
|
||||||
MINIO_ENDPOINT = settings.MINIO_ENDPOINT_URL
|
|
||||||
MINIO_ACCESS_KEY = settings.MINIO_ACCESS_KEY
|
|
||||||
MINIO_SECRET_KEY = settings.MINIO_SECRET_KEY
|
|
||||||
BUCKET_NAME = settings.MINIO_DEFAULT_BUCKET
|
|
||||||
|
|
||||||
# MinIO 클라이언트 생성
|
|
||||||
client = Minio(
|
|
||||||
MINIO_ENDPOINT,
|
|
||||||
access_key=MINIO_ACCESS_KEY,
|
|
||||||
secret_key=MINIO_SECRET_KEY,
|
|
||||||
secure=True,
|
|
||||||
http_client=urllib3.PoolManager(cert_reqs='CERT_NONE') # SSL 검증 비활성화
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def upload_image(request):
|
|
||||||
"""
|
|
||||||
이미지 업로드 및 URL 반환
|
|
||||||
"""
|
|
||||||
if request.method == 'POST' and 'image' in request.FILES:
|
|
||||||
image = request.FILES['image']
|
|
||||||
unique_filename = f"uploads/{uuid.uuid4()}_{image.name}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# MinIO에 파일 업로드
|
|
||||||
client.put_object(
|
|
||||||
bucket_name=BUCKET_NAME,
|
|
||||||
object_name=unique_filename,
|
|
||||||
data=image,
|
|
||||||
length=image.size,
|
|
||||||
content_type=image.content_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 전체 URL 생성
|
|
||||||
full_url = f"https://{MINIO_ENDPOINT}/{BUCKET_NAME}/{unique_filename}"
|
|
||||||
return JsonResponse({"url": full_url}, status=201)
|
|
||||||
|
|
||||||
except S3Error as e:
|
|
||||||
return JsonResponse({"error": str(e)}, status=500)
|
|
||||||
|
|
||||||
return JsonResponse({"error": "Invalid request"}, status=400)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def get_presigned_url(request):
|
|
||||||
"""
|
|
||||||
Presigned URL 생성 및 반환
|
|
||||||
"""
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
data = json.loads(request.body)
|
|
||||||
object_name = data.get('object_name')
|
|
||||||
|
|
||||||
if not object_name:
|
|
||||||
return JsonResponse({"error": "Missing object_name"}, status=400)
|
|
||||||
|
|
||||||
# Presigned URL 생성
|
|
||||||
presigned_url = client.presigned_get_object(BUCKET_NAME, object_name)
|
|
||||||
return JsonResponse({"presigned_url": presigned_url}, status=200)
|
|
||||||
except S3Error as e:
|
|
||||||
return JsonResponse({"error": str(e)}, status=500)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
|
||||||
|
|
||||||
return JsonResponse({"error": "Invalid request method"}, status=405)
|
|
Loading…
Reference in New Issue
Block a user