Crowny INT — 도시 통합 긴급방송 관제 구축 (VPS 메인 / 맥 서브, 블록체인 커밋)
개요
다운로드 폴더
package - Crowny INT(사장님 핸드오프: Standalone HTML 4종 + 설계문서 + CLAUDE/DEV 문서)를 실서비스로 이식해
int.crowny.org를 신규 구축했다. 요구대로
VPS가 메인, 맥이 서브인 2노드 구조이며, 발령·시험·메시지 이력은
SHA256 해시체인(블록체인 구조) 커밋으로 기록·복제된다.
무엇을 했는지
- 제품: Crowny INT = 도시 통합 긴급방송 관제(경기도청⇢용인시청⇢기흥/수지 12개 동). UI는 핸드오프 픽셀 그대로 서빙(브라운 다크 자체 디자인 시스템 보존 — 크라우니디자인 중앙CSS 미주입, 사장님 디자인 원본 보존 원칙).
- 서버:
INT서버.한선 (순수 한선씨, hanseonc_high, STRICT 경고 0) — sonnet 위임 작성 + 메인 세션 독립 재검증.
- 정적 5경로:
/(PC 268KB)·
/tablet·
/mobile·
/bar·
/design — 읽기() 64KB 절단 회피 위해 파일소켓전송 스트리밍(풀사이즈 실측).
- 체인 API: POST
/api/fire|test|message(X-INT-KEY 필수, 무키 403) → 블록 append. GET
/api/logs·
/api/status·
/api/chain/head·
/api/chain/blocks?from=N·
/api/chain/verify·
/health.
- 블록:
인덱스|이전해시|시각|종류|발신|본문|해시(해시=SHA256 of 앞6필드),
data/체인.psv append-only +
헤드.psv(높이|헤드해시 — 전체 재파싱 회피).
-
메인/서브: MAIN.txt(VPS)=메인(정본, 쓰기 수락) / 맥=서브(쓰기는 메인에 curl 포워드 후 풀동기화, 읽기 시 10초 스로틀 동기화). 메인 다운 시 서브는 정직 503
main_down(페이지 서빙은 계속).
- per-request 회수 = 문자열보호/마크/되감기 단독(가드레일 표준).
- UI 배선:
int-bridge.js(+한선씨 동반 int-bridge.한선 5/5) — 체인 배지(메인/서브·블록높이 15초 갱신), "발령 시작" 클릭→/api/fire 커밋, window.CrownyINT.commit() 수동 API. 4개 HTML에 script 주입(file:// 실행 시 무개입).
- 배포: VPS cafe24(1.234.20.240, takew — take/today 동거)
/opt/int — crownyc 바이너리 재사용(crownyc.c md5 맥과 동일, 스큐 없음), systemd int.service(CROWNY_TCP_RCVTIMEO_MS=1500). 맥 서브 = LaunchAgent org.crowny.int2(:9500, LOOPBACK+RCVTIMEO), 게이트웨이 int.crowny.org→:9500 기존 라우트 무수정. 배포.sh(재컴파일→rsync toau·public→VPS 재시작→헬스→맥 서브 재기동).
검증 (전부 실측)
- 로컬 2노드 e2e 8항목 PASS(에이전트) + 독립 재검증 8항목 PASS(메인 세션, 9588/9589 별도 페어).
- 공개 종단:
https://int.crowny.org fire → VPS 메인 체인 커밋(height 증가) → 서브 동기화 head 일치 → verify ok. 정적 5경로 200 풀사이즈.
- 발견·수리한 결함: 제네시스가 노드별 현재시간으로 생성돼 부트 시각이 다르면 체인 영구 분기(로컬 테스트는 같은 초 기동으로 우연 통과, 실배포에서 적발). → 제네시스 ts=0 고정(결정론)으로 수리, 양 노드 동일 제네시스 해시 실측 확인.
- 메인 다운 시나리오: 서브 fire → 503
main_down(날조 없음), 페이지 200 유지.
관련 파일
- 코드:
/Users/ef/crowny-int2/ — INT서버.한선(SSOT)·public/(HTML 5종+int-bridge)·배포.sh·배포.conf·org.crowny.int2.plist·int.service·시험.sh
- VPS:
root@1.234.20.240:/opt/int/ (crownyc·INT서버.toau·public·data/체인.psv·MAIN.txt·HOME.txt)
- 원본 패키지:
~/Downloads/package - Crowny INT/ (무수정 보존)
- 구버전:
/Users/ef/crowny-int(초통합긴급경보, node) 코드 보존 — com.crowny.int LaunchAgent unload(롤백 = load 복원)
잔여 이슈
- UI 실배선 심화: 현재 "발령 시작" 클릭 커밋 + 배지만. 시험가동/INT메시지 완주 이벤트의 체인 커밋 배선(window.CrownyINT.commit 활용) 잔여.
- X-INT-KEY가 데모 키(공개) — 실운영 시 기관별 키/세션 인증 필요(DEV.md 연동 체크리스트의 온품 API·센서 구축과 함께).
- DNS는 여전히 맥(112.144.147.144) 종단 — 완전 VPS 메인 전환(DNS int→VPS + VPS cert)은 사장님 DNS 권한 필요(현재는 맥 서브가 공개 프론트, 체인 정본은 VPS).
- WebRTC 육성방송(voice-link.js)은 패키지에 파일 미포함 — 핸드오프 원본 수령 시 추가.
2차 확장 (2026-07-23 오후 — guide/demo/module + API v2, 울트라 워크플로우 7에이전트)
- /guide: 이용 가이드 — 계정표(관리자 yongin/int-2580·사용자 guseong/int-1111·개발자 flexdev/int-9500·모듈 m1~m10), 페이지맵, 하드웨어 시뮬 사용법. 검증자가 클래스CSS 위반 전면 인라인 재작성.
- /demo: 체험하기 모드 — 관제 UI 사본(/demo/console, 체험 체인 분리) + 대화형 개발자 온보딩(채팅형 한 질문씩+확인: 아이디→비번→이름→기관명 지정→연결하기(12기관 칩)→세부 기능 지정→요약 확정→/api/demo/signup) + 전/후 상태 패널(now/prev diff, 10초 폴링 — 크라우니집사 등 외부 변경도 전후 확인 가능). 수리 2건: dup 재시도 시 입력값 보존, signup 시 설정 시드(전후 비교 즉시 가동).
- /module: 하드웨어 시뮬레이션(모바일) — 모듈1(용인시청) 긴급 알림 방송 버튼→마이크→/api/live/start→리스너 폴링→기관별 RTCPeerConnection 팬아웃(비-trickle, SDP base64 1줄 — 게이트웨이 \n 제거 함정 회피). 모듈2~10(동사무소) 로그인→수신 대기→live 감지→offer/answer 왕복→ontrack 스피커 재생+방송중 풀스크린 배너.
- 서버 API v2(INT서버.한선 확장, STRICT 0): auth(login/check 세션토큰), live(start/stop 권한 yongin·m1, 체인 커밋 "육성 긴급방송 시작/종료"), rtc(join/listeners/offer/answer/leave — /tmp flat 파일 시그널링, lid [a-z0-9-] 문자검증으로 경로주입 400 차단), demo(signup/state/config/fire/logs — 체험체인 분리·제네시스 ts=0).
- int-bridge 확장: /api/live 5초 폴링→전 관제 페이지 상단 "🔴 긴급 육성방송 방송중 — 용인시청 · 수신 N개 기관" 블링크 배너(관리자=활성 표시·사용자=방송중 안내 겸용), /demo 하위는 체험 체인으로 발령 분기. 동반 9/9.
- 검증: 로컬 e2e(auth 403/토큰·live 사이클·RTC offer/answer 왕복·주입 400·demo 전후비교·회귀 체인 verify) + 공개 4페이지 200 풀사이즈 + 서브 live 사이클·체인 커밋 실측. 실오디오(폰 마이크→스피커)는 기기 테스트 필요.
- 주의: 계정·비번은 체험용 공개 데모 계정(guide에 게시, 실운영 전 교체 배지). NAT 교차망은 STUN만(TURN 미구축)이라 동일 Wi-Fi 권장.
- 핫픽스(07-23 저녁, 사용자 신고 "module?m=1 접속 안됨"): 원인=로그인 응답 계약 불일치 — 클라이언트 모듈1 권한체크가
res.id를 참조하는데 서버 로그인 응답에 id 필드 부재(role도 'module')→ m1/yongin으로도 "방송 권한이 없습니다"로 영구 차단. 서버 응답에 id 추가 배포 후 크롬 실브라우저 검증: m1 로그인→송출 화면 진입, m2 로그인→수신 대기 standby 실측 PASS.
3차 확장 (2026-07-23 저녁 — 방송 세션 녹음 + 로그 + 성능수리)
사용자 요청(방송 클라우드 기록 확인·실제 전송 소리 기록·시작/종료 시간·하위 부서 수신 기록) 전량 구현·실측.
- 방송 녹음: 모듈1이 MediaRecorder로 송출 오디오 녹음→stop 시 base64 청크(≤18KB, 게이트웨이 32KB 웨지 회피) 순차 업로드→서버가 .b64 append 후 1회
base64 -d < file로 .webm 디코드. GET /rec/<bsid>.webm(audio/webm) 재생. 실측: 77초 방송=88KB WebM, 30KB 합성 3청크 md5 완전일치(업로드→디코드→다운로드).
- 세션 로그:
방송세션.psv(bsid|시작|종료|by|duration|수신수|녹음), live/start가 bsid 발급·응답, live/stop이 duration·distinct 수신모듈수 집계+체인 커밋 "종료(N개 부서 수신, D초)". /api/sessions(rec는 webm 파일 실시간 존재판정)·/api/session/receivers.
- 하위 부서 수신 기록:
수신로그.psv(bsid|module|org|join|ts), rtc/join 시 활성 bsid에 append. 실측: 모듈2(구성동) join 기록, listeners 집계 반영.
- RX VU 0% 수리: 수신 AudioContext가 자동재생 정책으로 suspended 시작→getByteFrequencyData 0. attachRemoteAudio·tapToPlay에 resume() 추가. (실측: 소리는 정상, 백그라운드 탭은 rAF 스로틀로 바만 멈춤=정상)
- ★"느림" 근본수리(중대): 누적 수신 루프가 Content-Length(바이트)를 글자수(문자)와 비교 → 한글 본문 POST마다 목표 초과추정→불필요 2차 read 블록. RCVTIMEO 없으면 빈 응답(fire 회귀), 있으면 매 한글 POST +1.5s. "짧은 읽기"(마지막 청크 <4095B=완료) 패턴으로 교정: 한글 fire 1.5s/빈응답 → 1.4ms. TCP읽기 4095B 캡 함정과 함께 가드레일 SSOT 반영.
- 미문서화 VM 함정 2건 발견: TCP읽기 4095B 단일 read 캡(crownyc.c:12239), macOS base64 -d 위치인자 거부(stdin 리다이렉션 필수) → 가드레일 등재.
- 검증: STRICT 0, 로컬 e2e(세션·수신·녹음 md5·주입400·회귀) + 크롬 실브라우저(모듈1 톤 방송→모듈2 수신 RMS 0.283→종료 녹음 업로드→88KB webm 공개 재생·세션 rec:true·수신자 m2). 서빙=맥 서브(녹음 파일 로컬 저장), 체인 정본=VPS.
4차 사전검증 + WebRTC/JIT 점검 (2026-07-23 밤 — 4브라우저 팬아웃)
사용자 요청: 핸드폰1 모듈1 발송 → 관리자/사용자 브라우저 반영 → 모듈2·모듈3 동시 음성수신 사전검증 + WebRTC/최적화/JIT 상황.
- 팬아웃 실검증(크롬 5탭): 모듈1(660Hz 톤 송출) → 모듈2·모듈3 동시 2연결(peerStates 둘 다 connected, 각 수신 RMS 0.283 실측) + 관리자(/)·사용자(/mobile) 방송중 배너 반영. 발송측 peers.size=2 팬아웃 확인.
- JIT 상황: crownyc에 CROWNY_JIT(opt-in, 기본 비활성) 컴파일돼 있음(self-test PASS, jit_enabled/jit_call_count 심볼). 현재 INT 서버는 JIT env 없이 인터프리터로 구동 — 1.4ms 응답은 JIT 없이 달성, 네트워크 I/O 바운드라 JIT 이득 미미. 필요시 CROWNY_JIT=1로 opt-in 가능.
- WebRTC 현황: 계정 m1(송출)·m2~m10(수신), STUN=google(TURN 미구축=교차망 제한), 비-trickle+SDP base64 1줄. 신호=/tmp flat 파일(lid 문자검증).
- 검증 중 발견·수리 3건:
1. ★리스너 카운트 항상 0: /api/live가
find -mmin(샌드박스 신뢰불가, 파일 실재해도 0 반환) 사용 →
_RTC활성리스너()(ls글롭+ts 신선도)로 통일. 관리자 배너 "수신 2개 기관" 정상화. 가드레일 SSOT 반영.
2. 하트비트 추가: 수신자가 join 후 lid 파일 40초 신선도 창을 넘기면 카운트 누락 → POST /api/rtc/ping(15초 주기, 연결 유지 중) + endReceive에서 정리. 장시간 방송에도 카운트 정확.
3. 세션 수신수 집계: 수신자가 방송 재시작을 넘겨 지속 연결 시 새 bsid에 join 로그 누락 → lid 파일에 모듈 저장("ts|module"), live/stop이 활성 수신자를 모듈로 집계·수신로그 보강. 실측: 세션 listeners 2·수신자 m2/m3 정확.
- 최적화: 연결 셋업 지연(최대 ~9초=검출5s+리스너3s+ICE) → 폴링 단축(checkLive 5→2초, pollListeners 3→2초, 서버 1.4ms라 부하 무시). 연결 체감 개선.
- 동반: 방송기록-판정.한선 19/19(리스너카운트·신선도 판정 추가). STRICT 0.
5차 (2026-07-23 — /module 관제화면 진입점)
사용자 요청: int.crowny.org/module에서 관리자·사용자 모듈 주소 선택 가능하게.
- /module 선택 화면에 "관제 화면" 섹션 추가: 관리자 관제(/ 용인시청 통합관제)·사용자 화면(/mobile 기관 사용자) 카드 2개 + 기존 "하드웨어 모듈" 1~10 그리드. consoleCard() 헬퍼 재사용, 인라인 스타일(클래스CSS 0)·팔레트 준수. 크롬 실검증: 관리자→/ 이동, 사용자→/mobile 이동 PASS.