Crowny Hall 서버 확장 — RBAC 집행·정산·대관 포털·원격 GO
개요
hall.crowny.org:9606 (LaunchAgent org.crowny.hall)에 4개 기능을 추가: 역할×모듈 RBAC 집행(A), 정산 초안(B), 외부 고객용 대관 포털(C), 원격 GO 브리지(D). 기존 엔드포인트·PSV 포맷·앱.html은 무접촉으로 유지, 서버.한선만 확장.
무엇을 했는지
A. RBAC 집행 (POST /api/action)
- body에
role 필드 추가 지원(없으면 sm 기본 — 하위호환). 앱.html은 이미 모든 /api/action 호출에 role을 포함하도록 구현되어 있었음(state.role, ROLES=[manager,sm,operator,booking]) — 이번 서버 확장이 그 프론트 계약의 짝(back half)임을 확인.
_모듈명(타입) / _역할허용(role,타입,liveLock) 헬퍼로 역할×모듈 매트릭스 판정. 거부 시 {"ok":0,"err":"권한 없음 — <역할>은 <모듈> 조작 불가"} (200) + 감사로그 "거부:" 접두.
estop은 매트릭스 검사를 건너뛰어 항상 허용(1i 안전계층).
- 신규 액션
live_lock(payload {lock:0|1}, sm 전용) — 상태키 live_lock 저장, lx_set/mute/scene/preset은 live_lock=1일 때 sm만 허용.
GET /api/state에 liveLock 필드 추가.
B. 정산 (POST/GET /api/settle)
- manager|booking만 허용(다른 역할은 RBAC 거부 응답).
- 대관료: 딜 sub 필드에서
₩8.4M 식 금액을 파싱(있으면 사용) 아니면 홀별 기본(대공연장 8,000,000 / 소공연장 1,800,000 / 리허설룸 320,000). 초과 600,000(고정 목), 인건비 2,640,000(22명×4h×25,000 + 야간수당 20%), 전력·설비 43,380(48.2kW×5h×180원, 정수 스케일 연산으로 자연반올림 함정 회피).
- ERP 가용성(
_ERP가용 재사용)에 따라 "상신 완료" / "상신 접수(연동 대기)" 문구 결정.
data/정산.psv(append)에 라인·합계·타임스탬프·ERP상태 저장. GET은 이름으로 마지막 초안 재조회(없으면 {"ok":0}).
C. 대관 포털 (GET /portal + /api/portal/*)
포털.html 신규 작성 — Hall 앱과 동일 토큰(canvas #F6F2EB, brand #3D2B1F, accent #C05B2E 등), data-crowny + 중앙 CSS 링크 포함. 히어로(홀 3종+요금표) · 가용 캘린더(주간 그리드, 확정 건 "예약됨"만 표시) · 신청 폼(→조회코드 발급) · 마이페이지(코드 조회 + 서명/보증금 목 버튼).
- 서버:
GET /api/portal/calendar(목 점유), POST /api/portal/inquiry(조회코드 발급 + data/포털.psv append + 딜.psv에 stage0 신규 딜 자동 유입 → Hall 파이프라인 '문의'와 실연동 확인), GET /api/portal/status?code=, POST /api/portal/ack.
- 포털은 Hall 내부 API(/api/action, /api/state)를 호출하지 않음(1h 권한 분리, 코드 리뷰로 확인).
- URL 쿼리(한글 값)는 퍼센트인코딩 UTF-8 디코드가 필요해
_URL디코드(바이트 재조립 + 글자변환) 신규 구현.
D. 원격 GO
POST /osc/go → 상태키 remote_go_seq +1, 감사로그. GET /api/goseq → {"seq":n} (프론트 폴링용, 가벼운 단일 읽기).
- LTC/MTC·MIDI/MSC는 하드웨어 필요 — 스텁 없이 미구현(지시대로).
발견한 VM 함정 (실측)
${i} 문자열보간 함정 재확인: 셸 for-루프 리터럴 date -v+${i}d를 그대로 넣으면 hanseonc_high가 "문자열보간"으로 오인해 미정의 변수 i 컴파일 에러. \${i}d로 이스케이프해 해결(가드레일 문서화된 함정, 실전 재발 확인).
% 균형나머지로 "뒤 N자리 코드" 생성 시도 시 오답: ts % 100000 은 음수 포함 균형나머지라 "마지막 5자리" 의미가 안 됨 → 문자열 슬라이스(문자열변환 + 부분)로 우회.
- 기존 코드의 잠재 버그 발견(내 변경 범위 밖):
_처리상태()의 _KVN("cueActive", cactive)는 cueActive가 숫자가 아닌 문자열(예: 다른 클라이언트가 payload.cue="Q21" 전송)일 때 인용부호 없이 JSON에 삽입되어 "cueActive":Q21(무효 JSON) 생성 — 실사용 중인 다른 세션/클라이언트가 이 상태를 만든 것을 라이브 검증 중 관측(이후 다른 클라이언트가 유효값으로 덮어써 자연 치유됨). 이번 작업 범위 밖이라 손대지 않았으나 다음 세션 참고용으로 기록.
관련 파일
/Users/ef/crowny-hall/서버.한선 (확장, ~1230줄)
/Users/ef/crowny-hall/포털.html (신규)
/Users/ef/crowny-hall/포털.한선 (한선씨 동반본 — URL 디코드 제외 핵심 판정로직: 코드생성/신청유효성/설명축약/단계명·다음안내)
/Users/ef/crowny-hall/서버.toau (재컴파일·배포 완료, CROWNY_STRICT=1 경고 0)
/Users/ef/crowny-hall/data/{딜,상태,감사,정산,포털}.psv
- 무접촉 확인:
/Users/ef/crowny-hall/앱.html(다른 에이전트 작업 중), /Users/ef/crowny-gateway/gateway.yaml, LaunchAgent plist 전부 mtime 미변경 확인.
검증
- 컴파일:
CROWNY_STRICT=1 hanseonc_high 서버.한선 → 에러 0, STRICT 경고 0.
- 배포: toau 교체 +
launchctl kickstart -k → 2초 후 /health 200.
- curl 전수 — 기존 회귀(health/api/state/deal_stage) + 신규 전부(RBAC 거부/허용, live_lock, estop 상시허용, settle 라인·합계, portal calendar/inquiry→state 유입 확인/status/ack, osc/go→goseq 증가, /portal 스트리밍 바이트 일치) 전부 PASS.
- 테스트 변이(테스트 딜·live_lock) 원복 완료 —
딜.psv 원본과 byte-diff 일치 확인. remote_go_seq는 지시대로 유지.
- 크라우니코드: lookup HIT 0건(정확매칭 없음, RBAC/포털/정산 도메인 유사 패턴만 존재) / MISS 3건 주요 로직 직접 생성 / learn 추가 3건(rbac_role_action_matrix_hall, url_percent_decode_utf8_hansuni, 정산_대관료_홀별기본_금액파싱).
2차 추가분 (같은 날 코디네이터 지시 — cueActive 수리·Space 브리지·정리)
1. cueActive 비숫자 오염 수리 (치명 버그)
- 헬퍼
_숫자문자열(s)/_숫자보정(s,기본) 신설. go/jump는 payload idx가 순수 숫자일 때만 cueActive 저장(idx 없음/비숫자 → 갱신 생략, 감사로그는 cue 라벨 유지). back은 idx 우선 + cue 숫자 폴백.
- 방출측 최종 가드(자기치유):
/api/state의 무인용 방출 전 필드(q22auto6/staffFilled/cueActive/liveLock/lx값/mute값/erp_up/today_up) + remote_go_seq 읽기 + RBAC live_lock 읽기 전부 _숫자보정 적용.
- 검증: go idx:8→cueActive=8, go cue만("Q9")→불변+JSON 무결, go idx:"Qx"→갱신 생략. 전부 PASS.
2. Space 실서비스 브리지 (9607)
/space/env(10초 캐시)·/space/interlock(실시간)을 127.0.0.1:9607 우선 중계, 유효 JSON({ 시작+글자수>0) 아니면 기존 목 폴백(1i: GO 흐름 안 끊김). /api/state에 space_up 추가. 브리지 전환은 상태변화 시에만 감사(이전상태 /tmp/hall_space_last.txt 비교).
- 실측 함정 발견: 9607 응답이 0.44~1.01s(단일스레드) —
curl -m 1은 절반가량 타임아웃해 브리지가 항상 폴백으로 떨어짐. -m 2(기존 _ERP가용 관례)로 수정 후 정상 중계(temp 25.2 실데이터 확인). 이 타임아웃 실패 동안 목 폴백이 정확히 작동한 것이 폴백 경로의 라이브 검증이 됨.
3. 검증 데이터 정리
- 포털.psv 테스트 항목(HALL-62878, HALL-60489) 제거, 딜.psv 7건 원복 확인, cueActive=6 복원 확인.
잔여 이슈
_처리상태()의 cueActive _KVN 무인용 삽입 버그 → 2차 추가분 1에서 수리 완료(저장·방출 양쪽 가드).
- 포털 캘린더의 stage3 딜→요일 매핑은 목(mock) 규칙(고정 인덱스)이며, 실제 딜의 날짜 필드(자유텍스트)를 파싱해 정확한 요일에 배치하는 고도화는 후속 과제.