crowny-project — 플렉시블팩토리 v3.0 (실사용 사이클 + 추천보상 + 라이프 그래프 + 12년 최적화 + 프로덕션 고도화)
개요
project.crowny.org:9730 — 플렉시블팩토리(고객) → 광주 은광교회 LED Wall 설치 건을 실제로 굴릴 수 있도록 PM 사이클 전반을 보강. 동시에 사용자 템플릿 생태계와 크로스-프로젝트 통합 시각화(라이프 그래프)를 추가.
무엇을 했는가
A. 프로젝트 라이프사이클 (실사용 기능)
테이블/엔드포인트 추가 (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개 버튼 추가 (체크리스트/스냅샷/이름수정/템플릿화/삭제)
B. 체크리스트 + 8종 프리셋
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() — 노드 트리에서 체크리스트 보기 + 프리셋 다중선택 promptC. 스냅샷/롤백
엔드포인트:
POST /api/projects/:id/snapshots— 현재 상태 캡처 (label, note)GET /api/projects/:id/snapshotsGET /api/projects/:id/snapshots/:sidPOST /api/projects/:id/snapshots/:sid/rollback— 복원 (실행 전 자동 백업 스냅샷 생성)
renderSnapshotPanel() — 생성/롤백/삭제D. 추천/보상 시스템 (사용자 템플릿 생태계)
핵심 로직:
- 1인 1표 토글 (재호출 시 취소)
- 자기 템플릿 추천 차단
- 비공개 템플릿 추천 차단, 팀 템플릿은 팀원만
- 공개 템플릿 첫 추천 시 작성자에게 +1맘 자동 지급 (system 발신, rewards 테이블 기록)
- 응답:
{ ok, voted, recommends, mamPaid }
popularity = recommends × 3 + downloads × 2UI: 템플릿 카드에 ♥ 추천 버튼 (voted 상태 색상 변화), 정렬 토글 4종 (최신/인기/추천/다운로드)
검증:
- tester2 → tester1 템플릿 추천 → tester1 지갑 mam +1 확인
- 자기 추천 시도 → 400 "자기 템플릿에는 추천 불가"
- 토글 재추천 → recommends 0으로 감소
E. 라이프 그래프 (크로스-프로젝트 시각화)
GET /api/life-graph 신규:
- timeline: 프로젝트별 진행률/비용/노드수/생성일
- rewardTimeline: 30일 일별 맘/포네 적립
- activityHeatmap: 30일 일별 활동 건수
- health: 전체 평균 건강도
- wallet, totalProjects, totalRewards
F. 안정성/보안 강화 (감사 후 보강)
서버 감사로 발견된 5개 이슈 모두 해결:
/api/life-graphtry/catch — 손상 노드 있어도 다른 프로젝트 집계 계속/api/projects/:id/gantttry/catch + canReadProject — 권한 없는 사용자 차단 + 손상 노드 안전 처리GET /api/teams/:id— 팀원만 조회 가능 (이전엔 누구나)/api/projects/:id/contacts— canReadProject 필요 (이전엔 누구나)/api/projects/:id/split— 자식 이름 3개 필수 + 공백 거절 + sanitizeStr
G. share.html 데이터 활용 보강
공개 API가 description/meta/tree 모두 반환했지만 share.html에서 미사용이던 부분 모두 표시:
- 설명 본문 (white-space:pre-wrap)
- 메타 태그 (유형/규모/고객/비전 — 칩 형태)
- 프로젝트 구조 (최대 20개 노드, TOAU 색상 + 진행바)
- 총 범주 수 (헤더에 표시)
검증 결과 (curl 스모크)
[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.html추가 완료 (2026-04-16 후속)
H. 인쇄 가능 견적서 페이지 ✅
GET /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=4I. 간트 차트 직접 진입 ✅
renderGantt()/loadGantt()(app.js:2447) 이미 구현되어 있었음 (분석 모달 내부)- 프로젝트 패널 툴바에 "간트" 버튼 추가 → 독립 카드
#ganttCard - 접기/펼치기 별도 핸들러 (
ganttExpandAll2/ganttCollapseAll2) - 검증:
/api/projects/:id/gantt→ items=4, 트리 분할 후 L0/L1 계층 표시 정상
잔여 이슈 / 다음 단계
채울 수 있는 기능 — 우선순위 순:
- 체크리스트 카운트 사이드바 노출 — 미완료 체크 항목 수를 프로젝트 카드/사이드바에
- 스냅샷 diff 뷰 — 두 스냅샷 비교 (어떤 노드가 변경됐는지). 현재는 롤백만 가능.
- 워크플로우 자동 트리거 검증 —
/api/workflow-presets5종 + 커스텀 룰은
- 실시간 라이프 그래프 갱신 — 현재는 페이지 진입 시 1회 fetch.
- 템플릿 가격 송금 검증 — public + price>0 deploy 시 송금 코드 있으나
- 공유 링크 만료/조회제한 — 현재
/share/:id무한 공개. 만료일/조회수
- 모바일 반응형 — 환영 대시보드 grid 4열이 좁은 화면에서 깨질 수 있음.
- 백엔드 일괄 try/catch 확장 — life-graph/gantt 외에도 analytics, insight,
- 간트 일정 편집 UX 개선 — 날짜 미설정 노드의 📅 버튼은 있으나 드래그로 바 조정은 미지원.
/schedule) 연결 필요.다음 세션 참고
- 추천 시스템 보상은 공개 템플릿 첫 추천만 지급 (반복 토글 시 0맘).
template_votes insert 전 rate limit 추가 검토.- 라이프 그래프 활동 히트맵은
db.select('activity', a => a.at >= day30)풀스캔.
canReadProject(pj, uname)/hasPermission(pj, uname, role)헬퍼는
- 사용자 템플릿 추가/수정 후
db.compact('user_templates')호출 누락 케이스 체크 필요.
J. 초대 시스템 (2026-04-16 추가)
권한 지정 초대 링크/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 → / 이동 → 자동 진입초대하기 버튼 → 모달 (이름/연락처/권한/유효일/메시지)E2E 스모크 통과
- PM 로그인 · 초대 생성 (JSON newline escape 정상)
- 초대 토큰으로 프리뷰 조회
mode:'register'로 신규 유저kimyoungsang가입 + share 생성- 신규 토큰으로 프로젝트 읽기 성공
- 같은 토큰 재사용 →
이미 사용된 초대(410 차단)
L. LED 카탈로그 + 설계 엔진 (2026-04-17 추가, DOT-QUOTE v45 포팅)
DOT-QUOTE v45 (4958줄 JSX)에서 핵심 로직 포팅:
카탈로그 DB (3테이블)
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)- mergeDB 패턴: 사용자 편집 보존 + 신규 기본값 자동 병합
설계 엔진 (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)
API
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 |
M. 현장 파악 (Site Survey) 모듈 (2026-04-17 추가)
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
}
API
POST /api/projects/:id/survey— 현장 조사 저장GET /api/projects/:id/survey— 목록PUT /api/projects/:id/survey/:sid— 수정DELETE /api/projects/:id/survey/:sid— 삭제
현장→설계 자동 전이
벽면 W/H 입력 → "→ 설계로 전이" 버튼 → LED 설계 패널에 desiredW/desiredH 자동 채움N. 하드웨어 견적 (BOM 기반) (2026-04-17 추가)
DOT-QUOTE의 calcPricing() 포팅 — 기존 인건비 견적(buildEstimate)과 별도.
가격 엔진 (calcHwPricing)
- 모듈 비용: moduleGroups[] → USD원가 × 패킹비(5%) × 환율 × ㎡
- 프로세서: 원가(krw) / 판가(marginPct% 기본 20%, 만원 올림)
- 자재 5종: ㎡ auto 또는 직접입력
- 노무 6종: ㎡ auto 또는 직접입력
- 마진: B2B(20%) / B2C(30%) / PUBLIC(15%) / CUSTOM
- VAT 10% → 할인% → 만원 절사
API
POST /api/projects/:id/hw-estimate— 견적 생성 (result 자동 계산)GET /api/projects/:id/hw-estimate— 이력POST /api/projects/:id/hw-estimate/calc— 계산만 (저장 없이)
프론트 3버튼 워크플로우
현장조사 → (벽면 W/H) → LED설계 → (모듈+㎡+프로세서) → 하드웨어견적 각 단계에서 "→ 다음 단계" 버튼으로 데이터 자동 전이스모크 테스트 결과 (P1.86 GOB, 3200×1920mm)
- 실제 크기: 3200×1920mm = 6.14㎡
- 캐비닛: 5×4 = 20ea, 모듈 120ea
- 해상도: 1720×1032 = 1,775,040px
- 포트: 3 → VX400Pro × 1
- 견적: 원가 ₩10,909K → 최종 ₩21,360K (마진 79.4%)
K. 튜토리얼 페이지 (2026-04-16 추가)
/tutorial,/help,/tutorial/*,/help/*→public/tutorial.html- 메인 앱 헤더에
📖 가이드링크 추가 (신규 탭) - 10개 섹션 + 사이드바 스크롤-스파이:
- 권한표(읽기/쓰기/관리자) · FAQ 포함
O. 12년 운영 최적화 (v2.6, 2026-04-16)
보안
- escHtml() 전역 XSS 방어 (pj.name, description, comments, notifications 등 전부)
- sanitizeStr() 강화:
on\w+=,javascript:,data:패턴 차단 - Rate Limiting 4등급: auth(10/min), write(30/min), export(5/min), general(120/min)
- 분할 깊이 제한(6단계), 이미 분할된 노드 재분할 차단
- CSV 내보내기 10,000노드 상한
CellDB 내구성
- WAL CRC32 체크섬: 각 WAL 행에
crc32|payload형식, 손상 행 자동 스킵 - 3세대 백업 로테이션:
.bak→.bak.1→.bak.2순차 회전 - 크래시 복구: 메인 파일 손상 시 3세대 백업 순차 시도, JSON 구조 검증
안정성
- _fatalCount 50회 초과 시 자동 셧다운 (좀비 프로세스 방지)
- unhandledRejection 비치명 WARN 처리
- TTL 자동정리: 세션(24h), 활동(90d), 알림(60d+읽음), 만료 초대 — 매시간
- O(n²) → O(1): 프로젝트 목록 중복 제거 Set 기반
프론트엔드
- 탭 비활성 시 알림 폴링 + WS 재연결 억제 (배터리/대역폭 절약)
- beforeunload: interval 해제, WS 정리 close
- 탭 복귀 시 즉시 알림 갱신 + WS 재연결
P. 프로덕션 고도화 (v2.7, 2026-04-17)
보안 헤더
- HSTS: max-age=31536000 (1년), includeSubDomains
- CSP: default-src 'self', script/style/img/connect/font 세분화
- Permissions-Policy: 카메라/마이크/위치 차단
공격 방어
- 경로 탐색: path.resolve() + publicDir 경계 체크, ../../ 요청 차단
- Slow Loris: server.setTimeout(30s), headersTimeout(15s), requestTimeout(30s)
- parseBody: 10초 타임아웃 (느린 클라이언트 소켓 강제 해제)
- 헤더 인젝션: Content-Disposition RFC 5987 인코딩
성능
- gzip: JSON 응답(1KB 이상) + 정적 파일(html/css/js/svg) 자동 압축
- 캐시: 정적 에셋 max-age=30일+immutable, HTML no-cache
안정성
- Graceful shutdown: server.close() → HTTP 드레인(10초) → DB 저장 → exit
- 503 응답: 셧다운 중 새 요청에 Retry-After:30 반환
- 세션: 슬라이딩 갱신(5분마다 lastLogin 갱신) + 절대 만료 30일
Q. 옵저버빌리티 + 방어 심화 (v2.8, 2026-04-17)
구조화 로깅
- JSON 형식:
{"t":"ISO","l":"info","m":"req","rid":"hex16","method":"GET","path":"/api/...","status":200,"ms":5} - 레벨 필터:
LOG_LEVEL=debug|info|warn|error|fatal환경변수 - 서버 에러, 셧다운, uncaughtException 등 핵심 경로 전환
옵저버빌리티
- Request ID: 요청마다
crypto.randomBytes(8).hex,X-Request-Id응답 헤더 - /api/metrics: 총 요청수, 메서드별, 상태코드별, 에러 수, 지연시간 avg/max, RSS/heap MB, PID
- /api/healthz: liveness (프로세스 생존)
- /api/ready: readiness (테이블 5개↑ + 디스크 여유)
보안
- 프로토타입 오염 차단:
stripProto()—__proto__/constructor/prototype키 재귀 제거 - WS 메시지 검증: 타입 화이트리스트(3종), 4KB 제한, 5분마다 토큰 재검증 → 만료 시
auth_expired전송 후 소켓 종료
데이터 보호
- 자동 백업: 6시간마다 전체 DB JSON →
data/auto-backups/backup-YYYY-MM-DDTHH-MM-SS.json - 최대 28개 유지 (7일 분), 초과 시 오래된 순 삭제
- 멱등성 키:
Idempotency-Key헤더 → 1시간 캐시, 동일 키 재요청 시 캐시된 응답 반환
API 개선
- 페이지네이션:
/api/activity,/api/projects/:id/activity—?limit=N&offset=M(max 200) - 응답:
{ items: [], total, limit, offset }
R. 데이터 무결성 + 접근성 (v2.9, 2026-04-18)
데이터 무결성
- 지갑 뮤텍스: per-user async 락 (Promise 기반 큐) → 동시 reward/tip 레이스 방지
- 금액 검증:
Number.isFinite(amount) && 0 < amount <= 1,000,000+Math.floor() - 프로젝트 삭제 캐스케이드: 9개 테이블(comments/activity/shares/invites/designs/surveys/hw_estimates/snapshots/checklists) 연쇄 정리
- 팀 삭제 고아 정리: 프로젝트 teamId→null + 팀 대상 shares 삭제
DB 성능
- 인덱스 추가:
sessions.username,invites.token,activity.projectId - 기존:
projects.id,sessions._key,logic.id,teams.id
프론트엔드 복원력
- API 함수: 네트워크 오류 + 503 에 지수 백오프(1s, 2s) 최대 2회 재시도
- JSON 파싱 실패 안전 처리
접근성 (ARIA)
- 트리:
role="tree",role="treeitem",tabindex="0",aria-expanded,aria-label - 모달:
role="dialog",aria-modal="true", 첫 입력 요소 자동 포커스
S. 입력 검증 강화 + WS 방어 + 증분 동기화 (v3.0, 2026-04-17)
수치 입력 검증 강화
- safeNum(): 범위 클램핑 유틸 (Number.isFinite + min/max/fallback)
- 적용: calcCostLine 인원/일수(1~9999), 팁 금액(1~1000), HW견적 discPct/customMargin/truncUnit
- HW견적 타입 검증: screens 배열 강제, materials/labor 객체 강제, bizType/status 화이트리스트
- 팁 타입 화이트리스트: mam/phone만 허용 (tip + tip-message 양쪽)
에러 메시지 정보 유출 차단
- 지갑 잔액 노출 제거 (맘 잔액 부족 → 보유액 미표시)
- 수신자 username 노출 제거
- 공격자에게 내부 상태 추론 불가
WebSocket 방어
- IP당 연결 제한: 최대 5개 (wsPerIp Map, connect/disconnect/ping sweep 동기화)
- 프레임 방어: 64비트 길이(len=127) → 즉시 연결 종료, 4KB 초과 → 연결 종료
- 불완전 프레임: buf.length < 4 체크 (len=126 경우)
WS 증분 동기화
- TOAU/assign 이벤트에
nodePath+node데이터 포함 - 프론트:
findNodeByPath()유틸 → 로컬 트리 노드 병합 →renderTree()재렌더링 - 병합 불가 시 폴백으로
openProject()전체 reload auth_expiredWS 이벤트 → 세션 만료 토스트