project.crowny.org:9730 — 플렉시블팩토리(고객) → 광주 은광교회 LED Wall 설치 건을 실제로 굴릴 수 있도록 PM 사이클 전반을 보강. 동시에 사용자 템플릿 생태계와 크로스-프로젝트 통합 시각화(라이프 그래프)를 추가.
테이블/엔드포인트 추가 (server.js):
user_templates (id, ownerId, sourceProjectId, name, visibility, teamId, tree, depth, tags, price, downloads, icon, createdAt)checklists (id, projectId, nodePath, items, updatedBy, updatedAt)snapshots (id, projectId, label, note, root, meta, description, createdBy, createdAt)template_votes (id, templateId, userId, at)DELETE /api/projects/:id — 프로젝트 삭제 (manager↑)POST /api/projects/:id/rename — 이름 변경POST /api/projects/:id/clone — 복제 (트리 깊은 복사)POST /api/projects/:id/to-template — 템플릿 변환visibility: personal | team | publicGET /api/user-templates?scope=&sort= — 사용자 템플릿 갤러리GET/DELETE /api/user-templates/:idPOST /api/user-templates/:id/deployPOST /api/user-templates/:id/recommend — 토글식 추천 (1인 1표)stripTreeForTemplate() 헬퍼 — 트리 → 템플릿 변환 시 PII/상태 제거 (assignee/managers/log/tiomtaum 초기화)
UI: 프로젝트 상세 패널에 5개 버튼 추가 (체크리스트/스냅샷/이름수정/템플릿화/삭제)
CHECKLIST_PRESETS 상수 (8종, 59항목):
install-pre / install-active / install-post / contract / site-survey / claim / delivery / meeting
엔드포인트:
GET /api/checklist-presets / GET /api/checklist-presets/:idGET/PUT /api/projects/:id/checklist?path= — 노드별 체크리스트GET /api/projects/:id/checklists — 전체 요약POST /api/projects/:id/checklist/apply — 프리셋 일괄 적용renderChecklistPanel() — 노드 트리에서 체크리스트 보기 + 프리셋 다중선택 prompt엔드포인트:
POST /api/projects/:id/snapshots — 현재 상태 캡처 (label, note)GET /api/projects/:id/snapshotsGET /api/projects/:id/snapshots/:sidPOST /api/projects/:id/snapshots/:sid/rollback — 복원 (실행 전 자동 백업 스냅샷 생성)renderSnapshotPanel() — 생성/롤백/삭제핵심 로직:
{ ok, voted, recommends, mamPaid }popularity = recommends × 3 + downloads × 2UI: 템플릿 카드에 ♥ 추천 버튼 (voted 상태 색상 변화), 정렬 토글 4종 (최신/인기/추천/다운로드)
검증:
GET /api/life-graph 신규:
서버 감사로 발견된 5개 이슈 모두 해결:
/api/life-graph try/catch — 손상 노드 있어도 다른 프로젝트 집계 계속/api/projects/:id/gantt try/catch + canReadProject — 권한 없는 사용자 차단 + 손상 노드 안전 처리GET /api/teams/:id — 팀원만 조회 가능 (이전엔 누구나)/api/projects/:id/contacts — canReadProject 필요 (이전엔 누구나)/api/projects/:id/split — 자식 이름 3개 필수 + 공백 거절 + sanitizeStr공개 API가 description/meta/tree 모두 반환했지만 share.html에서 미사용이던 부분 모두 표시:
[life-graph] GET /api/life-graph → projects=2, health=20, timeline OK ✅
[split 검증] POST /api/projects/:id/split (빈이름) → 400 "공백일 수 없음" ✅
[gantt 권한] GET /api/projects/:id/gantt (외부유저) → 403 "읽기 권한 없음" ✅
[추천 보상] POST /api/user-templates/:id/recommend → +1맘 작성자 자동 지급 ✅
[자기 추천] POST /api/user-templates/:id/recommend → 400 "자기 템플릿에는 추천 불가" ✅
[정렬 인기순] GET /api/user-templates?sort=popular → recommends×3+downloads×2 정렬 ✅
[공유 페이지] GET /api/public/projects/:id → name/desc/meta/tree 전부 반환 ✅
/Users/ef/crowny-project/server.js (4400+ 줄)db.table('user_templates'/'checklists'/'snapshots'/'template_votes') — 312~315CHECKLIST_PRESETS 8종 — search "install-pre"stripTreeForTemplate() — 3533POST /api/projects/:id/to-template — 3543GET /api/user-templates (sort/voted/popularity) — 3585POST /api/user-templates/:id/recommend (보상지급) — 3637GET /api/life-graph — 3099splitMatch 입력검증 — 4120ganttMatch try/catch + canReadProject — 4064teamMatch GET 팀원체크 — 2688contactsMatch canReadProject — 3020/Users/ef/crowny-project/public/js/app.jsrenderLifeGraph() Chart.js — 369openUserTemplatePanel(scope, sort) 정렬+추천 — 2680/Users/ef/crowny-project/public/share.htmlGET /api/public/estimates/:id — 공개 견적 JSON (estimate + project + issue 정보)GET /estimate/:id — 정적 HTML 리라이트 (server.js:2518)public/estimate.html — A4 인쇄 친화 견적서 (회사/고객/항목표/합계/조건)@media print → 브라우저 Ctrl+P → PDF 저장share.html 제출 결과에 "📄 견적서 보기 / PDF 저장 →" 링크 표시curl /api/public/estimates/e7b30282... → projectName=광주은광교회2026, total=13915000, lines=4renderGantt() / loadGantt() (app.js:2447) 이미 구현되어 있었음 (분석 모달 내부)#ganttCardganttExpandAll2 / ganttCollapseAll2)/api/projects/:id/gantt → items=4, 트리 분할 후 L0/L1 계층 표시 정상채울 수 있는 기능 — 우선순위 순:
/api/workflow-presets 5종 + 커스텀 룰은/share/:id 무한 공개. 만료일/조회수/schedule) 연결 필요.template_votes insert 전 rate limit 추가 검토.db.select('activity', a => a.at >= day30) 풀스캔.canReadProject(pj, uname) / hasPermission(pj, uname, role) 헬퍼는db.compact('user_templates') 호출 누락 케이스 체크 필요.권한 지정 초대 링크/SMS 발송 → 수락 시 자동 가입·로그인·프로젝트 입장.
invites 테이블 추가 (token 24 hex, 일회용, TTL 기본 14일 · 최대 90일)POST /api/projects/:id/invite — {inviteeName, inviteeContact, permission, ttlDays, note} → {invite, link, smsText}GET /api/projects/:id/invite — 목록DELETE /api/projects/:id/invites/:iid — 회수GET /api/invite/:token — 초대 정보 + 프로젝트 프리뷰 + 통계POST /api/invite/:token/accept — {mode:'register'|'login', username, password, displayName}/invite/:token → public/invite.html (무인증, 신규가입/기존로그인 탭)localStorage.setItem('pj_token', r.token) + pj_autoOpenProject=projectId → / 이동 → 자동 진입초대하기 버튼 → 모달 (이름/연락처/권한/유효일/메시지)mode:'register' 로 신규 유저 kimyoungsang 가입 + share 생성이미 사용된 초대 (410 차단)DOT-QUOTE v45 (4958줄 JSX)에서 핵심 로직 포팅:
led_modules: 16종 (P1.25 COB ~ P5.95 Outdoor), USD/㎡ 원가, priceMode(b2b/margin/custom)led_processors: 27종 (Novastar H2~H20, VX400~2000, TU15~4K, Pixelhue P10/P20/P80/Q8/U5, Media Server)led_cabinets: 4종 (640x480, 640x640, 960x480, 960x640)led_settings: usdRate(1500), b2bMulti(1.6), remitExtraPct(5)calcAutoLayout(dW, dH, fitMode) — 640mm 캐비닛 + 320mm 잔여 패킹calcVerticalMix(dH, fitMode) — 480/640 높이 혼합 최적화calcLedSize(dW, dH, mode, presetId, fit) — 캐비닛/모듈 모드, over/undergetResolution(pitch, cols, rows, cabW, cabH, rowLayout) — 픽셀 해상도calcPortGuide(totalPx, cabTotalPx, redundant) — VX/H/TU 프로세서 추천calcSparesAuto(sqm) — ≤10㎡:10개 / ≤20㎡:20개 / >20㎡:ceil(sqm)GET /api/catalog/{modules,processors,cabinets,settings,cost-defaults,status-map}PUT /api/catalog/{modules,processors,cabinets,settings} — 카탈로그 편집POST /api/projects/:id/design/calc — {desiredW, desiredH, fitMode, cabinetPreset, moduleId} → layout+resolution+portGuide+sparesPOST /api/projects/:id/design — 설계 저장| 항목 | ₩/㎡ |
|---|---|
| Steel Frame | 47,000 |
| Steel Labor | 80,000 |
| HF-IX Cable | 18,000 |
| CAT6 Cable | 10,000 |
| PSU | 64,000 |
| Harmonic Filter | 40,000 |
| Installation | 250,000 |
| Scaffold | 30,000 |
| EWP | 25,000 |
| Dismantling | 20,000 |
| Conduit | 15,000 |
DOT-QUOTE에 없는 신규 기능.
survey: {
id, projectId, surveyDate, surveyedBy, location,
photos: [{id, url, annotation, timestamp}],
dimensions: {wallW, wallH, ceilingH, powerKw, hangPoints, accessRoute},
environment: {indoorOutdoor, lighting, temperature, humidity},
notes
}
POST /api/projects/:id/survey — 현장 조사 저장GET /api/projects/:id/survey — 목록PUT /api/projects/:id/survey/:sid — 수정DELETE /api/projects/:id/survey/:sid — 삭제DOT-QUOTE의 calcPricing() 포팅 — 기존 인건비 견적(buildEstimate)과 별도.
POST /api/projects/:id/hw-estimate — 견적 생성 (result 자동 계산)GET /api/projects/:id/hw-estimate — 이력POST /api/projects/:id/hw-estimate/calc — 계산만 (저장 없이)/tutorial, /help, /tutorial/*, /help/* → public/tutorial.html📖 가이드 링크 추가 (신규 탭)on\w+=, javascript:, data: 패턴 차단crc32|payload 형식, 손상 행 자동 스킵.bak → .bak.1 → .bak.2 순차 회전{"t":"ISO","l":"info","m":"req","rid":"hex16","method":"GET","path":"/api/...","status":200,"ms":5}LOG_LEVEL=debug|info|warn|error|fatal 환경변수crypto.randomBytes(8).hex, X-Request-Id 응답 헤더stripProto() — __proto__/constructor/prototype 키 재귀 제거auth_expired 전송 후 소켓 종료data/auto-backups/backup-YYYY-MM-DDTHH-MM-SS.jsonIdempotency-Key 헤더 → 1시간 캐시, 동일 키 재요청 시 캐시된 응답 반환/api/activity, /api/projects/:id/activity — ?limit=N&offset=M (max 200){ items: [], total, limit, offset }Number.isFinite(amount) && 0 < amount <= 1,000,000 + Math.floor()sessions.username, invites.token, activity.projectIdprojects.id, sessions._key, logic.id, teams.idrole="tree", role="treeitem", tabindex="0", aria-expanded, aria-labelrole="dialog", aria-modal="true", 첫 입력 요소 자동 포커스nodePath + node 데이터 포함findNodeByPath() 유틸 → 로컬 트리 노드 병합 → renderTree() 재렌더링openProject() 전체 reloadauth_expired WS 이벤트 → 세션 만료 토스트