Compare commits
7 Commits
150046da1d
...
dev
Author | SHA1 | Date | |
---|---|---|---|
1d38fe26bd | |||
d8111f6070 | |||
af57b56e69 | |||
06d1853fb0 | |||
d34d4f8def | |||
bcfdfe776e | |||
819d554bac |
BIN
README-1.png
Normal file
BIN
README-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
59
README.md
59
README.md
@ -1,6 +1,57 @@
|
|||||||
# butler_ddochi
|
# butler_ddochi
|
||||||
|
|
||||||
### dev_0.0.21
|
butler_ddochi는 개인 테스트 환경 관리를 위한 목적으로 만들어지고 있습니다.
|
||||||
사이드메뉴에 unity 게임 등록.
|
* 클라우드 네이티브 환경을 이용한 서비스 환경 관리
|
||||||
* 쿠버네티스에 배포된 웹 게임 링크 등록.
|
* 쿠버네티스 관리
|
||||||
* 쿠버네티스 클러스터에 배포된 웹 게임 및 istio 설정 필요.
|
* 문의는 icurfer@gmail.com으로 부탁드립니다.
|
||||||
|
|
||||||
|
> 문서가 미흡하여 설치 및 구성이 어려울 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 목차
|
||||||
|
|
||||||
|
* [사전 구성요소 선택](#사전-구성-컴포넌트(선택))
|
||||||
|
* [butler_ddochi설치](#butler_ddochi설치)
|
||||||
|
|
||||||
|
### 사전 구성 컴포넌트(필수)
|
||||||
|
동작을 위한 최소한의 필수 컴포넌트입니다.
|
||||||
|
* MariaDB
|
||||||
|
* MinIO
|
||||||
|
|
||||||
|
### 사전 구성 컴포넌트(선택)
|
||||||
|
아래 컴포넌트들이 배포 되어있다면, 로그인 후 Edit Profile을 이용하여 웹콘솔 링크를 입력하여 사용합니다.
|
||||||
|

|
||||||
|
* 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을 호출하기전에 렌더링이 발생하는 에러 메세지입니다.
|
||||||
|
* 에러가 아니니 기능상 문제 없이 사용 가능합니다.
|
@ -5,10 +5,10 @@ from .models import Post
|
|||||||
class PostForm(forms.ModelForm):
|
class PostForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
fields = ['title', 'content', 'summary', 'tags']
|
fields = ['title', 'contents', 'summary', 'tags']
|
||||||
widgets = {
|
widgets = {
|
||||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
'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'}),
|
'summary': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'tags': TagWidget(attrs={'class': 'form-control', 'placeholder': 'Add tags'}),
|
'tags': TagWidget(attrs={'class': 'form-control', 'placeholder': 'Add tags'}),
|
||||||
}
|
}
|
||||||
|
18
blog/migrations/0005_rename_content_post_contents.py
Normal file
18
blog/migrations/0005_rename_content_post_contents.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
21
blog/migrations/0006_alter_post_author.py
Normal file
21
blog/migrations/0006_alter_post_author.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -1,21 +1,30 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
from markdown_it import MarkdownIt
|
from markdown_it import MarkdownIt
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
class Post(models.Model):
|
class Post(models.Model):
|
||||||
title = models.CharField(max_length=200) # 제목
|
title = models.CharField(max_length=200) # 제목
|
||||||
content = models.TextField() # 본문 (마크다운 저장)
|
contents = models.TextField() # 본문 (마크다운 저장)
|
||||||
summary = models.CharField(max_length=2000, blank=True, null=True) # 요약
|
summary = models.CharField(max_length=2000, blank=True, null=True) # 요약
|
||||||
created_at = models.DateTimeField(auto_now_add=True) # 작성일
|
created_at = models.DateTimeField(auto_now_add=True) # 작성일
|
||||||
updated_at = models.DateTimeField(auto_now=True) # 수정일
|
updated_at = models.DateTimeField(auto_now=True) # 수정일
|
||||||
tags = TaggableManager()
|
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):
|
def render_markdown(self):
|
||||||
"""마크다운을 HTML로 변환"""
|
"""마크다운을 HTML로 변환"""
|
||||||
md = MarkdownIt()
|
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):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
58
blog/templates/blog/_unarchived_create_post_default.html
Normal file
58
blog/templates/blog/_unarchived_create_post_default.html
Normal 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 %}
|
@ -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 %}
|
|
@ -22,11 +22,9 @@
|
|||||||
</div>
|
</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">
|
<div class="d-flex justify-content-end mt-4">
|
||||||
<button type="submit" class="btn btn-primary me-2">Create Post</button>
|
<button type="submit" class="btn btn-primary me-2">Create Post</button>
|
||||||
@ -41,18 +39,116 @@
|
|||||||
|
|
||||||
<!-- 마크다운 파서 및 스크립트 -->
|
<!-- 마크다운 파서 및 스크립트 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 마크다운 파서 초기화
|
// 마크다운 파서 초기화
|
||||||
const md = window.markdownit();
|
const md = window.markdownit({
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs
|
||||||
|
.highlight(str, {language: lang})
|
||||||
|
.value;
|
||||||
|
} catch (__) {}
|
||||||
|
}
|
||||||
|
return ''; // 기본 HTML 이스케이프 처리
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// content 필드와 미리보기 연결
|
// content 필드와 미리보기 연결
|
||||||
const textarea = document.getElementById("markdown-editor");
|
const textarea = document.getElementById("markdown-editor");
|
||||||
const preview = document.getElementById("preview");
|
const preview = document.getElementById("preview");
|
||||||
|
|
||||||
|
// 미리보기 업데이트 함수
|
||||||
|
async function updatePreview() {
|
||||||
|
const markdownContent = textarea.value;
|
||||||
|
const lines = markdownContent.split("\n");
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const match = lines[i].match(/!\[Image\]\((.+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const objectName = match[1];
|
||||||
|
try {
|
||||||
|
// Presigned URL 가져오기
|
||||||
|
const response = await fetch("/obs_minio/get_presigned_url/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({object_name: objectName})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const presignedUrl = data.presigned_url;
|
||||||
|
lines[i] = ``; // Presigned URL로 업데이트
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching presigned URL:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마크다운 렌더링 및 미리보기 업데이트
|
||||||
|
const renderedContent = md.render(lines.join("\n"));
|
||||||
|
preview.innerHTML = renderedContent;
|
||||||
|
|
||||||
|
// Highlight.js로 코드 블록 강조
|
||||||
|
document
|
||||||
|
.querySelectorAll("#preview pre code")
|
||||||
|
.forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 실시간 미리보기 업데이트
|
// 실시간 미리보기 업데이트
|
||||||
textarea.addEventListener("input", function () {
|
textarea.addEventListener("input", updatePreview);
|
||||||
const markdownContent = textarea.value; // textarea 내용 가져오기
|
|
||||||
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
|
// Ctrl+V로 이미지 붙여넣기 처리
|
||||||
|
textarea.addEventListener("paste", async function (event) {
|
||||||
|
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.startsWith("image/")) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 이미지 업로드 API 호출
|
||||||
|
const response = await fetch("/obs_minio/upload/", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const fullImageUrl = data.url; // 전체 URL
|
||||||
|
const objectName = fullImageUrl
|
||||||
|
.split("/")
|
||||||
|
.slice(-2)
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
// 마크다운 에디터에 이미지 삽입
|
||||||
|
const markdownImage = `\n`;
|
||||||
|
textarea.value += markdownImage;
|
||||||
|
|
||||||
|
// 미리보기 업데이트
|
||||||
|
updatePreview();
|
||||||
|
} else {
|
||||||
|
alert("Image upload failed. Please try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
alert("An error occurred during image upload.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
80
blog/templates/blog/create_post_bakcup.html
Normal file
80
blog/templates/blog/create_post_bakcup.html
Normal 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 %}
|
128
blog/templates/blog/create_post_url생성테스트.html
Normal file
128
blog/templates/blog/create_post_url생성테스트.html
Normal 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 = `\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 %}
|
@ -8,9 +8,12 @@
|
|||||||
<h5 class="text-muted">{{ post.summary }}</h5>
|
<h5 class="text-muted">{{ post.summary }}</h5>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Author -->
|
<!-- Author -->
|
||||||
<div class="text-muted fst-italic mb-2">{{ post.created_at }} <br>
|
<div class="text-muted fst-italic mb-2">{{ post.created_at }}
|
||||||
by. {{ post.author | upper }}</div>
|
<br>
|
||||||
<div>
|
by.
|
||||||
|
{{ post.author | upper }}</div>
|
||||||
|
<!-- Rendered Markdown Content -->
|
||||||
|
<div id="post-content">
|
||||||
{{ post.render_markdown|safe }}
|
{{ post.render_markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -22,5 +25,84 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
|
31
blog/templates/blog/post_detail_default.html
Normal file
31
blog/templates/blog/post_detail_default.html
Normal 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 %}
|
@ -12,7 +12,7 @@
|
|||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<h5>{{ post.title }}</h5>
|
<h5>{{ post.title }}</h5>
|
||||||
<p>{{ post.summary }}</p>
|
<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>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
154
blog/templates/blog/update_post.html
Normal file
154
blog/templates/blog/update_post.html
Normal 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">◇마크다운 미리보기는 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] = ``;
|
||||||
|
}
|
||||||
|
} 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 = `\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 %}
|
@ -7,4 +7,6 @@ urlpatterns = [
|
|||||||
path('', views.post_list, name='post_list'), # 포스트 리스트
|
path('', views.post_list, name='post_list'), # 포스트 리스트
|
||||||
path('create/', views.create_post, name='create_post'), # 포스트 작성
|
path('create/', views.create_post, name='create_post'), # 포스트 작성
|
||||||
path('<int:pk>/', views.post_detail, name='post_detail'), # 포스트 상세 보기
|
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
|
||||||
]
|
]
|
@ -3,6 +3,16 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from .models import Post
|
from .models import Post
|
||||||
from .forms import PostForm
|
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
|
@login_required
|
||||||
def create_post(request):
|
def create_post(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -17,10 +27,37 @@ def create_post(request):
|
|||||||
form = PostForm()
|
form = PostForm()
|
||||||
return render(request, 'blog/create_post.html', {'form': form})
|
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)
|
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
0
board_notice/__init__.py
Normal file
4
board_notice/admin.py
Normal file
4
board_notice/admin.py
Normal 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
6
board_notice/apps.py
Normal 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
11
board_notice/forms.py
Normal 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'}),
|
||||||
|
}
|
28
board_notice/migrations/0001_initial.py
Normal file
28
board_notice/migrations/0001_initial.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
board_notice/migrations/__init__.py
Normal file
0
board_notice/migrations/__init__.py
Normal file
27
board_notice/models.py
Normal file
27
board_notice/models.py
Normal 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
|
72
board_notice/templates/board_notice/create_notice.html
Normal file
72
board_notice/templates/board_notice/create_notice.html
Normal 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 %}
|
56
board_notice/templates/board_notice/notice_detail.html
Normal file
56
board_notice/templates/board_notice/notice_detail.html
Normal 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 %}
|
@ -3,26 +3,33 @@
|
|||||||
{% block title %}Notice{% endblock %}
|
{% block title %}Notice{% endblock %}
|
||||||
|
|
||||||
{% block main_area %}
|
{% 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">
|
<table class="table table-bordered table-hover">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">글번호</th>
|
<th scope="col">글번호</th>
|
||||||
<th scope="col">제목</th>
|
<th scope="col">제목</th>
|
||||||
|
<th scope="col">내용</th>
|
||||||
<th scope="col">작성자</th>
|
<th scope="col">작성자</th>
|
||||||
<th scope="col">작성일</th>
|
<th scope="col">작성일</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for record in records %}
|
{% for record in notices %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ record.pk }}</td>
|
<td>{{ record.pk }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ record.get_absolute_url }}">{{ record.title }}</a>
|
<a href="{{ record.get_absolute_url }}">{{ record.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ record.get_absolute_url }}">{{ record.contents|truncatechars:50 }}</a>
|
||||||
|
</td>
|
||||||
<td>{{ record.author }}</td>
|
<td>{{ record.author }}</td>
|
||||||
<td>{{ record.created_at|date:"Y-m-d H:i" }}</td>
|
<td>{{ record.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -32,4 +39,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">현재 공지사항이 없습니다.</p>
|
<p class="text-muted">현재 공지사항이 없습니다.</p>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endblock %}
|
58
board_notice/templates/board_notice/update_notice.html
Normal file
58
board_notice/templates/board_notice/update_notice.html
Normal 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
3
board_notice/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
12
board_notice/urls.py
Normal file
12
board_notice/urls.py
Normal 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
62
board_notice/views.py
Normal 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})
|
@ -1,5 +1,4 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import IPManagementRecord, NoticeBoard
|
from .models import IPManagementRecord
|
||||||
|
|
||||||
admin.site.register(IPManagementRecord)
|
admin.site.register(IPManagementRecord)
|
||||||
admin.site.register(NoticeBoard)
|
|
||||||
|
1
butler/forms.py
Normal file
1
butler/forms.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from django import forms
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
16
butler/migrations/0004_delete_noticeboard.py
Normal file
16
butler/migrations/0004_delete_noticeboard.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -7,7 +7,7 @@ class IPManagementRecord(models.Model):
|
|||||||
network_nm = models.CharField(max_length=100) # NETWORK_NM
|
network_nm = models.CharField(max_length=100) # NETWORK_NM
|
||||||
ip_addrs = models.CharField(max_length=50) # IP_ADDRS
|
ip_addrs = models.CharField(max_length=50) # IP_ADDRS
|
||||||
svr_nm = models.CharField(max_length=100) # SVR_NM
|
svr_nm = models.CharField(max_length=100) # SVR_NM
|
||||||
desc = models.TextField(blank=True, null=True) # DESC
|
contents = models.TextField(blank=True, null=True) # DESC
|
||||||
remark = models.TextField(blank=True, null=True) # REMARK
|
remark = models.TextField(blank=True, null=True) # REMARK
|
||||||
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
|
created_at = models.DateTimeField(auto_now_add=True) # 생성 시간 자동 기록
|
||||||
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
|
updated_at = models.DateTimeField(auto_now=True) # 수정 시간 자동 기록
|
||||||
@ -15,21 +15,4 @@ class IPManagementRecord(models.Model):
|
|||||||
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)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.network_nm} {self.ip_addrs} {self.svr_nm} {self.desc}'
|
return f'{self.network_nm} {self.ip_addrs} {self.svr_nm} {self.contents}'
|
||||||
|
|
||||||
|
|
||||||
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>
|
|
13
butler/templates/butler/_unarchived_landing.html
Normal file
13
butler/templates/butler/_unarchived_landing.html
Normal 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 %}
|
@ -3,11 +3,46 @@
|
|||||||
{% block title %}Landing Page{% endblock %}
|
{% block title %}Landing Page{% endblock %}
|
||||||
|
|
||||||
{% block main_area %}
|
{% block main_area %}
|
||||||
<article class="pt-3">
|
<div class="container mt-5">
|
||||||
<h2 class="fw-bold pb-2">Welcome!</h2>
|
<div class="row">
|
||||||
<h3>IT Infra 및 DevOps 자원관리 도구</h3>
|
<!-- NoticeBoard Section -->
|
||||||
* IP리스트 관리기능 제공 <br>
|
<div class="col-md-6">
|
||||||
* Public NHN Cloud API 기능 일부 제공
|
<h2>Latest Notices</h2>
|
||||||
<p>계속 기능 구현 중 입니다.</p>
|
<div class="row">
|
||||||
</article>
|
{% 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 %}
|
{% endblock %}
|
@ -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 %}
|
|
@ -4,9 +4,13 @@ from . import views
|
|||||||
app_name = 'butler'
|
app_name = 'butler'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.hello_view, name='landing'), # 루트 경로에서 hello_view 호출
|
# path('', views.hello_view, name='landing'), # 루트 경로에서 hello_view 호출
|
||||||
path('notice', views.notice_view, name='notice'),
|
# Landing Page
|
||||||
path('notice/<int:pk>/', views.notice_detail_view, name='notice_detail'),
|
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', views.ip_mgmt_view, name='ip_mgmt'),
|
||||||
path('ip-mgmt/add/', views.add_ip_record, name='ip_mgmt_add'), # 데이터 추가 경로
|
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 추가
|
path('ip-mgmt/delete/', views.delete_ip_records, name='ip_mgmt_delete'), # 삭제 URL 추가
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
from django.shortcuts import render, redirect, get_object_or_404
|
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
|
from pathlib import Path
|
||||||
import markdown
|
import markdown
|
||||||
import os
|
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
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
def hello_view(request):
|
class LandingPageView(TemplateView):
|
||||||
return render(
|
template_name = "butler/landing.html"
|
||||||
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})
|
|
||||||
|
|
||||||
|
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 ---
|
# --- ip management ---
|
||||||
# def ip_mgmt_view(request):
|
# def ip_mgmt_view(request):
|
||||||
|
@ -41,6 +41,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'taggit',
|
'taggit',
|
||||||
# Custom by.icurfer
|
# Custom by.icurfer
|
||||||
|
'board_notice',
|
||||||
'custom_auth',
|
'custom_auth',
|
||||||
'butler',
|
'butler',
|
||||||
'blog',
|
'blog',
|
||||||
@ -50,6 +51,7 @@ INSTALLED_APPS = [
|
|||||||
'nhnc_mgmt',
|
'nhnc_mgmt',
|
||||||
'mm_msg',
|
'mm_msg',
|
||||||
'ansible_manager',
|
'ansible_manager',
|
||||||
|
'obs_minio',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -166,6 +168,7 @@ LOGIN_REDIRECT_URL = '/'
|
|||||||
LOGOUT_REDIRECT_URL = '/'
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
# MinIO 설정
|
# MinIO 설정
|
||||||
# MINIO_STORAGE_MEDIA_URL = os.environ.get('MINIO_STORAGE_MEDIA_URL', '')
|
MINIO_ENDPOINT_URL = os.environ.get('MINIO_ENDPOINT_URL', '')
|
||||||
# MINIO_STORAGE_ACCESS_KEY = os.environ.get('MINIO_STORAGE_ACCESS_KEY', '')
|
MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY', '')
|
||||||
# MINIO_STORAGE_SECRET_KEY = os.environ.get('MINIO_STORAGE_SECRET_KEY', '')
|
MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY', '')
|
||||||
|
MINIO_DEFAULT_BUCKET=os.environ.get('MINIO_DEFAULT_BUCKET', '')
|
@ -24,11 +24,13 @@ urlpatterns = [
|
|||||||
path('', include('butler.urls')),
|
path('', include('butler.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('accounts/', include('custom_auth.urls')), # custom_auth URL 추가
|
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('nhncloud/', include('nhncloud.urls')),
|
||||||
path('nhnc_mgmt/', include('nhnc_mgmt.urls')),
|
path('nhnc_mgmt/', include('nhnc_mgmt.urls')),
|
||||||
path('mm_msg/', include('mm_msg.urls')),
|
path('mm_msg/', include('mm_msg.urls')),
|
||||||
path('blog/', include('blog.urls')),
|
|
||||||
path('ansible_manager/', include('ansible_manager.urls')),
|
path('ansible_manager/', include('ansible_manager.urls')),
|
||||||
|
path('obs_minio/', include('obs_minio.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
@ -1,40 +1,38 @@
|
|||||||
<div class="container-fluid d-flex justify-content-between align-items-center">
|
<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">
|
<h1 class="d-flex align-items-center fs-4 text-white mb-0">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<i class="fa-solid fa-dog"></i> DDoCHI</a>
|
<i class="fa-solid fa-dog"></i>
|
||||||
|
DDoCHI</a>
|
||||||
</h1>
|
</h1>
|
||||||
<!-- 로그인/로그아웃 및 회원가입 버튼 -->
|
<!-- 로그인/로그아웃 및 회원가입 버튼 -->
|
||||||
<ul class="navbar-nav flex-row">
|
<ul class="navbar-nav flex-row">
|
||||||
{% if user.is_authenticated and user.is_superuser %}
|
{% if user.is_authenticated and user.is_superuser %}
|
||||||
<!-- 관리자 콘솔 버튼 -->
|
<!-- 관리자 콘솔 버튼 -->
|
||||||
<li class="nav-item me-3">
|
<li class="nav-item me-3">
|
||||||
<a href="/admin" target="_blank" class="btn btn-outline-warning mb-2">ADMIN</a>
|
<a href="/admin" target="_blank" class="btn btn-outline-warning mb-2">ADMIN</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<!-- 회원정보 변경 버튼 -->
|
<!-- 회원정보 변경 버튼 -->
|
||||||
<li class="nav-item me-3">
|
<li class="nav-item me-3">
|
||||||
<button type="button" class="btn btn-outline-info mb-2" data-bs-toggle="modal"
|
<button type="button" class="btn btn-outline-info mb-2" data-bs-toggle="modal" data-bs-target="#editProfileModal">Edit Profile</button>
|
||||||
data-bs-target="#editProfileModal">Edit Profile</button>
|
</li>
|
||||||
</li>
|
<!-- 로그아웃 버튼 -->
|
||||||
<!-- 로그아웃 버튼 -->
|
<li class="nav-item">
|
||||||
<li class="nav-item">
|
<form method="post" action="{% url 'custom_auth:logout' %}">
|
||||||
<form method="post" action="{% url 'custom_auth:logout' %}">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<button type="submit" class="btn btn-outline-danger w-100">Logout</button>
|
||||||
<button type="submit" class="btn btn-outline-danger w-100">Logout</button>
|
</form>
|
||||||
</form>
|
</li>
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- 로그인 버튼 -->
|
<!-- 로그인 버튼 -->
|
||||||
<li class="nav-item me-3">
|
<li class="nav-item me-3">
|
||||||
<button type="button" class="btn btn-outline-primary mb-2" data-bs-toggle="modal"
|
<button type="button" class="btn btn-outline-primary mb-2" data-bs-toggle="modal" data-bs-target="#loginModal">Login</button>
|
||||||
data-bs-target="#loginModal">Login</button>
|
</li>
|
||||||
</li>
|
<!-- 회원가입 버튼 -->
|
||||||
<!-- 회원가입 버튼 -->
|
<li class="nav-item me-3">
|
||||||
<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>
|
||||||
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#signupModal">Sign
|
</li>
|
||||||
Up</button>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -50,11 +48,11 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- 로그인 실패 메시지 표시 -->
|
<!-- 로그인 실패 메시지 표시 -->
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- 로그인 폼 -->
|
<!-- 로그인 폼 -->
|
||||||
@ -123,7 +121,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="emailEdit" class="form-label">Email</label>
|
<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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="nhncIdEdit" class="form-label">NHNC ID</label>
|
<label for="nhncIdEdit" class="form-label">NHNC ID</label>
|
||||||
@ -133,6 +131,52 @@
|
|||||||
<label for="nhncApiTenantIdEdit" class="form-label">NHNC API Tenant ID</label>
|
<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 }}">
|
<input type="text" class="form-control" id="nhncApiTenantIdEdit" name="nhnc_api_tenant_id" value="{{ request.user.nhnc_api_tenant_id }}">
|
||||||
</div>
|
</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">
|
<div class="mb-3">
|
||||||
<label for="passwordEdit" class="form-label">새 비밀번호</label>
|
<label for="passwordEdit" class="form-label">새 비밀번호</label>
|
||||||
<input type="password" class="form-control" id="passwordEdit" name="password">
|
<input type="password" class="form-control" id="passwordEdit" name="password">
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
<nav class="sticky-xl-top small" id="toc">
|
<nav class="sticky-xl-top small" id="toc">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#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">
|
<ul class="list-unstyled ps-3 collapse" id="about-collapse">
|
||||||
<li>
|
<li>
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/notice">공지사항</a>
|
<a class="d-inline-flex align-items-center rounded" href="/notice">공지사항</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<hr>
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#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">
|
<ul class="list-unstyled ps-3 collapse" id="contents-collapse">
|
||||||
<li>
|
<li>
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/blog">Post</a>
|
<a class="d-inline-flex align-items-center rounded" href="/blog">Post</a>
|
||||||
@ -30,9 +31,8 @@
|
|||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#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">
|
<ul class="list-unstyled ps-3 collapse" id="nhnc-collapse">
|
||||||
<span>가이드</span>
|
|
||||||
{% if request.user.is_authenticated and request.user.is_staff %}
|
{% if request.user.is_authenticated and request.user.is_staff %}
|
||||||
<li>
|
<li>
|
||||||
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
|
<a class="d-inline-flex align-items-center rounded" href="/nhncloud/test">출력결과 검토</a>
|
||||||
@ -78,56 +78,51 @@
|
|||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#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">
|
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
|
||||||
<span>Dev</span>
|
<span>Dev</span>
|
||||||
<li>
|
<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>
|
||||||
<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>
|
||||||
<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>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<span>Ops</span>
|
<span>Ops</span>
|
||||||
{% if request.user.is_authenticated and request.user.is_staff %}
|
{% if request.user.is_authenticated and request.user.is_staff %}
|
||||||
<li>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<a class="d-inline-flex align-items-center rounded" href="https://kiali.icurfer.com/" target="_blank">Main Cluster Traffic - Kiali</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>
|
|
||||||
<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>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#forms-collapse" aria-controls="forms-collapse">Documents</button>
|
<button class="btn d-inline-flex align-items-center collapsed" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#other-collapse" aria-controls="other-collapse">Other Tools ▽</button>
|
||||||
<ul class="list-unstyled ps-3 collapse" id="forms-collapse">
|
<ul class="list-unstyled ps-3 collapse" id="other-collapse">
|
||||||
<li>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
149
components/templates/components/_unarchived/_nav_250125.html
Normal file
149
components/templates/components/_unarchived/_nav_250125.html
Normal 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>
|
@ -12,14 +12,32 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
# 사용자 필드 구성
|
# 사용자 필드 구성
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(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')}),
|
('Permissions', {'fields': ('is_staff', 'is_active')}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 읽기 전용 필드 추가
|
# 읽기 전용 필드 추가
|
||||||
readonly_fields = ('encrypted_private_key',)
|
readonly_fields = ('encrypted_private_key',)
|
||||||
|
|
||||||
search_fields = ('username', 'email')
|
search_fields = ('username', 'email', 'nhnc_id', 'nhnc_api_tenant_id')
|
||||||
ordering = ('username',)
|
ordering = ('username',)
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,22 @@ class CustomUserChangeForm(UserChangeForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
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):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -5,15 +5,29 @@ from cryptography.fernet import Fernet
|
|||||||
|
|
||||||
|
|
||||||
class CustomUser(AbstractUser):
|
class CustomUser(AbstractUser):
|
||||||
"""사용자 모델 - 기존 필드 + SSH Private Key 관리 필드"""
|
# 사용자 모델 - 기존 필드 + SSH Private Key 관리 필드
|
||||||
|
|
||||||
# 기존 필드 유지
|
|
||||||
grade = models.CharField(max_length=50, blank=True, null=True)
|
grade = models.CharField(max_length=50, blank=True, null=True)
|
||||||
nhnc_id = models.CharField(max_length=100, 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)
|
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 키
|
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:
|
def encrypt_private_key(self, private_key: str) -> bytes:
|
||||||
"""SSH Private Key 암호화"""
|
"""SSH Private Key 암호화"""
|
||||||
|
0
obs_minio/__init__.py
Normal file
0
obs_minio/__init__.py
Normal file
3
obs_minio/admin.py
Normal file
3
obs_minio/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
obs_minio/apps.py
Normal file
6
obs_minio/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ObsMinioConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'obs_minio'
|
0
obs_minio/migrations/__init__.py
Normal file
0
obs_minio/migrations/__init__.py
Normal file
3
obs_minio/models.py
Normal file
3
obs_minio/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
3
obs_minio/tests.py
Normal file
3
obs_minio/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
9
obs_minio/urls.py
Normal file
9
obs_minio/urls.py
Normal 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
76
obs_minio/views.py
Normal 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)
|
Reference in New Issue
Block a user