왜 만들었나
단어장은 '진통제 = 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개 호출
어떻게 동작하나
사용자별 데이터 로드
users/{uid} 아래 nodes·edges·examples·imports·inbox 컬렉션을 한꺼번에 읽어 오며, 보안 규칙이 본인 컬렉션만 허용합니다.
lib/useGraph.ts보이는 서브그래프 계산
펼친 노드와 그 1촌 이웃만 남기고, 40개를 넘으면 경로에 없는 오래된 가지부터 접습니다.
components/GraphExplorer.tsx힘 기반 배치
d3-force에서 가중치가 클수록 링크를 짧고 강하게 잡고, Canvas 2D에 선 굵기=가중치, 관계별 점선으로 그립니다.
components/GraphCanvas.tsx상황별 1순위
선택한 표현의 대응 번역 엣지를 상황으로 거른 뒤 언어별로 가중치가 가장 높은 상대 표현을 고릅니다.
lib/graph-utils.tsAI 뉘앙스 추출
시트를 25행씩 나눠 Claude에 JSON 스키마 출력으로 요청하고, Zod 검증과 언어·관계 필터를 거쳐 돌려줍니다.
app/api/extract-nuance/route.ts검수 후 반영
추출 결과는 inbox에 저장되고, 선택해 반영할 때 import_id로 묶어 writeBatch로 기록해 한 번에 되돌릴 수 있습니다.
lib/useGraph.ts
핵심 코드
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 추출은 바로 반영하지 않고 확신도와 함께 검수 대기열에 두어 잘못된 관계가 섞이지 않게 했습니다.