← 목록
기타 2026-07-17 18KB 읽기 19분

Crowny CAD 백엔드 (cad.crowny.org:9609)

개요

크라우니 CAD(파라메트릭 CAD, AutoCAD/Fusion360 대용) 백엔드를 순수 한선씨로 구축. 디자인 핸드오프(Crowny CAD.dc.html renderVals())의 계산 로직(재질DB·부피/질량·KPI·견적·DFM·집사 파싱·PLM 상태기계·간섭검사)을 정수 스케일 산술로 재현.

무엇을 했는지

  1. /Users/ef/crowny-cad/CAD엔진.한선 (196줄, 라이브러리, 함수 정의만) — 계산 코어
- 엔진_재질(재질,등급): 9종 재질×등급 조합(알루미늄/강철/수지/우드) → 밀도x100|인장|원가|가공성 - 엔진_부피질량: WHD − 구멍부피, 정수 스케일(부피cm3x10|질량g) - 엔진_KPI: SFx10(안전계수) + 질량/비용/시간/SF 통과여부 - 엔진_견적: 기계별(printer/wood/cnc3d) 시간·재료비·공정 - 엔진_DFM: 기계별 DFM 규칙(✓/⚠/✕) 멀티라인 - 엔진_집사파싱: 자연어→W/H/D/holeD/재질 (정규식 없이 문자 스캔) - 엔진_PLM전이: 작업 중→검토 중→릴리즈됨→작업 중(ECO) - 엔진_간섭검사: holeD 기준 M12 볼트 간섭
  1. /Users/ef/crowny-cad/서버.한선 (1036줄) — TCP 서버, 구조는 crowny-space/서버.한선 정본 복제
- 라우트 17종: /health / /index.html /api/doc(GET/POST) /api/eval /api/butler /api/plm /api/links(GET/POST/refresh) /api/plot /api/plotlog /api/audit /bus /api/editreq(GET/resolve) /api/twin - PSV append-log 영속화: 문서.psv/감사.psv/링크.psv/플롯로그.psv/수정요청.psv (latest-wins, WAL 재생 복원) - CRLF는 글자변환(13)+글자변환(10) 조립(가드레일 지시, 리터럴 \r 미사용) - URL디코드(한글 쿼리파라미터)는 crowny-building/빌딩서버.한선 패턴 이식

검증 결과 (전량 실측)

  • 컴파일: CROWNY_STRICT=1 양쪽 파일 STRICT 경고 0
  • 라우트 17종 curl 종단 실측 전부 PASS
  • /api/eval 3케이스(알루미늄 cnc3d 기본 / 우드 D>18 / 수지 printer holeD<5) — massG·SFx10·matCost·timeMin·DFM 행 전부 dc.html renderVals() 수치와 수동 대조 일치
  • /api/butler 2케이스("선반 브래킷 240×140, M8 구멍"→W240/H140/holeD8, "재질 강철로 변경"→강철) — 문서 반영 확인
  • PLM 3단 전이(작업 중→검토 중→릴리즈됨→작업 중+ver 15) 확인
  • /bus/api/editreq/api/editreq/resolve(apply=1) 왕복 후 문서 필드 반영 확인
  • 링크 stale 마킹(문서/집사 수정 시 전체 stale) + refresh 확인
  • 재기동 복원: 프로세스 kill 후 재기동 → /api/doc 저장값(W=220,H=160,mat=강철,ver=15) 완전 일치
  • 서버 프로세스 유지 중(PID는 재기동 시 변경, crownyc run 서버.toau cwd=/Users/ef/crowny-cad)
  • 알려진 제한

    • _JSON값(crowny-space 정본에서 그대로 차용한 단순 부분문자열 검색 파서)은 JSON 바디 안에 다른 필드의 값이 찾는 키 이름과 같은 문자열일 때 오탐 가능(예: {"type":"param","param":"H"} 에서 "param" 키를 찾다가 "type"의 값 "param"을 먼저 매치). 실제 라우트 로직 버그 아님 — 필드명이 우연히 다른 필드의 값과 겹치는 요청에서만 발생, 재현 및 회피 확인(비충돌 payload로는 정상). crowny-space/crowny-building 등 생태계 전반이 공유하는 동일 파서라 근본 수정은 별도 과제(간이 토크나이저 필요).
    • 앱.html은 병행 제작 중인 별도 에이전트 산출물(이미 존재·정상 스트리밍 확인) — 이 작업은 백엔드만 담당.
    • 셀코어 규칙 등록(_셀코어정책등록)은 이번 스코프에서 생략(태스크 스펙에 명시 없음, 잔여 항목).

    관련 파일

    • /Users/ef/crowny-cad/CAD엔진.한선
    • /Users/ef/crowny-cad/서버.한선
    • /Users/ef/crowny-cad/서버.toau (컴파일 산출물)
    • /Users/ef/crowny-cad/data/*.psv (영속 데이터)
    • 참조 정본: /Users/ef/crowny-space/서버.한선, /Users/ef/crowny-building/빌딩서버.한선
    • 스펙 SSOT: /Users/ef/Downloads/design_handoff_crowny_cad/Crowny CAD.dc.html

    잔여 이슈

    • LaunchAgent(plist) 미등록 — 공유 인프라 무접촉 원칙에 따라 이 세션에서는 건드리지 않음(메인 세션이 처리 필요 시 등록).
    • _JSON값 파서의 키/값 충돌 케이스는 위 "알려진 제한" 참조.

    후속: CrownyShared 라운드트립(오피스 계약) 확장 — 같은 세션 추가 작업

    정본 계약 확인: 오피스 임베드 전용 API는 없고 /Users/ef/Documents/CrownyShared/ 파일장부(docindex.psv·이벤트버스.psv)가 SSOT — ERP가 이미 이 방식으로 씀. 동일 규약으로 CAD도 참여.

    구현(서버.한선, 1103줄로 확장):

    • _오늘시각압축(): 체계("date +%Y%m%d-%H%M > ...") + 읽기() — 에포크 수동산술 금지 함정 회피, 문서ID용 "YYYYMMDD-HHMM" 생성.
    • _문서JSON(행): 문서 상태 JSON 빌더를 공용 헬퍼로 추출(GET /api/doc과 스냅샷 양쪽에서 재사용).
    • _CrownyShared발행(view, target, 타이틀): 문서ID 크라우니CAD-<view>-<압축시각> 생성 → <문서ID>.cdf 스냅샷(현재 문서 상태 + view/target) 기록 → docindex.psv에 문서ID|타이틀|cad|ts|module:<view> append → 이벤트버스.psv에 ts|cad|cad.view.published|{title,file,view,target} append.
    • POST /api/links: 본문에서 view/target(기본 dwg/doc) 추출해 _CrownyShared발행 호출, 시도/오류로 감싸 best-effort(CrownyShared 쪽이 실패해도 링크 저장 자체는 이미 완료된 상태 유지).
    • POST /api/doc: 저장 성공 후 이벤트버스.psv에 cad.doc.updated append(best-effort, 시도/오류).
    • 부트스트랩에 디렉토리생성(_쉐어드루트) 추가(이미 존재하는 디렉토리라 사실상 no-op, 멱등).
    검증(실측):
    • STRICT 재컴파일 경고 0.
    • 기존 테스트 서버 kill → 재기동 → /health·/api/doc GET·/api/eval 회귀 스팟 전부 정상.
    • POST /api/links {"doc":"브래킷-A 도면","view":"dwg","target":"doc"}docindex.psv크라우니CAD-dwg-20260717-1854|브래킷-A 도면|cad|1784282079|module:dwg append 확인(tail), 이벤트버스.psvcad.view.published 이벤트(JSON에 title/file/view/target) append 확인, .cdf 스냅샷 파일 내용도 문서 상태+view/target 그대로 기록됨.
    • POST /api/doc {"H":175}이벤트버스.psvcad.doc.updated append 확인, /api/doc GET으로 H=175 반영 확인.
    • ERP가 이미 쓴 기존 4행(docindex)·4행(이벤트버스)은 grep으로 전부 그대로 보존 확인(수정/삭제 없음, append-only 준수). 최종 docindex 5행/이벤트버스 6행.
    • 서버는 계속 살려둠(PID는 재기동으로 갱신, cwd /Users/ef/crowny-cad).

    후속 2: 앱.한선 — 프론트(앱.html) 한선씨 동반 정본

    정본 패턴: /Users/ef/crowny-tree/앱.한선(함수 8개 이하 + 자체시험 + 검증() top-level 호출 구조). CAD엔진.한선을 가져오기로 재사용(재질/부피질량/견적/KPI 등 중복구현 금지), 앱.html 고유 파생(포맷/판정) 로직만 8개 함수로 파리티 재구현.

    파일: /Users/ef/crowny-cad/앱.한선 (295줄, 독립 실행 파일 — 서버.한선이 import하지 않음, 검증 전용).

    구현 함수(각각 앱.html 줄번호 주석 포함):

    1. 질량라벨(질량g) — 앱.html:426, ≥1000g→"x.xx kg" / 미만→"N g"
    2. SF배지(SFx10) — 앱.html:458, 실SF≥100(SFx10≥1000)→"99+" / 미만→"x.x"
    3. KPI배지(통과) — 앱.html:466-467, 충족/미달
    4. 눈알상태(서프레스,i,rollback) — 앱.html:549-550,560, 🚫>⏸>👁 우선순위
    5. 간섭메시지(holeD) — 앱.html:778-781, holeD≥12 틈새 표시 / 미만 "+Nmm 확대 필요"
    6. 타임라인라벨(rollback,ver) — 앱.html:770
    7. 렌더상태문구(busy,done,품질,재질전체) — 앱.html:545, 3상태
    8. PLM카드메타(상태,ver) — 앱.html:588-591, 상태→{btn,hint} 맵
    버그 발견·수정: 정수부 추출에 /(자연반올림)을 그대로 쓰면 254/100이 2가 아니라 3으로 반올림돼 "2.54 kg"가 "3.46 kg"로 깨짐(첫 실행에서 실측 재현) → _절단나눗셈(a,b)(자연반올림 몫 계산 후 q*b>aq-1 보정) 헬퍼 추가로 질량라벨/SF배지/간섭메시지 3곳 모두 수정.

    검증: CROWNY_STRICT=1 컴파일 경고 0(1차 22 assert 중 1 FAIL 실측→수정→재컴파일도 경고 0) → crownyc run 실행 → 파리티 22/22 PASS(케이스 A 알루미늄 6061-T6 질량2543g·SFx10 1190→"99+", 케이스 B 강철 S45C 질량7394g→"7.39 kg"·KPI 미달, 케이스 C 우드 D>18 질량640g→"640 g"·SFx10 154→"15.4", 눈알상태 3종, 간섭메시지 2종, 타임라인라벨, 렌더상태문구 3종, PLM카드메타 3종). 라이브 서버(9609)는 무접촉 — health 재확인 정상.

    크라우니코드 학습: crownycode-learn.sh add "CAD프론트파리티판정" <앱.한선 전체> 완료.

    후속 3: 스케치 엔티티 + 다중 파일 백엔드 (2026-07-18, 같은 세션 이어서)

    엔티티 계약(프론트 공유, 변경 금지)

    {"id","kind":"line"|"rect","layer","ax","ay","bx","by"(mm정수),"filled":0/1,"extruded":0/1} — 문서는 docId("d"+ts)·name("*.ccad")·template("bracket"|"blank").

    데이터 모델 결정

    문서.psv 스키마를 트레일링 필드 추가(idx12=docId,13=name,14=template)로 확장 — 기존 idx0-11(rowId,W..rollback,updatedAt) 레이아웃은 손끝도 안 댐. 기존 13개 이력 행은 필드가 없어 _필드()가 ""를 반환하고, _문서메타JSON(행)rowId=="doc1"이면 docId="d0"·name="브래킷-A.ccad"·template="bracket"으로 유도(정적 마이그레이션, 파일 재작성 없음). 새 문서는 rowId=docId로 자기 자신이 PSV 키.

    활성 문서 포인터는 data/활성문서.psv 단일행 덧쓰기(전력적산식과 동일 패턴, _활성DocId()/_활성DocId설정()). _문서읽기()/_문서저장()은 내부에서 활성 docId→PSV rowId(_Doc행ID, d0→"doc1" 리터럴, 그 외는 docId 그대로)를 해석해 기존 호출부(문서수정/집사/PLM/editreq) 시그니처 무변경으로 다중 문서를 지원 — 콜사이트 수정 0건.

    엔티티는 data/엔티티.psvdocId|PSV이스케이프(entitiesJSON)|ts latest-wins 1행. 요청 본문에서 배열 구간 추출은 _JSON배열추출(json,키)(따옴표 인지 브래킷 카운팅, _JSON값은 스칼라 전용이라 별도 함수).

    라우트 5종 추가 (기존 17종 위에)

    POST /api/file/new(blank 문서 생성+활성전환) · GET /api/files(유니크 최신 목록) · POST /api/file/open(활성전환, d0 이외는 존재검증) · GET /api/entities(활성 문서 조회) · POST /api/entities(latest-wins 전체교체+cad.doc.updated 이벤트).

    CAD엔진.한선 추가 (파리티/검증용, 196→300줄)

    • 엔진_엔티티닫힘(kind): rect=1·line=-1
    • 엔진_돌출부피(entities직렬,D): _엔진객체목록()(따옴표 인지 브래킷 카운팅으로 최상위 {..} 분할, 글자() 핫루프 아닌 단일 선형스캔) + _엔진JSON값()(스칼라 추출) → filled&&extruded인 rect만 |bx-ax|*|by-ay|*D 합산
    • 엔진_스냅(v,그리드): 한선씨 나눗셈이 이미 자연반올림(|나머지|≤그리드/2)이라 q=v/그리드; q*그리드만으로 그리드 스냅 성립 — 별도 절단보정 불필요함을 0/양수/음수 3케이스로 실측 확인(스냅(12,5)=10, 스냅(13,5)=15, 스냅(-12,5)=-10)

    라우팅 최적화(요청 항목)

    • _경로매치(처리됨,경로,방식,대상경로,대상방식) 헬퍼로 "처리됨==0 그리고 경로==X[그리고 방식==Y]" 3-way AND 반복을 단일 호출로 축소 — 기존 17블록 + 신규 5블록 = 23개 호출지점이 전부 이 헬퍼 경유(간결화, 로직 100% 동일, 처리됨 플래그·순차 가드체인 구조는 그대로 유지 — "과최적화 금지" 준수).
    • _콤마접두(먼저) 헬퍼로 만약(먼저==0){결과=결과+","} JSON 배열 조립 패턴을 3곳(DFM/플롯로그/감사조회) + 신규 파일목록 1곳 = 4곳에서 통일.
    • 줄수: 서버.한선 1103→1348줄(+245, 순증가는 5라우트 핸들러+다중문서 인프라+헬퍼 정의분), CAD엔진.한선 196→300줄(+104, 엔티티 파서 3함수). 최적화 자체는 줄수를 늘리기보다 중복 조건식/조립 블록 개수를 줄이는 방향(23+4=27개 반복지점을 헬퍼 2개로 수렴) — 스페이스 골격(처리됨 플래그 체인)은 원형 유지, 라우터 라이브러리로의 전면 재작성 등 과설계는 하지 않음.

    검증 (전부 실측, cad.crowny.org:9609 라이브)

    라우트케이스결과
    POST /api/file/new{"name":"테스트설계.ccad"}{"docId":"d1784336070","name":"테스트설계.ccad"} PASS
    GET /api/files직후 조회d0(bracket)+신규(blank) 2건 PASS
    POST /api/entitiesline 1 + rect(filled+extruded) 1{"ok":1} PASS
    GET /api/entities활성=신규 docIdPOST 페이로드와 바이트 일치 왕복 PASS
    POST /api/file/open{"docId":"d0"}{"ok":1} PASS
    GET /api/doc (d0 복귀)W:200,H:120,D:40,holeD:12,mat:알루미늄,...ver:14,rollback:3 레거시 값 그대로 + docId:"d0",name:"브래킷-A.ccad",template:"bracket" 신규 3필드. 하위호환 PASS
    회귀 스팟 5개/healthOK cad 9609 PASS
    /api/doc위와 동일 PASS
    /api/eval(W200,H120,D40,holeD12,알루미늄)massG:2543, sfX10:1190, matCost:16500 — 스펙 기준수치 3종 정확 일치 PASS
    /api/butler"선반 브래킷 240x140, M8 구멍 알루미늄" → W:240,H:140,holeD:8,mat:알루미늄 PASS
    /api/links기존 4건 그대로 반환 PASS
    launchctl kickstart -k 재기동활성=신규docId 상태로 재기동/api/entities 저장값 그대로 복원, /api/files 2건 유지, /health 정상 — 영속 PASS
    엔진 엔진_돌출부피((20,20)-(140,100),D=40)rect filled+extruded384000 (=120×80×40) 실측 일치 PASS
    컴파일: CROWNY_STRICT=1 서버.한선+CAD엔진.한선 양쪽 경고 0(재확인 포함 2회). 실배포: hanseonc_high서버.toau 재생성 → launchctl kickstart -k gui/$(id -u)/org.crowny.cad만 사용(plist 무수정, 공유인프라 무접촉 준수).

    크라우니코드: lookup MISS 4건("다중 파일 문서"/"엔티티"/"브래킷 카운팅 JSON 배열 추출"/"PSV latest-wins 다중문서" — 검색결과 있었으나 게임 ECS 도메인이라 비재사용) / 생성 MISS→LLM 4건 / crownycode-brain.sh learn "CAD엔티티저장_다중파일" 1건 + learn.sh add(CAD돌출부피계산, 라우팅경로매치헬퍼) 2건 = 학습 추가 3건.

    잔여

    • 테스트로 생성한 d1784336070(테스트설계.ccad) 문서가 data/문서.psv·data/엔티티.psv에 append-only 로그로 영구 잔존(설계상 삭제 API 없음 — 기존 append-log 패턴 그대로 준수, 정리하려면 별도 파일 로테이션 과제).
    • 앱.html/앱.한선(프론트) 쪽에 신규 라우트 5종을 실제 소비하는 UI 배선은 이 작업 범위 밖(백엔드만 담당, 프론트 에이전트 후속 필요).
    • 문서 삭제(/api/file/delete 등)는 스펙에 없어 미구현.

    후속 3-1: 앱.한선 파리티에 blank 문서(스케치) 경로 assert 추가

    프론트 스케치(선/사각형/채우기/돌출) 라이브 확정 수치로 케이스D 추가: rect(20,20)-(140,100) filled+extruded, D=40.
    • 엔진_돌출부피(entD,40)(CAD엔진.한선 기존 함수 재사용, 중복구현 없음) == 384000 assert
    • 질량은 박스(120×80×40, 무구멍)로 환산해 엔진_부피질량(케이스A denA 재사용) 경유 산출 → 질량라벨(massD) == "1.04 kg" assert (massD=1037g, 하드코딩 아닌 엔진 산출값)
    • 엔진_스냅(23,5) == 25 assert
    • 전체 재실행 파리티 25/25 PASS(기존 22 + 신규 3), CROWNY_STRICT=1 경고 0, 앱검증.toau 재생성. 라이브 서버(9609) 무접촉(/health GET만 재확인).

    후속 4: POST /api/doc docId 미스라우팅 회귀 수리 (2026-07-18)

    사고: 본문에 docId 없는 POST /api/doc이 무조건 "당시 활성 문서" 행에 덮어써 데모 중 활성이 신규 blank 문서로 전환된 상태에서 프론트가 구형 페이로드(docId 없음)로 저장 → d0(브래킷-A)가 아니라 활성 문서에 반영됐어야 할 저장이 반대로 d0 오염(W0/v1)으로 나타난 사고(메인 세션이 d0 수동 복구). 근본 원인=_처리문서수정이 항상 활성 포인터 경유, 저장 대상을 요청이 명시할 방법이 없었음.

    수리(서버.한선): _문서읽기()/_문서저장()_문서읽기_대상(docId)/_문서저장_대상(docId,...) 코어 위 얇은 활성문서 래퍼로 리팩터(기존 콜사이트 전부 무변경). _처리문서수정이 본문 docId를 우선 사용(존재하면 그 문서 행에 저장, 활성 포인터는 안 건드림) → 없으면 기존 활성문서 동작 유지(하위호환). 신규 _Doc존재하나(docId) 가드로 미존재 docId는 {"ok":0,"error":"unknown docId"} 400 거부(오타 오염 원천 차단).

    검증(라이브 실측): CROWNY_STRICT=1 경고 0 → launchctl kickstart -k 재기동 → ①활성=d0에서 POST {"docId":"d1784336070","W":77} → d0 무변(W200 유지) · file/open 전환 확인 시 대상 문서 W77 반영 · d0 복귀 ②docId 없는 구형 POST({"H":121})→활성(d0)에 반영(하위호환, 이후 H120 원복) ③{"docId":"d999notreal","W":1}{"ok":0,"error":"unknown docId"} ④회귀 3종 health/eval(massG2543·sfX10 1190·matCost16500)/files 전부 PASS. 최종 d0=W200/H120/D40/⌀12/v14 기본값 확인(복구 불필요, 이미 정상).