Compare commits

..

7 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
bcfdfe776e notice 등록 기능 구현
All checks were successful
Build And Test / build-and-push (push) Successful in 5m19s
2025-01-17 15:41:42 +09:00
819d554bac notice 등록 기능 구현 2025-01-17 15:41:11 +09:00
61 changed files with 1755 additions and 212 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

@ -5,10 +5,10 @@ from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'summary', 'tags']
fields = ['title', 'contents', 'summary', 'tags']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'id': 'markdown-editor'}),
'contents': forms.Textarea(attrs={'class': 'form-control', 'id': 'markdown-editor'}),
'summary': forms.TextInput(attrs={'class': 'form-control'}),
'tags': TagWidget(attrs={'class': 'form-control', 'placeholder': 'Add tags'}),
}

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2025-01-17 15:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0004_remove_post_tags_post_tags'),
]
operations = [
migrations.RenameField(
model_name='post',
old_name='content',
new_name='contents',
),
]

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,21 +1,30 @@
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
class Post(models.Model):
title = models.CharField(max_length=200) # 제목
content = models.TextField() # 본문 (마크다운 저장)
contents = models.TextField() # 본문 (마크다운 저장)
summary = models.CharField(max_length=2000, blank=True, null=True) # 요약
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.content)
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

@ -0,0 +1,58 @@
{% 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>
<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

@ -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.content }}
</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>
{% 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

@ -3,26 +3,33 @@
{% block title %}Notice{% endblock %}
{% block main_area %}
<h2 class="fw-bold pt-3 pb-2">공지사항</h2>
<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>
@ -32,4 +39,10 @@
{% else %}
<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 '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)

1
butler/forms.py Normal file
View File

@ -0,0 +1 @@
from django import forms

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' %}" class="btn btn-secondary mt-3">Back to Notices</a>
{% endblock %}

View File

@ -4,9 +4,13 @@ from . import views
app_name = 'butler'
urlpatterns = [
path('', views.hello_view, name='landing'), # 루트 경로에서 hello_view 호출
path('notice', views.notice_view, name='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,32 +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 .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 ---
def notice_view(request):
records = NoticeBoard.objects.all()
return render(request, "butler/notice.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,16 +5,30 @@ 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 암호화"""
cipher = Fernet(self.get_encryption_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.21_r1
dev_0.0.26