Compare commits

...

5 Commits

Author SHA1 Message Date
1d38fe26bd minio 테스트 인증정보 삭제, 변수 처리 완료
All checks were successful
Build And Test / build-and-push (push) Successful in 4m25s
2025-01-26 01:16:26 +09:00
d8111f6070 minio이미지 업로드 기능 테스트 2025-01-25 22:45:27 +09:00
af57b56e69 공지사항관리 기능 분리 및 개선
All checks were successful
Build And Test / build-and-push (push) Successful in 4m23s
2025-01-25 18:02:49 +09:00
06d1853fb0 url link 저장 및 변경 관리 계정에서 할수 있도록 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 4m8s
2025-01-25 01:30:48 +09:00
d34d4f8def landing페이지 수정, 카테고리 리스트 정리
All checks were successful
Build And Test / build-and-push (push) Successful in 5m13s
2025-01-23 14:19:27 +09:00
59 changed files with 1679 additions and 240 deletions

BIN
README-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1,6 +1,57 @@
# butler_ddochi
### dev_0.0.21
사이드메뉴에 unity 게임 등록.
* 쿠버네티스에 배포된 웹 게임 링크 등록.
* 쿠버네티스 클러스터에 배포된 웹 게임 및 istio 설정 필요.
butler_ddochi는 개인 테스트 환경 관리를 위한 목적으로 만들어지고 있습니다.
* 클라우드 네이티브 환경을 이용한 서비스 환경 관리
* 쿠버네티스 관리
* 문의는 icurfer@gmail.com으로 부탁드립니다.
> 문서가 미흡하여 설치 및 구성이 어려울 수 있습니다.
---
## 목차
* [사전 구성요소 선택](#사전-구성-컴포넌트(선택))
* [butler_ddochi설치](#butler_ddochi설치)
### 사전 구성 컴포넌트(필수)
동작을 위한 최소한의 필수 컴포넌트입니다.
* MariaDB
* MinIO
### 사전 구성 컴포넌트(선택)
아래 컴포넌트들이 배포 되어있다면, 로그인 후 Edit Profile을 이용하여 웹콘솔 링크를 입력하여 사용합니다.
![alt text](README-1.png)
* Gitea
* Harbor
* ArgoCD
* CodeServer
* Rancher
* Prometheus
* OpenSearch
* Kiali
---
### butler_ddochi설치
docker 또는 kubernetes를 이용하여 배포 합니다.
> docker pull harbor.icurfer.com/py_prj/butler_ddochi:${version}
필수 환경변수
* DEBUG=0
* SQL_ENGINE='django.db.backends.mysql'
* SQL_HOST='DB_SVR_address'
* SQL_USER='DB_SVR_user'
* SQL_PASSWORD='DB_SVR_user_password'
* SQL_DATABASE='DB_SVR_database'
* SQL_PORT='DB_SVR_portnumber'
* CSRF_TRUSTED_ORIGINS=https://www.example.com,https://butler.example.com,https://butler.example.com:8000
* MM_URL=https://mm.example.com/hooks/${hash}
* (기능 미구현)MINIO_URL=https://minio.example.com/
## 특이사항
### markdown에디터에 삽입된 이미지 렌더링할때 404에러가 발생하지만, 실제 동작은 잘 되는 이유.
* MinIO와 연결하여 이미지 저장 기능을 사용 할 수 있도록 구현되어 있습니다.
* Bucket을 private로 설정하는 것을 선호하므로 Presigned URL로 호출해 오도록 설정되어 있습니다.
* 게시물에는 Presigned URL이 아닌 버킷에 저장된 이미지의 이름만 들어있으므로 get_presigned_url을 이용하여 Presigned URL을 호출하기전에 렌더링이 발생하는 에러 메세지입니다.
* 에러가 아니니 기능상 문제 없이 사용 가능합니다.

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.14 on 2025-01-25 14:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_rename_content_post_contents'),
]
operations = [
migrations.AlterField(
model_name='post',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blog_posts', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models
from django.conf import settings
from django.urls import reverse
from markdown_it import MarkdownIt
from taggit.managers import TaggableManager
@ -10,12 +11,20 @@ class Post(models.Model):
created_at = models.DateTimeField(auto_now_add=True) # 작성일
updated_at = models.DateTimeField(auto_now=True) # 수정일
tags = TaggableManager()
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
related_name='blog_posts'
)
def render_markdown(self):
"""마크다운을 HTML로 변환"""
md = MarkdownIt()
return md.render(self.contents)
def get_absolute_url(self):
return reverse('blog:post_detail', args=[str(self.pk)])
def __str__(self):
return self.title

View File

@ -1,8 +1,8 @@
{% extends "components/base.html" %}
{% block title %}Create Notice{% endblock %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Create New Notice</h1>
<h1 class="pt-3">Create New Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
@ -12,6 +12,14 @@
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>contents</h2>
@ -22,7 +30,7 @@
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Post</button>
<a href="{% url 'butler:notice_list' %}" class="btn btn-secondary">Cancel</a>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@ -1,39 +0,0 @@
{% extends "components/base.html" %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1>Create New Post</h1>
<div class="row">
<!-- 왼쪽: 마크다운 에디터 -->
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary mt-3">Save</button>
</form>
</div>
<!-- 오른쪽: 미리보기 -->
<div class="col-md-6">
<h2>Preview</h2>
<div id="preview" class="border p-3" style="background: #f8f9fa; min-height: 300px;"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
});
</script>
{% endblock %}

View File

@ -22,11 +22,9 @@
</div>
<!-- 마크다운 에디터 -->
<h2>contents</h2>
<h2>Contents</h2>
<textarea id="markdown-editor" name="contents" class="form-control" rows="10"></textarea>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Post</button>
@ -41,18 +39,116 @@
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 미리보기 업데이트 함수
async function updatePreview() {
const markdownContent = textarea.value;
const lines = markdownContent.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/!\[Image\]\((.+)\)/);
if (match) {
const objectName = match[1];
try {
// Presigned URL 가져오기
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({object_name: objectName})
});
if (response.ok) {
const data = await response.json();
const presignedUrl = data.presigned_url;
lines[i] = `![Image](${presignedUrl})`; // Presigned URL로 업데이트
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
}
// 마크다운 렌더링 및 미리보기 업데이트
const renderedContent = md.render(lines.join("\n"));
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
}
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
textarea.addEventListener("input", updatePreview);
// Ctrl+V로 이미지 붙여넣기 처리
textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file)
return;
const formData = new FormData();
formData.append("image", file);
try {
// 이미지 업로드 API 호출
const response = await fetch("/obs_minio/upload/", {
method: "POST",
body: formData
});
if (response.ok) {
const data = await response.json();
const fullImageUrl = data.url; // 전체 URL
const objectName = fullImageUrl
.split("/")
.slice(-2)
.join("/");
// 마크다운 에디터에 이미지 삽입
const markdownImage = `![Image](${objectName})\n`;
textarea.value += markdownImage;
// 미리보기 업데이트
updatePreview();
} else {
alert("Image upload failed. Please try again.");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("An error occurred during image upload.");
}
}
}
});
</script>
{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,80 @@
{% extends "components/base.html" %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Create New Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Post</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</div>
{% endblock %}

View File

@ -0,0 +1,128 @@
{% extends "components/base.html" %}
{% block title %}Create Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Create New Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<textarea id="markdown-editor" name="contents" class="form-control" rows="10"></textarea>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Post</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
// Ctrl+V로 이미지 붙여넣기 처리
textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file)
return;
const formData = new FormData();
formData.append("image", file);
try {
// 이미지 업로드 API 호출
const response = await fetch("/obs_minio/upload/", {
method: "POST",
body: formData
});
if (response.ok) {
const data = await response.json();
const imageUrl = data.url; // 업로드된 이미지 URL
// 마크다운 에디터에 이미지 삽입
const markdownImage = `![Image](${imageUrl})\n`;
textarea.value += markdownImage;
// 미리보기 업데이트
const markdownContent = textarea.value;
const renderedContent = md.render(markdownContent);
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
} else {
alert("Image upload failed. Please try again.");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("An error occurred during image upload.");
}
}
}
});
</script>
</div>
{% endblock %}

View File

@ -8,9 +8,12 @@
<h5 class="text-muted">{{ post.summary }}</h5>
{% endif %}
<!-- Author -->
<div class="text-muted fst-italic mb-2">{{ post.created_at }} <br>
by. {{ post.author | upper }}</div>
<div>
<div class="text-muted fst-italic mb-2">{{ post.created_at }}
<br>
by.
{{ post.author | upper }}</div>
<!-- Rendered Markdown Content -->
<div id="post-content">
{{ post.render_markdown|safe }}
</div>
<hr>
@ -22,5 +25,84 @@
<br/>
<br/>
{% endif %}
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary mt-3">Back to List</a>
{% endblock %}
{% if request.user == post.author %}
<div class="d-flex mt-3">
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary me-2">List</a>
<a href="{% url 'blog:update_post' post.pk %}" class="btn btn-warning me-2">Edit</a>
<form method="POST" action="{% url 'blog:delete_post' post.pk %}" class="d-inline" id="delete-form">
{% csrf_token %}
<button type="button" class="btn btn-danger" id="delete-button">Delete</button>
</form>
</div>
{% endif %}
<!-- Highlight.js Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// Initialize Highlight.js
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll("pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
// Function to update images with presigned URLs
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"); // src에 저장된 object_name
try {
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({object_name: objectName})
});
if (response.ok) {
const data = await response.json();
img.setAttribute("src", data.presigned_url); // Presigned URL로 src 업데이트
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
// 업데이트된 HTML을 다시 삽입
contentDiv.innerHTML = doc.body.innerHTML;
// Reapply syntax highlighting
document
.querySelectorAll("pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
}
// Call the function when the page loads
document.addEventListener("DOMContentLoaded", updateImagesWithPresignedUrls);
// Delete post
document
.getElementById("delete-button")
.addEventListener("click", function () {
const confirmed = confirm("Are you sure you want to delete this post?");
if (confirmed) {
document
.getElementById("delete-form")
.submit();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "components/base.html" %}
{% block title %}Post{% endblock %}
{% block main_area %}
<!-- Title -->
<h1 class="mt-4">{{ post.title }}</h1>
{% if post.summary %}
<h5 class="text-muted">{{ post.summary }}</h5>
{% endif %}
<!-- Author -->
<div class="text-muted fst-italic mb-2">{{ post.created_at }}
<br>
by.
{{ post.author | upper }}</div>
<div>
{{ post.render_markdown|safe }}
</div>
<hr>
{% if post.tags.exists %}
<i class="fa-solid fa-tags"></i>
{% for tag in post.tags.all %}
<span class="badge bg-primary">{{ tag }}</span>
{% endfor %}
<br/>
<br/>
{% endif %}
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary mt-3">List</a>
{% if request.user == post.author %}
<a href="{% url 'blog:update_post' post.pk %}" class="btn btn-warning">Edit</a>
{% endif %}
{% endblock %}

View File

@ -12,7 +12,7 @@
<li class="list-group-item">
<h5>{{ post.title }}</h5>
<p>{{ post.summary }}</p>
<a href="{% url 'blog:post_detail' post.pk %}" class="btn btn-secondary">Read More</a>
<a href="{{ post.get_absolute_url }}" class="btn btn-secondary">Read More</a>
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,154 @@
{% extends "components/base.html" %}
{% block title %}Update Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Update Post</h1>
<div class="container mt-3">
<h5 class="text-danger">&#9671;마크다운 미리보기는 Contents 편집 내용 입력시 동작합니다.</h5>
<div class="row mt-3">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 미리보기 업데이트 함수
async function updatePreview() {
const markdownContent = textarea.value;
const lines = markdownContent.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/!\[Image\]\((.+)\)/);
if (match) {
const objectName = match[1];
try {
const response = await fetch("/obs_minio/get_presigned_url/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({object_name: objectName})
});
if (response.ok) {
const data = await response.json();
const presignedUrl = data.presigned_url;
lines[i] = `![Image](${presignedUrl})`;
}
} catch (error) {
console.error("Error fetching presigned URL:", error);
}
}
}
const renderedContent = md.render(lines.join("\n"));
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
}
// 실시간 미리보기 업데이트
textarea.addEventListener("input", updatePreview);
// Ctrl+V로 이미지 붙여넣기 처리
textarea.addEventListener("paste", async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (!file)
return;
const formData = new FormData();
formData.append("image", file);
try {
// 이미지 업로드 API 호출
const response = await fetch("/obs_minio/upload/", {
method: "POST",
body: formData
});
if (response.ok) {
const data = await response.json();
const fullImageUrl = data.url; // 전체 URL
const objectName = fullImageUrl
.split("/")
.slice(-2)
.join("/");
// 마크다운 에디터에 이미지 삽입
const markdownImage = `![Image](${objectName})\n`;
textarea.value += markdownImage;
// 미리보기 업데이트
updatePreview();
} else {
alert("Image upload failed. Please try again.");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("An error occurred during image upload.");
}
}
}
});
</script>
</div>
{% endblock %}

View File

@ -7,4 +7,6 @@ urlpatterns = [
path('', views.post_list, name='post_list'), # 포스트 리스트
path('create/', views.create_post, name='create_post'), # 포스트 작성
path('<int:pk>/', views.post_detail, name='post_detail'), # 포스트 상세 보기
path('<int:pk>/update/', views.update_post, name='update_post'), # 글 수정 URL
path('<int:pk>/delete/', views.delete_post, name='delete_post'), # 삭제 URL
]

View File

@ -3,6 +3,16 @@ from django.contrib.auth.decorators import login_required
from .models import Post
from .forms import PostForm
# 게시글 목록
def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
# 게시글 상세 보기
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
@login_required
def create_post(request):
if request.method == 'POST':
@ -17,10 +27,37 @@ def create_post(request):
form = PostForm()
return render(request, 'blog/create_post.html', {'form': form})
def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, pk):
@login_required
def update_post(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
# 작성자만 수정 가능하도록 권한 검사
if post.author != request.user:
return redirect('blog:post_list')
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/update_post.html', {'form': form, 'post': post})
@login_required
def delete_post(request, pk):
post = get_object_or_404(Post, pk=pk)
# 작성자만 삭제 가능
if post.author != request.user:
return redirect('blog:post_list')
# POST 요청일 때 삭제
if request.method == 'POST':
post.delete()
return redirect('blog:post_list')
return render(request, 'blog/confirm_delete.html', {'post': post})

0
board_notice/__init__.py Normal file
View File

4
board_notice/admin.py Normal file
View File

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import BoardNotice
admin.site.register(BoardNotice)

6
board_notice/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BoardNoticeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'board_notice'

11
board_notice/forms.py Normal file
View File

@ -0,0 +1,11 @@
from django import forms
from .models import BoardNotice
class BoardNoticeForm(forms.ModelForm):
class Meta:
model = BoardNotice
fields = ['title', 'contents']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'contents': forms.Textarea(attrs={'class': 'form-control', 'id': 'markdown-editor'}),
}

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.14 on 2025-01-25 14:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BoardNotice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('contents', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='board_notice', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

27
board_notice/models.py Normal file
View File

@ -0,0 +1,27 @@
from django.db import models
from django.conf import settings
from django.urls import reverse
from markdown_it import MarkdownIt
class BoardNotice(models.Model):
title = models.CharField(max_length=200) # 제목
contents = models.TextField() # 본문 (마크다운 저장)
created_at = models.DateTimeField(auto_now_add=True) # 작성일
updated_at = models.DateTimeField(auto_now=True) # 수정일
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
related_name='board_notice'
)
def render_markdown(self):
"""마크다운을 HTML로 변환"""
md = MarkdownIt()
return md.render(self.contents)
def get_absolute_url(self):
return reverse('board_notice:notice_detail', args=[str(self.pk)])
def __str__(self):
return self.title

View File

@ -0,0 +1,72 @@
{% extends "components/base.html" %}
{% block title %}Create Notice{% endblock %}
{% block main_area %}
<h1 class="pt-3">Create New Notice</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Create Notice</button>
<a href="{% url 'board_notice:notice_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
</div>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "components/base.html" %}
{% block title %}Notice{% endblock %}
{% block main_area %}
<!-- Title -->
<h1 class="mt-4">{{ notice.title }}</h1>
<!-- Author -->
<div class="text-muted fst-italic mb-2">{{ notice.created_at }}
<br>
by.
{{ notice.author | upper }}</div>
<!-- Rendered Markdown Content -->
<div>
{{ notice.render_markdown|safe }}
</div>
<hr>
{% if request.user == notice.author %}
<div class="d-flex mt-3">
<a href="{% url 'board_notice:notice_list' %}" class="btn btn-secondary me-2">List</a>
<a href="{% url 'board_notice:update_notice' notice.pk %}" class="btn btn-warning me-2">Edit</a>
<form method="POST" action="{% url 'board_notice:delete_notice' notice.pk %}" class="d-inline" id="delete-form">
{% csrf_token %}
<button type="button" class="btn btn-danger" id="delete-button">Delete</button>
</form>
</div>
{% endif %}
<!-- Highlight.js Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// Initialize Highlight.js
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll("pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<!-- Delete Notice -->
<script>
document
.getElementById("delete-button")
.addEventListener("click", function () {
const confirmed = confirm("Are you sure you want to delete this Notice?");
if (confirmed) {
document
.getElementById("delete-form")
.submit();
}
});
</script>
{% endblock %}

View File

@ -5,27 +5,31 @@
{% block main_area %}
<h2 class="fw-bold pt-3 pb-2 d-flex justify-content-between">
공지사항
</h2>
<!-- 공지사항 목록 -->
{% if records %}
{% if notices %}
<table class="table table-bordered table-hover">
<thead class="table-dark">
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">내용</th>
<th scope="col">작성자</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
{% for record in records %}
{% for record in notices %}
<tr>
<td>{{ record.pk }}</td>
<td>
<a href="{{ record.get_absolute_url }}">{{ record.title }}</a>
</td>
<td>
<a href="{{ record.get_absolute_url }}">{{ record.contents|truncatechars:50 }}</a>
</td>
<td>{{ record.author }}</td>
<td>{{ record.created_at|date:"Y-m-d H:i" }}</td>
</tr>
@ -36,9 +40,9 @@
<p class="text-muted">현재 공지사항이 없습니다.</p>
{% endif %}
{% if request.user.is_authenticated %}
<!-- 버튼 컨테이너 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'butler:create_notice' %}" class="btn btn-primary">Create Notice</a>
</div>
<!-- 버튼 컨테이너 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'board_notice:create_notice' %}" class="btn btn-primary">Create Notice</a>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "components/base.html" %}
{% block title %}Update Post{% endblock %}
{% block main_area %}
<h1 class="pt-3">Update Post</h1>
<div class="container">
<div class="row">
<div class="col-md-6">
<form method="POST" id="post-form">
{% csrf_token %}
<div class="mb-3">
{{ form.title.label_tag }}
{{ form.title }}
</div>
<div class="mb-3">
{{ form.summary.label_tag }}
{{ form.summary }}
</div>
<div class="mb-3">
{{ form.tags.label_tag }}
{{ form.tags }}
</div>
<!-- 마크다운 에디터 -->
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
</div>
<!-- 버튼 -->
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<div id="preview" class="border p-3 bg-light h-100 overflow-auto"></div>
</div>
</div>
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
const preview = document.getElementById("preview");
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
});
</script>
</div>
{% endblock %}

3
board_notice/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
board_notice/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'board_notice'
urlpatterns = [
path('', views.notice_list, name='notice_list'), # 포스트 리스트
path('create/', views.create_notice, name='create_notice'), # 포스트 작성
path('<int:pk>/', views.notice_detail, name='notice_detail'), # 포스트 상세 보기
path('<int:pk>/update/', views.update_notice, name='update_notice'), # 글 수정 URL
path('<int:pk>/delete/', views.delete_notice, name='delete_notice'), # 삭제 URL
]

62
board_notice/views.py Normal file
View File

@ -0,0 +1,62 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import BoardNotice
from .forms import BoardNoticeForm
# 게시글 목록
def notice_list(request):
notices = BoardNotice.objects.all().order_by('-created_at')
return render(request, 'board_notice/notice_list.html', {'notices': notices})
# 게시글 상세 보기
def notice_detail(request, pk):
notice = get_object_or_404(BoardNotice, pk=pk)
return render(request, 'board_notice/notice_detail.html', {'notice': notice})
# 공지사항 게시글 등록
@login_required
def create_notice(request):
if request.method == 'POST':
form = BoardNoticeForm(request.POST)
if form.is_valid():
notice = form.save(commit=False)
notice.author = request.user # 작성자 정보 추가
notice.save()
return redirect('board_notice:notice_list')
else:
form = BoardNoticeForm()
return render(request, 'board_notice/create_notice.html', {'form': form})
# 공지사항 게시글 수정
@login_required
def update_notice(request, pk):
notice = get_object_or_404(BoardNotice, pk=pk)
# 작성자만 수정 가능하도록 권한 검사
if notice.author != request.user:
return redirect('board_notice:notice_list')
if request.method == 'POST':
form = BoardNoticeForm(request.POST, instance=notice)
if form.is_valid():
form.save()
return redirect('board_notice:notice_detail', pk=notice.pk)
else:
form = BoardNoticeForm(instance=notice)
return render(request, 'board_notice/update_notice.html', {'form': form, 'notice': notice})
# 공지사항 게시글 삭제
@login_required
def delete_notice(request, pk):
notice = get_object_or_404(BoardNotice, pk=pk)
# 작성자만 삭제 가능하도록 권한 검사
if notice.author != request.user:
return redirect('board_notice:notice_list')
if request.method == 'POST':
notice.delete()
return redirect('board_notice:notice_list')
return render(request, 'board_notice/confirm_delete.html', {'notice': notice})

View File

@ -1,5 +1,4 @@
from django.contrib import admin
from .models import IPManagementRecord, NoticeBoard
from .models import IPManagementRecord
admin.site.register(IPManagementRecord)
admin.site.register(NoticeBoard)

View File

@ -1,11 +1 @@
from django import forms
from .models import NoticeBoard
class PostForm(forms.ModelForm):
class Meta:
model = NoticeBoard
fields = ['title', 'contents']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'contents': forms.Textarea(attrs={'class': 'form-control', 'id': 'markdown-editor'}),
}

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-01-25 11:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('butler', '0002_noticeboard'),
]
operations = [
migrations.RenameField(
model_name='ipmanagementrecord',
old_name='desc',
new_name='contents',
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 4.2.14 on 2025-01-25 19:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('butler', '0003_rename_desc_ipmanagementrecord_contents'),
]
operations = [
migrations.DeleteModel(
name='NoticeBoard',
),
]

View File

@ -7,7 +7,7 @@ class IPManagementRecord(models.Model):
network_nm = models.CharField(max_length=100) # NETWORK_NM
ip_addrs = models.CharField(max_length=50) # IP_ADDRS
svr_nm = models.CharField(max_length=100) # SVR_NM
desc = models.TextField(blank=True, null=True) # DESC
contents = models.TextField(blank=True, null=True) # DESC
remark = models.TextField(blank=True, null=True) # REMARK
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
@ -15,21 +15,4 @@ class IPManagementRecord(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
def __str__(self):
return f'{self.network_nm} {self.ip_addrs} {self.svr_nm} {self.desc}'
class NoticeBoard(models.Model):
title = models.CharField(max_length=100)
contents = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
def __str__(self):
return f'{self.title} {self.author} {self.created_at}'
def get_absolute_url(self):
return reverse('butler:notice_detail', args=[str(self.pk)])
# <a href="{% url 'butler:notice_detail' record.pk %}">{{ record.title }}</a>
return f'{self.network_nm} {self.ip_addrs} {self.svr_nm} {self.contents}'

View File

@ -0,0 +1,13 @@
{% extends "components/base.html" %}
{% block title %}Landing Page{% endblock %}
{% block main_area %}
<article class="pt-3">
<h2 class="fw-bold pb-2">Welcome!</h2>
<h3>IT Infra 및 DevOps 자원관리 도구</h3>
* IP리스트 관리기능 제공 <br>
* Public NHN Cloud API 기능 일부 제공
<p>계속 기능 구현 중 입니다.</p>
</article>
{% endblock %}

View File

@ -3,11 +3,46 @@
{% block title %}Landing Page{% endblock %}
{% block main_area %}
<article class="pt-3">
<h2 class="fw-bold pb-2">Welcome!</h2>
<h3>IT Infra 및 DevOps 자원관리 도구</h3>
* IP리스트 관리기능 제공 <br>
* Public NHN Cloud API 기능 일부 제공
<p>계속 기능 구현 중 입니다.</p>
</article>
<div class="container mt-5">
<div class="row">
<!-- NoticeBoard Section -->
<div class="col-md-6">
<h2>Latest Notices</h2>
<div class="row">
{% for notice in board_notices %}
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ notice.title }}</h5>
<p class="card-text">{{ notice.contents|truncatechars:50 }}</p>
<a href="{{ notice.get_absolute_url }}" class="btn btn-primary">View Notice</a>
</div>
</div>
</div>
{% empty %}
<p>No notices available.</p>
{% endfor %}
</div>
</div>
<!-- Blog Posts Section -->
<div class="col-md-6">
<h2>Latest Blog Posts</h2>
<div class="row">
{% for post in blog_posts %}
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ post.title }}</h5>
<p class="card-text">{{ post.contents|truncatechars:50 }}</p>
<a href="{% url 'blog:post_detail' post.id %}" class="btn btn-primary">Read More</a>
</div>
</div>
</div>
{% empty %}
<p>No blog posts available.</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "components/base.html" %}
{% block title %}Notice Detail{% endblock %}
{% block main_area %}
<h2 class="fw-bold pt-3 pb-2">{{ notice.title }}</h2>
<hr>
<div>
{{ notice.contents|linebreaks }}
</div>
<hr>
<p class="text-muted">작성자:
{{ notice.author }}</p>
<p class="text-muted">작성일:
{{ notice.created_at|date:"Y-m-d H:i" }}</p>
<p class="text-muted">수정일:
{{ notice.updated_at|date:"Y-m-d H:i" }}</p>
<a href="{% url 'butler:notice_list' %}" class="btn btn-secondary mt-3">Back to Notices</a>
{% endblock %}

View File

@ -4,10 +4,13 @@ from . import views
app_name = 'butler'
urlpatterns = [
path('', views.hello_view, name='landing'), # 루트 경로에서 hello_view 호출
path('notice', views.notice_list, name='notice_list'),
path('create_notice/', views.create_notice, name='create_notice'), # 포스트 작성
path('notice/<int:pk>/', views.notice_detail_view, name='notice_detail'),
# path('', views.hello_view, name='landing'), # 루트 경로에서 hello_view 호출
# Landing Page
path('', views.LandingPageView.as_view(), name='landing'), # 클래스 기반 뷰(CBV) 호출
# path('notice', views.notice_list, name='notice_list'),
# path('create_notice/', views.create_notice, name='create_notice'), # 포스트 작성
# path('notice/<int:pk>/', views.notice_detail_view, name='notice_detail'),
path('ip_mgmt', views.ip_mgmt_view, name='ip_mgmt'),
path('ip-mgmt/add/', views.add_ip_record, name='ip_mgmt_add'), # 데이터 추가 경로
path('ip-mgmt/delete/', views.delete_ip_records, name='ip_mgmt_delete'), # 삭제 URL 추가

View File

@ -1,47 +1,24 @@
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
from .models import NoticeBoard, IPManagementRecord
from .forms import PostForm
from .models import IPManagementRecord
from blog.models import Post
from board_notice.models import BoardNotice
from django.db.models import Q
def hello_view(request):
return render(
request,
"butler/landing.html",
)
# --- notice ---
@login_required
def create_notice(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user # 작성자 정보 추가
post.save()
form.save_m2m()
return redirect('butler:notice_list')
else:
form = PostForm()
return render(request, 'butler/create_notice.html', {'form': form})
def notice_list(request):
records = NoticeBoard.objects.all()
return render(request, "butler/notice_list.html", {"records": records})
def notice_detail_view(request, pk):
try:
notice = NoticeBoard.objects.get(pk=pk)
except NoticeBoard.DoesNotExist:
return render(request, "404.html") # 선택적으로 에러 처리
return render(request, "butler/notice_detail.html", {"notice": notice})
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):

View File

@ -41,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'taggit',
# Custom by.icurfer
'board_notice',
'custom_auth',
'butler',
'blog',
@ -50,6 +51,7 @@ INSTALLED_APPS = [
'nhnc_mgmt',
'mm_msg',
'ansible_manager',
'obs_minio',
]
MIDDLEWARE = [
@ -166,6 +168,7 @@ LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# MinIO 설정
# MINIO_STORAGE_MEDIA_URL = os.environ.get('MINIO_STORAGE_MEDIA_URL', '')
# MINIO_STORAGE_ACCESS_KEY = os.environ.get('MINIO_STORAGE_ACCESS_KEY', '')
# MINIO_STORAGE_SECRET_KEY = os.environ.get('MINIO_STORAGE_SECRET_KEY', '')
MINIO_ENDPOINT_URL = os.environ.get('MINIO_ENDPOINT_URL', '')
MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY', '')
MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY', '')
MINIO_DEFAULT_BUCKET=os.environ.get('MINIO_DEFAULT_BUCKET', '')

View File

@ -24,11 +24,13 @@ urlpatterns = [
path('', include('butler.urls')),
path('admin/', admin.site.urls),
path('accounts/', include('custom_auth.urls')), # custom_auth URL 추가
path('notice/', include('board_notice.urls')),
path('blog/', include('blog.urls')),
path('nhncloud/', include('nhncloud.urls')),
path('nhnc_mgmt/', include('nhnc_mgmt.urls')),
path('mm_msg/', include('mm_msg.urls')),
path('blog/', include('blog.urls')),
path('ansible_manager/', include('ansible_manager.urls')),
path('obs_minio/', include('obs_minio.urls')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,40 +1,38 @@
<div class="container-fluid d-flex justify-content-between align-items-center">
<h1 class="d-flex align-items-center fs-4 text-white mb-0">
<a class="navbar-brand" href="/">
<i class="fa-solid fa-dog"></i> DDoCHI</a>
<i class="fa-solid fa-dog"></i>
DDoCHI</a>
</h1>
<!-- 로그인/로그아웃 및 회원가입 버튼 -->
<ul class="navbar-nav flex-row">
{% if user.is_authenticated and user.is_superuser %}
<!-- 관리자 콘솔 버튼 -->
<li class="nav-item me-3">
<a href="/admin" target="_blank" class="btn btn-outline-warning mb-2">ADMIN</a>
</li>
<!-- 관리자 콘솔 버튼 -->
<li class="nav-item me-3">
<a href="/admin" target="_blank" class="btn btn-outline-warning mb-2">ADMIN</a>
</li>
{% endif %}
{% if request.user.is_authenticated %}
<!-- 회원정보 변경 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-info mb-2" data-bs-toggle="modal"
data-bs-target="#editProfileModal">Edit Profile</button>
</li>
<!-- 로그아웃 버튼 -->
<li class="nav-item">
<form method="post" action="{% url 'custom_auth:logout' %}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger w-100">Logout</button>
</form>
</li>
<!-- 회원정보 변경 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-info mb-2" data-bs-toggle="modal" data-bs-target="#editProfileModal">Edit Profile</button>
</li>
<!-- 로그아웃 버튼 -->
<li class="nav-item">
<form method="post" action="{% url 'custom_auth:logout' %}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger w-100">Logout</button>
</form>
</li>
{% else %}
<!-- 로그인 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-primary mb-2" data-bs-toggle="modal"
data-bs-target="#loginModal">Login</button>
</li>
<!-- 회원가입 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#signupModal">Sign
Up</button>
</li>
<!-- 로그인 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-primary mb-2" data-bs-toggle="modal" data-bs-target="#loginModal">Login</button>
</li>
<!-- 회원가입 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#signupModal">Sign Up</button>
</li>
{% endif %}
</ul>
</div>
@ -50,11 +48,11 @@
<div class="modal-body">
<!-- 로그인 실패 메시지 표시 -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
{% for message in messages %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- 로그인 폼 -->
@ -123,7 +121,7 @@
{% csrf_token %}
<div class="mb-3">
<label for="emailEdit" class="form-label">Email</label>
<input type="email" class="form-control" id="emailEdit" name="email" value="{{ request.user.email }}" required>
<input type="email" class="form-control" id="emailEdit" name="email" value="{{ request.user.email }}" required="required">
</div>
<div class="mb-3">
<label for="nhncIdEdit" class="form-label">NHNC ID</label>
@ -133,6 +131,52 @@
<label for="nhncApiTenantIdEdit" class="form-label">NHNC API Tenant ID</label>
<input type="text" class="form-control" id="nhncApiTenantIdEdit" name="nhnc_api_tenant_id" value="{{ request.user.nhnc_api_tenant_id }}">
</div>
<!-- 추가된 URL 필드 -->
<div class="mb-3">
<label for="urlGiteaEdit" class="form-label">Gitea URL</label>
<input type="url" class="form-control" id="urlGiteaEdit" name="url_gitea" value="{{ request.user.url_gitea }}">
</div>
<div class="mb-3">
<label for="urlHarborEdit" class="form-label">Harbor URL</label>
<input type="url" class="form-control" id="urlHarborEdit" name="url_harbor" value="{{ request.user.url_harbor }}">
</div>
<div class="mb-3">
<label for="urlArgoCDEdit" class="form-label">ArgoCD URL</label>
<input type="url" class="form-control" id="urlArgoCDEdit" name="url_argocd" value="{{ request.user.url_argocd }}">
</div>
<div class="mb-3">
<label for="urlWebIdeEdit" class="form-label">Web IDE URL</label>
<input type="url" class="form-control" id="urlWebIdeEdit" name="url_web_ide" value="{{ request.user.url_web_ide }}">
</div>
<div class="mb-3">
<label for="urlRancherEdit" class="form-label">Rancher URL</label>
<input type="url" class="form-control" id="urlRancherEdit" name="url_rancher" value="{{ request.user.url_rancher }}">
</div>
<div class="mb-3">
<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 }}">
</div>
<div class="mb-3">
<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 }}">
</div>
<div class="mb-3">
<label for="urlOpensearchEdit" class="form-label">OpenSearch URL</label>
<input type="url" class="form-control" id="urlOpensearchEdit" name="url_opensearch" value="{{ request.user.url_opensearch }}">
</div>
<div class="mb-3">
<label for="urlKialiEdit" class="form-label">Kiali URL</label>
<input type="url" class="form-control" id="urlKialiEdit" name="url_kiali" value="{{ request.user.url_kiali }}">
</div>
<div class="mb-3">
<label for="urlNexusEdit" class="form-label">Nexus URL</label>
<input type="url" class="form-control" id="urlNexusEdit" name="url_nexus" value="{{ request.user.url_nexus }}">
</div>
<div class="mb-3">
<label for="urlMattermostEdit" class="form-label">Mattermost URL</label>
<input type="url" class="form-control" id="urlMattermostEdit" name="url_mattermost" value="{{ request.user.url_mattermost }}">
</div>
<!-- 비밀번호 변경 -->
<div class="mb-3">
<label for="passwordEdit" class="form-label">새 비밀번호</label>
<input type="password" class="form-control" id="passwordEdit" name="password">

View File

@ -2,15 +2,16 @@
<nav class="sticky-xl-top small" id="toc">
<ul class="list-unstyled">
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#about-collapse" aria-controls="about-collapse">About</button>
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#about-collapse" aria-controls="about-collapse">About</button>
<ul class="list-unstyled ps-3 collapse" id="about-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="/notice">공지사항</a>
</li>
</ul>
</li>
<hr>
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#contents-collapse" aria-controls="contents-collapse">Contents</button>
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#contents-collapse" aria-controls="contents-collapse">Contents</button>
<ul class="list-unstyled ps-3 collapse" id="contents-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="/blog">Post</a>
@ -30,9 +31,8 @@
</li>
<hr>
<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>
<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">
<span>가이드</span>
{% if request.user.is_authenticated and request.user.is_staff %}
<li>
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
@ -78,56 +78,51 @@
</li>
<hr>
<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>
<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">
<span>Dev</span>
<li>
<a class="d-inline-flex align-items-center rounded" href="https://gitea.icurfer.com/icurfer" target="_blank">Repository - Gitea</a>
<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="https://harbor.icurfer.com/" target="_blank">Registry - Harbor</a>
<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="https://argocd.icurfer.com/" target="_blank">Deploy - ArgoCD</a>
<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="https://code.icurfer.com/" target="_blank">Web VScode - CodeServer</a>
<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="https://rancher.icurfer.com/" target="_blank">Cluster Management - Rancher</a>
<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="https://grafana.icurfer.com/" target="_blank">Monitoring - Grafana</a>
<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="https://prometheus.icurfer.com/" target="_blank">Metrics - Prometheus</a>
<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="https://os.icurfer.com/" target="_blank">Container Log - OpenSearch</a>
<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="https://kiali.icurfer.com/" target="_blank">Main Cluster Traffic - Kiali</a>
</li>
</ul>
</li>
<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>
<ul class="list-unstyled ps-3 collapse" id="other-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="https://mm.icurfer.com/" target="_blank">Messenger - Mattermost</a>
<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>
<li class="my-2">
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#forms-collapse" aria-controls="forms-collapse">Documents</button>
<ul class="list-unstyled ps-3 collapse" id="forms-collapse">
<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>
<ul class="list-unstyled ps-3 collapse" id="other-collapse">
<li>
<a class="d-inline-flex align-items-center rounded" href="#">사용가이드</a>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_mattermost}}" target="_blank">Messenger - Mattermost</a>
</li>
<li>
<a class="d-inline-flex align-items-center rounded" href="{{request.user.url_nexus}}" target="_blank">Nexus</a>
</li>
</ul>
</li>

View File

@ -0,0 +1,149 @@
<div class="container-fluid d-flex justify-content-between align-items-center">
<h1 class="d-flex align-items-center fs-4 text-white mb-0">
<a class="navbar-brand" href="/">
<i class="fa-solid fa-dog"></i> DDoCHI</a>
</h1>
<!-- 로그인/로그아웃 및 회원가입 버튼 -->
<ul class="navbar-nav flex-row">
{% if user.is_authenticated and user.is_superuser %}
<!-- 관리자 콘솔 버튼 -->
<li class="nav-item me-3">
<a href="/admin" target="_blank" class="btn btn-outline-warning mb-2">ADMIN</a>
</li>
{% endif %}
{% if request.user.is_authenticated %}
<!-- 회원정보 변경 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-info mb-2" data-bs-toggle="modal"
data-bs-target="#editProfileModal">Edit Profile</button>
</li>
<!-- 로그아웃 버튼 -->
<li class="nav-item">
<form method="post" action="{% url 'custom_auth:logout' %}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger w-100">Logout</button>
</form>
</li>
{% else %}
<!-- 로그인 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-primary mb-2" data-bs-toggle="modal"
data-bs-target="#loginModal">Login</button>
</li>
<!-- 회원가입 버튼 -->
<li class="nav-item me-3">
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#signupModal">Sign
Up</button>
</li>
{% endif %}
</ul>
</div>
<!-- 로그인 모달 -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">Login</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 로그인 실패 메시지 표시 -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- 로그인 폼 -->
<form method="post" action="{% url 'custom_auth:login' %}">
{% csrf_token %}
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required="required">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required="required">
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
<!-- 회원가입 모달 -->
<div class="modal fade" id="signupModal" tabindex="-1" aria-labelledby="signupModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="signupModalLabel">Sign Up</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 회원가입 폼 -->
<form method="post" action="{% url 'custom_auth:signup' %}">
{% csrf_token %}
<div class="mb-3">
<label for="usernameSignup" class="form-label">Username</label>
<input type="text" class="form-control" id="usernameSignup" name="username" required="required">
</div>
<div class="mb-3">
<label for="passwordSignup" class="form-label">Password</label>
<input type="password" class="form-control" id="passwordSignup" name="password1" required="required">
</div>
<div class="mb-3">
<label for="passwordConfirmSignup" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="passwordConfirmSignup" name="password2" required="required">
</div>
<a href="{% url 'butler:privacy' %}" class="text-dark" target="_blank">개인정보 처리방침</a>
<p class="text-danger">회원 가입후 권한 신청 메일을 보내주세요.</p>
<p class="text-danger">회원 가입시 본 개인정보 처리방침에 동의하는 것으로 간주됩니다.</p>
<button type="submit" class="btn btn-success w-100">Sign Up</button>
</form>
</div>
</div>
</div>
</div>
<!-- 회원정보 변경 모달 -->
<div class="modal fade" id="editProfileModal" tabindex="-1" aria-labelledby="editProfileModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editProfileModalLabel">Edit Profile</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 회원정보 변경 폼 -->
<form method="post" action="{% url 'custom_auth:edit_profile' %}">
{% csrf_token %}
<div class="mb-3">
<label for="emailEdit" class="form-label">Email</label>
<input type="email" class="form-control" id="emailEdit" name="email" value="{{ request.user.email }}" required>
</div>
<div class="mb-3">
<label for="nhncIdEdit" class="form-label">NHNC ID</label>
<input type="text" class="form-control" id="nhncIdEdit" name="nhnc_id" value="{{ request.user.nhnc_id }}">
</div>
<div class="mb-3">
<label for="nhncApiTenantIdEdit" class="form-label">NHNC API Tenant ID</label>
<input type="text" class="form-control" id="nhncApiTenantIdEdit" name="nhnc_api_tenant_id" value="{{ request.user.nhnc_api_tenant_id }}">
</div>
<div class="mb-3">
<label for="passwordEdit" class="form-label">새 비밀번호</label>
<input type="password" class="form-control" id="passwordEdit" name="password">
</div>
<div class="mb-3">
<label for="passwordConfirmEdit" class="form-label">비밀번호 확인</label>
<input type="password" class="form-control" id="passwordConfirmEdit" name="password_confirm">
</div>
<button type="submit" class="btn btn-primary w-100">Save Changes</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -12,14 +12,32 @@ class CustomUserAdmin(UserAdmin):
# 사용자 필드 구성
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Personal Info', {'fields': ('email', 'encrypted_private_key')}),
('Personal Info', {
'fields': (
'email',
'encrypted_private_key',
'nhnc_id',
'nhnc_api_tenant_id',
'url_gitea',
'url_harbor',
'url_argocd',
'url_web_ide',
'url_rancher',
'url_grafana',
'url_prometheus',
'url_opensearch',
'url_kiali',
'url_nexus',
'url_mattermost',
)
}),
('Permissions', {'fields': ('is_staff', 'is_active')}),
)
# 읽기 전용 필드 추가
readonly_fields = ('encrypted_private_key',)
search_fields = ('username', 'email')
search_fields = ('username', 'email', 'nhnc_id', 'nhnc_api_tenant_id')
ordering = ('username',)

View File

@ -23,7 +23,22 @@ class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ["email", "nhnc_id", "nhnc_api_tenant_id"]
fields = [
'email',
'nhnc_id',
'nhnc_api_tenant_id',
'url_gitea',
'url_harbor',
'url_argocd',
'url_web_ide',
'url_rancher',
'url_grafana',
'url_prometheus',
'url_opensearch',
'url_kiali',
'url_nexus',
'url_mattermost',
]
def clean(self):
cleaned_data = super().clean()

View File

@ -0,0 +1,58 @@
# Generated by Django 4.2.14 on 2025-01-25 00:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_auth', '0005_customuser_encrypted_private_key'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='url_argocd',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_gitea',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_grafana',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_harbor',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_kiali',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_opensearch',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_prometheus',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_rancher',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_web_ide',
field=models.URLField(blank=True, null=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.14 on 2025-01-25 01:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_auth', '0006_customuser_url_argocd_customuser_url_gitea_and_more'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='url_mattermost',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='url_nexus',
field=models.URLField(blank=True, null=True),
),
]

View File

@ -5,15 +5,29 @@ from cryptography.fernet import Fernet
class CustomUser(AbstractUser):
"""사용자 모델 - 기존 필드 + SSH Private Key 관리 필드"""
# 기존 필드 유지
# 사용자 모델 - 기존 필드 + SSH Private Key 관리 필드
grade = models.CharField(max_length=50, blank=True, null=True)
nhnc_id = models.CharField(max_length=100, blank=True, null=True)
nhnc_api_tenant_id = models.CharField(max_length=100, blank=True, null=True)
"""사용자 모델 - SSH Private Key 관리 필드"""
# 사용자 모델 - SSH Private Key 관리 필드
encrypted_private_key = models.BinaryField(blank=True, null=True) # 암호화된 SSH 키
# Custom URL 필드 - 2025-01-25
# 여기 추가하면 components/_nav.html 수정
# custom_auth/forms.py 수정, custom_auth/views.py 수정
url_gitea = models.URLField(max_length=200, blank=True, null=True)
url_harbor = models.URLField(max_length=200, blank=True, null=True)
url_argocd = 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_grafana = 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_kiali = 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)
def encrypt_private_key(self, private_key: str) -> bytes:
"""SSH Private Key 암호화"""

0
obs_minio/__init__.py Normal file
View File

3
obs_minio/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
obs_minio/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ObsMinioConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'obs_minio'

View File

3
obs_minio/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
obs_minio/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
obs_minio/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = 'obs_minio'
urlpatterns = [
path('upload/', views.upload_image, name='upload_image'),
path('get_presigned_url/', views.get_presigned_url, name='get_presigned_url'),
]

76
obs_minio/views.py Normal file
View File

@ -0,0 +1,76 @@
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from minio import Minio
from minio.error import S3Error
import json
import uuid
import urllib3
# MinIO 설정
MINIO_ENDPOINT = settings.MINIO_ENDPOINT_URL
MINIO_ACCESS_KEY = settings.MINIO_ACCESS_KEY
MINIO_SECRET_KEY = settings.MINIO_SECRET_KEY
BUCKET_NAME = settings.MINIO_DEFAULT_BUCKET
# MinIO 클라이언트 생성
client = Minio(
MINIO_ENDPOINT,
access_key=MINIO_ACCESS_KEY,
secret_key=MINIO_SECRET_KEY,
secure=True,
http_client=urllib3.PoolManager(cert_reqs='CERT_NONE') # SSL 검증 비활성화
)
@csrf_exempt
def upload_image(request):
"""
이미지 업로드 및 URL 반환
"""
if request.method == 'POST' and 'image' in request.FILES:
image = request.FILES['image']
unique_filename = f"uploads/{uuid.uuid4()}_{image.name}"
try:
# MinIO에 파일 업로드
client.put_object(
bucket_name=BUCKET_NAME,
object_name=unique_filename,
data=image,
length=image.size,
content_type=image.content_type,
)
# 전체 URL 생성
full_url = f"https://{MINIO_ENDPOINT}/{BUCKET_NAME}/{unique_filename}"
return JsonResponse({"url": full_url}, status=201)
except S3Error as e:
return JsonResponse({"error": str(e)}, status=500)
return JsonResponse({"error": "Invalid request"}, status=400)
@csrf_exempt
def get_presigned_url(request):
"""
Presigned URL 생성 및 반환
"""
if request.method == 'POST':
try:
data = json.loads(request.body)
object_name = data.get('object_name')
if not object_name:
return JsonResponse({"error": "Missing object_name"}, status=400)
# Presigned URL 생성
presigned_url = client.presigned_get_object(BUCKET_NAME, object_name)
return JsonResponse({"presigned_url": presigned_url}, status=200)
except S3Error as e:
return JsonResponse({"error": str(e)}, status=500)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
return JsonResponse({"error": "Invalid request method"}, status=405)

View File

@ -1 +1 @@
dev_0.0.22
dev_0.0.26