Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
65cec0a1cb | |||
67a3bc05c7 | |||
e4e6dab226 | |||
f8ff4ac4c7 | |||
f860373c70 | |||
2796994608 | |||
da71282a05 | |||
99a3db95a8 | |||
ff2e624fc8 | |||
422c1a6341 | |||
06d6010c19 | |||
fd3ab674c3 | |||
0b0343d8ce | |||
4e66b1b957 | |||
c12d1b527c | |||
fb02b8dc8d | |||
928758c20d | |||
a65758a9a8 | |||
72b1dd8d37 | |||
7d1c508828 | |||
fc8bfc9db6 | |||
aacd0e47d2 |
@ -49,9 +49,5 @@ docker 또는 kubernetes를 이용하여 배포 합니다.
|
|||||||
* 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에러가 발생하지만, 실제 동작은 잘 되는 이유.
|
* https://butler.icurfer.com/notice/
|
||||||
* MinIO와 연결하여 이미지 저장 기능을 사용 할 수 있도록 구현되어 있습니다.
|
|
||||||
* Bucket을 private로 설정하는 것을 선호하므로 Presigned URL로 호출해 오도록 설정되어 있습니다.
|
|
||||||
* 게시물에는 Presigned URL이 아닌 버킷에 저장된 이미지의 이름만 들어있으므로 get_presigned_url을 이용하여 Presigned URL을 호출하기전에 렌더링이 발생하는 에러 메세지입니다.
|
|
||||||
* 에러가 아니니 기능상 문제 없이 사용 가능합니다.
|
|
||||||
|
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
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-04-09 13:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ansible_manager', '0003_ansiblejob_delete_ansibletask'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ansiblejob',
|
||||||
|
name='inventory_content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ansiblejob',
|
||||||
|
name='playbook_content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -11,8 +11,8 @@ class AnsibleJob(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
playbook_content = models.TextField(help_text="Ansible Playbook YAML 내용")
|
playbook_content = models.TextField(blank=True, null=True) # Ansible Playbook YAML 내용
|
||||||
inventory_content = models.TextField(help_text="Ansible Inventory 내용")
|
inventory_content = models.TextField(blank=True, null=True) # Ansible Inventory 내용
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
||||||
|
@ -6,12 +6,13 @@
|
|||||||
<label for="id_name">Name:</label>
|
<label for="id_name">Name:</label>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
|
|
||||||
<label for="id_inventory_content">Inventory content:</label>
|
|
||||||
{{ form.inventory_content }}
|
|
||||||
|
|
||||||
<label for="id_playbook_content">Playbook content:</label>
|
<label for="id_playbook_content">Playbook content:</label>
|
||||||
{{ form.playbook_content }}
|
{{ form.playbook_content }}
|
||||||
|
|
||||||
|
<label for="id_inventory_content">Inventory content:</label>
|
||||||
|
{{ form.inventory_content }}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Create</button>
|
<button type="submit" class="btn btn-primary">Create</button>
|
||||||
|
<a href="{% url 'ansible_manager:job_list' %}" class="btn btn-secondary">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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")
|
||||||
|
@ -1,19 +1,39 @@
|
|||||||
{% extends "components/base.html" %}
|
{% extends "components/base.html" %}
|
||||||
|
|
||||||
{% block title %}Post{% endblock %}
|
{% block title %}Blog Posts{% endblock %}
|
||||||
|
|
||||||
{% block main_area %}
|
{% block main_area %}
|
||||||
<h1 class="pt-3">Blog Posts</h1>
|
<h1 class="pt-3 mb-4 fw-bold">📝 Latest Posts</h1>
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<a href="{% url 'blog:create_post' %}" class="btn btn-primary mb-3">Create New Post</a>
|
{% if request.user.is_authenticated %}
|
||||||
{% endif %}
|
<div class="mb-4">
|
||||||
<ul class="list-group">
|
<a href="{% url 'blog:create_post' %}" class="btn btn-success">
|
||||||
{% for post in posts %}
|
<i class="bi bi-pencil-square"></i> Write a New Post
|
||||||
<li class="list-group-item">
|
</a>
|
||||||
<h5>{{ post.title }}</h5>
|
</div>
|
||||||
<p>{{ post.summary }}</p>
|
{% endif %}
|
||||||
<a href="{{ post.get_absolute_url }}" class="btn btn-secondary">Read More</a>
|
|
||||||
</li>
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||||
{% endfor %}
|
{% for post in posts %}
|
||||||
</ul>
|
<div class="col">
|
||||||
{% endblock %}
|
<div class="card h-100 shadow-sm">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h5 class="card-title fw-bold">{{ post.title }}</h5>
|
||||||
|
<p class="card-text text-muted">{{ post.summary }}</p>
|
||||||
|
<p class="card-text text-muted">{{ post.contents|truncatechars:50 }}</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="{{ post.get_absolute_url }}" class="btn btn-outline-primary w-100">
|
||||||
|
Read More →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-end small text-muted">
|
||||||
|
{{ post.author }} · {{ post.created_at|date:"Y-m-d" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted">No posts available yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
@ -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">◇마크다운 미리보기는 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,13 +26,15 @@
|
|||||||
<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>
|
||||||
</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,8 +44,8 @@
|
|||||||
<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)) {
|
||||||
@ -54,16 +55,16 @@
|
|||||||
.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() {
|
|
||||||
|
async function generatePresignedUrls() {
|
||||||
const markdownContent = textarea.value;
|
const markdownContent = textarea.value;
|
||||||
const lines = markdownContent.split("\n");
|
const lines = markdownContent.split("\n");
|
||||||
|
|
||||||
@ -71,30 +72,47 @@
|
|||||||
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const objectName = match[1];
|
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) {
|
if (!presignedUrlMap.has(objectName)) {
|
||||||
const data = await response.json();
|
try {
|
||||||
const presignedUrl = data.presigned_url;
|
const response = await fetch("/obs_minio/get_presigned_url/", {
|
||||||
lines[i] = ``;
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({object_name: objectName})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
presignedUrlMap.set(objectName, data.presigned_url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching presigned URL:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.error("Error fetching presigned URL:", error);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
const markdownContent = textarea.value;
|
||||||
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
|
const match = previewLines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const objectName = match[1];
|
||||||
|
if (presignedUrlMap.has(objectName)) {
|
||||||
|
const presignedUrl = presignedUrlMap.get(objectName);
|
||||||
|
previewLines[i] = ``;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedContent = md.render(lines.join("\n"));
|
preview.innerHTML = md.render(previewLines.join("\n"));
|
||||||
preview.innerHTML = renderedContent;
|
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
document
|
||||||
.querySelectorAll("#preview pre code")
|
.querySelectorAll("#preview pre code")
|
||||||
.forEach((block) => {
|
.forEach((block) => {
|
||||||
@ -102,10 +120,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
textarea.addEventListener("input", updatePreview);
|
await generatePresignedUrls();
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.addEventListener("input", function () {
|
||||||
|
updatePreview(); // 입력 시에도 presigned 치환 적용
|
||||||
|
});
|
||||||
|
|
||||||
// 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 +142,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 +149,29 @@
|
|||||||
|
|
||||||
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("/");
|
||||||
|
|
||||||
// 마크다운 에디터에 이미지 삽입
|
|
||||||
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.");
|
||||||
@ -150,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,26 +48,114 @@
|
|||||||
.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 저장
|
||||||
textarea.addEventListener("input", function () {
|
|
||||||
const markdownContent = textarea.value; // textarea 내용 가져오기
|
// 미리보기 렌더링 (fetch 없이 빠르게)
|
||||||
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
|
function updatePreview() {
|
||||||
preview.innerHTML = renderedContent;
|
const markdownContent = textarea.value;
|
||||||
|
const lines = markdownContent.split("\n");
|
||||||
|
const previewLines = [...lines];
|
||||||
|
|
||||||
|
for (let i = 0; i < previewLines.length; i++) {
|
||||||
|
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"));
|
||||||
|
|
||||||
// Highlight.js로 코드 블록 강조
|
|
||||||
document
|
document
|
||||||
.querySelectorAll("#preview pre code")
|
.querySelectorAll("#preview pre code")
|
||||||
.forEach((block) => {
|
.forEach((block) => {
|
||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 붙여넣기 처리
|
||||||
|
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("/");
|
||||||
|
|
||||||
|
// textarea에 object_name 삽입
|
||||||
|
const markdownImage = `\n`;
|
||||||
|
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();
|
||||||
|
} else {
|
||||||
|
alert("Image upload failed. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
alert("An error occurred during image upload.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 입력할 때는 부드럽게 미리보기만 갱신
|
||||||
|
textarea.addEventListener("input", function () {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
updatePreview();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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,29 +31,68 @@
|
|||||||
|
|
||||||
<!-- 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 () {
|
||||||
const confirmed = confirm("Are you sure you want to delete this Notice?");
|
const confirmed = confirm("Are you sure you want to delete this Notice?");
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
document
|
document
|
||||||
.getElementById("delete-form")
|
.getElementById("delete-form")
|
||||||
.submit();
|
.submit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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,13 +26,15 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
@ -40,18 +42,151 @@
|
|||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
<!-- 마크다운 파서 및 스크립트 -->
|
||||||
<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>
|
||||||
<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>
|
||||||
const md = window.markdownit();
|
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
<script>
|
||||||
const textarea = document.getElementById("markdown-editor");
|
const md = window.markdownit({
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs
|
||||||
|
.highlight(str, {language: lang})
|
||||||
|
.value;
|
||||||
|
} catch (__) {}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const textarea = document.getElementById("markdown-editor"); // ✅ 반드시 id="markdown-editor" 되어야 함
|
||||||
const preview = document.getElementById("preview");
|
const preview = document.getElementById("preview");
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
let presignedUrlMap = new Map(); // object_name => presigned_url 매핑
|
||||||
|
|
||||||
|
// textarea 내용에서 presigned URL을 받아오기
|
||||||
|
async function generatePresignedUrls() {
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (!presignedUrlMap.has(objectName)) { // 이미 있으면 다시 안 불러옴
|
||||||
|
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();
|
||||||
|
presignedUrlMap.set(objectName, data.presigned_url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching presigned URL:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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++) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 처음 열릴 때
|
||||||
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
|
await generatePresignedUrls(); // presigned URL 가져오기
|
||||||
|
updatePreview(); // 그리고 미리보기 렌더링
|
||||||
|
});
|
||||||
|
|
||||||
|
// 키 입력할 때
|
||||||
textarea.addEventListener("input", function () {
|
textarea.addEventListener("input", function () {
|
||||||
const markdownContent = textarea.value; // textarea 내용 가져오기
|
updatePreview(); // 항상 최신 입력을 presigned 매핑으로 렌더링
|
||||||
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 {
|
||||||
|
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 = `\n`;
|
||||||
|
|
||||||
|
// 현재 커서 위치에 붙여넣기
|
||||||
|
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 {
|
||||||
|
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>
|
||||||
|
@ -7,7 +7,7 @@ class IPManagementRecord(models.Model):
|
|||||||
network_nm = models.CharField(max_length=100) # NETWORK_NM
|
network_nm = models.CharField(max_length=100) # NETWORK_NM
|
||||||
ip_addrs = models.CharField(max_length=50) # IP_ADDRS
|
ip_addrs = models.CharField(max_length=50) # IP_ADDRS
|
||||||
svr_nm = models.CharField(max_length=100) # SVR_NM
|
svr_nm = models.CharField(max_length=100) # SVR_NM
|
||||||
contents = models.TextField(blank=True, null=True) # DESC
|
contents = models.TextField(blank=True, null=True) # desc -> contents
|
||||||
remark = models.TextField(blank=True, null=True) # REMARK
|
remark = models.TextField(blank=True, null=True) # REMARK
|
||||||
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
|
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
|
||||||
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
|
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
|
||||||
|
200
butler/templates/butler/_unused_ip_mgmt.html
Normal file
200
butler/templates/butler/_unused_ip_mgmt.html
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
{% extends "components/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}IP Management{% endblock %}
|
||||||
|
|
||||||
|
{% block main_area %}
|
||||||
|
<h2 class="fw-bold pt-3 pb-2">IP 관리 대장</h2>
|
||||||
|
{% if not request.user.is_authenticated %}
|
||||||
|
<p class="text-danger">비로그인 익명사용자로 접근 중입니다.
|
||||||
|
<br>로그인시 로그인 사용자가 등록한 데이터만 조회됩니다.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<!-- 검색 폼 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<form method="get" class="mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="var_search" class="form-control" placeholder="Search by Author or Network Name..."
|
||||||
|
value="{{ var_search }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- IP 레코드 목록 -->
|
||||||
|
<form id="recordForm" method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table class="table table-striped table-hover table-bordered">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Select</th>
|
||||||
|
<th scope="col">Network Name</th>
|
||||||
|
<th scope="col">Location</th>
|
||||||
|
<th scope="col">Server Name</th>
|
||||||
|
<th scope="col">IP Address</th>
|
||||||
|
<th scope="col">Remark</th>
|
||||||
|
<th scope="col">Author</th>
|
||||||
|
<th scope="col">Updated At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in records %}
|
||||||
|
<tr data-record-id="{{ record.id }}">
|
||||||
|
<td class="text-center">
|
||||||
|
<input type="checkbox" name="selected_records" value="{{ record.id }}"
|
||||||
|
class="form-check-input record-checkbox">
|
||||||
|
</td>
|
||||||
|
<td>{{ record.network_nm }}</td>
|
||||||
|
<td>{{ record.contents }}</td>
|
||||||
|
<td>{{ record.svr_nm }}</td>
|
||||||
|
<td>{{ record.ip_addrs }}</td>
|
||||||
|
<td>{{ record.remark }}</td>
|
||||||
|
<td>{{ record.author }}</td>
|
||||||
|
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 수정 모달 -->
|
||||||
|
<div class="modal fade" id="editDataModal-{{ record.id }}" tabindex="-1"
|
||||||
|
aria-labelledby="editDataModalLabel-{{ record.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning text-dark">
|
||||||
|
<h5 class="modal-title" id="editDataModalLabel-{{ record.id }}">Edit IP Record</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- 데이터 수정 폼 -->
|
||||||
|
<form id="editDataForm-{{ record.id }}" method="post" action="{% url 'butler:ip_mgmt_edit' record.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="networkNm-{{ record.id }}" class="form-label">Network Name</label>
|
||||||
|
<input type="text" class="form-control" id="networkNm-{{ record.id }}" name="network_nm"
|
||||||
|
value="{{ record.network_nm }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ipAddrs-{{ record.id }}" class="form-label">IP Address</label>
|
||||||
|
<input type="text" class="form-control" id="ipAddrs-{{ record.id }}" name="ip_addrs"
|
||||||
|
value="{{ record.ip_addrs }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="svrNm-{{ record.id }}" class="form-label">Server Name</label>
|
||||||
|
<input type="text" class="form-control" id="svrNm-{{ record.id }}" name="svr_nm"
|
||||||
|
value="{{ record.svr_nm }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="contents-{{ record.id }}" class="form-label">Location</label>
|
||||||
|
<textarea class="form-control" id="contents-{{ record.id }}" name="contents">{{ record.contents }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="remark-{{ record.id }}" class="form-label">Remark</label>
|
||||||
|
<textarea class="form-control" id="remark-{{ record.id }}"
|
||||||
|
name="remark">{{ record.remark }}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">No records found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<!-- 버튼 컨테이너 -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<!-- 데이터 등록 버튼 -->
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addDataModal">
|
||||||
|
<i class="bi bi-plus-lg"></i>
|
||||||
|
Add New IP Record
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 전체 삭제 버튼 -->
|
||||||
|
<button type="submit" formaction="{% url 'butler:ip_mgmt_delete' %}" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 수정 버튼 -->
|
||||||
|
<button type="button" id="editSelectedButton" class="btn btn-warning" disabled="disabled">
|
||||||
|
<i class="bi bi-pencil-square"></i>
|
||||||
|
Edit Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 데이터 등록 모달 -->
|
||||||
|
<div class="modal fade" id="addDataModal" tabindex="-1" aria-labelledby="addDataModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title" id="addDataModalLabel">Add New IP Record</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- 데이터 등록 폼 -->
|
||||||
|
<form id="addDataForm" method="post" action="{% url 'butler:ip_mgmt_add' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="networkNm" class="form-label">Network Name</label>
|
||||||
|
<input type="text" class="form-control" id="networkNm" name="network_nm" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ipAddrs" class="form-label">IP Address</label>
|
||||||
|
<input type="text" class="form-control" id="ipAddrs" name="ip_addrs" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="svrNm" class="form-label">Server Name</label>
|
||||||
|
<input type="text" class="form-control" id="svrNm" name="svr_nm" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="contents" class="form-label">Location</label>
|
||||||
|
<textarea class="form-control" id="contents" name="contents"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="remark" class="form-label">Remark</label>
|
||||||
|
<textarea class="form-control" id="remark" name="remark"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const editButton = document.getElementById('editSelectedButton');
|
||||||
|
const checkboxes = document.querySelectorAll('.record-checkbox');
|
||||||
|
|
||||||
|
// Event listener to enable/disable the edit button
|
||||||
|
document.addEventListener('change', function () {
|
||||||
|
const selected = [...checkboxes].filter(checkbox => checkbox.checked);
|
||||||
|
if (selected.length === 1) {
|
||||||
|
editButton.disabled = false;
|
||||||
|
editButton.dataset.recordId = selected[0].value;
|
||||||
|
} else {
|
||||||
|
editButton.disabled = true;
|
||||||
|
delete editButton.dataset.recordId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener to open the edit modal
|
||||||
|
editButton.addEventListener('click', function () {
|
||||||
|
const recordId = editButton.dataset.recordId;
|
||||||
|
if (recordId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById(`editDataModal-${recordId}`));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -3,198 +3,183 @@
|
|||||||
{% block title %}IP Management{% endblock %}
|
{% block title %}IP Management{% endblock %}
|
||||||
|
|
||||||
{% block main_area %}
|
{% block main_area %}
|
||||||
<h2 class="fw-bold pt-3 pb-2">IP 관리 대장</h2>
|
<h2 class="fw-bold pt-3 pb-2">IP 관리 대장</h2>
|
||||||
{% if not request.user.is_authenticated %}
|
{% if not request.user.is_authenticated %}
|
||||||
<p class="text-danger">비로그인 익명사용자로 접근 중입니다.
|
<p class="text-danger">비로그인 익명사용자로 접근 중입니다.
|
||||||
<br>로그인시 로그인 사용자가 등록한 데이터만 조회됩니다.
|
<br>로그인시 로그인 사용자가 등록한 데이터만 조회됩니다.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- 검색 폼 -->
|
<!-- 검색 폼 -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4">
|
<div class="col-4"></div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
<div class="col-4">
|
||||||
|
<form method="get" class="mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="var_search" class="form-control" placeholder="Search by Author or Network Name..." value="{{ var_search }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<form method="get" class="mb-3">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="var_search" class="form-control" placeholder="Search by Author or Network Name..."
|
|
||||||
value="{{ var_search }}">
|
|
||||||
<button type="submit" class="btn btn-outline-primary">Search</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- IP 레코드 목록 -->
|
|
||||||
<form id="recordForm" method="post" action="">
|
|
||||||
{% csrf_token %}
|
|
||||||
<table class="table table-striped table-hover table-bordered">
|
|
||||||
<thead class="table-dark">
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Select</th>
|
|
||||||
<th scope="col">Network Name</th>
|
|
||||||
<th scope="col">IP Address</th>
|
|
||||||
<th scope="col">Server Name</th>
|
|
||||||
<th scope="col">Description</th>
|
|
||||||
<th scope="col">Remark</th>
|
|
||||||
<th scope="col">Author</th>
|
|
||||||
<th scope="col">Updated At</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for record in records %}
|
|
||||||
<tr data-record-id="{{ record.id }}">
|
|
||||||
<td class="text-center">
|
|
||||||
<input type="checkbox" name="selected_records" value="{{ record.id }}"
|
|
||||||
class="form-check-input record-checkbox">
|
|
||||||
</td>
|
|
||||||
<td>{{ record.network_nm }}</td>
|
|
||||||
<td>{{ record.ip_addrs }}</td>
|
|
||||||
<td>{{ record.svr_nm }}</td>
|
|
||||||
<td>{{ record.desc }}</td>
|
|
||||||
<td>{{ record.remark }}</td>
|
|
||||||
<td>{{ record.author }}</td>
|
|
||||||
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- 수정 모달 -->
|
<!-- IP 레코드 목록 -->
|
||||||
<div class="modal fade" id="editDataModal-{{ record.id }}" tabindex="-1"
|
<form id="recordForm" method="post" action="">
|
||||||
aria-labelledby="editDataModalLabel-{{ record.id }}" aria-hidden="true">
|
{% csrf_token %}
|
||||||
<div class="modal-dialog">
|
<table class="table table-striped table-hover table-bordered">
|
||||||
<div class="modal-content">
|
<thead class="table-dark">
|
||||||
<div class="modal-header bg-warning text-dark">
|
<tr>
|
||||||
<h5 class="modal-title" id="editDataModalLabel-{{ record.id }}">Edit IP Record</h5>
|
<th scope="col">Select</th>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<th scope="col">Network Name</th>
|
||||||
</div>
|
<th scope="col">Server Name</th>
|
||||||
<div class="modal-body">
|
<th scope="col">Location</th>
|
||||||
<!-- 데이터 수정 폼 -->
|
<th scope="col">IP Address</th>
|
||||||
<form id="editDataForm-{{ record.id }}" method="post" action="{% url 'butler:ip_mgmt_edit' record.id %}">
|
<th scope="col">Remark</th>
|
||||||
{% csrf_token %}
|
<th scope="col">Author</th>
|
||||||
<div class="mb-3">
|
<th scope="col">Updated At</th>
|
||||||
<label for="networkNm-{{ record.id }}" class="form-label">Network Name</label>
|
</tr>
|
||||||
<input type="text" class="form-control" id="networkNm-{{ record.id }}" name="network_nm"
|
</thead>
|
||||||
value="{{ record.network_nm }}" required="required">
|
<tbody>
|
||||||
</div>
|
{% for record in records %}
|
||||||
<div class="mb-3">
|
<tr data-record-id="{{ record.id }}">
|
||||||
<label for="ipAddrs-{{ record.id }}" class="form-label">IP Address</label>
|
<td class="text-center">
|
||||||
<input type="text" class="form-control" id="ipAddrs-{{ record.id }}" name="ip_addrs"
|
<input type="checkbox" name="selected_records" value="{{ record.id }}" class="form-check-input record-checkbox">
|
||||||
value="{{ record.ip_addrs }}" required="required">
|
</td>
|
||||||
</div>
|
<td>{{ record.network_nm }}</td>
|
||||||
<div class="mb-3">
|
<td>{{ record.svr_nm }}</td>
|
||||||
<label for="svrNm-{{ record.id }}" class="form-label">Server Name</label>
|
<td>{{ record.contents }}</td>
|
||||||
<input type="text" class="form-control" id="svrNm-{{ record.id }}" name="svr_nm"
|
<td>{{ record.ip_addrs }}</td>
|
||||||
value="{{ record.svr_nm }}" required="required">
|
<td>{{ record.remark }}</td>
|
||||||
</div>
|
<td>{{ record.author }}</td>
|
||||||
<div class="mb-3">
|
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
|
||||||
<label for="desc-{{ record.id }}" class="form-label">Description</label>
|
</tr>
|
||||||
<textarea class="form-control" id="desc-{{ record.id }}" name="desc">{{ record.desc }}</textarea>
|
{% empty %}
|
||||||
</div>
|
<tr>
|
||||||
<div class="mb-3">
|
<td colspan="9" class="text-center">No records found.</td>
|
||||||
<label for="remark-{{ record.id }}" class="form-label">Remark</label>
|
</tr>
|
||||||
<textarea class="form-control" id="remark-{{ record.id }}"
|
{% endfor %}
|
||||||
name="remark">{{ record.remark }}</textarea>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
{% if request.user.is_authenticated %}
|
||||||
</form>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
</div>
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addDataModal">
|
||||||
|
<i class="bi bi-plus-lg"></i>
|
||||||
|
Add New IP Record
|
||||||
|
</button>
|
||||||
|
<button type="submit" formaction="{% url 'butler:ip_mgmt_delete' %}" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
<button type="button" id="editSelectedButton" class="btn btn-warning" disabled="disabled">
|
||||||
|
<i class="bi bi-pencil-square"></i>
|
||||||
|
Edit Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 수정 모달들 (form 중첩 제거) -->
|
||||||
|
{% for record in records %}
|
||||||
|
<div class="modal fade" id="editDataModal-{{ record.id }}" tabindex="-1" aria-labelledby="editDataModalLabel-{{ record.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning text-dark">
|
||||||
|
<h5 class="modal-title" id="editDataModalLabel-{{ record.id }}">Edit IP Record</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editDataForm-{{ record.id }}" method="post" action="{% url 'butler:ip_mgmt_edit' record.id %}{% if var_search %}?var_search={{ var_search }}{% endif %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if var_search %}<input type="hidden" name="var_search" value="{{ var_search }}">{% endif %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="networkNm-{{ record.id }}" class="form-label">Network Name</label>
|
||||||
|
<input type="text" class="form-control" id="networkNm-{{ record.id }}" name="network_nm" value="{{ record.network_nm }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ipAddrs-{{ record.id }}" class="form-label">IP Address</label>
|
||||||
|
<input type="text" class="form-control" id="ipAddrs-{{ record.id }}" name="ip_addrs" value="{{ record.ip_addrs }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="svrNm-{{ record.id }}" class="form-label">Server Name</label>
|
||||||
|
<input type="text" class="form-control" id="svrNm-{{ record.id }}" name="svr_nm" value="{{ record.svr_nm }}" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="contents-{{ record.id }}" class="form-label">Location</label>
|
||||||
|
<textarea class="form-control" id="contents-{{ record.id }}" name="contents">{{ record.contents }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="remark-{{ record.id }}" class="form-label">Remark</label>
|
||||||
|
<textarea class="form-control" id="remark-{{ record.id }}" name="remark">{{ record.remark }}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
</div>
|
||||||
<tr>
|
{% endfor %}
|
||||||
<td colspan="9" class="text-center">No records found.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<!-- 버튼 컨테이너 -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<!-- 데이터 등록 버튼 -->
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addDataModal">
|
|
||||||
<i class="bi bi-plus-lg"></i>
|
|
||||||
Add New IP Record
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 전체 삭제 버튼 -->
|
<!-- 등록 모달 -->
|
||||||
<button type="submit" formaction="{% url 'butler:ip_mgmt_delete' %}" class="btn btn-danger">
|
<div class="modal fade" id="addDataModal" tabindex="-1" aria-labelledby="addDataModalLabel" aria-hidden="true">
|
||||||
<i class="bi bi-trash"></i>
|
<div class="modal-dialog">
|
||||||
Delete Selected
|
<div class="modal-content">
|
||||||
</button>
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title" id="addDataModalLabel">Add New IP Record</h5>
|
||||||
<!-- 수정 버튼 -->
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
<button type="button" id="editSelectedButton" class="btn btn-warning" disabled="disabled">
|
</div>
|
||||||
<i class="bi bi-pencil-square"></i>
|
<div class="modal-body">
|
||||||
Edit Selected
|
<form id="addDataForm" method="post" action="{% url 'butler:ip_mgmt_add' %}">
|
||||||
</button>
|
{% csrf_token %}
|
||||||
</div>
|
<div class="mb-3">
|
||||||
{% endif %}
|
<label for="networkNm" class="form-label">Network Name</label>
|
||||||
</form>
|
<input type="text" class="form-control" id="networkNm" name="network_nm" required="required">
|
||||||
|
</div>
|
||||||
<!-- 데이터 등록 모달 -->
|
<div class="mb-3">
|
||||||
<div class="modal fade" id="addDataModal" tabindex="-1" aria-labelledby="addDataModalLabel" aria-hidden="true">
|
<label for="ipAddrs" class="form-label">IP Address</label>
|
||||||
<div class="modal-dialog">
|
<input type="text" class="form-control" id="ipAddrs" name="ip_addrs" required="required">
|
||||||
<div class="modal-content">
|
</div>
|
||||||
<div class="modal-header bg-success text-white">
|
<div class="mb-3">
|
||||||
<h5 class="modal-title" id="addDataModalLabel">Add New IP Record</h5>
|
<label for="svrNm" class="form-label">Server Name</label>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<input type="text" class="form-control" id="svrNm" name="svr_nm" required="required">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="mb-3">
|
||||||
<!-- 데이터 등록 폼 -->
|
<label for="contents" class="form-label">Location</label>
|
||||||
<form id="addDataForm" method="post" action="{% url 'butler:ip_mgmt_add' %}">
|
<textarea class="form-control" id="contents" name="contents"></textarea>
|
||||||
{% csrf_token %}
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="networkNm" class="form-label">Network Name</label>
|
<label for="remark" class="form-label">Remark</label>
|
||||||
<input type="text" class="form-control" id="networkNm" name="network_nm" required="required">
|
<textarea class="form-control" id="remark" name="remark"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
<label for="ipAddrs" class="form-label">IP Address</label>
|
</form>
|
||||||
<input type="text" class="form-control" id="ipAddrs" name="ip_addrs" required="required">
|
</div>
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="svrNm" class="form-label">Server Name</label>
|
|
||||||
<input type="text" class="form-control" id="svrNm" name="svr_nm" required="required">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="desc" class="form-label">Description</label>
|
|
||||||
<textarea class="form-control" id="desc" name="desc"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="remark" class="form-label">Remark</label>
|
|
||||||
<textarea class="form-control" id="remark" name="remark"></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const editButton = document.getElementById('editSelectedButton');
|
const editButton = document.getElementById('editSelectedButton');
|
||||||
const checkboxes = document.querySelectorAll('.record-checkbox');
|
const checkboxes = document.querySelectorAll('.record-checkbox');
|
||||||
|
|
||||||
// Event listener to enable/disable the edit button
|
document.addEventListener('change', function () {
|
||||||
document.addEventListener('change', function () {
|
const selected = [...checkboxes].filter(checkbox => checkbox.checked);
|
||||||
const selected = [...checkboxes].filter(checkbox => checkbox.checked);
|
if (selected.length === 1) {
|
||||||
if (selected.length === 1) {
|
editButton.disabled = false;
|
||||||
editButton.disabled = false;
|
editButton.dataset.recordId = selected[0].value;
|
||||||
editButton.dataset.recordId = selected[0].value;
|
} else {
|
||||||
} else {
|
editButton.disabled = true;
|
||||||
editButton.disabled = true;
|
delete editButton.dataset.recordId;
|
||||||
delete editButton.dataset.recordId;
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
editButton.addEventListener('click', function () {
|
||||||
|
const recordId = editButton.dataset.recordId;
|
||||||
|
if (recordId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById(`editDataModal-${recordId}`));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
// Event listener to open the edit modal
|
{% endblock %}
|
||||||
editButton.addEventListener('click', function () {
|
|
||||||
const recordId = editButton.dataset.recordId;
|
|
||||||
if (recordId) {
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById(`editDataModal-${recordId}`));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<!-- Grafana IFrame -->
|
<!-- Grafana IFrame -->
|
||||||
<div class="ratio ratio-16x9">
|
<div class="ratio ratio-16x9">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://grafana.nativedeck.com/d/PwMJtdvnz/1-k8s-for-prometheus-dashboard-20211010-en?orgId=2&from=1731468637504&to=1731470437504&kiosk"
|
src="https://grafana.icurfer.com/public-dashboards/40d2a2615010433d81f5cf40caa03541"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="800"
|
height="800"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
@ -16,5 +16,5 @@ urlpatterns = [
|
|||||||
path('ip-mgmt/delete/', views.delete_ip_records, name='ip_mgmt_delete'), # 삭제 URL 추가
|
path('ip-mgmt/delete/', views.delete_ip_records, name='ip_mgmt_delete'), # 삭제 URL 추가
|
||||||
path('ip-mgmt/edit/<int:pk>/', views.edit_ip_record, name='ip_mgmt_edit'), # 수정 URL 추가
|
path('ip-mgmt/edit/<int:pk>/', views.edit_ip_record, name='ip_mgmt_edit'), # 수정 URL 추가
|
||||||
path('privacy/', views.privacy_view, name='privacy'),
|
path('privacy/', views.privacy_view, name='privacy'),
|
||||||
path('test/', views.test_view, name='test'),
|
# path('test/', views.test_view, name='test'),
|
||||||
]
|
]
|
||||||
|
@ -4,11 +4,16 @@ from django.views.generic import TemplateView
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import markdown
|
import markdown
|
||||||
import os
|
import os
|
||||||
|
import logging # 2025-04-14 Log 등록
|
||||||
from .models import IPManagementRecord
|
from .models import IPManagementRecord
|
||||||
from blog.models import Post
|
from blog.models import Post
|
||||||
from board_notice.models import BoardNotice
|
from board_notice.models import BoardNotice
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from opentelemetry import trace
|
||||||
|
|
||||||
|
# 2025-04-14 Log & Trace 등록
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
tracer = trace.get_tracer(__name__) # 트레이서 가져오기
|
||||||
|
|
||||||
class LandingPageView(TemplateView):
|
class LandingPageView(TemplateView):
|
||||||
template_name = "butler/landing.html"
|
template_name = "butler/landing.html"
|
||||||
@ -56,53 +61,96 @@ def ip_mgmt_view(request):
|
|||||||
records = records.order_by("ip_addrs")
|
records = records.order_by("ip_addrs")
|
||||||
return render(request, "butler/ip_mgmt.html", {"records": records, "var_search": query})
|
return render(request, "butler/ip_mgmt.html", {"records": records, "var_search": query})
|
||||||
|
|
||||||
|
|
||||||
def add_ip_record(request):
|
def add_ip_record(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
network_nm = request.POST.get("network_nm")
|
network_nm = request.POST.get("network_nm")
|
||||||
ip_addrs = request.POST.get("ip_addrs")
|
ip_addrs = request.POST.get("ip_addrs")
|
||||||
svr_nm = request.POST.get("svr_nm")
|
svr_nm = request.POST.get("svr_nm")
|
||||||
desc = request.POST.get("desc")
|
contents = request.POST.get("contents")
|
||||||
remark = request.POST.get("remark")
|
remark = request.POST.get("remark")
|
||||||
# 작성자 (author)는 로그인된 사용자로 설정
|
|
||||||
author = request.user
|
author = request.user
|
||||||
# 데이터 저장
|
|
||||||
IPManagementRecord.objects.create(
|
# 2025-04-14 트레이스 span 생성
|
||||||
network_nm=network_nm,
|
with tracer.start_as_current_span("create_ip_record") as span:
|
||||||
ip_addrs=ip_addrs,
|
# 데이터 저장
|
||||||
svr_nm=svr_nm,
|
IPManagementRecord.objects.create(
|
||||||
desc=desc,
|
network_nm=network_nm,
|
||||||
remark=remark,
|
ip_addrs=ip_addrs,
|
||||||
author=author,
|
svr_nm=svr_nm,
|
||||||
)
|
contents=contents,
|
||||||
|
remark=remark,
|
||||||
|
author=author,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2025-04-14 Jaeger용 Event 추가
|
||||||
|
span.add_event(
|
||||||
|
"Saved IPManagementRecord",
|
||||||
|
attributes={
|
||||||
|
"ip_addrs": ip_addrs,
|
||||||
|
"svr_nm": svr_nm,
|
||||||
|
"desc": "jager logs test",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2025-04-14 로그 등록 (콘솔+FluentBit용)
|
||||||
|
logger.info(f"Create_Record_IP_ADDRS: {ip_addrs}")
|
||||||
|
|
||||||
return redirect("/ip_mgmt")
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
def delete_ip_records(request):
|
def delete_ip_records(request):
|
||||||
print(f"삭제동작")
|
# print(f"Delete_Record_IP_ADDRS")
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
selected_ids = request.POST.getlist("selected_records")
|
selected_ids = request.POST.getlist("selected_records")
|
||||||
if selected_ids:
|
if selected_ids:
|
||||||
IPManagementRecord.objects.filter(id__in=selected_ids).delete()
|
IPManagementRecord.objects.filter(id__in=selected_ids).delete()
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger.info(f"Delete_Record_IP_ADDRS_idx: {selected_ids}")
|
||||||
return redirect("/ip_mgmt")
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
def edit_ip_record(request, pk):
|
def edit_ip_record(request, pk):
|
||||||
print(f"수정동작")
|
# print(f"Edit_Record_IP_ADDRS")
|
||||||
record = get_object_or_404(IPManagementRecord, pk=pk)
|
record = get_object_or_404(IPManagementRecord, pk=pk)
|
||||||
|
|
||||||
|
# 검색 키워드 유지
|
||||||
|
var_search = request.GET.get("var_search") or request.POST.get("var_search")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# 디버깅 메시지 추가
|
# 2025-04-14 trace span
|
||||||
# print(f"체크: {request.POST}")
|
with tracer.start_as_current_span("edit_ip_record") as span:
|
||||||
record.network_nm = request.POST.get("network_nm")
|
record.network_nm = request.POST.get("network_nm")
|
||||||
record.ip_addrs = request.POST.get("ip_addrs")
|
record.ip_addrs = request.POST.get("ip_addrs")
|
||||||
record.svr_nm = request.POST.get("svr_nm")
|
record.svr_nm = request.POST.get("svr_nm")
|
||||||
record.desc = request.POST.get("desc")
|
record.contents = request.POST.get("contents")
|
||||||
record.remark = request.POST.get("remark")
|
record.remark = request.POST.get("remark")
|
||||||
record.save()
|
record.save()
|
||||||
|
|
||||||
|
# 2025-04-14 Jaeger용 Event 추가
|
||||||
|
span.add_event(
|
||||||
|
"Edit IPManagementRecord",
|
||||||
|
attributes={
|
||||||
|
"ip_addrs": record.ip_addrs,
|
||||||
|
"svr_nm": record.svr_nm,
|
||||||
|
"location": record.contents,
|
||||||
|
"remark": record.remark,
|
||||||
|
"desc": "jager event log insert",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger.info(f"Edit_Record_IP_ADDRS: {record.ip_addrs}")
|
||||||
|
|
||||||
|
if var_search:
|
||||||
|
return redirect(f"/ip_mgmt?var_search={var_search}")
|
||||||
return redirect("/ip_mgmt")
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
return render(request, "butler/ip_mgmt.html", {"record": record})
|
return render(request, "butler/ip_mgmt.html", {"record": record})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- privacy
|
# --- privacy
|
||||||
def privacy_view(request):
|
def privacy_view(request):
|
||||||
# 'docs/privacy.md' 파일을 읽기
|
# 'docs/privacy.md' 파일을 읽기
|
||||||
|
157
butler/views.py.bakcup
Normal file
157
butler/views.py.bakcup
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from pathlib import Path
|
||||||
|
import markdown
|
||||||
|
import os
|
||||||
|
import logging # 2025-04-14 Log 등록
|
||||||
|
from .models import IPManagementRecord
|
||||||
|
from blog.models import Post
|
||||||
|
from board_notice.models import BoardNotice
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class LandingPageView(TemplateView):
|
||||||
|
template_name = "butler/landing.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
# context['var'] var를 templates로 전달해서 보여지는 것.
|
||||||
|
context['blog_posts'] = Post.objects.order_by('-created_at')[:3]
|
||||||
|
context['board_notices'] = BoardNotice.objects.order_by('-created_at')[:3]
|
||||||
|
return context
|
||||||
|
|
||||||
|
# --- ip management ---
|
||||||
|
# def ip_mgmt_view(request):
|
||||||
|
# # records = IPManagementRecord.objects.all()
|
||||||
|
# if request.user.is_authenticated:
|
||||||
|
# # records = IPManagementRecord.objects.filter(author=request.user).order_by(
|
||||||
|
# records = IPManagementRecord.objects.order_by(
|
||||||
|
# "ip_addrs"
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# # records = IPManagementRecord.objects.none()
|
||||||
|
# records = IPManagementRecord.objects.all().order_by("ip_addrs")
|
||||||
|
# return render(request, "butler/ip_mgmt.html", {"records": records})
|
||||||
|
|
||||||
|
|
||||||
|
def ip_mgmt_view(request):
|
||||||
|
query = request.GET.get("var_search", "").strip()
|
||||||
|
|
||||||
|
records = IPManagementRecord.objects.order_by(
|
||||||
|
"ip_addrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
if query:
|
||||||
|
records = records.filter(
|
||||||
|
# Q(author__username__icontains=query) | Q(author__email__icontains=query)
|
||||||
|
Q(author__username__icontains=query) | Q(network_nm__icontains=query)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
SELECT * FROM ip_management_record AS ip JOIN auth_user AS au
|
||||||
|
ON ip.author_id = au.id
|
||||||
|
WHERE au.username LIKE CONCAT('%', 조회할 값, '%')
|
||||||
|
OR au.email LIKE CONCAT('%', 조회할 값, '%')
|
||||||
|
ORDER BY ip.ip_addrs;
|
||||||
|
"""
|
||||||
|
records = records.order_by("ip_addrs")
|
||||||
|
return render(request, "butler/ip_mgmt.html", {"records": records, "var_search": query})
|
||||||
|
|
||||||
|
|
||||||
|
def add_ip_record(request):
|
||||||
|
# print(f"Create_Record_IP_ADDRS")
|
||||||
|
if request.method == "POST":
|
||||||
|
network_nm = request.POST.get("network_nm")
|
||||||
|
ip_addrs = request.POST.get("ip_addrs")
|
||||||
|
svr_nm = request.POST.get("svr_nm")
|
||||||
|
contents = request.POST.get("contents")
|
||||||
|
remark = request.POST.get("remark")
|
||||||
|
# 작성자 (author)는 로그인된 사용자로 설정
|
||||||
|
author = request.user
|
||||||
|
# 데이터 저장
|
||||||
|
IPManagementRecord.objects.create(
|
||||||
|
network_nm=network_nm,
|
||||||
|
ip_addrs=ip_addrs,
|
||||||
|
svr_nm=svr_nm,
|
||||||
|
contents=contents,
|
||||||
|
remark=remark,
|
||||||
|
author=author,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger.info(f"Create_Record_IP_ADDRS: {ip_addrs}")
|
||||||
|
|
||||||
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_ip_records(request):
|
||||||
|
# print(f"Delete_Record_IP_ADDRS")
|
||||||
|
if request.method == "POST":
|
||||||
|
selected_ids = request.POST.getlist("selected_records")
|
||||||
|
if selected_ids:
|
||||||
|
IPManagementRecord.objects.filter(id__in=selected_ids).delete()
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger.info(f"Delete_Record_IP_ADDRS_idx: {selected_ids}")
|
||||||
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
|
def edit_ip_record(request, pk):
|
||||||
|
# print(f"Edit_Record_IP_ADDRS")
|
||||||
|
record = get_object_or_404(IPManagementRecord, pk=pk)
|
||||||
|
|
||||||
|
# 검색 키워드 유지
|
||||||
|
var_search = request.GET.get("var_search") or request.POST.get("var_search")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
record.network_nm = request.POST.get("network_nm")
|
||||||
|
record.ip_addrs = request.POST.get("ip_addrs")
|
||||||
|
record.svr_nm = request.POST.get("svr_nm")
|
||||||
|
record.contents = request.POST.get("contents")
|
||||||
|
record.remark = request.POST.get("remark")
|
||||||
|
record.save()
|
||||||
|
|
||||||
|
# 2025-04-14 Log 등록
|
||||||
|
logger.info(f"Edit_Record_IP_ADDRS: {record.ip_addrs}")
|
||||||
|
|
||||||
|
if var_search:
|
||||||
|
return redirect(f"/ip_mgmt?var_search={var_search}")
|
||||||
|
return redirect("/ip_mgmt")
|
||||||
|
|
||||||
|
|
||||||
|
return render(request, "butler/ip_mgmt.html", {"record": record})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- privacy
|
||||||
|
def privacy_view(request):
|
||||||
|
# 'docs/privacy.md' 파일을 읽기
|
||||||
|
file_path = os.path.join("docs", "docs_md_files/privacy.md")
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
text = file.read()
|
||||||
|
|
||||||
|
file_path = Path("docs/docs_md_files/privacy.md")
|
||||||
|
|
||||||
|
with file_path.open("r", encoding="utf-8") as file:
|
||||||
|
text = file.read()
|
||||||
|
# Markdown을 HTML로 변환
|
||||||
|
# html_content = markdown.markdown(text)
|
||||||
|
# Markdown을 HTML로 변환, tables 확장 활성화
|
||||||
|
html_content = markdown.markdown(
|
||||||
|
text,
|
||||||
|
extensions=[
|
||||||
|
"tables", # 테이블 지원 확장
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 변환된 HTML을 템플릿에 전달
|
||||||
|
return render(request, "butler/privacy.html", {"content": html_content})
|
||||||
|
|
||||||
|
|
||||||
|
def test_view(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"butler/test.html",
|
||||||
|
)
|
@ -16,7 +16,17 @@ from pathlib import Path
|
|||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
load_dotenv(os.path.join(BASE_DIR, '.env.dev'))
|
# 우선순위: .env.dev > .env.prd > .env
|
||||||
|
if os.path.exists(os.path.join(BASE_DIR, '.env.dev')):
|
||||||
|
print("Read Environment File > Used : .env.dev")
|
||||||
|
load_dotenv(os.path.join(BASE_DIR, '.env.dev'))
|
||||||
|
elif os.path.exists(os.path.join(BASE_DIR, '.env.prd')):
|
||||||
|
print("Read Environment File > Used : .env.prd")
|
||||||
|
load_dotenv(os.path.join(BASE_DIR, '.env.prd'))
|
||||||
|
else:
|
||||||
|
print("None Environment File > Used : local_env")
|
||||||
|
|
||||||
|
# load_dotenv(os.path.join(BASE_DIR, '.env.dev'))
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
@ -26,7 +36,59 @@ SECRET_KEY = 'django-insecure-fh+awf3$$^el9(#*-dpuv&++#rck@1+s=o1mx+#etv)!lpq@_5
|
|||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = int(os.environ.get('DEBUG', 1))
|
DEBUG = int(os.environ.get('DEBUG', 1))
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False, # Django 기본 로거 유지
|
||||||
|
'formatters': {
|
||||||
|
'standard': {
|
||||||
|
'format': '[{asctime}] {levelname} {name}:{lineno} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler', # 콘솔 출력
|
||||||
|
'formatter': 'standard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO', # 기본 레벨 (애플리케이션 코드)
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO', # Django 프레임워크 전반
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'ERROR', # 요청 관련 에러만 (500 에러 같은 것)
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'django.db.backends': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING', # DB 쿼리 경고만
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'django.security': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING', # 보안 관련 경고
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
# mattermost send message log 너무 많이 나와서 조정
|
||||||
|
'apscheduler': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING', # INFO 로그 안 보이게 함 | 'CRITICAL'로 맞추면 사실상 아무것도 안 찍힘
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# if DEBUG:
|
||||||
|
# LOGGING['loggers']['django.db.backends']['level'] = 'DEBUG'
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
|
||||||
@ -52,6 +114,7 @@ INSTALLED_APPS = [
|
|||||||
'mm_msg',
|
'mm_msg',
|
||||||
'ansible_manager',
|
'ansible_manager',
|
||||||
'obs_minio',
|
'obs_minio',
|
||||||
|
'telemetry_dashboard',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -31,6 +31,7 @@ urlpatterns = [
|
|||||||
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')),
|
path('obs_minio/', include('obs_minio.urls')),
|
||||||
|
path('tm_dsbd/', include('telemetry_dashboard.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
@ -1,16 +1,38 @@
|
|||||||
"""
|
|
||||||
WSGI config for butler_ddochi project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# ✅ Django 설정을 미리 불러온다
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
|
||||||
|
|
||||||
|
from django.conf import settings # <<<< 이거 추가
|
||||||
|
|
||||||
|
# ✅ DEBUG 모드 아닐 때만 OpenTelemetry 초기화
|
||||||
|
if not settings.DEBUG:
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||||
|
|
||||||
|
trace.set_tracer_provider(
|
||||||
|
TracerProvider(
|
||||||
|
resource=Resource.create({
|
||||||
|
"service.name": "butler_ddochi",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
otlp_exporter = OTLPSpanExporter(
|
||||||
|
endpoint="http://jaeger-collector.istio-system:4317",
|
||||||
|
insecure=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
trace.get_tracer_provider().add_span_processor(
|
||||||
|
BatchSpanProcessor(otlp_exporter)
|
||||||
|
)
|
||||||
|
|
||||||
|
DjangoInstrumentor().instrument()
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
16
butler_ddochi/wsgi.py.backup
Normal file
16
butler_ddochi/wsgi.py.backup
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for butler_ddochi project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'butler_ddochi.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
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; (선택) 가운데 정렬 */
|
||||||
|
}
|
||||||
|
|
@ -156,6 +156,10 @@
|
|||||||
<label for="urlGrafanaEdit" class="form-label">Grafana URL</label>
|
<label for="urlGrafanaEdit" class="form-label">Grafana URL</label>
|
||||||
<input type="url" class="form-control" id="urlGrafanaEdit" name="url_grafana" value="{{ request.user.url_grafana }}">
|
<input type="url" class="form-control" id="urlGrafanaEdit" name="url_grafana" value="{{ request.user.url_grafana }}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="urlGrafanaDashboard_01-Edit" class="form-label">Grafana Dashboard URL</label>
|
||||||
|
<input type="url" class="form-control" id="urlGrafanaDashboard01Edit" name="url_grafana_dashboard_01" value="{{ request.user.url_grafana_dashboard_01 }}">
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="urlPrometheusEdit" class="form-label">Prometheus URL</label>
|
<label for="urlPrometheusEdit" class="form-label">Prometheus URL</label>
|
||||||
<input type="url" class="form-control" id="urlPrometheusEdit" name="url_prometheus" value="{{ request.user.url_prometheus }}">
|
<input type="url" class="form-control" id="urlPrometheusEdit" name="url_prometheus" value="{{ request.user.url_prometheus }}">
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<a class="d-inline-flex align-items-center rounded" href="/ip_mgmt">IP관리대장</a>
|
<a class="d-inline-flex align-items-center rounded" href="/ip_mgmt">IP관리대장</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/game/">game(kimchirun)</a>
|
<a class="d-inline-flex align-items-center rounded" href="/game/" target="_blank">game(kimchirun)</a>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_authenticated and request.user.is_staff %}
|
{% if request.user.is_authenticated and request.user.is_staff %}
|
||||||
<li>
|
<li>
|
||||||
@ -28,92 +28,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
{% include "components/_sidebar_tm_dashboard.html" %}
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#nhnc-collapse" aria-controls="nhnc-collapse">NHN Cloud API ▽</button>
|
|
||||||
<ul class="list-unstyled ps-3 collapse" id="nhnc-collapse">
|
|
||||||
{% if request.user.is_authenticated and request.user.is_staff %}
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/preparations">주의 사항</a>
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
<span>관리대장</span>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhnc_mgmt/igw">인터넷 게이트웨이</a>
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
<span>Network</span>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoVpcListRequest">VPC 조회 및 삭제</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createVpcRequest">VPC 생성</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoClusterListRequest">NKS Cluseter 조회 및 삭제</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createClusterOnlyRequest">NKS Cluseter 생성</a>
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
<span>k8s Components deploy</span>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">Ingress Deploy</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">FluentBit Deploy</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Logging Deploy(미구현)</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Monitoring Deploy(미구현)</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
{% include "components/_sidebar_nhn.html" %}
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">DevOpsTools ▽</button>
|
<hr>
|
||||||
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
|
{% include "components/_sidebar_devtools.html" %}
|
||||||
<span>Dev</span>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_gitea}}" target="_blank">Repository - Gitea</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_harbor}}" target="_blank">Registry - Harbor</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_argocd}}" target="_blank">Deploy - ArgoCD</a>
|
|
||||||
</li>
|
|
||||||
<hr>
|
|
||||||
<span>Ops</span>
|
|
||||||
{% if request.user.is_authenticated and request.user.is_staff %}
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_web_ide}}" target="_blank">Web VScode - CodeServer</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_rancher}}" target="_blank">Cluster Management - Rancher</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_grafana}}" target="_blank">Monitoring - Grafana</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_prometheus}}" target="_blank">Metrics - Prometheus</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_opensearch}}" target="_blank">Container Log - OpenSearch</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_kiali}}" target="_blank">Main Cluster Traffic - Kiali</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#other-collapse" aria-controls="other-collapse">Other Tools ▽</button>
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#other-collapse" aria-controls="other-collapse">Other Tools ▽</button>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#ansible-collapse" aria-controls="ansible-collapse">On-premise Ansible</button>
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#ansible-collapse" aria-controls="ansible-collapse">On-premise Ansible ▽</button>
|
||||||
|
|
||||||
<ul class="list-unstyled ps-3 collapse" id="ansible-collapse">
|
<ul class="list-unstyled ps-3 collapse" id="ansible-collapse">
|
||||||
<li>
|
<li>
|
||||||
|
40
components/templates/components/_sidebar_devtools.html
Normal file
40
components/templates/components/_sidebar_devtools.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<li class="my-2">
|
||||||
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">DevOpsTools ▽</button>
|
||||||
|
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
|
||||||
|
{% if request.user.is_authenticated and request.user.is_staff %}
|
||||||
|
<span>Dev</span>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_gitea}}" target="_blank">Repository - Gitea</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_harbor}}" target="_blank">Registry - Harbor</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_argocd}}" target="_blank">Deploy - ArgoCD</a>
|
||||||
|
</li>
|
||||||
|
<hr>
|
||||||
|
<span>Ops</span>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_web_ide}}" target="_blank">Web VScode - CodeServer</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_rancher}}" target="_blank">Cluster Management - Rancher</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_grafana}}" target="_blank">Monitoring - Grafana</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_prometheus}}" target="_blank">Metrics - Prometheus</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_opensearch}}" target="_blank">Container Log - OpenSearch</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_kiali}}" target="_blank">Main Cluster Traffic - Kiali</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<p>로그인이 필요합니다.</p>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
48
components/templates/components/_sidebar_nhn.html
Normal file
48
components/templates/components/_sidebar_nhn.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<li class="my-2">
|
||||||
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#nhnc-collapse" aria-controls="nhnc-collapse">NHN Cloud API ▽</button>
|
||||||
|
<ul class="list-unstyled ps-3 collapse" id="nhnc-collapse">
|
||||||
|
{% if request.user.is_authenticated and request.user.is_staff %}
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/preparations">주의 사항</a>
|
||||||
|
</li>
|
||||||
|
<hr>
|
||||||
|
<span>관리대장</span>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhnc_mgmt/igw">인터넷 게이트웨이</a>
|
||||||
|
</li>
|
||||||
|
<hr>
|
||||||
|
<span>Network</span>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoVpcListRequest">VPC 조회 및 삭제</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createVpcRequest">VPC 생성</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/infoClusterListRequest">NKS Cluseter 조회 및 삭제</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/createClusterOnlyRequest">NKS Cluseter 생성</a>
|
||||||
|
</li>
|
||||||
|
<hr>
|
||||||
|
<span>k8s Components deploy</span>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">Ingress Deploy</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/nksApply">FluentBit Deploy</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Logging Deploy(미구현)</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="text-secondary d-inline-flex align-items-center rounded disabled" href="#">Monitoring Deploy(미구현)</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<p>로그인이 필요합니다.</p>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
@ -0,0 +1,8 @@
|
|||||||
|
<li class="my-2">
|
||||||
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#tm_dsbd-collapse" aria-controls="tm_dsbd-collapse">Telemetry Dashboard ▽</button>
|
||||||
|
<ul class="list-unstyled ps-3 collapse" id="tm_dsbd-collapse">
|
||||||
|
<li>
|
||||||
|
<a class="d-inline-flex align-items-center rounded" href="/tm_dsbd/grafana">grafana</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
@ -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>
|
||||||
|
@ -33,6 +33,7 @@ class CustomUserChangeForm(UserChangeForm):
|
|||||||
'url_web_ide',
|
'url_web_ide',
|
||||||
'url_rancher',
|
'url_rancher',
|
||||||
'url_grafana',
|
'url_grafana',
|
||||||
|
'url_grafana_dashboard_01',
|
||||||
'url_prometheus',
|
'url_prometheus',
|
||||||
'url_opensearch',
|
'url_opensearch',
|
||||||
'url_kiali',
|
'url_kiali',
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-04-11 11:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('custom_auth', '0007_customuser_url_mattermost_customuser_url_nexus'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='url_grafana_dashboard_01',
|
||||||
|
field=models.URLField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -22,12 +22,12 @@ class CustomUser(AbstractUser):
|
|||||||
url_web_ide = models.URLField(max_length=200, blank=True, null=True)
|
url_web_ide = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_rancher = models.URLField(max_length=200, blank=True, null=True)
|
url_rancher = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_grafana = models.URLField(max_length=200, blank=True, null=True)
|
url_grafana = models.URLField(max_length=200, blank=True, null=True)
|
||||||
|
url_grafana_dashboard_01 = models.URLField(max_length=200, blank=True, null=True) # 2025-04-11 추가
|
||||||
url_prometheus = models.URLField(max_length=200, blank=True, null=True)
|
url_prometheus = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_opensearch = models.URLField(max_length=200, blank=True, null=True)
|
url_opensearch = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_kiali = models.URLField(max_length=200, blank=True, null=True)
|
url_kiali = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_nexus = models.URLField(max_length=200, blank=True, null=True)
|
url_nexus = models.URLField(max_length=200, blank=True, null=True)
|
||||||
url_mattermost = models.URLField(max_length=200, blank=True, null=True)
|
url_mattermost = models.URLField(max_length=200, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
def encrypt_private_key(self, private_key: str) -> bytes:
|
def encrypt_private_key(self, private_key: str) -> bytes:
|
||||||
"""SSH Private Key 암호화"""
|
"""SSH Private Key 암호화"""
|
||||||
|
@ -9,6 +9,7 @@ certifi==2024.7.4
|
|||||||
cffi==1.16.0
|
cffi==1.16.0
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
cryptography==43.0.0
|
cryptography==43.0.0
|
||||||
|
Deprecated==1.2.18
|
||||||
dj-rest-auth==6.0.0
|
dj-rest-auth==6.0.0
|
||||||
Django==4.2.14
|
Django==4.2.14
|
||||||
django-allauth==0.63.6
|
django-allauth==0.63.6
|
||||||
@ -18,6 +19,8 @@ django-taggit==6.0.0
|
|||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
durationpy==0.9
|
durationpy==0.9
|
||||||
google-auth==2.35.0
|
google-auth==2.35.0
|
||||||
|
googleapis-common-protos==1.69.2
|
||||||
|
grpcio==1.71.0
|
||||||
gunicorn==22.0.0
|
gunicorn==22.0.0
|
||||||
idna==3.7
|
idna==3.7
|
||||||
importlib_metadata==8.4.0
|
importlib_metadata==8.4.0
|
||||||
@ -31,8 +34,21 @@ mdurl==0.1.2
|
|||||||
minio==7.2.7
|
minio==7.2.7
|
||||||
mysqlclient==2.2.4
|
mysqlclient==2.2.4
|
||||||
oauthlib==3.2.2
|
oauthlib==3.2.2
|
||||||
|
opentelemetry-api==1.32.0
|
||||||
|
opentelemetry-exporter-otlp==1.32.0
|
||||||
|
opentelemetry-exporter-otlp-proto-common==1.32.0
|
||||||
|
opentelemetry-exporter-otlp-proto-grpc==1.32.0
|
||||||
|
opentelemetry-exporter-otlp-proto-http==1.32.0
|
||||||
|
opentelemetry-instrumentation==0.53b0
|
||||||
|
opentelemetry-instrumentation-django==0.53b0
|
||||||
|
opentelemetry-instrumentation-wsgi==0.53b0
|
||||||
|
opentelemetry-proto==1.32.0
|
||||||
|
opentelemetry-sdk==1.32.0
|
||||||
|
opentelemetry-semantic-conventions==0.53b0
|
||||||
|
opentelemetry-util-http==0.53b0
|
||||||
packaging==24.1
|
packaging==24.1
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
|
protobuf==5.29.4
|
||||||
pyasn1==0.6.1
|
pyasn1==0.6.1
|
||||||
pyasn1_modules==0.4.1
|
pyasn1_modules==0.4.1
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
@ -52,4 +68,5 @@ tzlocal==5.2
|
|||||||
urllib3==2.2.2
|
urllib3==2.2.2
|
||||||
websocket-client==1.8.0
|
websocket-client==1.8.0
|
||||||
whitenoise==6.7.0
|
whitenoise==6.7.0
|
||||||
|
wrapt==1.17.2
|
||||||
zipp==3.20.0
|
zipp==3.20.0
|
||||||
|
0
telemetry_dashboard/__init__.py
Normal file
0
telemetry_dashboard/__init__.py
Normal file
3
telemetry_dashboard/admin.py
Normal file
3
telemetry_dashboard/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
telemetry_dashboard/apps.py
Normal file
6
telemetry_dashboard/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryDashboardConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'telemetry_dashboard'
|
0
telemetry_dashboard/migrations/__init__.py
Normal file
0
telemetry_dashboard/migrations/__init__.py
Normal file
3
telemetry_dashboard/models.py
Normal file
3
telemetry_dashboard/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "components/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Landing Page{% endblock %}
|
||||||
|
|
||||||
|
{% block main_area %}
|
||||||
|
<article class="pt-3">
|
||||||
|
<h2 class="fw-bold pb-2">Grafana</h2>
|
||||||
|
<!-- Grafana IFrame -->
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
{% if request.user.url_grafana_dashboard_01 %}
|
||||||
|
<iframe src="{{request.user.url_grafana_dashboard_01}}" width="100%" height="800" frameborder="0" allowfullscreen></iframe>
|
||||||
|
{% else %}
|
||||||
|
<h3 class="text-danger">접속 후 Profile에 Dashboard URL을 등록하세요.</h3>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
3
telemetry_dashboard/tests.py
Normal file
3
telemetry_dashboard/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
8
telemetry_dashboard/urls.py
Normal file
8
telemetry_dashboard/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'tm_dsbd'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('grafana/', views.grafana_view, name='grafana'),
|
||||||
|
]
|
11
telemetry_dashboard/views.py
Normal file
11
telemetry_dashboard/views.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
# from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
|
def grafana_view(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"telemetry_dashboard/grafana.html",
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user