Compare commits
5 Commits
3ddf920e56
...
ac545651e6
| Author | SHA1 | Date | |
|---|---|---|---|
| ac545651e6 | |||
| 76c82ed24e | |||
| da1653a8ea | |||
| 4a3e5636c8 | |||
| 761b1a1936 |
86
package-lock.json
generated
86
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
"markdown-it": "^14.3.0",
|
||||||
"openid-client": "^5.7.0",
|
"openid-client": "^5.7.0",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1"
|
||||||
},
|
},
|
||||||
@@ -32,6 +33,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -242,6 +249,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -562,6 +581,25 @@
|
|||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@@ -574,6 +612,33 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
|
||||||
|
"integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.5.0",
|
||||||
|
"linkify-it": "^5.0.2",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -583,6 +648,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -898,6 +969,15 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.2",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
@@ -1135,6 +1215,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uid-safe": {
|
"node_modules/uid-safe": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
"markdown-it": "^14.3.0",
|
||||||
"openid-client": "^5.7.0",
|
"openid-client": "^5.7.0",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,6 @@
|
|||||||
.tool-card.admin .tool-icon { background: var(--admin-bg); border-color: var(--admin-bd); }
|
.tool-card.admin .tool-icon { background: var(--admin-bg); border-color: var(--admin-bd); }
|
||||||
.tool-name { display: block; margin-top: 14px; font-size: 14.5px; font-weight: 700; color: var(--fg); }
|
.tool-name { display: block; margin-top: 14px; font-size: 14.5px; font-weight: 700; color: var(--fg); }
|
||||||
.tool-desc { display: block; margin-top: 3px; font-size: 12.5px; color: var(--fg2); line-height: 1.45; }
|
.tool-desc { display: block; margin-top: 3px; font-size: 12.5px; color: var(--fg2); line-height: 1.45; }
|
||||||
.sso-badge {
|
|
||||||
position: absolute; top: 13px; right: 13px; font-size: 10px; font-weight: 700; letter-spacing: .06em;
|
|
||||||
color: var(--accent-fg); background: var(--accent-bg); border: 1px solid var(--accent-bd); border-radius: 4px; padding: 2px 6px;
|
|
||||||
}
|
|
||||||
.tool-admin-badge {
|
.tool-admin-badge {
|
||||||
display: inline-flex; align-items: center; gap: 5px; margin-top: 11px; font-size: 11px; font-weight: 600;
|
display: inline-flex; align-items: center; gap: 5px; margin-top: 11px; font-size: 11px; font-weight: 600;
|
||||||
color: var(--admin-fg); background: var(--admin-bg); border: 1px solid var(--admin-bd); border-radius: 999px; padding: 2px 8px; white-space: nowrap;
|
color: var(--admin-fg); background: var(--admin-bg); border: 1px solid var(--admin-bd); border-radius: 999px; padding: 2px 8px; white-space: nowrap;
|
||||||
@@ -56,7 +52,7 @@
|
|||||||
.pcard-titlewrap { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
.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-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-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-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-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); }
|
.pcard-stats { display: flex; align-items: center; gap: 13px; font-size: 12px; color: var(--fg2); }
|
||||||
@@ -71,8 +67,11 @@
|
|||||||
}
|
}
|
||||||
.vis-badge.public { color: var(--success-fg); background: var(--success-bg); border: 1px solid var(--success-bd); }
|
.vis-badge.public { color: var(--success-fg); background: var(--success-bg); border: 1px solid var(--success-bd); }
|
||||||
.vis-badge.private { color: var(--fg2); background: var(--panel); border: 1px solid var(--border); }
|
.vis-badge.private { color: var(--fg2); background: var(--panel); border: 1px solid var(--border); }
|
||||||
.toggle-share-btn { background: transparent; border: 0; padding: 0; cursor: pointer; font-family: inherit; font-size: 11.5px; font-weight: 600; color: var(--fg3); white-space: nowrap; }
|
.toggle-share-btn {
|
||||||
.toggle-share-btn:hover { color: var(--fg); }
|
display: inline-flex; align-items: center; gap: 5px; background: var(--btn-bg); border: 1px solid var(--btn-bd); border-radius: 7px; padding: 4px 10px;
|
||||||
|
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, border-color .14s ease;
|
||||||
|
}
|
||||||
|
.toggle-share-btn:hover { background: var(--btn-hover); color: var(--fg); border-color: var(--border); }
|
||||||
.private-note { font-size: 11.5px; color: var(--fg3); }
|
.private-note { font-size: 11.5px; color: var(--fg3); }
|
||||||
.share-mini-btn {
|
.share-mini-btn {
|
||||||
display: inline-flex; align-items: center; gap: 5px; background: var(--success);
|
display: inline-flex; align-items: center; gap: 5px; background: var(--success);
|
||||||
@@ -127,7 +126,7 @@
|
|||||||
.react-btn .ico-on, .modal-action .ico-on { display: none; }
|
.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-on, .modal-action.on .ico-on { display: inline-flex; }
|
||||||
.react-btn.on .ico-off, .modal-action.on .ico-off { display: none; }
|
.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-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-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; }
|
.fcard-author { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ html[data-aidev-theme="dark"] .theme-ico-sun { display: inline-flex; }
|
|||||||
/* ===================== 푸터 ===================== */
|
/* ===================== 푸터 ===================== */
|
||||||
.footer { border-top: 1px solid var(--border-soft); }
|
.footer { border-top: 1px solid var(--border-soft); }
|
||||||
.footer-inner { max-width: 1180px; margin: 0 auto; padding: 20px 24px 28px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
.footer-inner { max-width: 1180px; margin: 0 auto; padding: 20px 24px 28px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
||||||
.footer-brand { display: flex; align-items: center; gap: 8px; color: var(--fg3); font-size: 12px; }
|
.footer-left { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
||||||
|
.footer-brand { display: inline-flex; align-items: center; gap: 8px; color: var(--fg3); font-size: 12px; }
|
||||||
.footer-tech { font-size: 11.5px; color: var(--fg3); font-family: 'SF Mono', ui-monospace, Menlo, monospace; }
|
.footer-tech { font-size: 11.5px; color: var(--fg3); font-family: 'SF Mono', ui-monospace, Menlo, monospace; }
|
||||||
/* 버그 제보 안내 — 자체 줄로 내려 작게 표시 */
|
/* 버그 제보 안내 — 브랜드 옆에 한 줄로 이어 표시 */
|
||||||
.footer-bug { flex-basis: 100%; font-size: 11.5px; color: var(--fg3); }
|
.footer-bug { font-size: 11.5px; color: var(--fg3); }
|
||||||
.footer-bug a { color: var(--link); text-decoration: none; }
|
.footer-bug a { color: var(--link); text-decoration: none; }
|
||||||
.footer-bug a:hover { text-decoration: underline; }
|
.footer-bug a:hover { text-decoration: underline; }
|
||||||
|
|||||||
@@ -19,6 +19,30 @@
|
|||||||
.modal-close:hover { background: var(--panel); border-color: var(--border-soft); }
|
.modal-close:hover { background: var(--panel); border-color: var(--border-soft); }
|
||||||
.modal-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
|
.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 { 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-actions { margin-top: 18px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
.modal-action {
|
.modal-action {
|
||||||
display: inline-flex; align-items: center; gap: 7px; background: var(--btn-bg); border: 1px solid var(--btn-bd); border-radius: 8px;
|
display: inline-flex; align-items: center; gap: 7px; background: var(--btn-bg); border: 1px solid var(--btn-bd); border-radius: 8px;
|
||||||
@@ -55,6 +79,8 @@
|
|||||||
|
|
||||||
.modal-foot { padding: 14px 24px 22px; border-top: 1px solid var(--divider); margin-top: 6px; background: var(--bg-soft); }
|
.modal-foot { padding: 14px 24px 22px; border-top: 1px solid var(--divider); margin-top: 6px; background: var(--bg-soft); }
|
||||||
.reply-banner { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 9px; background: var(--accent-bg); border: 1px solid var(--accent-bd); border-radius: 7px; padding: 6px 10px; }
|
.reply-banner { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 9px; background: var(--accent-bg); border: 1px solid var(--accent-bd); border-radius: 7px; padding: 6px 10px; }
|
||||||
|
/* hidden 속성이 켜져 있으면 확실히 숨긴다 (display:flex 가 [hidden] 기본 규칙을 이겨 항상 보이던 버그 방지). */
|
||||||
|
.reply-banner[hidden] { display: none; }
|
||||||
.reply-banner span { font-size: 12px; color: var(--accent-strong); }
|
.reply-banner span { font-size: 12px; color: var(--accent-strong); }
|
||||||
.reply-banner button { background: transparent; border: 0; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 600; color: var(--link); }
|
.reply-banner button { background: transparent; border: 0; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 600; color: var(--link); }
|
||||||
.comment-form { display: flex; gap: 10px; align-items: flex-end; }
|
.comment-form { display: flex; gap: 10px; align-items: flex-end; }
|
||||||
@@ -74,6 +100,14 @@
|
|||||||
.share-label .hint { font-weight: 500; color: var(--fg3); }
|
.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 { 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-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); }
|
.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-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; }
|
.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";
|
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) {
|
export function openDetail(id) {
|
||||||
const tpl = $('#detailStore template[data-detail-id="' + id + '"]');
|
const tpl = $('#detailStore template[data-detail-id="' + id + '"]');
|
||||||
const overlay = $("#detailModal");
|
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-close-detail]")) { e.preventDefault(); closeDetail(); return; }
|
||||||
if (e.target.closest("[data-open-share]")) { e.preventDefault(); openShare(); 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("[data-close-share]")) { e.preventDefault(); closeShare(); return; }
|
||||||
|
if (e.target.closest("#readmeImport")) { e.preventDefault(); importReadme(); return; }
|
||||||
|
|
||||||
const detail = $("#detailModal");
|
const detail = $("#detailModal");
|
||||||
if (detail && !detail.hidden && e.target === detail) { closeDetail(); return; }
|
if (detail && !detail.hidden && e.target === detail) { closeDetail(); return; }
|
||||||
|
|||||||
28
src/gitea.js
28
src/gitea.js
@@ -48,3 +48,31 @@ export async function fetchMainLang(repoUrl) {
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 레포의 README 원문(마크다운)을 반환. 실패/없음이면 "".
|
||||||
|
// Gitea raw 엔드포인트로 기본 브랜치의 README 파일을 후보 순서대로 시도한다.
|
||||||
|
export async function fetchReadme(repoUrl) {
|
||||||
|
const r = parseRepo(repoUrl);
|
||||||
|
if (!r) return "";
|
||||||
|
const headers = {};
|
||||||
|
if (config.giteaToken) headers.Authorization = `token ${config.giteaToken}`;
|
||||||
|
const base = `${r.base}/api/v1/repos/${encodeURIComponent(r.owner)}/${encodeURIComponent(r.repo)}/raw`;
|
||||||
|
const candidates = ["README.md", "README.MD", "readme.md", "README", "README.markdown"];
|
||||||
|
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), 4000);
|
||||||
|
try {
|
||||||
|
for (const name of candidates) {
|
||||||
|
const res = await fetch(`${base}/${name}`, { headers, signal: ctrl.signal });
|
||||||
|
if (!res.ok) continue;
|
||||||
|
const text = await res.text();
|
||||||
|
// 너무 큰 README 는 앞부분만(설명란이 감당할 범위). 20KB 컷.
|
||||||
|
return text.length > 20000 ? text.slice(0, 20000) : text;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 뷰 헬퍼 — 빌드/프런트 프레임워크 없이 서버에서 SVG 를 직접 만들어 EJS 로 출력한다.
|
// 뷰 헬퍼 — 빌드/프런트 프레임워크 없이 서버에서 SVG 를 직접 만들어 EJS 로 출력한다.
|
||||||
// (디자인 레퍼런스의 Octicon 패스 / identicon 알고리즘 / 언어색을 그대로 포팅)
|
// (디자인 레퍼런스의 Octicon 패스 / identicon 알고리즘 / 언어색을 그대로 포팅)
|
||||||
// res.locals 에 실어 모든 뷰에서 icon()/identicon()/langColor() 로 사용.
|
// res.locals 에 실어 모든 뷰에서 icon()/identicon()/langColor()/renderMarkdown() 로 사용.
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
|
||||||
// GitHub Octicon 16x16 패스 모음.
|
// GitHub Octicon 16x16 패스 모음.
|
||||||
const OCTICONS = {
|
const OCTICONS = {
|
||||||
@@ -39,6 +40,39 @@ export function icon(name, opts = {}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 프로젝트 설명(README 형식) 마크다운 렌더링.
|
||||||
|
// html:false → 원문 속 raw HTML 은 태그가 아니라 텍스트로 이스케이프되어 XSS 를 막는다.
|
||||||
|
// markdown-it 기본 validateLink 가 javascript:/vbscript:/data: 링크도 차단한다.
|
||||||
|
const md = new MarkdownIt({ html: false, linkify: true, breaks: true });
|
||||||
|
// 마크다운 링크는 새 탭으로 열고 noopener 를 붙인다.
|
||||||
|
const defaultLinkOpen =
|
||||||
|
md.renderer.rules.link_open ||
|
||||||
|
function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
|
||||||
|
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||||
|
tokens[idx].attrSet("target", "_blank");
|
||||||
|
tokens[idx].attrSet("rel", "noopener nofollow");
|
||||||
|
return defaultLinkOpen(tokens, idx, options, env, self);
|
||||||
|
};
|
||||||
|
export function renderMarkdown(src) {
|
||||||
|
return md.render(String(src || ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 카드 미리보기용 — 마크다운 기호를 걷어낸 순수 텍스트 한 덩어리.
|
||||||
|
// (제목 #, 목록 -, 강조 *_`, 링크 [텍스트](url) → 텍스트, 코드펜스 ``` 제거)
|
||||||
|
export function mdPlain(src) {
|
||||||
|
return String(src || "")
|
||||||
|
.replace(/```[\s\S]*?```/g, " ") // 코드펜스 블록 제거
|
||||||
|
.replace(/`([^`]*)`/g, "$1") // 인라인 코드
|
||||||
|
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ") // 이미지
|
||||||
|
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // 링크 → 텍스트
|
||||||
|
.replace(/^\s{0,3}#{1,6}\s+/gm, "") // 제목 마커
|
||||||
|
.replace(/^\s{0,3}>\s?/gm, "") // 인용 마커
|
||||||
|
.replace(/^\s*[-*+]\s+/gm, "") // 목록 마커
|
||||||
|
.replace(/[*_~]{1,3}/g, "") // 강조 기호
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
// 언어 → 색(카드의 언어 점).
|
// 언어 → 색(카드의 언어 점).
|
||||||
const LANG_COLORS = {
|
const LANG_COLORS = {
|
||||||
Python: "#3572A5", TypeScript: "#3178c6", JavaScript: "#f1e05a",
|
Python: "#3572A5", TypeScript: "#3178c6", JavaScript: "#f1e05a",
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import path from "path";
|
|||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
import { toolsForUser } from "./sites.js";
|
import { toolsForUser } from "./sites.js";
|
||||||
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js";
|
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js";
|
||||||
import { icon, identicon, langColor, langFromTags } from "./helpers.js";
|
import { icon, identicon, langColor, langFromTags, renderMarkdown, mdPlain } from "./helpers.js";
|
||||||
import { fetchMainLang } from "./gitea.js";
|
import { fetchMainLang, fetchReadme } from "./gitea.js";
|
||||||
import * as db from "./db.js";
|
import * as db from "./db.js";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -35,6 +35,8 @@ app.use((req, res, next) => {
|
|||||||
res.locals.icon = icon;
|
res.locals.icon = icon;
|
||||||
res.locals.identicon = identicon;
|
res.locals.identicon = identicon;
|
||||||
res.locals.langColor = langColor;
|
res.locals.langColor = langColor;
|
||||||
|
res.locals.renderMarkdown = renderMarkdown;
|
||||||
|
res.locals.mdPlain = mdPlain;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,7 +157,17 @@ app.post("/projects/:id/visibility", requireAuth, async (req, res) => {
|
|||||||
const isPublic = req.body.is_public === "1";
|
const isPublic = req.body.is_public === "1";
|
||||||
await db.setVisibility({ id: req.params.id, sabun: req.session.user.sabun, isPublic });
|
await db.setVisibility({ id: req.params.id, sabun: req.session.user.sabun, isPublic });
|
||||||
const msg = isPublic ? "공개로 전환됐습니다 · 피드에 표시됩니다" : "비공개로 전환됐습니다";
|
const msg = isPublic ? "공개로 전환됐습니다 · 피드에 표시됩니다" : "비공개로 전환됐습니다";
|
||||||
res.redirect("/?toast=" + encodeURIComponent(msg));
|
// 모달에서 전환하면 return_to(=/?open=<id>) 로 돌아가 모달을 다시 연다. 카드에서는 "/".
|
||||||
|
const back = safeBack(req.body.return_to, "/");
|
||||||
|
const sep = back.includes("?") ? "&" : "?";
|
||||||
|
res.redirect(back + sep + "toast=" + encodeURIComponent(msg));
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- README 가져오기 (등록 모달) — repo_url 의 Gitea README 를 마크다운 원문으로 반환 ---
|
||||||
|
app.get("/api/readme", requireAuth, async (req, res) => {
|
||||||
|
const content = await fetchReadme((req.query.repo_url || "").toString().trim());
|
||||||
|
if (!content) return res.status(404).json({ ok: false });
|
||||||
|
res.json({ ok: true, content });
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 프로젝트 삭제 (본인) ---
|
// --- 프로젝트 삭제 (본인) ---
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export const sites = [
|
|||||||
// --- 개발 핵심 (모두에게) ---
|
// --- 개발 핵심 (모두에게) ---
|
||||||
{ key: "coder", name: "Coder", desc: "클라우드 IDE", url: config.tools.coder, icon: "💻", sso: true },
|
{ key: "coder", name: "Coder", desc: "클라우드 IDE", url: config.tools.coder, icon: "💻", sso: true },
|
||||||
{ key: "gitea", name: "Gitea", desc: "Git 저장소", url: config.tools.gitea, icon: "🍵", sso: true },
|
{ key: "gitea", name: "Gitea", desc: "Git 저장소", url: config.tools.gitea, icon: "🍵", sso: true },
|
||||||
{ key: "kubero", name: "Kubero", desc: "배포 (PaaS)", url: config.tools.kubero, icon: "🚀" },
|
// Kubero: Keycloak SSO 로 로그인 가능(단, 랜딩에서 "OAuth 로 로그인"을 한 번 더 눌러야 함).
|
||||||
{ key: "harbor", name: "Harbor", desc: "컨테이너 레지스트리", url: config.tools.harbor, icon: "📦" },
|
{ key: "kubero", name: "Kubero", desc: "배포 (PaaS)", url: config.tools.kubero, icon: "🚀", sso: true },
|
||||||
// --- 인프라/운영 (관리자 전용) ---
|
// --- 인프라/운영 (관리자 전용) ---
|
||||||
|
{ key: "harbor", name: "Harbor", desc: "컨테이너 레지스트리", url: config.tools.harbor, icon: "📦", adminOnly: true },
|
||||||
{ key: "openeverest", name: "OpenEverest", desc: "DB 콘솔", url: config.tools.openeverest, icon: "🗄️", adminOnly: true },
|
{ key: "openeverest", name: "OpenEverest", desc: "DB 콘솔", url: config.tools.openeverest, icon: "🗄️", adminOnly: true },
|
||||||
{ key: "minio", name: "MinIO", desc: "오브젝트 스토리지", url: config.tools.minio, icon: "🪣", adminOnly: true },
|
{ key: "minio", name: "MinIO", desc: "오브젝트 스토리지", url: config.tools.minio, icon: "🪣", adminOnly: true },
|
||||||
{ key: "keycloak", name: "Keycloak", desc: "계정 / SSO 관리", url: config.tools.keycloak, icon: "🔑", adminOnly: true },
|
{ key: "keycloak", name: "Keycloak", desc: "계정 / SSO 관리", url: config.tools.keycloak, icon: "🔑", adminOnly: true },
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-meta">
|
<div class="modal-meta">
|
||||||
<%- identicon(p.owner_sabun, 24) %>
|
<%- identicon(p.owner_sabun, 24) %>
|
||||||
<span class="sabun"><%= p.owner_sabun %></span>
|
<span class="sabun" title="<%= p.owner_sabun %>"><%= p.owner_name || p.owner_sabun %></span>
|
||||||
<span class="date">· <%= reltime(p.created_at) %> 공유</span>
|
<span class="date">· <%= reltime(p.created_at) %> 공유</span>
|
||||||
<% if (p.lang) { %><span class="lang"><span class="lang-dot" style="background:<%= langColor(p.lang) %>"></span><%= p.lang %></span><% } %>
|
<% if (p.lang) { %><span class="lang"><span class="lang-dot" style="background:<%= langColor(p.lang) %>"></span><%= p.lang %></span><% } %>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,11 @@
|
|||||||
<% if (pdTags.length) { %>
|
<% if (pdTags.length) { %>
|
||||||
<div class="modal-tags"><% pdTags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
<div class="modal-tags"><% pdTags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<p class="modal-desc"><%= p.description || "설명 없음" %></p>
|
<% if (p.description && p.description.trim()) { %>
|
||||||
|
<div class="modal-desc md-body"><%- renderMarkdown(p.description) %></div>
|
||||||
|
<% } else { %>
|
||||||
|
<p class="modal-desc modal-desc-empty">설명 없음</p>
|
||||||
|
<% } %>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button type="button" class="modal-action heart<%= p.hearted ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="heart">
|
<button type="button" class="modal-action heart<%= p.hearted ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="heart">
|
||||||
<span class="ico ico-on"><%- icon("heartFill", { size: 15 }) %></span><span class="ico ico-off"><%- icon("heart", { size: 15 }) %></span>
|
<span class="ico ico-on"><%- icon("heartFill", { size: 15 }) %></span><span class="ico ico-off"><%- icon("heart", { size: 15 }) %></span>
|
||||||
@@ -55,7 +59,12 @@
|
|||||||
<a class="modal-action ghost" href="<%= p.repo_url %>" target="_blank" rel="noopener"><%- icon("code", { size: 14 }) %>저장소</a>
|
<a class="modal-action ghost" href="<%= p.repo_url %>" target="_blank" rel="noopener"><%- icon("code", { size: 14 }) %>저장소</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% if (p.owner_sabun === me) { %>
|
<% if (p.owner_sabun === me) { %>
|
||||||
<form method="post" action="/projects/<%= p.id %>/delete" style="margin-left:auto">
|
<form method="post" action="/projects/<%= p.id %>/visibility" style="margin-left:auto">
|
||||||
|
<input type="hidden" name="is_public" value="<%= p.is_public ? "0" : "1" %>" />
|
||||||
|
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
||||||
|
<button type="submit" class="modal-action"><%- icon(p.is_public ? "lock" : "globe", { size: 14 }) %><%= p.is_public ? "비공개로 전환" : "공개로 전환" %></button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/projects/<%= p.id %>/delete">
|
||||||
<input type="hidden" name="return_to" value="/" />
|
<input type="hidden" name="return_to" value="/" />
|
||||||
<button type="submit" class="comment-link comment-del" data-confirm="이 프로젝트를 삭제할까요? 댓글·반응도 함께 삭제됩니다." style="font-size:12.5px"><%- icon("trash", { size: 13 }) %>프로젝트 삭제</button>
|
<button type="submit" class="comment-link comment-del" data-confirm="이 프로젝트를 삭제할까요? 댓글·반응도 함께 삭제됩니다." style="font-size:12.5px"><%- icon("trash", { size: 13 }) %>프로젝트 삭제</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -72,13 +81,13 @@
|
|||||||
<%- identicon(c.author_sabun, 28) %>
|
<%- identicon(c.author_sabun, 28) %>
|
||||||
<div class="comment-main">
|
<div class="comment-main">
|
||||||
<div class="comment-head">
|
<div class="comment-head">
|
||||||
<span class="comment-author"><%= c.author_sabun %></span>
|
<span class="comment-author" title="<%= c.author_sabun %>"><%= c.author_name || c.author_sabun %></span>
|
||||||
<span class="comment-time"><%= reltime(c.created_at) %></span>
|
<span class="comment-time"><%= reltime(c.created_at) %></span>
|
||||||
<% if (isLgtm(c.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
<% if (isLgtm(c.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-text"><%= c.body %></div>
|
<div class="comment-text"><%= c.body %></div>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
<button type="button" class="comment-link" data-reply-to="<%= c.id %>" data-reply-name="<%= c.author_sabun %>"><%- icon("reply", { size: 13 }) %>답글</button>
|
<button type="button" class="comment-link" data-reply-to="<%= c.id %>" data-reply-name="<%= c.author_name || c.author_sabun %>"><%- icon("reply", { size: 13 }) %>답글</button>
|
||||||
<% if (c.author_sabun === me) { %>
|
<% if (c.author_sabun === me) { %>
|
||||||
<form method="post" action="/comments/<%= c.id %>/delete" data-stop style="display:inline">
|
<form method="post" action="/comments/<%= c.id %>/delete" data-stop style="display:inline">
|
||||||
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
||||||
@@ -95,7 +104,7 @@
|
|||||||
<%- identicon(r.author_sabun, 24) %>
|
<%- identicon(r.author_sabun, 24) %>
|
||||||
<div class="comment-main">
|
<div class="comment-main">
|
||||||
<div class="comment-head">
|
<div class="comment-head">
|
||||||
<span class="comment-author"><%= r.author_sabun %></span>
|
<span class="comment-author" title="<%= r.author_sabun %>"><%= r.author_name || r.author_sabun %></span>
|
||||||
<span class="comment-time"><%= reltime(r.created_at) %></span>
|
<span class="comment-time"><%= reltime(r.created_at) %></span>
|
||||||
<% if (isLgtm(r.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
<% if (isLgtm(r.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
<div class="tools-grid">
|
<div class="tools-grid">
|
||||||
<% tools.forEach(function (t) { %>
|
<% tools.forEach(function (t) { %>
|
||||||
<a class="tool-card<%= t.adminOnly ? " admin" : "" %>" href="<%= t.url %>" target="_blank" rel="noopener">
|
<a class="tool-card<%= t.adminOnly ? " admin" : "" %>" href="<%= t.url %>" target="_blank" rel="noopener">
|
||||||
<% if (t.sso) { %><span class="sso-badge">SSO</span><% } %>
|
|
||||||
<span class="tool-icon"><%= t.icon %></span>
|
<span class="tool-icon"><%= t.icon %></span>
|
||||||
<span class="tool-name"><%= t.name %></span>
|
<span class="tool-name"><%= t.name %></span>
|
||||||
<span class="tool-desc"><%= t.desc %></span>
|
<span class="tool-desc"><%= t.desc %></span>
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
<%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %>
|
<%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pcard-desc"><%= p.description || "설명 없음" %></div>
|
<div class="pcard-desc"><%= mdPlain(p.description) || "설명 없음" %></div>
|
||||||
<% if (tags.length) { %>
|
<% if (tags.length) { %>
|
||||||
<div class="pcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
<div class="pcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -153,14 +152,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcard-desc"><%= p.description || "설명 없음" %></div>
|
<div class="fcard-desc"><%= mdPlain(p.description) || "설명 없음" %></div>
|
||||||
<% if (tags.length) { %>
|
<% if (tags.length) { %>
|
||||||
<div class="fcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
<div class="fcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<div class="fcard-foot">
|
<div class="fcard-foot">
|
||||||
<div class="fcard-author">
|
<div class="fcard-author">
|
||||||
<%- identicon(p.owner_sabun, 22) %>
|
<%- identicon(p.owner_sabun, 22) %>
|
||||||
<span class="sabun"><%= p.owner_sabun %></span>
|
<span class="sabun" title="<%= p.owner_sabun %>"><%= p.owner_name || p.owner_sabun %></span>
|
||||||
<span class="date">· <%= fdate(p.created_at) %></span>
|
<span class="date">· <%= fdate(p.created_at) %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fcard-meta">
|
<div class="fcard-meta">
|
||||||
@@ -179,11 +178,11 @@
|
|||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<div class="footer-brand"><span><%- icon("code", { size: 14 }) %></span><%= brand %> · 사내 개발 포털</div>
|
<div class="footer-left">
|
||||||
<div class="footer-tech">Express + Postgres + Keycloak SSO · deployed on Kubero</div>
|
<span class="footer-brand"><span><%- icon("code", { size: 14 }) %></span><%= brand %></span>
|
||||||
<div class="footer-bug">
|
<span class="footer-bug">· 버그가 있다면 <a href="https://gitea.bokdev.in/playground/ai-dev-portal/issues" target="_blank" rel="noopener">이슈</a>에 제보바랍니다!</span>
|
||||||
버그가 있다면 <a href="https://gitea.bokdev.in/playground/ai-dev-portal/issues" target="_blank" rel="noopener">이슈</a> 에 제보바랍니다! 🐛
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="footer-tech">Express + Postgres + Keycloak SSO · deployed on Kubero</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -211,8 +210,11 @@
|
|||||||
<input class="share-input" name="title" required placeholder="예) 방화벽 정책 DSL 변환기" />
|
<input class="share-input" name="title" required placeholder="예) 방화벽 정책 DSL 변환기" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span class="share-label">한 줄 설명</span>
|
<span class="share-label share-label-row">
|
||||||
<input class="share-input" name="description" placeholder="무엇을 하는 프로젝트인가요?" />
|
<span>설명 <span class="hint">Markdown 지원 · 제목·목록·코드·링크</span></span>
|
||||||
|
<button type="button" class="readme-btn" id="readmeImport"><%- icon("repo", { size: 12 }) %>저장소 README 가져오기</button>
|
||||||
|
</span>
|
||||||
|
<textarea class="share-input share-textarea" name="description" rows="5" placeholder="무엇을 하는 프로젝트인가요? ## 예시 - Markdown 문법을 그대로 쓸 수 있어요 - 저장소 URL 을 넣으면 README 를 가져올 수 있어요"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span class="share-label">태그 <span class="hint">쉼표로 구분 · 최대 5개</span></span>
|
<span class="share-label">태그 <span class="hint">쉼표로 구분 · 최대 5개</span></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user