본문으로 건너뛰기Hwanseok Sim

← 홈으로

  • 전환 준비

GT PBX — 주차장 콜센터 PBX와 운영 웹앱

주차장 정산기·인터폰이 인터넷을 거쳐 거는 SIP 호출을 24시간 받는 콜센터용 Asterisk PBX와, 설정·실시간 현황·통계·통화 이력·녹취를 한곳에서 다루는 자체 웹앱. 기존 상용 PBX를 단계적으로 대체하려고 만들었고, 지금은 EC2 스테이징에서 검증하고 있다.

기간
2026.10 –
역할
단독 설계·개발·배포
  • TypeScript
  • Asterisk 22 (PJSIP · Queue · AMI)
  • PostgreSQL 16 (ODBC Realtime)
  • Node.js 24 · Express 5
  • React 19 · Mantine 9
  • SSE
  • Gemini 함수 호출
  • systemd · nftables

주차장 정산기·인터폰이 인터넷을 거쳐 거는 SIP 호출을 24시간 받는 콜센터용 PBX다. 오픈소스 Asterisk 22 위에 설정·실시간 현황·통계·통화 이력·녹취를 다루는 웹앱을 직접 얹었고, 회사 콜센터가 쓰는 기존 상용 PBX를 단계적으로 대체하는 것이 목표다.

2026년 10월 23일 첫 커밋부터 10월 30일까지 혼자 설계·개발·배포했다. 구현은 단계마다 계약 문서(범위·DB·API·화면·시험·분담)를 먼저 쓰고, AI 코딩 에이전트 여러 개에 나눠 맡긴 뒤 검토·통합했다. 지금은 EC2 스테이징 서버에 배포해 검증하는 단계다. 현장 전환은 하지 않았고, 실제 상담원 전화기·현장 장비·통신사 트렁크로 하는 검증이 남아 있다.

참고

마스킹 안내. 회사·협력사·고객사·사이트 이름, 레거시 PBX 제품 이름, 실제 장비·내선 번호, 계정 이름, 서버 주소와 인스턴스 정보, 비밀번호 값은 싣지 않았다. 수치는 GT PBX 저장소의 단계별 계약 문서 §구현 결과, docs/TODO.md, 부하 시험 문서, AI 평가 결과 파일에서 옮겼고 측정 환경(OrbStack 개발 VM, EC2 t3.medium 등)과 날짜를 함께 적었다. 개발 과정은 GT PBX 개발기 8편에 나눠 썼다.

항목내용
서비스주차장 현장 장비 호출 수신 · 상담원 큐 분배 · 자동 녹취 · 상담원 외부 발신 · 운영 웹앱
사용자회사 콜센터 상담원(약 20석), 슈퍼바이저·관리자(웹), 일부 사이트의 협력사 상담원
역할단독 설계·개발·배포. 단계별 계약 문서를 쓰고 AI 코딩 에이전트 여러 개에 구현을 나눠 맡긴 뒤 감독·검토·통합
기간2026-10-23 첫 커밋 → 10-26 M1·M2 → 10-29 M3·M4·X2·X4 → 10-30 AI 설정 도우미·최근 커밋 (커밋 207개, 그중 10-29 하루 156개)
상태EC2 스테이징(t3.medium) 배포·검증 중, 현장 전환 전
규모REST API 라우트 93개(README 기준), DB 마이그레이션 20개(00010020), AI 도우미 도구 40개, 결정 기록 D1D31
코드서버 약 1.9만 줄 + 단위·DB 통합 시험 약 1.3만 줄, 웹 약 1.9만 줄 + 시험 약 5.7천 줄, 공유 패키지 약 1.9천 줄, SQL 마이그레이션 약 3.1천 줄 (wc -l, 마지막 커밋 149a8bd 기준)
시험단위(shared·서버·웹 vitest), DB 통합(PostgreSQL 16 컨테이너), VM 통합 러너 10개(m1~m4, x1·x2·x5, rec, devpw, assistant), 음성 판정 voice, EC2 부하 시험 tests/load

목차: 1. 개요 · 2. 만든 것 · 3. 시스템 구성 · 4. 통화 흐름 · 5. 웹 화면 · 6. 기술적 도전 · 7. 연혁 · 8. 배포·운영 · 9. 품질 · 10. 결정 기록 · 11. 한계 · 12. 역량


1. 개요

1.1 풀어야 했던 문제

문제왜 어려운가GT PBX의 접근
현장 장비가 인터넷에 열려 있다장비 대부분이 유동 IP라 IP 허용 목록을 쓸 수 없고, SIP가 인터넷에 노출된다 (D14·D19)SIP 계정으로 장비를 식별, 외부로 나갈 길이 없는 장비 전용 context, 장비당 동시 통화 1, nftables SIP 속도 제한, fail2ban, 선택 GeoIP
설정을 바꿀 때 통화에 영향이 없어야 한다24시간 받는 PBX라 reload·재시작에 기대면 반영 시점을 잡기 어렵다PJSIP·큐를 ODBC Realtime으로 DB에서 읽고, 웹은 트랜잭션을 커밋한 뒤 AMI로 반영만 확인한다
레거시 설정을 그대로 옮기면 복잡도도 따라온다기존 상용 PBX에는 사이트별 링 그룹·일정·휴일 라우팅이 쌓여 있다단일 큐 원칙(D13·D20)을 지키고 사이트 라우팅은 옮기지 않았다(D23). 필요한 협력사 시간대만 따로 모델링했다(D27)
녹취와 통화 기록이 빠지면 안 된다민원 확인과 통계가 녹취·queue_log에 기댄다응답 시점부터 스테레오 WAV, CDR·CEL·queue_log를 DB에 적재하고 통화 요약 테이블을 트리거로 만든다
외부 발신은 요금 사기 표적이다트렁크로 나가는 길이 하나라도 잘못 열리면 사고가 된다상담원 context에서만 발신, prefix 기본 거부, 판정이 실패하면 막는다(fail-closed), 동시 발신 제한, 거부 시도도 기록
자연어로 설정을 바꾸게 하고 싶다모델이 대상을 잘못 고르거나 메모에 섞인 지시(인젝션)에 넘어가면 설정이 잘못 바뀐다모델에게 쓰기 도구를 주지 않는다. AI는 변경안만 만들고, 반영은 사람이 [적용]을 눌러 서버가 한다

1.2 운영 조건 (결정 기록 docs/PLAN.md)

  • 인입: 현장 장비 호출뿐이다. 일반 고객 대표번호와 IVR은 없다 (D2·D13).
  • 응대: 자사 상담원 약 20석이 단일 큐 field를 함께 받는다. 24시간 운영이고, 응답이 늦어도 다른 곳으로 넘기지 않고 계속 대기한다 (D1·D13·D20).
  • 예외: 협력사 시간표가 있는 사이트는 정해진 시간대에 협력사 큐로 간다. 협력사 상담원이 0명이면 자사 큐로 되돌린다 (D27).
  • 민원 처리: 상담원은 사내 원격 지원 도구(RustDesk 팝업)로 사이트 PC에 접속해 처리한다. 새 웹앱은 이 팝업을 대체하지 않는다 (D12·D21).
  • 녹취와 설치: 녹취는 1년 보관·WAV 원본(D9). 사내 서버 1대 + NAS 백업(D8), Ubuntu 24.04에 컨테이너 없이 네이티브 설치(D22).

2. 만든 것

구성요소내용
PBX 코어 (M1)Asterisk 22.11.0 소스 빌드(sha256 고정), PJSIP·app_queue·MixMonitor, ODBC Realtime, from-field·from-agent·from-partner·from-trunk context, 녹음 마무리 스크립트, CDR·CEL·queue_log 적재, watchdog, nftables·fail2ban
웹앱 골격 (M2)로그인·역할 4개·CSRF·scrypt, PBX 설정 화면, 통화 이력·녹취 재생·감사 로그
실시간 (M3)AMI 2연결, 이벤트 정규화·상태·스냅샷, SSE, 월보드와 TV 모드, 상담원 기능 코드·내 상태, 장비 마지막 등록 시각
통계 (M4)15분 사전 집계(5분 증분 + 매일 04:10 KST 재집계), 큐·상담원·장비·그룹·협력사·유형 지표, 지표 기준값, 의존성 없는 XLSX 작성기
레거시 이관 (X1·X5·D28~D30)장비 그룹·위치, 레거시 단말 목록 CSV 일괄 등록(미리보기·CP949), 협력사 시간표와 부재 되돌림, 녹취 켬/끔, 공통 기본 SIP 비밀번호
장비 간 통화 (X2)같은 그룹 장비끼리 직접 통화·녹음, 통화 이력 "장비 간" 유형
외부 발신 (X4)트렁크 템플릿 1개, 발신 규칙·prefix 허용/차단, 동시 발신 제한, 녹음, 통화 이력 "외부 발신" 유형
AI 설정 도우미 (D31)Gemini 함수 호출, 도구 40개, 서버가 매기는 위험 등급, 변경안 카드와 [적용], 사용량 탭과 월 상한
배포·운영멱등 설치·배포 스크립트, systemd unit, 방화벽 적용 스크립트, chrony, logrotate, 관리자 CLI gtpbx-admin
시험 도구SIPp 시나리오와 VM 통합 러너, TTS 음성을 흘려 녹취 채널을 판정하는 tests/voice, EC2 부하 시험 제어 서버, AI 평가 세트

3. 시스템 구성

flowchart LR
    Field["주차장 정산기·인터폰<br/>(인터넷, 유동 IP)"] -->|SIP 등록·호출| AST
    Phones["상담원 IP 전화기"] -->|SIP| AST
    PartnerPh["협력사 상담원 전화기"] -->|SIP, 받기 전용| AST
    AST["Asterisk 22 LTS<br/>PJSIP · Queue · MixMonitor"] <-->|SIP/RTP| Trunk["통신사 SIP 트렁크<br/>(외부 발신 전용)"]
    AST -->|"ODBC: Realtime · CDR · CEL · queue_log · func_odbc"| DB[("PostgreSQL 16")]
    AST -->|스테레오 WAV| REC[("녹취 디렉터리")]
    AST <-->|"AMI 2연결: 명령 · 이벤트"| API["gtpbx-web<br/>Node.js 24 + Express 5"]
    API --> DB
    API --> REC
    API -->|"REST + SSE, HTTPS"| WEB["웹앱<br/>React 19 + Mantine 9"]
    API -->|"함수 호출 (선택)"| GEM["Gemini API"]
    WD["watchdog<br/>(systemd timer 40초)"] -.->|"프로세스 · CLI · PJSIP 점검"| AST
  • gtpbx-web 한 프로세스가 REST API, 정적 웹, AMI 클라이언트, 실시간 수집기, 통계 배치, AI 도우미 정리 작업을 함께 돌린다. 웹이 멈춰도 통화와 녹음은 Asterisk만으로 계속된다.
  • Asterisk와 DB 사이는 res_odbc + psqlODBC 하나로 Realtime·CDR·CEL·queue_log·func_odbc를 모두 처리한다.
  • 다이얼플랜과 .conf는 저장소 asterisk/가 원본이다. 운영자가 바꾸는 값(차단·라우팅·녹취 사용·장비 간 통화·외부 발신 판정)만 func_odbc로 DB에서 조회한다. Asterisk 재시작이 필요한 설정은 웹에서 바꾸지 않는다.
  • API 경로 표는 공유 패키지 한 곳(packages/shared의 API_ROUTES)에 있고 서버 핸들러가 1:1로 대응한다. 라우트를 빠뜨리면 컴파일 오류가 난다.
  • 시각은 UTC로 저장하고 KST로 보여 준다. 서버 시각은 chrony로 맞춘다.

4. 통화 흐름

4.1 현장 장비 호출

flowchart TD
    F["현장 장비 호출 (from-field)"] --> B{"차단 중?"}
    B -->|예| X["거절"]
    B -->|아니오| P{"같은 그룹 장비 번호? (X2)"}
    P -->|예| D["그 장비로 직접 Dial · 녹음"]
    P -->|아니오| R{"그룹 협력사 시간대? (X5)"}
    R -->|예| QP["협력사 큐<br/>고지·녹음 없음"]
    QP -.->|"협력사 상담원 0명 → 기록"| N
    R -->|아니오| N{"녹취 사용? 장비 → 그룹 (D28)"}
    N -->|켬| G["녹음 고지 → MixMonitor"] --> QF["field 큐<br/>(ringall, 대기 제한 없음)"]
    N -->|끔| QF
    QF --> A["로그인한 자사 상담원 전원 동시 울림"]
  • 장비 context(from-field)는 무엇을 누르든 위 경로로만 간다. 외부 발신이나 상담원 내선으로 나갈 길이 없다.
  • 상담원 context(from-agent)는 내선 간 호출, 기능 코드, 외부 발신을 쓴다. 협력사 내선(from-partner)은 받기만 하고, 트렁크 수신(from-trunk)은 모두 거절한다.
  • 상담원 기능 코드: *11 큐 로그인, *12 로그아웃, *20 대기 복귀, *21 처리 중, *22 휴식, *23 식사, *24 교육.
  • 녹음은 날짜별 디렉터리에 스테레오 WAV(왼쪽 장비, 오른쪽 상담원)로 남고 통화 요약 gtpbx_calls에 연결된다.

4.2 상담원 외부 발신 (X4)

flowchart TD
    S["상담원이 숫자 번호 발신 (from-agent)"] --> J["func_odbc GTPBX_OUTBOUND_ROUTE<br/>(상담원, 번호)"]
    J --> R1{"발신 규칙 패턴에 맞나"}
    R1 -->|아니오| E404["404 · NOTICE 로그만<br/>(내부 번호 오입력, 통화 이력 행 없음)"]
    R1 -->|예| R2{"상담원 내선 · 외부 발신 켬 · prefix 허용"}
    R2 -->|아니오| E403["403 · failed 행 + 사유"]
    R2 -->|예| V{"결과가 allow 모양 그대로인가"}
    V -->|"아니오 (빈 값 · ODBC 오류 · 형식 오류)"| E403
    V -->|예| T{"트렁크 있음 · 동시 발신 한도 안"}
    T -->|아니오| E503["503 · TRUNK_DOWN / LIMIT 행"]
    T -->|예| OK["트렁크로 Dial · 설정대로 녹음"]

판정 순서는 규칙 → 상담원 → 설정 → prefix다. prefix는 번호에 맞는 가장 긴 것이 정하고, 같은 길이면 차단이 이기며, 아무것도 맞지 않으면 거부한다. 동시 발신 수는 기본 4(1~50)이고 울리는 중인 채널도 센다.


5. 웹 화면과 권한

역할할 수 있는 일세션
관리자 admin전부(설정 변경, 계정 관리, 감사 로그, 녹취 다운로드, AI 설정·사용량)8시간
슈퍼바이저 supervisor실시간 현황, 통계, 통화 이력·내보내기, 녹취 재생, 장비 조회, 상담원 대행 로그인·로그아웃, AI 도우미 조회8시간
상담원 agent본인 통화 이력·녹취, 내 상태(연결 내선이 있을 때)12시간
조회자 viewer실시간 현황(월보드 TV)만8시간
메뉴화면
통화실시간 현황(대기 호출·최장 대기·상담원 상태·당일 지표, TV 표시 모드), 통계(기간·근무조별 요약, 추이, 상담원·장비·그룹·협력사·유형 표, XLSX), 통화 이력(조건 검색, 상세 타임라인, 녹취 재생 ±5초·배속·A–B 반복, CSV), 내 상태
PBX 설정장비 계정(생성·CSV 일괄 등록·정렬·내보내기·일시 차단·비밀번호 변경·녹취 설정), 장비 그룹, 협력사·시간표, 상담원 내선, 큐, 안내 멘트(업로드 → 8kHz 변환·버전·미리 듣기), 근무조, 외부 발신, 지표 기준값, 변경 이력(이전 값 비교·되돌리기·다시 만들기), AI 도우미 설정
관리계정 관리, 감사 로그, AI 사용량
  • 모든 쓰기 요청은 CSRF 토큰(x-csrf-token)이 필요하다. 웹 비밀번호는 scrypt, 세션은 HttpOnly·Secure·SameSite=Strict 쿠키(DB에는 해시)이고, 로그인을 15분에 5회 실패하면 잠근다.
  • 설정 변경은 변경 이력과 감사 로그에 남고, 비밀번호 값은 남기지 않는다. 감사 로그는 DB 트리거로 수정·삭제를 막는다(append-only). 녹취 다운로드는 관리자만, 사유를 입력해야 한다.
  • 화면 오른쪽에는 AI 도우미 패널이 있다(기능이 켜져 있고 권한이 있을 때).

6. 기술적 도전과 해결

각 항목은 저장소의 계약 문서·커밋·시험 결과가 뒷받침하는 범위까지만 적었다.

6.1 설정을 reload 없이 반영하고, 확인이 안 되면 되돌린다 (M2, 2026-10-26)

  • 해결: 상담원·협력사 내선, 장비 계정(PJSIP), 큐는 Sorcery/ODBC Realtime으로 DB에서 읽는다. 웹은 DB 트랜잭션을 커밋한 뒤 AMI PJSIPShowEndpoint·QueueStatus로 반영됐는지만 확인하고, 확인에 실패하면 보상 변경을 하고 503을 돌려준다. AMI Reload는 대기 음악 교체에만 쓴다.
  // 반영 확인 (Realtime 이라 바로 보여야 한다). 하나라도 안 되면 전부 되돌린다.
  let ok = ctx.deps.pbx.connected;
  for (let i = 0; ok && i < results.out.length; i += 10) {
    const batch = results.out.slice(i, i + 10);
    const checks = await Promise.all(batch.map((item) => confirmEndpoint(ctx.deps.pbx, item.id, true)));
    ok = checks.every(Boolean);
  }
  if (!ok) {
    await compensateCreated(db, actor, ctx.ip, results);
    throw pbxUnavailable();
  }
  • 순서를 바꾼 경우: 비밀번호 재발급과 삭제는 웹 DB 역할이 이전 해시를 읽을 수 없어 되돌릴 수 없다. 그래서 이 둘은 DB 존재 확인 → AMI 확인 → 변경 순서로 하고, 사후 확인 실패는 경고만 남긴다.
  • 구현 중 발견: Set(CALLERID(all)=...)는 <> 안 번호의 -·.을 지운다. 장비 ID m2-1이 queue_log에 m21로 기록됐다. 다이얼플랜은 번호와 이름을 따로 넣고, 통화 요약은 CDR이 들어오면 장비 칸을 바로잡도록 했다(0008).
  • 검증: OrbStack VM에서 tests/m2/run.sh --with-e2e 231/231, M1 회귀 55/55, DB 통합 51/51(2026-10-26, M2-CONTRACT §10).

6.2 레거시 이관: 옮길 것과 옮기지 않을 것 (X1·X5·D23~D30, 2026-10-26 ~ 10-29)

  • 옮기지 않은 것: 기존 상용 PBX의 사이트별 링 그룹·일정·휴일 라우팅(D23). 모든 사이트를 단일 큐로 받는 원칙을 유지했다. 일정 라우팅 중 협력사 응대만 D27로 일부 번복했다.
  • 그대로 가져온 것: 장비 계정 번호는 레거시 내선번호를 그대로 쓴다(D25). 장비 그룹 개념은 X1로 옮겼다.
  • CSV 일괄 등록 (X1): 첫 줄에 아는 머리글이 있으면 머리글로 보고, 비밀번호 열은 파싱 단계에서 값을 버린다. 인코딩은 UTF-8 → CP949(TextDecoder("euc-kr", { fatal: true })) 순서로 판정한다. 합성 CP949 300행 fixture로 시험했고, 일괄 등록 뒤 변경 이력·감사 로그·서비스 로그에 레거시 비밀번호 값이 없음을 확인했다. tests/x1 86/86.
  • 협력사 시간표 (X5): 그룹에 협력사와 요일별 응대 시간대를 지정한다. 협력사 상담원이 0명이면 큐 진입 자체가 비고(JOINEMPTY), 다이얼플랜이 GTPBXFALLBACK 이벤트를 남기고 자사 field 큐로 되돌린다. 시험에서 ENTERQUEUE → EXITEMPTY → GTPBXFALLBACK → ENTERQUEUE → CONNECT 순서와 대기 시간 합산을 확인했다. tests/x5 84/84. 시간표 편집기의 독립 리뷰 지적 2건은 5b39434에서 고쳤다.
  • 녹취 켬/끔 (D28): 그룹은 켬/끔, 장비는 따름/켬/끔이고 장비 설정이 우선한다. 끈 호출은 녹음 고지도 틀지 않는다. 끈 호출 표시는 CDR 사용자 칸 대신 queue_log 사용자 이벤트 GTPBXNOREC로 했다. CDR은 통화가 끝나야 오지만 이 이벤트는 통화 중에도 바로 보이기 때문이다. tests/rec 85/0.
  • 공통 기본 SIP 비밀번호 (D29·D30): 사용자 지시로 상담원 내선과 장비 계정을 무작위 값 대신 공통 기본값 하나로 만들고, 설치를 마친 뒤 계정별 값으로 바꾸는 절차로 정했다. 위험도 결정 기록(PLAN §9)에 적었다. 상담원 내선과 장비 계정은 네트워크 제한 없이 등록되므로, 기본값이 새면 아무 곳에서나 "기본 비밀번호 사용 중"인 계정으로 등록할 수 있고 인증이 성공하니 fail2ban도 막지 않는다. 그래서 값은 서버 설정 파일(root 0600)에만 두고, 목록에 "기본 비밀번호" 배지를 띄워 바꿔야 할 계정이 보이게 했다. 기본값이 설정된 서버에서는 기존 통합 러너가 사전 점검에서 멈추는 가드를 넣었다. 표시 계산 때 기본값이 DB 함수의 바인드 매개변수로 가므로, 나중에 느린 쿼리 로그를 켤 때는 log_parameter_max_length = 0을 함께 두도록 계약에 적었다. tests/devpw 65/65.

6.3 EC2 동시 통화 부하 시험 (L1, 2026-10-28)

  • 방법: 개발 맥의 OrbStack VM(gtpbx-sim)에서 SIPp가 장비와 상담원을 흉내 내고, 인터넷을 거쳐 EC2 t3.medium(2 vCPU)의 PBX에 통화를 건다. tests/load/의 제어 서버가 시험을 돌리며 CPU·steal·RTP·녹취·queue_log를 모은다. 중단 기준은 호출 실패율 1% 초과, PBX CPU p95 80% 초과, steal 평균 5% 이상, RTP 손실 3% 초과, 녹취·queue_log 불일치 1건이다.
  • S2 계단 결과 (장비 수 N = 상담원 수, 1건/초, 소스 bbb0392, LOAD-TEST-PLAN §11.1):
N성공CPU p95 / 평균통화당 CPU (전체 / asterisk)RTP 손실 · 지터 최대큐 대기 p50 / p95 / 최대 (초)
1050/5013.5 / 9.5%0.88 / 0.63%0.05% · 13ms1 / 3.5 / 7
2099/9919.2 / 14.7%0.68 / 0.52%0.03% · 15ms1 / 7 / 15
30144/14426.1 / 19.8%0.60 / 0.54%0.11% · 18ms1 / 6 / 8
50157/15737.2 / 27.1%0.53 / 0.50%0.01% · 29ms2 / 8 / 10
75187/18755.8 / 41.5%0.56 / 0.51%0% · 22ms2 / 12 / 14
100200/20070.7 / 51.6%0.54 / 0.51%0.09% · 36ms4 / 21 / 32
  • 해석: N=100까지 PBX 쪽 호출 실패 0, 녹취·queue_log 전부 일치, Asterisk ERROR·WARNING 0이었다. CPU p95를 직선으로 맞추면 80%에는 N≈110115에서 닿지만, N=100에서 지터 최대 36ms가 품질 기준(30ms)을 먼저 넘어 이 인스턴스의 실용 상한은 N≈75100으로 적었다. 운영 기준(동시 30, CPU p95 < 30%)은 26.1%로 충족하지만 여유가 작다. 최종 대상은 사내 서버라 통화당 CPU(약 0.54%, t3.medium 전체 대비)로 환산해 남겼고, 같은 측정을 사내 서버에서 다시 하기로 했다. S2-150은 사용자 지시로 돌리지 않았다.
  • 시험이 찾아낸 설정 문제:
    • 상담원 엔드포인트의 contact ACL 때문에 NAT 뒤(사무실 공유기)에서 등록이 모두 거절됐다. 다른 네트워크의 상담원도 있으므로 제한을 풀었다(0010).
    • 발생기 경로의 UDP NAT 매핑은 20~30초에 끊기는데 qualify가 60초라, 장비가 BYE를 받지 못했다. qualify를 25초로 줄였다(0011).
    • S2-75 첫 시도에서 18건이 bye_timeout이었다. 장비 AOR 등록 만료가 120초인데 시험 장비가 통화 중 재등록하지 않아서였다. 시험 장비가 90초마다 재등록하게 고친 뒤 187/187이 됐다. 실제 장비의 재등록 주기는 현장 확인 항목으로 남겼다.
    • EC2 steal이 매분 :00~:03초에만 튀어 p95 판정이 N과 무관하게 경계에 걸렸다. PBX 없이 더미 부하로도 재현돼 판정을 steal 평균으로 바꿨다(사용자 결정).
  • S3 실제 피크 모양 (상담원 20명, 동시 30건, 10분): 큐 연결 순서는 거의 FIFO였다(어긋난 호출 6/201, 최대 1자리). 첫 실행에서 진행 중이던 녹취 15건이 같은 순간 멈추고 통화는 계속되는 일이 있었다. 재실행에서는 계측을 붙였고 199/199, 녹취와 연결 시간 차 −0.58~+0.88초로 재현되지 않았다. 원인은 미확정(RTP 경로 변경 + strictrtp, 또는 MixMonitor 쪽)으로 남겼다.
  • ringall의 신호량: 호출 1건이 빈 상담원 전원을 울려, 상담원 10명이면 PBX로 오는 패킷이 호출당 약 37개다(OrbStack 실측). 한 명씩 울리면 약 4개로 줄지만 "모두 동시에 울리고 먼저 받는 사람"이라는 업무 방식이 바뀐다. 사양은 유지하고 S4에서 비교하기로 했다.

6.4 AMI 이벤트로 실시간 현황 모으기 (M3, 2026-10-29)

  • 구조: 설정 확인용 AMI 연결(Events: off)과 별도로 이벤트 연결을 하나 더 연다. 이벤트를 정규화해 상태를 만들고 스냅샷을 SSE로 보낸다(250ms 합치기). OrbStack에서 queue_log·CEL 시각부터 SSE 스냅샷까지 0.250~0.267초였다(요구 1초 이하).
  • Asterisk 소스로 확인한 사실: 계약에 적은 ContactStatus 값 Created는 22.11에서 NonQualified로 나온다. 옛 contact의 Removed가 새 contact 등록보다 늦게 오면 장비가 미등록으로 굳을 수 있어, 등록 판단을 엔드포인트의 contact(URI) 집합 단위로 바꿨다.
  • 고친 문제: 새로 만든 장비의 첫 대기 호출에 장비 이름이 최대 30초 비었다. 표시 이름을 30초마다 다시 읽었기 때문이다. 대기 목록에 새 장비가 처음 나오면 그때 바로 읽게 했다(f254604).
  • 리뷰 반영: 클라이언트가 느리면 drain 때 최신 스냅샷 하나만 보내고, 쓰기 버퍼가 1MB를 넘으면 스트림을 닫는다. TCP keepalive는 30초다. 서버 독립 리뷰 9건 중 8건을 반영했다(6d46c69).
  • AMI가 끊길 때: 수집기 소켓만 끊으면 0.26초 뒤 ami:false, 1.29초 뒤 복구됐다. 끊긴 동안에도 당일 지표(DB 집계)는 계속 보인다. tests/m3 119/119.

6.5 통계를 15분 단위로 미리 집계 (M4, 2026-10-29)

  • 지표 정의: 인입은 통화 요약 행 수, 응답은 연결 시각이 있는 행, 단기 포기는 대기 5초 미만 포기, 서비스레벨은 대기 20초 이하 응답이다. 기준값은 화면에서 바꿀 수 있다.
  • 배치: 15분 버킷을 5분마다 증분 집계하고, 매일 04:10 KST에 전일을 다시 집계한다. 동시 실행은 advisory lock으로 막는다.
  • 리뷰에서 고친 것 (805491a): 늦게 커밋된 행을 놓치지 않도록 증분 커서를 10분 겹치게 했다. 7일보다 오래된 변경은 pending_from으로 넘겨 재집계 대상에 남기고, 긴 정지 뒤의 재집계는 하루씩 트랜잭션을 나눈다. 기준값을 바꾸면 새 버킷부터 적용하고, 소급은 CLI stats-rebuild만 한다.
  • 성능 (개발 맥 docker, 합성 데이터): 1년 50만 통화 전체 재집계 30.3초(버킷 행 1,229,793), 지난달 요약 54ms, 연간 81ms, XLSX 44ms(목표 3초).
  • XLSX: 외부 라이브러리 없이 시트 7개를 쓴다. 독립 ZIP 리더, unzip -t, 시트 XML 파싱으로 검증했다.
  • 검증: tests/m4 142/142. 오늘 리포트 합계가 통화 요약 SQL 결과, M3 월보드 당일 값과 같았다. 리뷰 반영본은 main 병합 뒤 VM에서 다시 돌려 142/0이었다.

6.6 장비 간 통화와 외부 발신 (X2·X4, 2026-10-29)

  • X2: 같은 그룹의 장비 번호를 누르면 그 장비로 직접 Dial하고 녹음한다. 미등록 장비를 그대로 Dial하면 cause 3 → 404와 chan_pjsip ERROR가 났다. 그래서 Dial 전에 PJSIP_DIAL_CONTACTS로 걸러 480(cause 20)으로 끝내게 했다. 통화 중은 486, 무응답은 480(cause 19)이다. tests/x2 165/0.
  • X4 fail-closed: X2·X5의 DB 조회는 실패하면 자사 큐로 보내는 fail-open이다. 호출을 잃는 것보다 낫기 때문이다. 외부 발신은 반대로, 판정 결과가 정확히 allow 모양이 아니면 모두 거부한다. 무단 발신이 더 나쁘기 때문이다.
exten => _X.,1,Set(AGENT=${CHANNEL(endpoint)})
 same => n,Set(GTPBX_OB=${GTPBX_OUTBOUND_ROUTE(${AGENT},${EXTEN})})
 same => n,Set(GTPBX_OB_CAUSE=21)
 ; 허용 결과는 모양 전체를 본다 (트렁크 이름·숫자 칸이 깨진 값으로 Dial 하지 않게).
 same => n,GotoIf($[${REGEX("^allow[|][a-z0-9_-]{1,20}[|][0-9]{2,20}[|][0-9]{1,2}[|](on|off)[|](yes|no)([|][0-9]{1,3})?$" ${GTPBX_OB})}]?allowed)
 same => n,Set(GTPBX_OB_REASON=${TOUPPER(${CUT(GTPBX_OB,|,2)})})
 same => n,ExecIf($["${GTPBX_OB_REASON}" = "PREFIX_DENIED"]?Set(GTPBX_OB_REASON=DENIED_PREFIX))
 same => n,ExecIf($["${CUT(GTPBX_OB,|,1)}" != "deny" | ${REGEX("^(NOT_AGENT|DISABLED|DENIED_PREFIX|NO_RULE)$" ${GTPBX_OB_REASON})} = 0]?Set(GTPBX_OB_REASON=ERROR))
 same => n,Goto(denied)
  • 나갈 길을 구조로 막기: 외부 발신 context는 상담원 context의 Goto로만 들어온다. 어디에도 include하지 않아서 장비·협력사 context에서는 닿을 수 없다. 트렁크 비밀번호는 배포 때 렌더링하는 Asterisk 설정 파일에만 둔다.
  • 기록: 거부된 시도도 통화 이력에 "외부 발신 · 실패(사유)"로 남기고 NOTICE 로그를 쓴다. 이상 발신을 감시할 근거다. 다만 어떤 규칙에도 맞지 않는 번호(내선 오입력 등)는 외부 번호가 아니라고 보아 행 없이 404로 끝낸다. 내부 번호 오입력이 통화 이력을 채우지 않게 하려는 결정이다(계약 v0.1.2).
  • 검증 범위: 타입체크, 웹 vitest 242, 서버 단위 72, DB 통합 240을 통과했고 VM 회귀로 기존 경로에 영향이 없음을 확인한 뒤 EC2에 배포했다. X4 시나리오 통합 러너(tests/x4)는 아직 작성하지 않았고, 실제 통신사 회선으로는 걸어 보지 않았다.

6.7 AI 설정 도우미: 변경안은 AI가, 반영은 사람이 (D31, 2026-10-29 ~ 10-30)

  • 원칙: 관리자·슈퍼바이저가 "OO 그룹 녹취 꺼 줘"처럼 말하면 Gemini가 조회 도구로 현재 값을 확인하고 변경안을 만든다. 반영은 사용자가 변경안 카드의 [적용]을 눌러야 하고, 서버가 기존 API 핸들러로 실행한다. 반영 결과는 변경 이력에 출처 "AI 도우미(모델)"로 남아 되돌릴 수 있다.
  • 도구 40개: 설정 조회 12개, 통화 조회 3개, 제안 25개. 사용자의 권한으로 부를 수 있는 도구만 모델에 보내고, 실행할 때 라우트 권한을 다시 검사한다. 통화 조회는 외부 상대 번호를 모델에 보내지 않는다.
  • 위험 등급은 서버가 매긴다:
등급예확인 방식
1 단순이름·메모·위치 1건이전 → 이후 + [적용]
2 동작 영향협력사 시간표, 녹취 켬/끔, 그룹 이동, 일시 차단, 큐 설정, 2~5건 묶음영향 설명 + [적용]
3 위험삭제, 외부 발신 규칙·허용 prefix 추가, 6건 이상 묶음, 변경 이력 되돌리기관리자만, 확인 문구 직접 입력
  • 그 밖의 제한: 변경안은 본인·같은 세션·10분 안에 한 번만 적용된다. 비밀번호·웹 계정·트렁크·방화벽·서버 설정, 녹취 재생, 통화 걸기는 도구가 없다. 질문에 알려진 비밀값이 들어 있으면 저장·전송 전에 거절한다. 가짜 Gemini를 쓰는 VM 시험에서 생성 비밀번호와 키 더미가 DB 5개 표·가짜 Gemini 요청 기록·서비스 로그에 0건임을 확인했다(tests/assistant 137/0, 통화 조회 추가 뒤 152/0).
  • 평가 세트: 실제 모델로 돌리는 케이스 58개(범주 10개)를 만들고, 판정 규칙을 고정한 뒤 프롬프트를 바꿔 가며 돌렸다. 합격 기준은 모호한 요청에 되묻기 95% 이상, 금지 항목 거절 100%, 인젝션으로 등급·대상·칸이 바뀐 건 0이다.
프롬프트gemini-3.8-flashgemini-4-argon메모
2026-10-30.157/58, 모호 14/15 미달56/58, 합격 3개 충족"6건 이상이면 먼저 묻기" 추가
2026-10-30.2폐기–프롬프트 예시("야간에만", 18:00)가 평가 케이스와 겹쳐 답을 새어 줌(독립 검토)
2026-10-30.358/58, 합격 3개 충족57/58, 금지 거절 9/10모호한 값 예시를 평가와 겹치지 않는 표현으로
2026-10-30.4 (66케이스)65/66, 통화 조회 8/865/66, 통화 조회 8/8통화 조회 케이스 8개 추가
  • 판정 규칙의 한계도 적었다: 30.3에서 Argon의 실패 1건은 "대신 처리할 수는 없습니다"를 거절 정규식이 놓친 것이었다. 사람이 보면 명백한 거절이지만 그 라운드의 판정은 바꾸지 않고, 규칙 보강은 다음 평가부터 적용했다. 30.4의 Flash 실패 1건도 같은 유형("제공되지 않습니다")이었다.
  • 모델 선택: 기본 모델은 gemini-3.8-flash로 두었다. 30.3 기준 평균 지연 8.7초(p95 22.5초)로 Argon(14.3초, p95 38.1초)보다 빨랐다. Argon은 "느릴 수 있음" 안내와 함께 선택 모델로 남겼다.

7. 개발 연혁

날짜단계한 일
2026-10-23–저장소 생성(첫 커밋)
2026-10-26M1계획(PLAN.md)·기능 설계(FEATURES.md)·M1 계약과 함께 Asterisk 22.11.0 소스 빌드, ODBC Realtime, from-field context, field 큐, 녹음, CDR·CEL·queue_log, watchdog, nftables·fail2ban, SIPp 통합 시험 (955104e)
2026-10-26M2인증·권한, PBX 설정 화면, 통화 이력·녹취 재생·감사 로그 (1966927)
2026-10-26X1장비 그룹·위치, 레거시 단말 목록 CSV 일괄 등록, tests/voice (521c937, 6e8fc44)
2026-10-28L1부하 시험 도구, 상담원 네트워크 제한 해제(0010), qualify 25초(0011), EC2 S2 계단 시험·S3 (25167b6, 9b00ee8)
2026-10-28X5협력사 시간표와 부재 되돌림, 시간표 편집 화면 (bbb0392)
2026-10-28D28·D29그룹·장비별 녹취 켬/끔, 상담원 공통 기본 SIP 비밀번호 (a737574, 341e332)
2026-10-29 오전B·CD28·D29 반영본 EC2 배포(09:11, 5b39434). 비교 조사에서 나온 화면 문제 11건 중 10건 수정(변경 이력 표, 등록 상태 용어, 빌드 커밋 표시, 장비 목록 정렬·CSV, 24시간제 시각 입력 등, 1건은 넣지 않기로 결정), 문서 정정 6건, chrony 시각 동기화 스크립트, EC2 배포(11:02)
2026-10-29M3·X2실시간 수집기·SSE·월보드(1025e5b), 같은 그룹 장비 간 통화(c52f4c3), 통합 회귀 뒤 EC2 배포
2026-10-29M4·X415분 사전 집계·통계 화면·XLSX(186a44d), 상담원 외부 발신(d9d3a5d), EC2 배포 뒤 과거 기간 재집계
2026-10-29D30장비 계정 공통 기본 비밀번호, 재발급 대신 "비밀번호 변경" (b5a1566)
2026-10-29 ~ 10-30AIAI 설정 도우미 계획·계약·구현, 평가 세트와 프롬프트 30.1~30.3, EC2 배포(0f2fcb0), 통화 조회 도구와 프롬프트 30.4(a2da012), 종합 README

10-29 하루에 커밋 156개가 몰린 것은 감독 세션이 계약을 승인하고 작업 세션 둘에 단계를 배정해 병렬로 구현·병합했기 때문이다. 병합·푸시·EC2 배포는 감독 세션만 했다.


8. 배포와 운영

8.1 네이티브 설치와 systemd

컨테이너와 PM2를 쓰지 않았다(D22). SIP/RTP는 호스트 네트워크를 써야 하고 nftables·fail2ban·watchdog도 호스트 기준이라 컨테이너로 얻는 것이 적다고 판단했다. Asterisk, gtpbx-web, watchdog timer(40초)를 모두 systemd unit으로 관리한다. gtpbx-web unit은 샌드박스 옵션으로 묶었고(systemd-analyze security 1.5), TLS 인증서는 LoadCredential로 넘긴다.

순서스크립트하는 일
1deploy/install.sh패키지, 서비스 사용자, Node 24(tarball sha256·GPG 확인), Asterisk 빌드, 디렉터리·ACL, systemd, chrony, PostgreSQL 역할·DB, ODBC, TLS, watchdog, logrotate
2deploy/deploy-config.sh [--restart]설정 템플릿 렌더링 → /etc/asterisk 백업·교체 → 멘트 준비 → web.env → 마이그레이션 → core reload
3deploy/deploy-app.shnpm ci && npm run build → unit 설치·재시작 → /api/health 200 대기, 빌드 커밋을 버전으로 표시
수시deploy/migrate.sh미적용 마이그레이션을 번호 순서로 한 파일씩 트랜잭션 실행
수시deploy/apply-firewall.shnftables·fail2ban 확인·출력·적용

모든 스크립트는 root로 돌리고, 다시 실행해도 안전하게(멱등) 만들었다. 서버 설정은 /etc/gtpbx/gtpbx.env(root 0600) 한 파일에 두고 값은 저장소와 문서에 남기지 않는다. 웹 프로세스는 그중 필요한 값만 추린 web.env(0600)를 읽는다.

8.2 방화벽은 확인하지 않으면 되돌린다

원격 서버에 방화벽 규칙을 잘못 넣으면 SSH가 끊겨 고칠 방법이 없어진다. --apply는 규칙을 적용한 뒤 60초 안에 새 SSH 세션으로 접속을 확인하고 yes를 입력해야 영구 반영한다.

confirm_or_rollback() {
    if ((ASSUME_YES)); then
        return
    fi
    if [[ ! -t 0 ]]; then
        nft delete table inet gtpbx
        die "확인 입력을 받을 수 없어(터미널 아님) 되돌렸습니다. 확인 없이 적용하려면 --yes"
    fi
    echo
    echo "방화벽을 적용했습니다. ${CONFIRM_TIMEOUT_SEC}초 안에 '새' SSH 세션으로 접속되는지 확인한 뒤 yes 를 입력하세요."
    echo "입력이 없거나 yes 가 아니면 적용 전 상태로 되돌립니다."
    local answer=""
    read -r -t "$CONFIRM_TIMEOUT_SEC" -p "유지할까요? (yes): " answer || true
    if [[ "$answer" != "yes" ]]; then
        nft delete table inet gtpbx
        die "확인되지 않아 방화벽을 되돌렸습니다 (inet gtpbx 테이블 삭제)"
    fi
}

8.3 개발 환경과 스테이징

  • 개발 VM: 맥(Apple Silicon)에서 OrbStack Ubuntu VM 2대를 쓴다. gtpbx-pbx는 운영과 같은 install.sh로 설치하고, gtpbx-sim은 SIPp·sox·tcpdump로 장비와 상담원을 흉내 낸다. dev/orb-up.sh가 VM 생성부터 앱 배포까지 멱등으로 하고, 여러 작업 세션이 VM을 함께 쓰므로 배포 잠금으로 한 번에 한 워크트리만 쓰게 했다.
  • EC2 스테이징: 10-28 부하 시험 때부터 시험할 커밋을 올리고 파일 해시로 대조했고, docs/TODO.md의 배포 기록 기준으로는 2026-10-29 09:11부터 10-30 14:04까지 8번 배포했다. 배포마다 /api/health의 버전(커밋)으로 반영을 확인했다.
  • 개발 중 배운 것 (TODO.md §G): 병렬 브랜치 시험에서 마이그레이션이 번호 순서를 어기자 CHECK 재정의가 기존 행에 막혔다. 마이그레이션은 번호 순서로만 적용한다. sorcery.conf가 바뀌는 배포는 reload가 아니라 Asterisk 재시작이 필요하다. macOS awk는 UTF-8 로케일에서 다른 한글 문자열을 같다고 볼 수 있어 시험 스크립트는 LC_ALL=C로 비교한다.

9. 품질과 테스트

9.1 VM 통합 러너 (OrbStack 개발 VM)

SIPp로 실제 통화를 일으켜 DB·녹취·화면(Playwright)까지 확인한다. 수치는 각 계약 §구현 결과와 TODO.md에 기록된 값이다.

러너확인하는 것결과 (날짜)
m1장비 호출 → field 큐 → 응답 → 녹취·queue_log·CDR·CEL52/52(M1 완료 시), 이후 회귀 55/0
m2인증·권한, 설정 화면, 통화 이력·녹취·감사231/231 --with-e2e (10-26), 233/0 (10-29)
x1장비 그룹·CSV 가져오기86/86 (10-26)
x5협력사 분기·되돌림84/84 (10-28)
rec녹취 켬/끔85/0 (10-28)
m3AMI 수집기·SSE·월보드·기능 코드·presence·AMI 끊김119/119 (10-29)
x2같은 그룹 장비 간 통화 11개 시나리오165/0 (10-29)
m415분 집계·리포트 API·XLSX·기준값142/142 (10-29)
devpw장비 기본 비밀번호·비밀번호 변경65/65 (10-29)
assistant가짜 Gemini로 AI 도우미 시나리오137/0 (10-29), 152/0 (10-30)
voiceTTS 음성을 통화로 흘려 녹취 좌·우 채널을 받아쓰기로 판정좌·우 일치 100%, 반대쪽 섞임 9~14% (10-26, 2회)

새 단계를 병합할 때마다 기존 러너를 회귀로 다시 돌렸다. 예를 들어 X2와 M3 통합 회귀(10-29)는 x2 170/0, m3 119/0, m1 55, m2 231, x1 86, x5 84, rec 85가 모두 실패 0이었다. 공통 기본 비밀번호가 설정된 서버(EC2)에서는 러너가 안전장치로 멈추므로, 통합 시험은 개발 VM에서만 돌린다.

9.2 단위·DB 통합

범위결과 (2026-10-30, AI 통화 조회 반영 뒤)
npm run typecheck통과
서버 단위144/0
DB 통합 (PostgreSQL 16 컨테이너)323 통과 / 0 실패 / 1 건너뜀(성능 시험, 환경 변수가 있을 때만)
웹 vitest329/0
shared10/0
스크립트shellcheck, watchdog·방화벽·migrate·렌더러 단위 시험

9.3 아직 검증하지 못한 것

  • 실제 상담원 IP 전화기와 현장 장비로 하는 M3·X2·X5 확인, M3의 장시간 SSE·메모리.
  • X4 시나리오 통합 러너(tests/x4)와 실제 통신사 트렁크 회선.
  • 부하 시험 S4(몰림)·S5(1시간)·S6(속도 제한), S3 녹취 잘림의 원인.
  • 웹 vitest 전체 실행에서 한 번 실패하고 이후 4회 연속 통과한 불안정 테스트 1건(이름 미식별).

10. 기술 스택과 결정 기록

영역사용 기술
OSUbuntu Server 24.04 LTS
PBXAsterisk 22.11.0 소스 빌드(sha256 고정), PJSIP, app_queue, MixMonitor, res_prometheus
DBPostgreSQL 16, Asterisk와는 res_odbc + psqlODBC (Realtime·CDR·CEL·queue_log·func_odbc)
백엔드Node.js 24, TypeScript strict ESM, Express 5, Kysely, pg, zod
프론트엔드React 19, Vite 8, Mantine 9, TanStack Query 5, React Router, ECharts 6
실시간AMI 이벤트 → 서버 정규화 → SSE
운영systemd, nftables(기본 drop·SIP 속도 제한·선택 GeoIP), fail2ban, chrony, logrotate
AI (선택)Gemini 함수 호출, gemini-3.8-flash(기본)·gemini-4-argon
시험SIPp, sox, Playwright, vitest, Node 내장 테스트
결정이유감수한 비용
단일 큐, 24시간 대기 (D13·D20)모든 상담원이 모든 사이트를 받는 현재 업무 방식 그대로사이트별 세밀한 분배는 하지 않는다
사이트 라우팅은 옮기지 않음 (D23)레거시 링 그룹·일정·휴일 설정을 그대로 가져오면 복잡도도 같이 온다협력사 응대만 D27로 따로 모델링
Realtime + AMI 확인, reload 없음설정 변경이 진행 중 통화에 닿지 않고 다음 호출부터 반영Asterisk 재시작이 필요한 설정은 웹에서 다루지 않는다
컨테이너 없는 네이티브 설치 (D22)SIP/RTP·방화벽·watchdog이 모두 호스트 기준설치 스크립트를 멱등으로 직접 관리
외부 발신은 fail-closed (X4)호출 유실보다 무단 발신이 더 나쁘다DB 조회가 잠깐 실패해도 외부 발신이 막힌다
ringall 유지"모두 동시에 울리고 먼저 받는 사람"이라는 업무 방식상담원 수에 비례하는 신호량, 같은 IP 뒤 상담원이 많으면 SIP 속도 제한에 걸릴 수 있다
공통 기본 SIP 비밀번호 (D29·D30)공통 값으로 전화기·장비를 설치하고 나중에 계정별 값으로 바꾸는 절차 (사용자 지시)기본값이 새면 해당 계정 전부가 위험하다. 값은 서버 설정에만, 목록 배지와 교체 절차로 보완
AI는 변경안만, 반영은 사람 (D31)모델이 틀려도 사람이 이전/이후를 보고 거른다. 서버가 등급과 권한을 다시 검사한 번에 끝나지 않고 [적용]을 눌러야 한다

11. 한계와 다음 단계

과제현재 상태
현장 전환하지 않았다. EC2 스테이징에서만 검증했고, 사내 서버는 같은 소스로 따로 빌드할 예정이다(EC2 전용 값은 설정 파일·보안 그룹에만)
실제 전화기·현장 장비 검증M3·X2·X5를 실제 상담원 전화기와 현장 장비로 확인해야 한다. 현장 장비의 재등록 주기·keepalive도 확인 항목이다
외부 발신tests/x4 러너와 실제 트렁크 회선 검증이 남았다
부하 시험S4~S6, S2 고부하 구간의 bye_timeout, S3 녹취 잘림 원인
M5 운영장비 상태, 알림 규칙(장기 대기·상담원 0명·협력사 되돌림), NAS 백업, 녹취 만료 파기, 시스템 로그, GeoIP — 계획
X3 과거 데이터레거시 녹취·CDR 가져오기(D26). 레거시 서버 접근 권한 확인 대기
M6 보조 PBX 연동보류. 보조 PBX의 수신 방식 분석이 먼저다
운영 값사이트별 녹취 켬/끔 입력, 공통 기본 비밀번호를 계정별 값으로 바꾸는 절차, 월보드 TV 세션 길이
AI 도우미시범 사용 단계. 사용 피드백을 받는 일과 운영용 전용 키가 남았다. 30.4에서 거절 정규식이 놓친 표현("제공되지 않습니다")도 판정 규칙에 남은 과제다

12. 이 프로젝트로 증명한 역량

역량근거
전화망 설계와 Asterisk 운영소스 빌드·버전 고정, Realtime·func_odbc 설계, context로 나갈 길을 막는 다이얼플랜, queue_log 사용자 이벤트로 통화 요약 만들기
무중단 설정 반영트랜잭션 커밋 → AMI 확인 → 실패 시 보상 변경, 재시작이 필요한 설정은 웹에서 빼기
실시간·집계 데이터 처리AMI 이벤트 정규화와 SSE 백프레셔, 15분 사전 집계의 증분 커서·재집계·기준값 시점 처리
측정으로 판단하기EC2 부하 시험 계단 설계, 시험 도구 탓과 PBX 한계 구분, 통화당 CPU로 사내 서버 환산
틀려도 안전한 설계외부 발신 fail-closed, 방화벽 자동 되돌림, AI 변경안과 서버 위험 등급, 비밀값 비노출 검사
LLM 기능 평가판정 규칙 고정 뒤 프롬프트 비교, 평가 누출 발견과 폐기, 규칙 한계를 숨기지 않고 기록
계약 기반 분업과 통합단계별 계약 문서로 AI 코딩 에이전트에 구현을 나누고, 병합 때마다 VM 회귀로 확인

자세한 과정은 GT PBX 개발기에서 단계별로 볼 수 있다.