11from cms .admin_mixins import RelatedReadonlyFieldsMixin
22from cms .models import Page , Section , Sitemap
3+ from django import forms
34from django .contrib import admin
5+ from django .utils .html import format_html
46
7+ _CODE_PREVIEW_TEMPLATE = """
8+ <style>
9+ /* ── 래퍼 기본 ── */
10+ .code-preview-wrapper {{
11+ display: flex;
12+ flex-direction: column;
13+ width: 800px;
14+ gap: 8px;
15+ background: #fff;
16+ }}
17+ .code-preview-wrapper > textarea {{ display: none !important; }}
518
19+ /* ── 툴바 ── */
20+ .cp-toolbar {{
21+ display: flex;
22+ align-items: center;
23+ gap: 6px;
24+ padding: 4px;
25+ background: #333;
26+ border-bottom: 1px solid #444;
27+ position: relative;
28+ z-index: 2001; /* admin nav 위 */
29+ }}
30+ .cp-toolbar button {{
31+ padding: 4px 8px;
32+ font-size: 13px;
33+ border: 1px solid #555;
34+ background: #444;
35+ color: #fff;
36+ cursor: pointer;
37+ }}
38+ .cp-toolbar .preview-toggle {{ display: inline-block; }}
39+ .cp-toolbar .lang-toggle,
40+ .cp-toolbar .theme-toggle {{ display: none !important; }}
41+ .code-preview-wrapper.fullscreen .cp-toolbar .lang-toggle,
42+ .code-preview-wrapper.fullscreen .cp-toolbar .theme-toggle {{
43+ display: inline-block !important;
44+ }}
45+ .lang-toggle.js-mode.active {{ background: #f1c40f; border-color: #d4ac0d; color: #333; }}
46+ .lang-toggle.ts-mode.active {{ background: #3498db; border-color: #2e86c1; color: #fff; }}
47+ .theme-toggle.active {{ background: #888; border-color: #666; color: #fff; }}
48+
49+ /* ── 에디터·프리뷰 컨테이너 ── */
50+ .cp-main {{
51+ display: flex;
52+ flex-direction: column;
53+ gap: 8px;
54+ min-height: 0;
55+ }}
56+ .code-preview-wrapper:not(.fullscreen) .cp-main {{
57+ display: none !important;
58+ }}
59+
60+ /* 에디터 블록 */
61+ .editor-block {{
62+ flex: 1;
63+ min-height: 0;
64+ position: relative;
65+ }}
66+ .editor-block .CodeMirror,
67+ .editor-block .CodeMirror-scroll {{
68+ height: 100% !important;
69+ overflow: auto !important;
70+ }}
71+
72+ /* 프리뷰 iframe */
73+ .cp-main iframe {{
74+ flex: 1;
75+ min-height: 0;
76+ overflow: auto;
77+ }}
78+
79+ /* ── 풀스크린 모드 ── */
80+ .code-preview-wrapper.fullscreen {{
81+ position: fixed !important;
82+ top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
83+ width: 100vw !important;
84+ height: 100vh !important;
85+ margin: 0; padding: 0;
86+ display: flex;
87+ flex-direction: column;
88+ background: #fff;
89+ z-index: 2000;
90+ }}
91+ .code-preview-wrapper.fullscreen .cp-main {{
92+ flex: 1;
93+ display: flex;
94+ flex-direction: row;
95+ min-height: 0;
96+ }}
97+ .code-preview-wrapper.fullscreen .editor-block {{
98+ max-height: none !important;
99+ }}
100+ .code-preview-wrapper.fullscreen .cp-toolbar {{
101+ flex-shrink: 0;
102+ }}
103+
104+ /* ── 다크모드 ── */
105+ .dark-editor .CodeMirror {{ background: #2d2d2d !important; color: #ccc; }}
106+ .dark-preview iframe {{ background: #2d2d2d; }}
107+ </style>
108+
109+ <div class="code-preview-wrapper" id="cpw_{name}">
110+ <div class="cp-toolbar">
111+ <button type="button" class="preview-toggle">Preview Mode</button>
112+ <button type="button" class="lang-toggle js-mode active" data-lang="js">JS</button>
113+ <button type="button" class="lang-toggle ts-mode" data-lang="ts">TS</button>
114+ <button type="button" class="theme-toggle" data-target="editor">Editor Dark</button>
115+ <button type="button" class="theme-toggle" data-target="preview">Preview Dark</button>
116+ </div>
117+ {ta}
118+ <div class="cp-main">
119+ <div class="editor-block" id="editor_{name}"></div>
120+ <iframe id="preview_{name}"></iframe>
121+ </div>
122+ </div>
123+ """
124+
125+
126+ class CodeEditorWidget (forms .Textarea ):
127+ class Media :
128+ css = {
129+ "all" : (
130+ "https://unpkg.com/codemirror@5.65.5/lib/codemirror.css" ,
131+ "https://unpkg.com/codemirror@5.65.5/theme/dracula.css" ,
132+ )
133+ }
134+ js = (
135+ "https://unpkg.com/codemirror@5.65.5/lib/codemirror.js" ,
136+ "https://unpkg.com/codemirror@5.65.5/mode/javascript/javascript.js" ,
137+ "https://unpkg.com/@babel/standalone/babel.min.js" ,
138+ "/static/admin/js/editor.js" ,
139+ )
140+
141+ def render (self , name , value , attrs = None , renderer = None ):
142+ ta = super ().render (name , value , attrs , renderer )
143+ return format_html (_CODE_PREVIEW_TEMPLATE , name = name , ta = ta )
144+
145+
146+ class SectionAdminForm (forms .ModelForm ):
147+ class Meta :
148+ model = Section
149+ fields = "__all__"
150+ widgets = {"body" : CodeEditorWidget ()}
151+
152+
153+ @admin .register (Sitemap )
6154class SitemapAdmin (RelatedReadonlyFieldsMixin , admin .ModelAdmin ):
7155 fields = ["id" , "parent_sitemap" , "page" , "name" , "order" , "display_start_at" , "display_end_at" ]
8156 readonly_fields = ["id" ]
@@ -43,7 +191,9 @@ class PageAdmin(admin.ModelAdmin):
43191 pass
44192
45193
194+ @admin .register (Section )
46195class SectionAdmin (RelatedReadonlyFieldsMixin , admin .ModelAdmin ):
196+ form = SectionAdminForm
47197 fields = ["id" , "page" , "order" , "css" , "body" ]
48198 readonly_fields = ["id" ]
49199 related_readonly_config = {"page" : ["id" , "is_active" , "css" , "title" , "subtitle" ]}
@@ -66,6 +216,4 @@ def get_queryset(self, request):
66216 return super ().get_queryset (request ).select_related ("page" )
67217
68218
69- admin .site .register (Sitemap , SitemapAdmin )
70- admin .site .register (Page , PageAdmin )
71- admin .site .register (Section , SectionAdmin )
219+ admin .site .register (Page )
0 commit comments