공지사항관리 기능 분리 및 개선
All checks were successful
Build And Test / build-and-push (push) Successful in 4m23s

This commit is contained in:
icurfer 2025-01-25 18:02:49 +09:00
parent 06d1853fb0
commit af57b56e69
38 changed files with 710 additions and 188 deletions

BIN
README-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
</div>
<!-- 마크다운 에디터 -->
<h2>contents</h2>
<h2>Contents</h2>
<div class="col-md-12">
{{ form.contents }}
@ -41,9 +41,22 @@
<!-- 마크다운 파서 및 스크립트 -->
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// 마크다운 파서 초기화
const md = window.markdownit();
const md = window.markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs
.highlight(str, {language: lang})
.value;
} catch (__) {}
}
return ''; // 기본 HTML 이스케이프 처리
}
});
// content 필드와 미리보기 연결
const textarea = document.getElementById("markdown-editor");
@ -52,7 +65,16 @@
// 실시간 미리보기 업데이트
textarea.addEventListener("input", function () {
const markdownContent = textarea.value; // textarea 내용 가져오기
preview.innerHTML = md.render(markdownContent); // 마크다운 -> HTML 변환
const renderedContent = md.render(markdownContent); // 마크다운 -> HTML 변환
preview.innerHTML = renderedContent;
// Highlight.js로 코드 블록 강조
document
.querySelectorAll("#preview pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
{% endblock %}
</div>
{% endblock %}

View File

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

View File

@ -8,8 +8,11 @@
<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 class="text-muted fst-italic mb-2">{{ post.created_at }}
<br>
by.
{{ post.author | upper }}</div>
<!-- Rendered Markdown Content -->
<div>
{{ post.render_markdown|safe }}
</div>
@ -22,5 +25,43 @@
<br/>
<br/>
{% endif %}
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary mt-3">Back to List</a>
{% if request.user == post.author %}
<div class="d-flex mt-3">
<a href="{% url 'blog:post_list' %}" class="btn btn-secondary me-2">List</a>
<a href="{% url 'blog:update_post' post.pk %}" class="btn btn-warning me-2">Edit</a>
<form method="POST" action="{% url 'blog:delete_post' post.pk %}" class="d-inline" id="delete-form">
{% csrf_token %}
<button type="button" class="btn btn-danger" id="delete-button">Delete</button>
</form>
</div>
{% endif %}
<!-- Highlight.js Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- Highlight.js Script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
// Initialize Highlight.js
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll("pre code")
.forEach((block) => {
hljs.highlightElement(block);
});
});
</script>
<!-- Delete post -->
<script>
document
.getElementById("delete-button")
.addEventListener("click", function () {
const confirmed = confirm("Are you sure you want to delete this post?");
if (confirmed) {
document
.getElementById("delete-form")
.submit();
}
});
</script>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

0
board_notice/__init__.py Normal file
View File

4
board_notice/admin.py Normal file
View File

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

6
board_notice/apps.py Normal file
View File

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

11
board_notice/forms.py Normal file
View File

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

View File

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

View File

27
board_notice/models.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

3
board_notice/tests.py Normal file
View File

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

12
board_notice/urls.py Normal file
View File

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

62
board_notice/views.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,46 +3,46 @@
{% block title %}Landing Page{% endblock %}
{% block main_area %}
<div class="container mt-5">
<div class="row">
<!-- NoticeBoard Section -->
<div class="col-md-6">
<h2>Latest Notices</h2>
<div class="row">
{% for notice in 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.content|truncatewords:20 }}</p>
<a href="{% url 'butler:notice_detail' notice.id %}" class="btn btn-primary">View Notice</a>
<div class="container mt-5">
<div class="row">
<!-- NoticeBoard Section -->
<div class="col-md-6">
<h2>Latest Notices</h2>
<div class="row">
{% for notice in board_notices %}
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ notice.title }}</h5>
<p class="card-text">{{ notice.contents|truncatechars:50 }}</p>
<a href="{{ notice.get_absolute_url }}" class="btn btn-primary">View Notice</a>
</div>
</div>
</div>
</div>
{% empty %}
<p>No notices available.</p>
{% endfor %}
</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.summary|truncatewords:20 }}</p>
<a href="{% url 'blog:post_detail' post.id %}" class="btn btn-primary">Read More</a>
<!-- 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>
</div>
{% empty %}
<p>No blog posts available.</p>
{% endfor %}
</div>
{% empty %}
<p>No blog posts available.</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

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

View File

@ -4,9 +4,9 @@ from django.views.generic import TemplateView
from pathlib import Path
import markdown
import os
from .models import NoticeBoard, IPManagementRecord
from .models import IPManagementRecord
from blog.models import Post
from .forms import PostForm
from board_notice.models import BoardNotice
from django.db.models import Q
@ -15,39 +15,11 @@ class LandingPageView(TemplateView):
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['notices'] = NoticeBoard.objects.order_by('-created_at')[:3]
context['board_notices'] = BoardNotice.objects.order_by('-created_at')[:3]
return context
# --- notice ---
@login_required
def create_notice(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user # 작성자 정보 추가
post.save()
form.save_m2m()
return redirect('butler:notice_list')
else:
form = PostForm()
return render(request, 'butler/create_notice.html', {'form': form})
def notice_list(request):
records = NoticeBoard.objects.all()
return render(request, "butler/notice_list.html", {"records": records})
def notice_detail_view(request, pk):
try:
notice = NoticeBoard.objects.get(pk=pk)
except NoticeBoard.DoesNotExist:
return render(request, "404.html") # 선택적으로 에러 처리
return render(request, "butler/notice_detail.html", {"notice": notice})
# --- ip management ---
# def ip_mgmt_view(request):
# # records = IPManagementRecord.objects.all()

View File

@ -41,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'taggit',
# Custom by.icurfer
'board_notice',
'custom_auth',
'butler',
'blog',

View File

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

View File

@ -1 +1 @@
dev_0.0.24
dev_0.0.25