02-types-person

Dec 7, 2025

PersonNode

📁 관련 코드: lib/types/nodes/person.ts

이메일에서 추출된 사람 정보를 저장합니다.

핵심 개념: 이메일이 Primary Key

왜 이메일인가?

같은 사람의 다양한 이름 표기:
- "김철수"
- "Cheolsu Kim"
- "CS Kim"
- "철수"
- "Kim, Cheolsu"

하지만 이메일은 하나:
- cheolsu.kim@company.com

식별자 후보

장점

단점

이름

직관적

동명이인, 표기 다양성

이메일

고유함

한 사람이 여러 이메일 가질 수 있음

전화번호

고유함

이메일에서 추출 어려움

선택: 이메일을 primary key로, 여러 이메일은 alternateEmails로 처리

Aliases: 이름 통합 문제 해결

aliases: string[];  // ["김철수", "Cheolsu Kim", "CS Kim"]

작동 방식

이메일 1: From: "김철수" <cheolsu@company.com>
이메일 2: From: "Cheolsu Kim" <cheolsu@company.com>
이메일 3: From: "CS" <cheolsu@company.com>

┌─────────────────────────────────────┐
PersonNode                          
├─────────────────────────────────────┤
email: cheolsu@company.com          
displayName: "김철수"                
aliases: ["Cheolsu Kim", "CS"]      
└─────────────────────────────────────┘

displayName 선택 로직

  1. 사용자가 직접 설정한 이름 (USER_INPUT)

  2. 가장 자주 사용된 이름

  3. 가장 최근에 사용된 이름

  4. 첫 번째로 발견된 이름

Organization 필드들

organization?: string;  // "ABC회사"
department?: string;    // "개발팀"
role?: string;          // "Engineer"
title?: string;         // "Senior Software Engineer"

role vs title

필드

의미

예시

role

직무 역할

"Engineer", "Designer", "PM"

title

공식 직함

"Senior Software Engineer", "Design Lead"

왜 분리하는가?

"저는 ABC회사 개발팀의 시니어 엔지니어 김철수입니다"

role: "Engineer"     역할 기반 검색: "엔지니어들 찾아줘"
title: "Senior Software Engineer"  정확한 직함

Interaction Statistics

firstContactAt: Date;      // 첫 접촉
lastContactAt: Date;       // 마지막 접촉
interactionCount: number;  // 상호작용 횟수

왜 통계를 저장하는가?

시나리오: "자주 연락하는 사람이 누구야?"

// 상호작용 횟수로 정렬
const frequentContacts = people.sort(
  (a, b) => b.interactionCount - a.interactionCount
);

시나리오: "최근에 연락한 사람들 보여줘"

// 마지막 접촉일로 정렬
const recentContacts = people.sort(
  (a, b) => b.lastContactAt.getTime() - a.lastContactAt.getTime()
);

통계 업데이트 시점

// 새 이메일 처리 시
function processEmail(email: Email, person: PersonNode): PersonNode {
  return {
    ...person,
    lastContactAt: email.date,
    interactionCount: person.interactionCount + 1,
    updatedAt: new Date(),
  };
}

Person Merge: 동일인 발견 시

문제 상황

PersonNode A: cheolsu@company.com (회사 이메일)
PersonNode B: cheolsu.kim@gmail.com (개인 이메일)

같은 사람임을 나중에 발견

mergePersonNodes 함수

const merged = mergePersonNodes(primaryPerson, secondaryPerson);

병합 규칙:

필드

병합 방식

email

primary 유지

displayName

primary 유지

aliases

합집합 (중복 제거)

alternateEmails

합집합 + secondary.email 추가

firstContactAt

더 이른 날짜

lastContactAt

더 최근 날짜

interactionCount

합산

병합 예시

const companyPerson: PersonNode = {
  email: 'cheolsu@company.com',
  displayName: '김철수',
  aliases: ['Cheolsu Kim'],
  firstContactAt: new Date('2024-01-10'),
  interactionCount: 50,
  // ...
};

const personalPerson: PersonNode = {
  email: 'cheolsu.kim@gmail.com',
  name: 'CS Kim',
  aliases: [],
  firstContactAt: new Date('2023-06-01'),
  interactionCount: 10,
  // ...
};

const merged = mergePersonNodes(companyPerson, personalPerson);
// {
//   email: 'cheolsu@company.com',
//   displayName: '김철수',
//   aliases: ['Cheolsu Kim', 'CS Kim'],
//   alternateEmails: ['cheolsu.kim@gmail.com'],
//   firstContactAt: '2023-06-01',  ← 더 이른 날짜
//   interactionCount: 60,          ← 합산
// }

Factory Helper

const person = createPersonNode({
  id: generateUUID(),
  email: 'cheolsu@company.com',
  name: '김철수',
  organization: 'ABC회사',
  role: 'Engineer',
  firstContactAt: new Date(),
  lastContactAt: new Date(),
  sourceType: SourceType.BOOTSTRAPPED,
  confidence: 0.95,
});

그래프에서의 관계

[PersonNode: 김철수]
        
        ├── SENT ──────────→ [ThreadNode: 프로젝트 논의]
        
        ├── INVOLVED_IN ───→ [MemoryNode: Q1 목표 확정]
        
        ├── WORKS_AT ──────→ [EntityNode: ABC회사]
        
        └── ASSIGNED_TO ←── [TaskNode: 보고서 제출]

다음 문서

  • → ThreadNode: 이메일 스레드 스키마

  • → EntityNode: 회사/제품 엔티티 스키마