상세 설계 문서

계약서 저장/PDF 설계

계약서 템플릿 저장/PDF 설계 v1

1. 목표

MVP 계약 작성 기능은 일반 입력폼이 아니라 실제 계약서 양식 위에 필요한 입력 필드를 얹는 방식으로 구현한다. 사용자는 화면에서 계약서 원본 모양을 보면서 소재지, 금액, 당사자, 특약 등 필요한 칸만 입력하고, 시스템은 화면 전체 HTML을 저장하지 않고 구조화된 inputJson을 저장한다.

핵심 목표는 다음과 같다.

  • 실제 계약서에 가까운 작성 경험 제공
  • 계약 입력값을 고객, 매물, 계약 데이터와 연결
  • 템플릿 변경 후에도 기존 계약서를 당시 템플릿 버전으로 재출력
  • 저장, PDF 생성, 인쇄 흐름을 동일한 데이터 기준으로 유지
  • 추후 Next.js 구현 시 화면, API, DB 모델을 바로 나눌 수 있는 구조 제공

2. MVP 범위

포함한다.

  • 계약서 종류 선택
  • 계약서 템플릿 버전 선택
  • 실제 계약서 모양의 작성 화면
  • 템플릿 위 좌표 기반 입력 필드 렌더링
  • inputJson 저장
  • 고객 연결 및 신규 고객 자동 생성 후보 처리
  • 매물 연결
  • 계약 저장 및 계약관리 목록 노출
  • 저장된 입력값으로 PDF 재생성
  • 인쇄 전용 HTML 또는 PDF 기반 인쇄
  • 계약서 수정 이력 저장

제외한다.

  • 전자서명
  • 복잡한 템플릿 편집기
  • 한글/HWP 원본 직접 편집
  • 법정 서식 자동 업데이트
  • PDF 위 직접 드래그 편집
  • 카카오/문자 발송

3. 기본 원칙

계약서 데이터는 세 층으로 분리한다.

설명 저장 대상
템플릿 계약서 종류별 고정 양식, 필드 정의, 좌표, 출력 스타일 ContractTemplate, ContractTemplateVersion
계약 특정 계약 건의 상태, 담당자, 고객, 매물, 일정 Contract
입력값 계약 작성 화면에서 사용자가 입력한 실제 값 ContractVersion.inputJson

저장 원칙:

  • 계약서 화면 HTML 전체를 저장하지 않는다.
  • 사용자 입력값은 inputJson으로 저장한다.
  • 저장 시점의 templateVersionId를 계약 버전에 고정한다.
  • PDF 생성 시 templateVersionId + inputJson으로 다시 렌더링한다.
  • 계약 수정은 기존 데이터를 덮어쓰지 않고 새 ContractVersion을 만든다.
  • 계약 목록 검색에 자주 쓰는 값은 Contract 또는 별도 정규화 테이블에도 중복 저장한다.

4. 화면 구조

4.1 계약 작성 진입

계약하기
→ 계약서 종류 선택
→ 고객/매물 연결 선택
→ 계약서 작성 화면
→ 임시저장 또는 저장
→ PDF 미리보기
→ PDF 저장 또는 인쇄

계약서 종류 예시:

code 이름 MVP 여부
residential_sale 주거용 매매 계약서 포함
residential_lease 주거용 임대차 계약서 포함
commercial_lease 상가 임대차 계약서 구조만 준비
land_sale 토지 매매 계약서 구조만 준비

4.2 계약서 작성 화면

작성 화면은 좌우 분할을 기본으로 한다.

상단 툴바
  - 계약서 종류
  - 저장 상태
  - 임시저장
  - 저장
  - PDF 미리보기
  - 인쇄

좌측 패널
  - 고객 연결
  - 매물 연결
  - 필수 입력 누락 목록
  - 특약 관리

중앙 계약서 캔버스
  - 실제 계약서 페이지
  - 페이지 위 입력 필드
  - 페이지 넘김

우측 패널
  - 선택한 필드 상세 입력
  - 금액/날짜 변환 도움
  - 고객/매물에서 가져온 값 확인

모바일 MVP에서는 계약서 전체 캔버스 편집을 제한하고, 섹션형 입력폼과 PDF 미리보기를 제공해도 된다. 단, 저장 데이터 구조는 데스크톱과 동일하게 유지한다.

4.3 계약서 캔버스

계약서 캔버스는 페이지 단위로 구성한다.

  • A4 비율 고정
  • 배경은 HTML/CSS 템플릿 또는 이미지화된 서식 사용
  • 입력 필드는 절대 좌표로 배치
  • 좌표 기준은 페이지 내부 x, y, width, height 비율값 사용
  • 화면 확대/축소와 PDF 출력에서 같은 좌표가 유지되어야 함

좌표는 픽셀 대신 0부터 1 사이 비율을 기본으로 한다.

{
  "page": 1,
  "x": 0.184,
  "y": 0.312,
  "width": 0.420,
  "height": 0.026
}

5. 템플릿 설계

5.1 ContractTemplate

계약서 종류의 논리적 묶음이다.

필드 타입 설명
id string 템플릿 ID
code string residential_sale 같은 고정 코드
name string 사용자 표시명
category string sale, lease, commercial, land
status string active, inactive
latestVersionId string 현재 기본 버전
createdAt datetime 생성일
updatedAt datetime 수정일

5.2 ContractTemplateVersion

실제 렌더링에 필요한 버전 단위 데이터이다.

필드 타입 설명
id string 템플릿 버전 ID
templateId string 상위 템플릿
versionNo number 1, 2, 3 순번
versionLabel string 2026-05 MVP 같은 표시명
status string draft, published, archived
pageSize json A4 기준 폭/높이
pagesJson json 페이지 배경과 섹션 정의
fieldsJson json 입력 필드 목록
printStyleJson json PDF/인쇄 스타일
publishedAt datetime 배포일
createdBy string 생성자
createdAt datetime 생성일

배포된 버전은 수정하지 않는다. 오탈자나 서식 변경이 필요하면 새 버전을 만든다.

5.3 fieldsJson 예시

[
  {
    "key": "property.address",
    "label": "부동산 소재지",
    "type": "text",
    "required": true,
    "page": 1,
    "rect": { "x": 0.18, "y": 0.24, "width": 0.62, "height": 0.026 },
    "fontSize": 11,
    "align": "left",
    "source": {
      "kind": "listing",
      "path": "address.full"
    }
  },
  {
    "key": "deal.price.amount",
    "label": "매매대금",
    "type": "money",
    "required": true,
    "page": 1,
    "rect": { "x": 0.22, "y": 0.42, "width": 0.20, "height": 0.026 },
    "format": "number"
  },
  {
    "key": "parties.seller.0.name",
    "label": "매도인 성명",
    "type": "customerRef",
    "required": true,
    "page": 2,
    "rect": { "x": 0.16, "y": 0.68, "width": 0.18, "height": 0.026 },
    "partyRole": "seller"
  },
  {
    "key": "specialTerms",
    "label": "특약사항",
    "type": "textarea",
    "required": false,
    "page": 2,
    "rect": { "x": 0.10, "y": 0.22, "width": 0.80, "height": 0.24 },
    "overflow": "paginate"
  }
]

필드 타입 MVP:

type 설명
text 일반 문자열
textarea 여러 줄 문자열
money 금액
date 날짜
select 고정 선택값
checkbox 체크 여부
customerRef 고객 연결 필드
listingRef 매물 연결 필드

6. 계약 저장 데이터 구조

6.1 Contract

계약의 현재 상태와 검색용 대표 값을 저장한다.

필드 타입 설명
id string 계약 ID
officeId string 부동산 사무소 ID
templateId string 계약서 종류
currentVersionId string 현재 계약 버전
writerUserId string 작성자
managerUserId string 담당자
listingId string nullable 연결 매물
status string draft, active, completed, cancelled
dealType string sale, jeonse, monthly_rent
contractDate date nullable 계약일
startDate date nullable 임대 시작일
endDate date nullable 임대 종료일
closingDate date nullable 잔금일
primaryCustomerName string 목록 표시용 고객명
primaryCustomerPhone string 목록 검색용 연락처
propertyAddress string 목록 표시용 소재지
amountSummary string 금액 요약
createdAt datetime 생성일
updatedAt datetime 수정일

6.2 ContractVersion

계약 저장 이력이다. 수정할 때마다 한 줄씩 추가한다.

필드 타입 설명
id string 계약 버전 ID
contractId string 계약 ID
versionNo number 계약 수정 버전
templateVersionId string 저장 시점의 템플릿 버전
inputJson json 입력값 전체
snapshotJson json 고객/매물 표시값 스냅샷
validationJson json 저장 당시 필수값 검증 결과
pdfFileId string nullable 마지막 생성 PDF 파일
changeMemo string nullable 수정 사유
createdBy string 저장 사용자
createdAt datetime 저장일

snapshotJson은 고객이나 매물 정보가 나중에 바뀌어도 계약서 출력값을 유지하기 위한 읽기용 복사본이다.

6.3 inputJson 예시

{
  "contract": {
    "contractDate": "2026-05-25",
    "dealType": "sale"
  },
  "property": {
    "listingId": "lst_123",
    "address": "서울특별시 강남구 테헤란로 100",
    "buildingName": "샘플아파트",
    "unit": "101동 1203호",
    "exclusiveAreaM2": 84.91
  },
  "deal": {
    "price": {
      "amount": 950000000,
      "display": "금 구억오천만원정"
    },
    "deposit": {
      "amount": 95000000,
      "paidAt": "2026-05-25"
    },
    "middlePayment": {
      "amount": 200000000,
      "paidAt": "2026-06-25"
    },
    "balance": {
      "amount": 655000000,
      "paidAt": "2026-08-25"
    }
  },
  "parties": {
    "seller": [
      {
        "customerId": "cus_001",
        "name": "홍길동",
        "phone": "010-1111-2222",
        "address": "서울특별시 강남구 ..."
      }
    ],
    "buyer": [
      {
        "customerId": null,
        "name": "김영희",
        "phone": "010-3333-4444",
        "address": "서울특별시 송파구 ..."
      }
    ]
  },
  "brokerage": {
    "officeName": "제주수공인중개사사무소",
    "registrationNo": "12345-67890",
    "representativeName": "박중개",
    "phone": "02-123-4567",
    "address": "서울특별시 ..."
  },
  "coBrokerage": [],
  "specialTerms": "현 시설물 상태에서의 계약이며, 잔금일 전까지 관리비를 정산한다."
}

7. 고객/매물/계약 연결

7.1 고객 연결

계약서 당사자는 ContractParty로 정규화하고, 고객관리의 Customer와 연결할 수 있다.

필드 타입 설명
id string 계약 당사자 ID
contractId string 계약 ID
contractVersionId string 입력이 확정된 계약 버전
customerId string nullable 연결 고객
partyRole string seller, buyer, lessor, lessee
sortOrder number 공동명의 순서
name string 계약서 표시 성명
phone string 연락처
address string 주소

저장 흐름:

당사자 입력
→ 전화번호/이름으로 기존 고객 후보 조회
→ 사용자가 기존 고객 연결 또는 신규 고객 생성 선택
→ ContractParty 저장
→ 신규 고객 생성 선택 시 Customer 생성
→ Customer.managerUserId는 계약 담당자 기준으로 설정

MVP에서는 자동 병합하지 않는다. 같은 전화번호가 있어도 후보만 보여주고 사용자가 선택한다.

7.2 매물 연결

계약 작성 시작 전 또는 작성 중 매물을 연결할 수 있다.

매물 검색
→ 매물 선택
→ 템플릿 필드의 source 정의에 따라 주소/면적/가격 후보 자동 채움
→ 사용자가 계약서 값으로 확정
→ Contract.listingId 저장
→ inputJson.property.listingId 저장
→ snapshotJson.property에 출력용 매물 정보 복사

매물 원본이 수정되어도 이미 저장된 계약서의 출력값은 바뀌지 않는다. 단, 사용자가 계약 수정 화면에서 "현재 매물 정보 다시 가져오기"를 누르면 새 계약 버전에 반영한다.

7.3 계약 연결

계약 저장 후 다음 목록에 즉시 반영한다.

  • 계약관리: Contract 기준
  • 고객 상세: ContractParty.customerId 기준
  • 매물 상세: Contract.listingId 기준
  • 대시보드: 담당자와 일정 기준

권한은 PRD v2 원칙을 따른다.

  • 일반 사용자: officeId + writerUserId/managerUserId
  • 부동산 오너 관리자: officeId
  • 본사관리자: 전체 조회 가능하되 개인정보 접근 로그 권장

8. PDF/인쇄 플로우

8.1 PDF 생성

사용자가 PDF 미리보기 또는 저장 클릭
→ contractVersionId 조회
→ ContractVersion.templateVersionId 조회
→ ContractTemplateVersion.fieldsJson/pagesJson 조회
→ inputJson과 snapshotJson을 템플릿에 주입
→ PDF 전용 HTML 생성
→ 서버에서 PDF 렌더링
→ 파일 저장
→ ContractVersion.pdfFileId 갱신
→ 브라우저에서 미리보기 또는 다운로드

MVP 구현 옵션:

옵션 설명 MVP 판단
인쇄 전용 HTML + 브라우저 인쇄 구현이 빠름 1차 필수
Playwright/Chromium 서버 PDF 동일 HTML을 PDF로 저장 1차 권장
외부 PDF API 운영 비용/개인정보 이슈 보류

PDF는 항상 서버에서 재생성 가능해야 한다. 저장된 PDF 파일은 편의용 캐시이며 원본 데이터는 templateVersionId + inputJson이다.

8.2 인쇄

인쇄 버튼은 두 가지 경로를 제공한다.

빠른 인쇄
→ 현재 계약 버전의 print HTML 새 창 열기
→ window.print()

PDF 인쇄
→ PDF 생성 또는 기존 pdfFileId 조회
→ PDF 뷰어 열기
→ 브라우저 인쇄

MVP에서는 빠른 인쇄를 기본으로 하고, PDF 저장 기능이 안정화되면 PDF 인쇄를 기본으로 바꿀 수 있다.

8.3 출력 품질 기준

  • A4 세로 기준 여백 고정
  • 입력 필드 텍스트가 칸 밖으로 넘치지 않아야 함
  • 금액은 숫자와 한글 표시를 분리 저장 가능해야 함
  • 특약사항이 길면 다음 페이지로 넘기거나 별지 출력
  • PDF와 인쇄 전용 HTML은 같은 렌더러 컴포넌트를 사용
  • 출력용 화면에서는 입력 테두리, 포커스 스타일, 도움말을 숨김

9. Next.js 구현 구조 초안

9.1 라우트

/contracts/new
  계약서 종류 선택

/contracts/new/[templateCode]
  새 계약 작성

/contracts/[contractId]
  계약 상세

/contracts/[contractId]/edit
  현재 계약 기반 수정

/contracts/[contractId]/versions/[versionId]
  특정 계약 버전 보기

/contracts/[contractId]/versions/[versionId]/print
  인쇄 전용 HTML

/api/contracts
  계약 생성/목록

/api/contracts/[contractId]
  계약 상세/상태 변경

/api/contracts/[contractId]/versions
  계약 버전 생성/조회

/api/contracts/[contractId]/versions/[versionId]/pdf
  PDF 생성/조회

/api/contract-templates
  템플릿 목록

/api/contract-templates/[templateCode]/latest
  최신 배포 템플릿 버전 조회

9.2 컴포넌트

ContractTemplatePicker
ContractEditorShell
ContractCanvas
ContractPage
ContractFieldOverlay
ContractSidePanel
PartyConnector
ListingConnector
SpecialTermsEditor
ContractValidationPanel
ContractPrintRenderer
PdfPreviewDialog

ContractCanvasContractPrintRenderer는 같은 템플릿 데이터와 같은 값 해석 함수를 사용한다.

9.3 핵심 함수

type ContractInputJson = Record<string, unknown>;

function getValueByFieldKey(inputJson: ContractInputJson, fieldKey: string): unknown;

function setValueByFieldKey(
  inputJson: ContractInputJson,
  fieldKey: string,
  value: unknown
): ContractInputJson;

function buildContractSummary(inputJson: ContractInputJson): {
  contractDate?: string;
  startDate?: string;
  endDate?: string;
  closingDate?: string;
  primaryCustomerName?: string;
  primaryCustomerPhone?: string;
  propertyAddress?: string;
  amountSummary?: string;
};

function validateContractInput(
  fieldsJson: ContractTemplateField[],
  inputJson: ContractInputJson
): ContractValidationResult;

10. 저장 API 흐름

10.1 신규 저장

POST /api/contracts
body:
  templateId
  templateVersionId
  listingId
  inputJson
  linkedCustomers
  changeMemo

server:
  권한 확인
  템플릿 버전이 published인지 확인
  inputJson 검증
  Contract 생성
  ContractVersion versionNo=1 생성
  ContractParty 생성
  신규 고객 생성 요청 처리
  Contract 요약 필드 갱신
  응답으로 contractId, versionId 반환

10.2 수정 저장

POST /api/contracts/[contractId]/versions
body:
  baseVersionId
  inputJson
  linkedCustomers
  changeMemo

server:
  권한 확인
  기존 Contract 조회
  기본값은 기존 templateVersionId 유지
  inputJson 검증
  ContractVersion versionNo + 1 생성
  ContractParty를 새 버전 기준으로 재생성 또는 갱신
  Contract.currentVersionId 갱신
  Contract 요약 필드 갱신

기존 계약을 수정할 때 템플릿 버전을 자동으로 최신화하지 않는다. 별도 "새 템플릿으로 이전" 기능은 MVP 이후로 둔다.

11. 템플릿 버전 관리

버전 상태:

상태 설명
draft 내부 작성 중, 계약 작성에 사용 불가
published 새 계약 작성에 사용 가능
archived 새 계약 작성에는 숨김, 기존 계약 재출력 가능

규칙:

  • 새 계약은 published 중 최신 버전을 기본 사용
  • 기존 계약 재출력은 ContractVersion.templateVersionId를 사용
  • 배포된 템플릿 버전은 수정 금지
  • 법정 서식 또는 회사 문구 변경 시 새 버전 생성
  • 기존 계약을 새 템플릿으로 옮기는 기능은 별도 마이그레이션으로 처리

12. MVP 데이터 모델 추가안

PRD v2의 데이터 모델에 다음 필드를 보강한다.

ContractTemplate
  id
  code
  name
  category
  status
  latestVersionId
  createdAt
  updatedAt

ContractTemplateVersion
  id
  templateId
  versionNo
  versionLabel
  status
  pageSize
  pagesJson
  fieldsJson
  printStyleJson
  publishedAt
  createdBy
  createdAt

Contract
  id
  officeId
  templateId
  currentVersionId
  writerUserId
  managerUserId
  listingId
  status
  dealType
  contractDate
  startDate
  endDate
  closingDate
  primaryCustomerName
  primaryCustomerPhone
  propertyAddress
  amountSummary
  createdAt
  updatedAt

ContractVersion
  id
  contractId
  versionNo
  templateVersionId
  inputJson
  snapshotJson
  validationJson
  pdfFileId
  changeMemo
  createdBy
  createdAt

ContractParty
  id
  contractId
  contractVersionId
  customerId
  partyRole
  sortOrder
  name
  phone
  address

ContractFile
  id
  contractId
  contractVersionId
  fileType
  storageKey
  fileName
  mimeType
  size
  createdBy
  createdAt

인덱스 후보:

  • Contract.officeId
  • Contract.officeId + managerUserId
  • Contract.officeId + contractDate
  • Contract.officeId + closingDate
  • Contract.officeId + primaryCustomerPhone
  • Contract.listingId
  • ContractVersion.contractId + versionNo
  • ContractParty.customerId
  • ContractParty.contractId + contractVersionId

13. 검증 규칙

저장 시 최소 검증:

  • 필수 필드 입력 여부
  • 날짜 형식
  • 금액 숫자 형식
  • 계약 당사자 이름 입력 여부
  • 연결 고객이 현재 officeId 범위에 속하는지 확인
  • 연결 매물이 접근 가능한 매물인지 확인
  • 템플릿 버전 상태가 published 또는 기존 계약 재출력 가능한 상태인지 확인

검증 실패는 계약서 캔버스와 좌측 누락 목록에 함께 표시한다.

14. 구현 우선순위

  1. 템플릿/버전 시드 데이터 작성
  2. 계약서 종류 선택 화면
  3. 좌표 기반 계약서 캔버스
  4. inputJson 편집 및 필수값 검증
  5. 고객/매물 연결
  6. 계약 및 계약 버전 저장 API
  7. 계약관리 목록 요약 필드 노출
  8. 인쇄 전용 HTML
  9. PDF 생성 및 파일 저장
  10. 계약 버전 조회와 재출력

MVP의 성공 기준은 "한 계약서를 작성하고 저장한 뒤, 계약관리에서 다시 열어 같은 모양으로 출력할 수 있는 것"이다.