7. 팀 분업

⚠ 2026-08-28 방향 전환 — 이 페이지보다 결정 기록이 우선한다

도메인을 다산콜센터 하나로 줄이고 외국인 고객 지원을 차별점으로 세웠다 (_project/decisions/201, 비공개). 아래 본문의 금융보험·쇼핑·질병관리본부 서술은 그 시점의 사실이라 지우지 않았다 — 절대 원칙 8(“실패를 지운 기록은 기록이 아니다”)과 같은 이유로, 계획이 바뀐 사실 자체가 기록이다.

이 페이지에서 바뀐 것: 4인 체제 + rev.5 신규 기능 배정. 기획서 7.1절이 rev.5 에서 갱신됐다 — 신규 기능 셋(A-5 통번역 · C-6 콜 가드 · D-5 감정분석)이 전부 ai/(류준)로 갔다. 셋 다 모델을 쓰는데 server/.importlinterserver/ 안에서 모델 라이브러리 import 를 막기 때문이다. ⚠ 7.2절이 경고한 부하 집중이 다시 생겼다 — A-5 스파이크 결과를 보고 재분배를 검토한다.

팀명: SOLIDBOB (4인 — 2026-08-26 개편)

원래 3인 체제(정성윤·류준·장민석)에서 플러터(Flutter) 앱 개발을 중단하고, 장민석이 앱·프론트엔드에서 류준과 함께 백엔드·AI로 옮겼다. 조서희가 신규 합류해 프론트엔드(웹)를 전담한다. 정성윤은 AWS·인프라를 그대로 유지한다. 근거·되돌리는 법: _project/decisions/005-팀-개편-4인-체제.md.

7.1 담당

담당 인원 범위
AWS·인프라 정성윤 Node.js WebSocket 게이트웨이, Google STT 연동, 화자 분리, PostgreSQL 스키마, Docker·배포, 레이턴시 측정 인프라, CI 운영
백엔드·AI — AI (ai/) 류준 데이터셋 기반 모델 학습, 지식베이스 청킹, BM25·임베딩·하이브리드 검색(RRF)·리랭킹, LangChain·LangGraph 오케스트레이션, 평가 하네스. 배포 ai.solidbob.cloud, 브랜치 ai
백엔드·AI — 서버 (server/) 장민석 C-5 마스킹(2026-08-27 이관, _project/decisions/019), 파이프라인 구축과 배선, 클린 아키텍처 유지, 계약(포트·DTO) 정의, HTTP 경계·설정·저장(DB), F-2 게이트. 배포 server.solidbob.cloud, 브랜치 server
프론트엔드 조서희 React 상담원 대시보드(apps/dashboard: 자막·경고·하단 책갈피 카드·종결 모달), WebSocket 클라이언트, 데모 시나리오, 결과 시각화. 앱(Flutter)은 만들지 않는다 — 웹만. 고객 화면은 014로 철회(_project/decisions/014).
공동 4인 골든셋 작성·라벨링, 발표 자료

ai/server/ 의 경계 — 어느 쪽인지 헷갈리면 이렇게 가른다.

"품질을 만들거나 재는 코드인가?"              → 예: ai/     (류준)
"요청 하나를 처리하는 데 반드시 실행되는가?"   → 예: server/ (장민석)

C-5 마스킹·F-2 게이트가 server/에 있는 이유가 이것이다 — 둘 다 규칙 기반 판정이고 요청 경로에서 매번 실행된다. 모델이 관여하지 않는다(절대 원칙 9 “판정은 규칙이, 설명만 LLM이”). 각 디렉터리 루트의 CLAUDE.md가 이 경계를 영역 규칙으로 못박아 두고 있다. 브랜치 이름도 디렉터리 이름과 같다ai(류준) · server(장민석), _project/decisions/015.

7.2 부하 재분배 — 개편 이후 상태

이전에는 류준이 백엔드와 AI를 혼자 맡아 구조적으로 과부하였다. 4·5주차 핵심 실험과 F-2가 모두 한 사람에게 걸려 있었다. 2026-08-26 팀 개편으로 장민석이 백엔드·AI에 합류하면서 이 문제는 구조적으로 해소됐다.

기존 완화 조치는 계속 유지한다 — 과부하 회피 목적이 아니라 각자의 전문성에 맞는 정상 분업으로 재해석한다.

조치 내용
C-5는 정성윤 담당 전사 직후 처리 지점이라 STT 파이프라인 담당이 자연스럽다. 정규식 중심이라 AI 전문성 의존도가 낮다
CI 운영은 정성윤 담당 백엔드·AI(류준·장민석)는 하네스 설계에 집중하고, 운영·유지는 정성윤이 맡는다
골든셋은 4인 공동 라벨링은 도메인 이해만 있으면 되므로 분산 가능하다. F-2 케이스는 규정 작성자와 다른 사람이 만든다(보완지시서 3번)
조서희가 시각화 담당 matplotlib 성능 곡선·레이턴시 분포를 프론트 담당이 가져간다
백엔드·AI는 디렉터리로 나눈다 처음에는 “둘이 함께”로 정했으나(2026-08-26), 같은 날 fastapi/ai/·server/로 갈리면서 나눌 경계가 실제로 생겨 그 경계를 담당 경계로 삼았다 — 류준 ai/(품질을 만들고 재는 쪽), 장민석 server/(요청이 흐르는 길). 의존 방향은 ai → server 한쪽뿐이고 server/.importlinter가 역방향을 막는다. 경계에 걸친 작업(계약 변경 등)은 무엇이 깨지는지 먼저 확인한다. 합의를 절차로 요구하지는 않는다 — 같은 공간에서 일하므로 필요하면 그 자리에서 맞춘다(_project/decisions/023). _project/decisions/012
F-2는 여전히 조건부 코어 작업이 지연되면 F-2를 설계 문서로 전환한다 (8절 체크포인트). 다만 담당이 2인으로 늘어 지연 위험은 낮아졌다

7.3 인터페이스 계약 우선

1주차에 모듈 간 스키마를 확정한다. 이것이 병렬 작업의 전제 조건이다.

v2 — 2026-08-25 조건부 컨펌 반영. v1(위 초안)은 정성윤이 DB 스키마·골든셋과 대조해 불일치 4건을 확인, 아래처럼 수정해 최종 확정했다. 결정 기록: _project/decisions/003-인터페이스-스키마-v2.md (v1 최초 컨펌 기록은 _project/decisions/002-인터페이스-스키마-확정.md에 남아 있다.)

// 전사 이벤트 (마스킹 적용 후)
{
  "call_id": "c_001",
  "segment_id": "seg_0031",
  "speaker": "customer",
  "text": "카드번호는 **** 입니다",
  "masked": [{"type": "P2", "span": [7, 11]}],
  "is_final": true,
  "utterance_end_ms": 3100
}

// 추천 카드
{
  "call_id": "c_001",
  "trigger_at_ms": 3150,
  "cards": [
    {
      "title": "부정사용 보상 기준 안내",
      "summary": "분실 신고 접수 이전의 부정사용액은 고의·중과실이 없는 한 보상됩니다.",
      "source": {"doc_id": "FIN-TERM-2.2", "title": "이용약관 2장 2항"},
      "similarity_score": 0.87
    }
  ],
  "internal_latency_ms": 780,
  "e2e_latency_ms": 1240
}

// 종결 판정 (F-2)  도메인·closure_type별로 evidence의 키가 다르다 (아래  참고)
{
  "call_id": "c_001",
  "closure_type": "상품해지",
  "reason": "고지 완료",
  "evidence": {"중도해지수수료_안내": true, "약정혜택소멸_안내": false, "고객확인_기록": false},
  "verdict": "blocked",
  "missing": ["약정혜택소멸_안내", "고객확인_기록"],
  "source": {"doc_id": "FIN-POLICY-CLOSE-1", "title": "내부 처리 규정 — 상품 해지 처리"}
}

2026-08-26 도메인 4종 정리 반영: 위 예시는 v2 확정 당시(2026-08-25) 통신 도메인 값(TERM-3.2, closure_type: "해지")을 썼으나, 그 문서 ID·처리유형은 지금 knowledge-base/에 존재하지 않는다. 구조(필드명·타입)는 v2 그대로이고 예시 만 실제 스키마에 맞춰 금융보험 도메인 예시로 교체했다 — db/schema.sql도 같은 날 closure_type·evidence 컬럼을 이 값들로 갱신했다(_project/decisions/006). 메시지에 domain 필드를 명시적으로 포함할지(현재는 call_id로 서버가 call.domain을 조회하는 방식을 가정)는 3.2절 도메인 라우팅과 함께 아직 미결이다.

v1 → v2 수정 사항 (DB db/schema.sql·골든셋과 대조해 발견, 정성윤 확인)

필드 v1 v2 이유
verdict (예시에 blocked만 노출) "approved" \| "blocked"로 명시 DB closure.verdict ENUM('approved','blocked'), 골든셋 expected_verdict, golden_set.py 전부 이미 approved 기준으로 통일돼 있었다. allowed가 아니다
source {"doc": "요금제약관", "clause": "3.2"} (사람이 읽는 이름) {"doc_id": "TERM-3.2", "title": "..."} (문서 ID + 표시용 제목) recommendation_card.source_doc_iddocument(document_id)를 참조하고, 골든셋 정답도 expected_doc_ids: ["TERM-3.2"] 형식이라 ID 기준이어야 Recall@5 채점 때 변환 없이 바로 비교된다
evidence 전체 컬럼을 전부 채우는 것처럼 보임 closure_type별 부분집합 — 원칙은 v2 그대로 유지. 2026-08-26 도메인 4종 정리 후 실제 값은 상품해지(중도해지수수료_안내·약정혜택소멸_안내·고객확인_기록) / 보상(사고경위_확인·귀책여부_확인, 이상 금융보험) / 반품(환불금액_안내·환불기간_안내·상품상태_확인) / 교환(교환가능_확인·재고_확인, 이상 쇼핑). 나머지는 응답에 아예 포함하지 않는다(NULL) closure 테이블 10개 boolean 컬럼이 유형별 전용이다. missingevidencefalse인 키만 추출한다 — 해당 없는(NULL) 항목을 넣으면 다른 유형 항목이 오탐으로 잡힌다
전사 이벤트 segment_id 없음 segment_id 추가 20초 발화에 interim이 199건(V4 실측) 오는데, call_id만으로는 프론트가 “어느 발화의 갱신인지” 구분할 수 없다. transcript_segment.segment_id와 1:1
score score similarity_score recommendation_card.similarity_score와 이름을 맞춰 직렬화 매핑 코드를 없앤다
rank 없음 배열 순서 = rank (서버가 채움, 별도 필드 없음) recommendation_card.rank는 있지만 배열 순서로 이미 표현되므로 필드를 중복시키지 않는다
span 정의 없음 문자(코드포인트) 오프셋으로 확정 masking_event.span_start/end는 INT. 한글은 UTF-8에서 3바이트라 byte 기준이면 프론트·백이 어긋난다
latency 정의 정의 없음 internal_latency_ms = 트리거 발동 → 카드 응답 완료 / e2e_latency_ms = utterance_end_ms → 화면 표시 오늘 V4 실측(최종 결과 = 발화 종료 +346ms)이 e2e_latency_ms에 이미 포함되는 값이라는 걸 명확히 해둔다

그대로 유지: masked[].type(P1~P7, masking_event.pattern과 일치) · is_final/utterance_end_ms(transcript_segment와 1:1) · closure_type이 DB ENUM과 정확히 일치해야 한다는 원칙(값 자체는 2026-08-26 "상품해지"/"보상"/"반품"/"교환"으로 갱신, 16절 ERD).

자막 렌더링 (정성윤 제안, 장민석이 프론트를 맡던 시점의 합의 — 현재 프론트는 조서희, 백엔드·AI 내부 분담은 7.2절 참고): interim은 누적하지 않고 최신 것만 segment_id 기준으로 교체, 갱신은 requestAnimationFrame 또는 100ms 디바운스로 묶는다. DB에는 is_final: true만 저장 — interim까지 저장하면 통화 1건에 수천 행이 쌓인다.

✅ 반영 완료 (2026-08-26): 위 두 항목(evidence·closure_type)에서 예고했던 v3 변경 — 도메인 접두어 문서 ID(FIN-TERM-3.2), 도메인별 closure_type 값 집합 (금융보험: 상품해지/보상, 쇼핑: 반품/교환) — 을 위 JSON 예시와 db/schema.sql에 함께 반영했다(_project/decisions/006). 메시지에 별도 domain 필드를 넣을지는 3.2절과 함께 아직 미결 — call_id로 서버가 call.domain을 조회하는 방식을 임시로 가정한다.


← 개발목차로 돌아가기