언어·읽기

Nuance Graph

다국어 통역사·번역사가 표현 사이의 관계와 상황별 적합도를 가중치로 기록하는 개인 뉘앙스 그래프 웹앱.

분야언어·읽기플랫폼Web기술Next.js 16 · React 19 · TypeScript · Firebase Auth · Firestore · d3-force · Canvas 2D · Claude API · Zod · SheetJS · Playwright
90%45%일상 고백 사랑해 好き 愛してる I like you 좋아해 люблю
저장소에 공개된 화면 이미지가 없어, 동작 원리를 그림으로 대신합니다.

왜 만들었나

단어장은 '진통제 = painkiller / analgesic'처럼 1:1 대응만 남겨, 환자 앞과 차트에서 무엇을 골라야 하는지 같은 상황별 뉘앙스가 사라집니다. 표현을 노드로, 관계와 상황별 적합도를 가중치 엣지로 남겨 바로 찾아보게 했습니다.

주요 기능

  • 관계 6종(대응 번역·유사·상위·하위·혼동 주의·함께 쓰임)과 0~100% 적합도, 상황 태그
  • 상황을 고르면 언어별 1순위 대응 표현이 바뀌는 패널과 실전 사용 +1 카운트
  • 중심 표현 + 1촌 이웃만 그리고 가지를 펼치고 접는 그래프 탐색(최대 40노드)
  • 엑셀·CSV·구글 시트 가져오기, Claude API로 비정형 메모에서 표현·관계 추출 후 검수 대기열
  • 의료·법률 통역 스타터 팩(한·영·일·러), import_id 단위 가져오기 되돌리기

숫자로 보면

스타터 팩 2종: 의료 53개·법률 45개 개념, 4개 언어
관계 유형 6종, 그래프 한 화면 최대 40노드
AI 추출 25행 단위 청크, 동시 2개 호출

어떻게 동작하나

  1. 사용자별 데이터 로드

    users/{uid} 아래 nodes·edges·examples·imports·inbox 컬렉션을 한꺼번에 읽어 오며, 보안 규칙이 본인 컬렉션만 허용합니다.

    lib/useGraph.ts
  2. 보이는 서브그래프 계산

    펼친 노드와 그 1촌 이웃만 남기고, 40개를 넘으면 경로에 없는 오래된 가지부터 접습니다.

    components/GraphExplorer.tsx
  3. 힘 기반 배치

    d3-force에서 가중치가 클수록 링크를 짧고 강하게 잡고, Canvas 2D에 선 굵기=가중치, 관계별 점선으로 그립니다.

    components/GraphCanvas.tsx
  4. 상황별 1순위

    선택한 표현의 대응 번역 엣지를 상황으로 거른 뒤 언어별로 가중치가 가장 높은 상대 표현을 고릅니다.

    lib/graph-utils.ts
  5. AI 뉘앙스 추출

    시트를 25행씩 나눠 Claude에 JSON 스키마 출력으로 요청하고, Zod 검증과 언어·관계 필터를 거쳐 돌려줍니다.

    app/api/extract-nuance/route.ts
  6. 검수 후 반영

    추출 결과는 inbox에 저장되고, 선택해 반영할 때 import_id로 묶어 writeBatch로 기록해 한 번에 되돌릴 수 있습니다.

    lib/useGraph.ts

핵심 코드

lib/graph-utils.tsGitHub에서 보기 ↗
export function bestByLang(
  // …
): Array<{ lang: string; other: GraphNode; weight: number; situation: string | null }> {
  const m = new Map<string, { lang: string; other: GraphNode; weight: number; situation: string | null }>();
  for (const e of edges) {
    if (e.source_id !== nodeId && e.target_id !== nodeId) continue;
    if (relationFrom(e, nodeId) !== "equivalent") continue;
    if (situation && e.situation !== situation) continue;
    const other = nodes.get(otherEnd(e, nodeId));
    if (!other) continue;
    const cur = m.get(other.lang);
    if (!cur || e.weight > cur.weight) m.set(other.lang, { lang: other.lang, other, weight: e.weight, situation: e.situation });
  }
  return Array.from(m.values());
}

선택한 표현에서 상황 태그로 거른 대응 번역 중 언어별 최고 가중치 표현을 뽑는 랭킹 로직입니다.

어려웠던 점

표현이 수백 개로 늘면 전체 그래프가 읽을 수 없게 되어, 중심 표현과 1촌만 보여 주고 펼친 가지 수를 40개로 제한하면서 경로(브레드크럼)는 보호하는 탐색 방식을 만들었습니다. AI 추출은 바로 반영하지 않고 확신도와 함께 검수 대기열에 두어 잘못된 관계가 섞이지 않게 했습니다.