feat(projects): 설명 마크다운 렌더링 + 저장소 README 가져오기
등록 모달의 '한 줄 설명' input 을 여러 줄 textarea 로 바꾸고, 설명을 markdown-it 로 렌더링한다(html:false 로 raw HTML 이스케이프·javascript: 링크 차단 — XSS 안전). 상세 화면은 마크다운으로, 카드 미리보기는 기호를 걷어낸 순수 텍스트(mdPlain) + 3줄 클램프로 표시. '저장소 README 가져오기' 버튼과 GET /api/readme 라우트를 추가해 repo_url 의 Gitea README 를 설명란으로 불러올 수 있다(gitea.js fetchReadme). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@
|
||||
.pcard-titlewrap { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
||||
.pcard-repoico { display: inline-flex; color: var(--fg2); flex: 0 0 auto; }
|
||||
.pcard-title { font-size: 14px; font-weight: 700; color: var(--link); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pcard-desc { margin-top: 8px; font-size: 12.5px; color: var(--fg2); line-height: 1.5; flex: 1; }
|
||||
.pcard-desc { margin-top: 8px; font-size: 12.5px; color: var(--fg2); line-height: 1.5; flex: 1; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; white-space: pre-wrap; word-break: break-word; }
|
||||
.pcard-tags { margin-top: 11px; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.pcard-foot { margin-top: 13px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.pcard-stats { display: flex; align-items: center; gap: 13px; font-size: 12px; color: var(--fg2); }
|
||||
@@ -130,7 +130,7 @@
|
||||
.react-btn .ico-on, .modal-action .ico-on { display: none; }
|
||||
.react-btn.on .ico-on, .modal-action.on .ico-on { display: inline-flex; }
|
||||
.react-btn.on .ico-off, .modal-action.on .ico-off { display: none; }
|
||||
.fcard-desc { margin-top: 8px; font-size: 13px; color: var(--fg2); line-height: 1.55; }
|
||||
.fcard-desc { margin-top: 8px; font-size: 13px; color: var(--fg2); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; white-space: pre-wrap; word-break: break-word; }
|
||||
.fcard-tags { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.fcard-foot { margin-top: 15px; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||||
.fcard-author { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
|
||||
@@ -19,6 +19,30 @@
|
||||
.modal-close:hover { background: var(--panel); border-color: var(--border-soft); }
|
||||
.modal-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
|
||||
.modal-desc { margin: 14px 0 0; font-size: 14px; color: var(--fg); line-height: 1.65; white-space: pre-wrap; }
|
||||
.modal-desc-empty { color: var(--fg3); }
|
||||
/* 설명(README) 마크다운 본문 */
|
||||
.md-body { margin: 14px 0 0; font-size: 14px; color: var(--fg); line-height: 1.65; word-break: break-word; }
|
||||
.md-body > :first-child { margin-top: 0; }
|
||||
.md-body > :last-child { margin-bottom: 0; }
|
||||
.md-body h1, .md-body h2, .md-body h3, .md-body h4 { margin: 18px 0 8px; font-weight: 800; line-height: 1.3; letter-spacing: -.01em; }
|
||||
.md-body h1 { font-size: 19px; padding-bottom: 6px; border-bottom: 1px solid var(--divider); }
|
||||
.md-body h2 { font-size: 16.5px; padding-bottom: 5px; border-bottom: 1px solid var(--divider); }
|
||||
.md-body h3 { font-size: 15px; }
|
||||
.md-body h4 { font-size: 14px; }
|
||||
.md-body p { margin: 10px 0; }
|
||||
.md-body ul, .md-body ol { margin: 10px 0; padding-left: 22px; }
|
||||
.md-body li { margin: 3px 0; }
|
||||
.md-body a { color: var(--link); text-decoration: none; }
|
||||
.md-body a:hover { text-decoration: underline; }
|
||||
.md-body code { font-family: 'SF Mono', ui-monospace, Menlo, monospace; font-size: 12.5px; background: var(--inset); border: 1px solid var(--border-soft); border-radius: 5px; padding: 1px 5px; }
|
||||
.md-body pre { background: var(--inset); border: 1px solid var(--border-soft); border-radius: 9px; padding: 12px 14px; overflow-x: auto; margin: 12px 0; }
|
||||
.md-body pre code { background: transparent; border: 0; padding: 0; font-size: 12.5px; }
|
||||
.md-body blockquote { margin: 12px 0; padding: 2px 14px; border-left: 3px solid var(--border); color: var(--fg2); }
|
||||
.md-body hr { border: 0; border-top: 1px solid var(--divider); margin: 16px 0; }
|
||||
.md-body table { border-collapse: collapse; margin: 12px 0; display: block; overflow-x: auto; }
|
||||
.md-body th, .md-body td { border: 1px solid var(--border); padding: 6px 12px; font-size: 13px; }
|
||||
.md-body th { background: var(--panel); font-weight: 700; }
|
||||
.md-body img { max-width: 100%; border-radius: 8px; }
|
||||
.modal-actions { margin-top: 18px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.modal-action {
|
||||
display: inline-flex; align-items: center; gap: 7px; background: var(--btn-bg); border: 1px solid var(--btn-bd); border-radius: 8px;
|
||||
@@ -76,6 +100,14 @@
|
||||
.share-label .hint { font-weight: 500; color: var(--fg3); }
|
||||
.share-input { width: 100%; font-family: inherit; font-size: 14px; color: var(--fg); background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px; outline: none; }
|
||||
.share-input.mono { font-family: 'SF Mono', ui-monospace, Menlo, monospace; font-size: 13px; }
|
||||
.share-textarea { resize: vertical; min-height: 96px; line-height: 1.6; }
|
||||
.share-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.readme-btn {
|
||||
display: inline-flex; align-items: center; gap: 5px; background: var(--btn-bg); border: 1px solid var(--btn-bd); border-radius: 7px; padding: 4px 9px;
|
||||
cursor: pointer; font-family: inherit; font-size: 11.5px; font-weight: 600; color: var(--fg2); white-space: nowrap; transition: background .14s ease, color .14s ease;
|
||||
}
|
||||
.readme-btn:hover { background: var(--btn-hover); color: var(--fg); }
|
||||
.readme-btn:disabled { opacity: .6; cursor: default; }
|
||||
.share-input:focus { border-color: var(--link); box-shadow: 0 0 0 3px var(--focus-ring); }
|
||||
.vis-toggle { display: inline-flex; background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 3px; gap: 3px; }
|
||||
.vis-opt { display: inline-flex; align-items: center; gap: 5px; background: transparent; border: 0; border-radius: 6px; padding: 6px 16px; cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 600; color: var(--fg2); white-space: nowrap; transition: background .14s ease, color .14s ease; }
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
// 상세 모달 · 공유(등록) 모달 — 열기/닫기 및 공개범위 토글.
|
||||
import { $, $$ } from "./util.js";
|
||||
import { $, $$, toast } from "./util.js";
|
||||
import { applyReactions } from "./reactions.js";
|
||||
|
||||
// 등록 모달의 "저장소 README 가져오기" — repo_url 의 README 를 받아 설명란을 채운다.
|
||||
async function importReadme() {
|
||||
const m = $("#shareModal"); if (!m) return;
|
||||
const btn = $("#readmeImport", m);
|
||||
const repoInput = $('input[name="repo_url"]', m);
|
||||
const ta = $('textarea[name="description"]', m);
|
||||
const repo = ((repoInput && repoInput.value) || "").trim();
|
||||
if (!repo) { toast("먼저 저장소 URL 을 입력하세요"); if (repoInput) repoInput.focus(); return; }
|
||||
if (ta && ta.value.trim() && !window.confirm("현재 설명 내용을 README 로 덮어쓸까요?")) return;
|
||||
const label = btn ? btn.innerHTML : "";
|
||||
if (btn) { btn.disabled = true; btn.textContent = "가져오는 중…"; }
|
||||
try {
|
||||
const res = await fetch("/api/readme?repo_url=" + encodeURIComponent(repo), { headers: { "X-Requested-With": "fetch" } });
|
||||
const data = res.ok ? await res.json() : null;
|
||||
if (data && data.content && ta) { ta.value = data.content; toast("README 를 가져왔어요"); ta.focus(); }
|
||||
else toast("README 를 찾지 못했어요 · URL 을 확인해 주세요");
|
||||
} catch (e) {
|
||||
toast("가져오기에 실패했어요");
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = label; }
|
||||
}
|
||||
}
|
||||
|
||||
export function openDetail(id) {
|
||||
const tpl = $('#detailStore template[data-detail-id="' + id + '"]');
|
||||
const overlay = $("#detailModal");
|
||||
@@ -49,6 +72,7 @@ export function initModals() {
|
||||
if (e.target.closest("[data-close-detail]")) { e.preventDefault(); closeDetail(); return; }
|
||||
if (e.target.closest("[data-open-share]")) { e.preventDefault(); openShare(); return; }
|
||||
if (e.target.closest("[data-close-share]")) { e.preventDefault(); closeShare(); return; }
|
||||
if (e.target.closest("#readmeImport")) { e.preventDefault(); importReadme(); return; }
|
||||
|
||||
const detail = $("#detailModal");
|
||||
if (detail && !detail.hidden && e.target === detail) { closeDetail(); return; }
|
||||
|
||||
Reference in New Issue
Block a user