Crowny INT — 방송 세션 녹음 + 세션/수신 로그
개요
Crowny INT(도시 통합 긴급방송 관제,
/Users/ef/crowny-int2)의 육성 긴급방송 기능에
①실제 전송 음성 서버 녹음저장 ②방송 시작/종료 세션 로그(duration 포함) ③하위 부서(모듈)
수신 로그를 추가했다. 방송 자체가 체인에 커밋되는 것은 기존 기능(변경 없음, note만 강화).
무엇을 했는지
서버 (INT서버.한선)
- 데이터 경로 3종 추가:
data/방송세션.psv, data/수신로그.psv, data/녹음/(디렉토리, 기동 시 생성).
- 라이브.psv를
on|since|by|bsid 4필드로 확장(3필드 구파일 하위호환) — _라이브읽기() 헬퍼로 통합.
POST /api/live/start: bsid(b<ts>-<rand>) 생성 → 라이브.psv에 저장, 방송세션.psv에 시작 행 append, 응답에 bsid 필드 추가.
POST /api/live/stop: duration 계산(종료ts-시작ts), 수신로그.psv에서 해당 bsid의 join distinct 모듈 수 카운트(awk sort -u), 녹음파일 존재 확인(test -s), 방송세션.psv에 종료 행 append. 체인 커밋 note를 "육성 긴급방송 종료 (bsid, N개 부서 수신, D초)"로 강화.
POST /api/rtc/join: 방송 활성 시 수신로그.psv에 bsid|module|org|join|ts append.
POST /api/rtc/leave: 방송 활성 시 bsid|-|-|leave|ts append(모듈 매핑 불가 — join이 핵심 로그).
POST /api/rec/chunk: base64 청크(≤18KB, 게이트웨이 웨지 회피)를 녹음/<bsid>.b64에 순차 append, 마지막 청크(last:"1")에서 체계("base64 -d < ... > ... .webm") 1회 디코드.
GET /rec/<bsid>.webm: bsid 문자클래스 검증([a-z0-9-]) 후 _정적서빙으로 오디오 서빙.
GET /api/sessions?limit=N: 방송세션.psv bsid별 최신 1행(awk dedup)만 최근 N개 JSON 배열.
GET /api/session/receivers?bsid=B: 수신로그.psv의 해당 bsid join 이벤트 전부 JSON 배열.
- 디스패처(요청처리)를 "아니면 만약" 33+ 중첩 체인에서 매치 플래그를 각 독립 만약이 덮어쓰는 평탄화 패턴으로 재작성(hanseonc_high ~35중첩 파서 한계 회피 가드레일 선제 적용).
클라이언트 (public/module.html)
- 모듈1(송출):
startBroadcast에서 MediaRecorder(mimeType 우선순위 webm;opus→webm→기본) 생성+start, live/start 응답의 bsid 전역 저장. stopBroadcast에서 recorder.stop()→Blob→FileReader base64→18000자 청크 슬라이스→순차 /api/rec/chunk 업로드(best-effort, 실패해도 방송 종료 진행).
- 방송 화면에 세션 정보(bsid·경과시간) 카드 + 종료 후 "이번 방송 기록 저장됨" + 녹음 재생 링크(
/rec/<bsid>.webm) 카드 추가.
- 수신단 RX VU 미터 0% 버그 수리:
rAudioCtx 생성 직후 state==='suspended'면 resume() 호출(자동재생 정책으로 suspended 시작 → getByteFrequencyData 항상 0). tapToPlay()에서도 동일 처리.
동반 한선씨
public/방송기록-판정.한선 — bsid 형식검증([a-z0-9-])·base64 문자셋검증·duration 계산·수신 distinct 카운트(콤마 마커 부분문자열 dedup, sort\|u 미러) 판정 로직 미러. 자체시험 14/14 통과.
실측으로 발견한 VM 함정 (가드레일 미기재, 신규 발견)
TCP읽기(fd,max)는 단일 read() 호출이 4095바이트로 하드캡됨(crownyc.c:12239) — 인자로 넘긴 max(예: 65536)와 무관하게 항상 min(max,4095)만 읽는다. 헤더+본문 합 4KB를 넘는 POST 요청은 조용히 절단된다. 기존 서버들의 소형 JSON payload(로그인·라이브 등)는 우연히 이 캡 아래였을 뿐 — 18KB 녹음 청크에서 처음 노출됨.
- 처방:
Content-Length 헤더를 파싱해 목표 길이에 도달할 때까지
TCP읽기를 반복 호출+문자열 연결로 누적(
_본문길이/
_본문시작 헬퍼 + 메인 루프 누적 while). 빈 읽기(타임아웃/EOF) 시 즉시 루프 탈출(TCP수락 기본 5초 recv 타임아웃과 결합해 무한 대기 방지).
- 이 수정은 INT서버 전체(모든 POST 엔드포인트)에 적용됨 — 향후 대형 payload를 받는 다른 한선씨 서버도 동일 패턴 필요.
- macOS(BSD)
base64 -d는 위치인자 파일명을 받지 않는다(base64: invalid argument) — -i 플래그 또는 stdin 리다이렉트(<) 필요. GNU base64(Linux)는 위치인자를 허용하므로 이 차이를 모르면 macOS에서만 조용히 빈 파일이 나온다(exit 64, 하지만 > 리다이렉트가 이미 빈 파일을 만들어 실패가 안 보임). 처방: base64 -d < '경로' > '출력' (GNU/BSD 공통 안전).
두 함정 모두
~/.claude/templates/한선씨-가드레일.md에 없어 이번 세션 실측으로 처음 확인. 다음 세션 참고용으로 이 문서에 남긴다(가드레일 파일 자체 갱신은 별도 태스크).
관련 파일
/Users/ef/crowny-int2/INT서버.한선 (서버, 1608줄)
/Users/ef/crowny-int2/public/module.html (클라이언트, 749줄)
/Users/ef/crowny-int2/public/방송기록-판정.한선 (동반 판정, 107줄, 신규)
- 데이터:
data/방송세션.psv, data/수신로그.psv, data/녹음/*.webm (신규 스키마, 배포 시 라이브 서버 재기동 필요 — 이 세션에서는 배포하지 않음)
검증 결과
CROWNY_STRICT=1 hanseonc_high INT서버.한선 — 경고 0.
- 테스트 인스턴스(
INT_ROOT=/tmp/intr INT_MODE=main INT_PORT=9566) e2e 14/14 PASS: login·live/start(bsid)·rtc/join(m2,m3)·session/receivers(2건)·rec/chunk 3청크 업로드 md5 일치·GET /rec/ 200+md5 일치·live/stop·sessions(duration/listeners=2/rec=true)·잘못된 bsid 400·잘못된 base64 400·회귀(health/fire/module/404) 전부 정상.
- 방송기록-판정.한선 자체시험 14/14 통과.
- module.html 인라인 스크립트
node --check 통과.
- 회귀: 기존 정적 페이지(pc/tablet/mobile/bar/design/int-bridge.js) 무수정 확인(mtime 대조).
잔여 이슈
- 라이브 배포/plist/게이트웨이 반영은 이번 작업 범위 밖(호출자 담당) — 신규 파일·수정 파일만 산출.
TCP읽기 4095B 하드캡은 crownyc.c(VM) 레벨 제약이라 서버 쪽 반복읽기 우회로 해결했지만, 근본적으로는 VM 정본에 opcode 493을 루프 방식으로 고치는 게 장기적으로 더 안전(다른 모든 한선씨 서버에 잠재하는 동일 취약점) — 별도 크라우니OS 태스크로 이관 권장.