7. 팀 분업

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

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

이 페이지에서 바뀐 것: 4인 체제 + rev.5 신규 기능 배정. 기획서 7.1절이 rev.5 에서 갱신됐다 — 신규 기능 셋(A-5 통번역 · C-6 콜 가드 · D-5 감정분석)이 전부 ai/(류준)로 갔다. 셋 다 모델을 쓰는데 server/.importlinter 가 server/ 안에서 모델 라이브러리 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 — 배포 도메인 없음(라이브러리라 server 와 한 컨테이너, _project/decisions/105)
백엔드·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인 골든셋 작성·라벨링, 발표 자료

⚠ 위 표는 «주 담당» 이지 편집 잠금이 아니다 (2026-09-10, _project/decisions/302). ai/·server/·infra/ 는 정성윤·류준·장민석 누구나 고친다. 전담이 남는 곳은 조서희의 프론트엔드(apps/) 하나뿐이다.

푼 이유는 기능 하나가 한 디렉터리 안에서 끝나지 않기 때문이다 — 배선은 server/, 구현체는 ai/, 배포는 infra/ 라서 각자 자기 몫을 끝내도 호출부만 있고 구현체가 없는 구멍이 남았다. 잠금 대신 남는 것은 기계 검사(.importlinter 계층 계약 · 런북 선행 읽기 훅)와 손대기 전 grep 으로 영향 범위 확인(_project/decisions/023)이다.

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

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

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

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

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

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

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

7.3 인터페이스 계약 우선

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

⚠ 이 절은 2026-08-25 v2 기록이다 — 지금 코드와 다르다 (2026-09-15 표시). 실제로 붙일 계약의 정본은 저장소 _project/plan.md 7.3절(사이트 밖)과 서버 /openapi.json 이다. 크게 달라진 것: 응답 값은 전부 문자열("true"·"3150", null 은 그대로) · 카드는 fired 로 발동 여부를 먼저 알린다 · F-2 는 closure_type·approved/blocked 가 아니라 procedure·verdict: complete/incomplete(decisions/305) · 블랙리스트 요청은 상담원 토큰 헤더로 요청자를 정한다(decisions/307). 아래 v2 본문은 당시 기록이라 고치지 않는다(절대 원칙 8).

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_id가 document(document_id)를 참조하고, 골든셋 정답도 expected_doc_ids: ["TERM-3.2"] 형식이라 ID 기준이어야 Recall@5 채점 때 변환 없이 바로 비교된다
evidence 전체 컬럼을 전부 채우는 것처럼 보임 closure_type별 부분집합 — 원칙은 v2 그대로 유지. 2026-08-26 도메인 4종 정리 후 실제 값은 상품해지(중도해지수수료_안내·약정혜택소멸_안내·고객확인_기록) / 보상(사고경위_확인·귀책여부_확인, 이상 금융보험) / 반품(환불금액_안내·환불기간_안내·상품상태_확인) / 교환(교환가능_확인·재고_확인, 이상 쇼핑). 나머지는 응답에 아예 포함하지 않는다(NULL) closure 테이블 10개 boolean 컬럼이 유형별 전용이다. missing은 evidence 중 false인 키만 추출한다 — 해당 없는(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을 조회하는 방식을 임시로 가정한다.


← 개발목차로 돌아가기