계약서 템플릿 저장/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
ContractCanvas와 ContractPrintRenderer는 같은 템플릿 데이터와 같은 값 해석 함수를 사용한다.
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.officeIdContract.officeId + managerUserIdContract.officeId + contractDateContract.officeId + closingDateContract.officeId + primaryCustomerPhoneContract.listingIdContractVersion.contractId + versionNoContractParty.customerIdContractParty.contractId + contractVersionId
13. 검증 규칙
저장 시 최소 검증:
- 필수 필드 입력 여부
- 날짜 형식
- 금액 숫자 형식
- 계약 당사자 이름 입력 여부
- 연결 고객이 현재
officeId범위에 속하는지 확인 - 연결 매물이 접근 가능한 매물인지 확인
- 템플릿 버전 상태가
published또는 기존 계약 재출력 가능한 상태인지 확인
검증 실패는 계약서 캔버스와 좌측 누락 목록에 함께 표시한다.
14. 구현 우선순위
- 템플릿/버전 시드 데이터 작성
- 계약서 종류 선택 화면
- 좌표 기반 계약서 캔버스
inputJson편집 및 필수값 검증- 고객/매물 연결
- 계약 및 계약 버전 저장 API
- 계약관리 목록 요약 필드 노출
- 인쇄 전용 HTML
- PDF 생성 및 파일 저장
- 계약 버전 조회와 재출력
MVP의 성공 기준은 "한 계약서를 작성하고 저장한 뒤, 계약관리에서 다시 열어 같은 모양으로 출력할 수 있는 것"이다.