From 79be87e7e9e9c1896860f59249bd65f01491a1ea Mon Sep 17 00:00:00 2001 From: king Date: Wed, 16 Sep 2026 00:23:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=EC=97=85=EB=AC=B4=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=EB=9E=80=EC=9D=84=20=EA=B0=84=EB=8B=A8=ED=95=9C=20?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=ED=94=84=EB=A1=9C=EC=84=B8=EC=84=9C=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=EB=A1=9C=20=E2=80=94=20=EC=84=9C?= =?UTF-8?q?=EC=8B=9D=20=EB=8F=84=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 굵게/기울임/밑줄/글머리목록/번호목록/인용구/링크/서식지우기 버튼을 설명란 위에 추가. execCommand 기반(같은 파일의 이미지 삽입에서 이미 insertHTML 로 쓰던 것과 같은 패턴)이고, 생성되는 태그(b/i/u/ul/ol/li/ blockquote/a)는 전부 기존 sanitize_description_html 허용 목록 안이라 서버 쪽 변경 없음. 버튼 mousedown 에서 preventDefault 해 클릭 시 에디터 선택범위가 풀리는 것 방지, 커서 위치에 따라 버튼 활성 상태 표시. Co-Authored-By: Claude Sonnet 5 --- .../project/templates/project/inbox.html | 4 +-- .../project/templates/project/index.html | 18 ++++++++-- .../project/templates/project/project.html | 18 ++++++++-- app/static/project.css | 13 +++++++ app/static/project.js | 34 +++++++++++++++++++ 5 files changed, 79 insertions(+), 8 deletions(-) diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 4be6fc9..5843b19 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -45,5 +45,5 @@ {% endif %} - + {% endblock %} diff --git a/app/modules/project/templates/project/index.html b/app/modules/project/templates/project/index.html index 48ea035..969619f 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -290,12 +290,24 @@ - +
설명
+
+ + + + + + + + + + +
@@ -380,5 +392,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 48f4b68..57c1220 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + @@ -277,12 +277,24 @@ - +
설명
+
+ + + + + + + + + + +
@@ -393,5 +405,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index 5c07af8..9ceed28 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -597,6 +597,16 @@ /* ── 업무 설명 — contenteditable(이미지 붙여넣기/업로드 삽입) ── */ .pj-desc-toolbar { display: flex; align-items: center; gap: 8px; margin: -6px 0 6px; } .pj-desc-toolbar .pj-muted-sm { font-size: 11px; color: #9aa1a9; } +/* 간단 워드프로세서 서식 도구 모음 */ +.pj-desc-fmtbar { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; margin-bottom: 6px; } +.pj-desc-fmtbtn { + min-width: 26px; height: 26px; padding: 0 6px; border: 1px solid transparent; border-radius: 6px; + background: transparent; color: #444; font-size: 13px; font-family: inherit; cursor: pointer; + display: inline-flex; align-items: center; justify-content: center; +} +.pj-desc-fmtbtn:hover { background: #f1f2f4; } +.pj-desc-fmtbtn.is-active { background: var(--pj-accent-tint); border-color: var(--pj-accent); color: var(--pj-accent-dark); } +.pj-desc-fmtsep { width: 1px; height: 16px; background: #e2e4ea; margin: 0 4px; } .pj-desc-editor { min-height: 64px; max-height: 260px; overflow-y: auto; padding: 8px 10px; border: 1px solid #d8dce2; border-radius: 8px; font-size: 13.5px; font-family: inherit; @@ -605,6 +615,9 @@ .pj-desc-editor:empty::before { content: attr(data-placeholder); color: #b7bdc5; } .pj-desc-editor img { max-width: 100%; border-radius: 6px; margin: 4px 0; display: block; cursor: pointer; } .pj-desc-editor img.pj-img-selected { outline: 2px solid var(--pj-accent); outline-offset: 1px; } +.pj-desc-editor blockquote { margin: 6px 0; padding: 2px 12px; border-left: 3px solid var(--pj-border); color: #565c66; } +.pj-desc-editor ul, .pj-desc-editor ol { margin: 4px 0; padding-left: 22px; } +.pj-desc-editor a { color: var(--pj-accent-dark); } .pj-desc-editor[contenteditable="false"] { background: #f6f7f8; color: #9aa1a9; } /* 설명란 이미지 선택 시 오버레이(삭제 × / 우하단 크기조절 핸들) — wrap 은 diff --git a/app/static/project.js b/app/static/project.js index b160998..e6b8a28 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -1999,6 +1999,40 @@ const hint = el("pj-desc-hint"); let taskId = null; + // ── 서식 도구 모음(간단 워드프로세서) — execCommand 는 구식 API지만 + // 크롬/엣지는 여전히 지원하고, 이미 같은 파일의 이미지 삽입에서도 쓰고 + // 있어(insertHTML) 일관된 패턴이다. 버튼 mousedown 에서 preventDefault + // 안 하면 클릭 순간 에디터가 포커스를 잃어 선택 범위가 풀린다. + const fmtbar = el("pj-desc-fmtbar"); + if (fmtbar) { + fmtbar.addEventListener("mousedown", function (e) { e.preventDefault(); }); + fmtbar.addEventListener("click", function (e) { + const btn = e.target.closest(".pj-desc-fmtbtn"); + if (!btn) return; + box.focus(); + const cmd = btn.dataset.cmd; + let arg = btn.dataset.arg || null; + if (cmd === "createLink") { + arg = prompt("연결할 주소(URL)를 입력하세요", "https://"); + if (!arg) return; + } + try { document.execCommand(cmd, false, arg); } catch (_) {} + updateFmtState(); + }); + function updateFmtState() { + fmtbar.querySelectorAll(".pj-desc-fmtbtn[data-cmd]").forEach(function (btn) { + const cmd = btn.dataset.cmd; + if (cmd === "createLink" || cmd === "removeFormat") return; + let active = false; + try { active = document.queryCommandState(cmd); } catch (_) { active = false; } + btn.classList.toggle("is-active", active); + }); + } + box.addEventListener("keyup", updateFmtState); + box.addEventListener("mouseup", updateFmtState); + box.addEventListener("focus", updateFmtState); + } + // ── 설명란 이미지 선택 → 크기 조절 핸들 + 삭제 버튼 오버레이 ── // 크기는 style이 아니라 width 속성으로 저장한다(서버 sanitize가 style은 // 버리지만 img width/height 속성은 허용 목록에 있어 저장 후에도 유지된다).