프로젝트 전중후 좋은 아이디어가 생각났을경우 지속적인 회의 등을 거치는것도 좋지만
자칫 상대의 집중시간을 방해하거나 채택되지않을 경우 일정에 영향을 줄 수 있다.
comming soon 화면을 생성해 아이디어를 push하면 모두에게 공유가 되고 파일형으로도 자유롭게 따로 저장도 할 수 있도록 구현해보자.
config의 urls.py에 comming-soon/은 apps.commingsoon.urls를 include하도록 setting 되어있고
apps/commingsoon/urls.py에는 comming soon 페이지와, comming soon에 들어갈 아이디어 목록, 상세, 수정, 삭제 기능을 넣자.
from django.urls import path
from apps.comingsoon import views
app_name = 'comingsoon'
urlpatterns = [
path('', views.index, name='index'),
path('blocked/', views.blocked, name='blocked'),
path('manage/', views.manage, name='manage'),
path('manage/new/', views.editor, name='editor_new'),
path('manage/<slug:slug>/edit/', views.editor, name='editor'),
path('manage/<slug:slug>/delete/', views.delete, name='delete'),
path('<slug:slug>/', views.detail, name='detail'),
]
기능 미리보기에는 모두가 접근이 가능하나 관리는 관리자만 가능하다 .


목록화면




storage.py에는 태그, 메타데이터, 파일 저장폴더, 삭제파일폴더, url용 slug만들기, 마크다운 만들기, 목록, 상세, 추가/수정, 삭제를 정의했다.
"""Coming Soon 문서 파일 저장소.
기획안에 따라 DB가 아닌 Markdown 파일로 추가 / 수정 / 삭제를 관리한다.
삭제한 문서는 지우지 않고 deleted 폴더로 이동한다.
"""
import re
from datetime import date
import yaml
from django.conf import settings
STATUS_CHOICES = ['아이디어', '연구 예정', '개발 예정', '보류', '완료']
CATEGORY_CHOICES = ['Agent', 'LLM', 'Data', 'Engineering', 'Infra', 'ETC']
# 모든 문서가 공유하는 메타데이터 공통 form
META_FORM = {
'title': '',
'summary': '',
'status': '아이디어',
'category': 'Agent',
'technologies': [],
'related_agents': [],
'created_at': '',
'updated_at': '',
}
FRONT_MATTER_RE = re.compile(r'^---\s*\n(.*?)\n---\s*\n?(.*)$', re.DOTALL)
def _root():
path = settings.COMING_SOON_DIR
path.mkdir(parents=True, exist_ok=True)
return path
def _deleted_root():
path = settings.COMING_SOON_DELETED_DIR
path.mkdir(parents=True, exist_ok=True)
return path
def slugify(title):
slug = re.sub(r'[^0-9A-Za-z가-힣]+', '-', title).strip('-').lower()
if not re.fullmatch(r'[-a-zA-Z0-9_]+', slug):
raise ValueError(
'제목에 한글 또는 URL에서 사용할 수 없는 문자가 포함되어 있습니다.'
)
return slug
def _split_front_matter(text):
match = FRONT_MATTER_RE.match(text)
if not match:
return {}, text
meta = yaml.safe_load(match.group(1)) or {}
return meta, match.group(2)
def _dump(meta, body):
front = yaml.safe_dump(meta, allow_unicode=True, sort_keys=False).strip()
return f'---\n{front}\n---\n\n{body.strip()}\n'
def _to_list(value):
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if not value:
return []
return [item.strip() for item in str(value).split(',') if item.strip()]
def normalize(meta):
normalized = dict(META_FORM)
normalized.update({key: value for key, value in meta.items() if key in META_FORM})
normalized['technologies'] = _to_list(normalized['technologies'])
normalized['related_agents'] = _to_list(normalized['related_agents'])
return normalized
def list_documents(keyword='', status='', category=''):
documents = []
for path in sorted(_root().glob('*.md')):
document = read_document(path.stem)
if not document:
continue
if keyword and keyword.lower() not in (document['title'] + document['summary']).lower():
continue
if status and document['status'] != status:
continue
if category and document['category'] != category:
continue
documents.append(document)
return documents
def read_document(slug):
path = _root() / f'{slug}.md'
if not path.exists():
return None
meta, body = _split_front_matter(path.read_text(encoding='utf-8'))
document = normalize(meta)
document['slug'] = slug
document['body'] = body.strip()
return document
def save_document(slug, meta, body):
document = normalize(meta)
today = date.today().isoformat()
document['created_at'] = document['created_at'] or today
document['updated_at'] = today
new_slug = slug or slugify(document['title'])
path = _root() / f'{new_slug}.md'
path.write_text(_dump(document, body), encoding='utf-8')
return new_slug
def delete_document(slug):
path = _root() / f'{slug}.md'
if not path.exists():
return False
target = _deleted_root() / f'{slug}-{date.today().isoformat()}.md'
path.rename(target)
return True
views.py에는 각 기능을 정의했다.
markdown을 html로 변환하고 모든 함수는 @login_required 로그인이 필수로 index(), detail(), blocked(), manage(), editor()
storage 객체 read_document, save_document, delete_document 와 templates view를 연결한다.
import markdown
from django.http import Http404
from django.shortcuts import redirect, render
from apps.accounts.session import admin_required, login_required
from apps.comingsoon import storage
def _render_markdown(body):
return markdown.markdown(body, extensions=['fenced_code', 'tables'])
@login_required
def index(request):
context = {
'documents': storage.list_documents(),
'active_menu': 'comingsoon',
}
return render(request, 'comingsoon/index.html', context)
@login_required
def detail(request, slug):
document = storage.read_document(slug)
if not document:
raise Http404('coming soon document not found')
context = {
'document': document,
'body_html': _render_markdown(document['body']),
'active_menu': 'comingsoon',
}
return render(request, 'comingsoon/detail.html', context)
@login_required
def blocked(request):
"""관리자 전용 메뉴에 접근한 일반 / 체험 사용자 안내 화면."""
return render(request, 'comingsoon/blocked.html', {'active_menu': 'comingsoon'})
@admin_required
def manage(request):
keyword = request.GET.get('q', '').strip()
status = request.GET.get('status', '')
category = request.GET.get('category', '')
context = {
'documents': storage.list_documents(keyword, status, category),
'filters': {'q': keyword, 'status': status, 'category': category},
'statuses': storage.STATUS_CHOICES,
'categories': storage.CATEGORY_CHOICES,
'active_menu': 'comingsoon_manage',
}
return render(request, 'comingsoon/manage.html', context)
@admin_required
def editor(request, slug=None):
document = storage.read_document(slug) if slug else dict(storage.META_FORM, slug='', body='')
if slug and not document:
raise Http404('coming soon document not found')
if request.method == 'POST':
meta = {
'title': request.POST.get('title', '').strip(),
'summary': request.POST.get('summary', '').strip(),
'status': request.POST.get('status', storage.STATUS_CHOICES[0]),
'category': request.POST.get('category', storage.CATEGORY_CHOICES[0]),
'technologies': request.POST.get('technologies', ''),
'related_agents': request.POST.get('related_agents', ''),
'created_at': document.get('created_at', ''),
}
new_slug = storage.save_document(slug, meta, request.POST.get('body', ''))
return redirect('comingsoon:editor', slug=new_slug)
context = {
'document': document,
'statuses': storage.STATUS_CHOICES,
'categories': storage.CATEGORY_CHOICES,
'active_menu': 'comingsoon_manage',
}
return render(request, 'comingsoon/editor.html', context)
@admin_required
def delete(request, slug):
if request.method == 'POST':
storage.delete_document(slug)
return redirect('comingsoon:manage')
'Portfolio' 카테고리의 다른 글
| S brain - front - Scaffolding - run.bat 설정하기 (0) | 2026.08.20 |
|---|---|
| S brain - front - Scaffolding - manage.py - execute_from_command_line() (feat. settings.py) (0) | 2026.08.20 |
| S brain - front - Scaffolding - Sample Pages (0) | 2026.08.19 |
| SHS RAG 기반 교내 규정 질의응답 서비스 (feat.LLM) (0) | 2026.08.10 |
| S board OTT 이탈 위험도 랭킹 시스템 (feat. Deep learning모델) (0) | 2026.06.25 |