증명서 발급 (전자증명서)
주민등록등본, 주민등록초본, 가족관계증명서를 간편인증으로 발급하는 기능입니다. HYPHEN과 CODEF 두 벤더를 선택적으로 사용할 수 있는 멀티 벤더 구조(v2)로 운영됩니다.
전체 흐름
사용자 입력 → 프론트 암호화 → 서버 복호화 → 벤더 규격 재암호화 → 벤더 API- 사용자가 주민등록번호, 주소 등 민감 정보를 입력
- 프론트에서 얼마집 전용 AES-256 키로 암호화하여 서버에 전송
- 서버에서 복호화 후, 선택된 벤더(HYPHEN 또는 CODEF) API 규격에 맞게 재암호화
- 벤더를 통해 간편인증 요청 → 사용자 승인 → 증명서 PDF 발급
2단계 인증 흐름
모든 벤더는 간편인증이 필요하므로 2회의 API 호출로 증명서가 발급됩니다.
[1차 요청] 간편인증 요청
프론트 → POST /v2/user/certificate/resident-certificate
{ vendor, birthDate, identityEnc, loginOrgCd, sido, sigg }
서버 ← { stepData: "..." } ← HYPHEN인 경우
{ twoWayInfo: { ... } } ← CODEF인 경우
[사용자가 휴대폰에서 간편인증 승인]
[2차 요청] 증명서 발급
프론트 → POST /v2/user/certificate/resident-certificate
{ ..., stepData: "..." } ← HYPHEN인 경우
{ ..., twoWayInfo: { ... } } ← CODEF인 경우
서버 ← { preSignedUrl: "https://s3..." }벤더에 따라 인증 데이터 형식이 다릅니다:
| 벤더 | 1차 응답 | 2차 요청에 포함 |
|---|---|---|
| HYPHEN | stepData (string) | stepData |
| CODEF | twoWayInfo (object) | twoWayInfo |
암호화
얼마집 전용 키 (CERTIFICATE_AES_KEY)
프론트에서 민감 데이터를 암호화할 때 사용하는 AES-256-CBC 키입니다.
- 알고리즘: AES-256-CBC
- IV: 랜덤 16바이트
- 출력 형식:
Base64(IV + 암호문) - 환경변수:
NEXT_PUBLIC_CERTIFICATE_AES_KEY
import {encryptCertificateField} from '@howmuchhome-web/utils-ts';
// 주민등록등본/초본: 주민번호 뒷자리만 암호화
const identityEnc = encryptCertificateField(birthPassword); // 7자리
// 가족관계증명서: 주민번호 뒷자리만 암호화
const rrn2Enc = encryptCertificateField(birthPassword); // 7자리v1과 v2 암호화 차이
| v1 | v2 | |
|---|---|---|
| 알고리즘 | AES-128-CBC | AES-256-CBC |
| 키 | ENCRYPTION_KEY (16바이트) | NEXT_PUBLIC_CERTIFICATE_AES_KEY (32바이트) |
| IV | userId 기반 고정 IV | 랜덤 IV (매 호출 다름) |
| 등본/초본 | encryptField(birth + birthPassword) 전체 암호화 | birthDate 평문 + encryptCertificateField(birthPassword) |
| 가족관계 | rrn1Enc, rrn2Enc 모두 암호화 | rrn1 평문 + rrn2Enc 뒷자리만 암호화 |
v2에서는 주민번호 앞자리(생년월일)를 평문으로, 뒷자리만 암호화합니다. 서버에서 벤더별 규격에 맞게 재암호화하기 때문입니다.
API 엔드포인트
주민등록등본
POST /v2/user/certificate/resident-certificate주민등록초본
POST /v2/user/certificate/resident-abstract가족관계증명서
POST /v2/user/certificate/family-relations요청 — 주민등록등본/초본
type CreateResidentCertificateV2Request = {
vendor: CertificateVendor; // 'HYPHEN' | 'CODEF'
loginOrgCd: string; // 간편인증 서비스 코드 (예: 'kakao', 'toss')
birthDate: string; // 주민번호 앞 6자리 (평문)
identityEnc: string; // 주민번호 뒷 7자리 (AES-256 암호화)
sido: string; // 시/도 (예: '서울특별시')
sigg: string; // 시/군/구 (예: '강남구')
stepData?: string | null; // HYPHEN 2단계 인증 데이터 (2차 요청 시)
twoWayInfo?: TwoWayInfo | null; // CODEF 2단계 인증 데이터 (2차 요청 시)
assignmentId?: string | null; // 과제 ID (과제에서 진입 시)
};요청 — 가족관계증명서
type CreateFamilyRelationsV2Request = {
vendor: CertificateVendor;
loginOrgCd: string;
rrn1: string; // 주민번호 앞 6자리 (평문)
rrn2Enc: string; // 주민번호 뒷 7자리 (AES-256 암호화)
fthrNm?: string | null; // 부 성명
mthrNm?: string | null; // 모 성명
sposNm?: string | null; // 배우자 성명
childNm?: string | null; // 자녀 성명 (가족 관계 최소 1개 필수)
stepData?: string | null;
twoWayInfo?: TwoWayInfo | null;
assignmentId?: string | null;
};응답 (공통)
// 1차 응답: 간편인증 대기
{
residentCertificate: { // 또는 residentAbstract, familyRelationsCertificate
preSignedUrl: null,
stepData: "...", // HYPHEN
twoWayInfo: null,
}
}
// 또는
{
residentCertificate: {
preSignedUrl: null,
stepData: null,
twoWayInfo: { // CODEF
jobIndex: 0,
threadIndex: 0,
jti: "...",
twoWayTimestamp: 1234567890
},
}
}
// 2차 응답: 발급 완료
{
residentCertificate: {
preSignedUrl: "https://s3...signed-url",
stepData: null,
twoWayInfo: null,
}
}벤더 (CertificateVendor)
enum CertificateVendor {
HYPHEN = 'HYPHEN',
CODEF = 'CODEF',
}벤더 값은 과제(assignment) 데이터의 certificateVendor 필드에서 내려옵니다.
프론트에서는 이 값을 queryString으로 전달받아 사용합니다.
- 과제에서 진입:
AssignmentPage→ queryStringvendor→ apply page - 전자증명서 메뉴에서 직접 진입: 기본값
CertificateVendor.HYPHEN
간편인증 서비스 (loginOrgCd)
| 코드 | 서비스 | HYPHEN | CODEF |
|---|---|---|---|
kakao | 카카오톡 | O | O |
naver | 네이버 | O | O |
toss | 토스 | O | O |
kb | KB인증서 | O | O |
shinhan | 신한인증서 | O | O |
CODEF에서 지원하지 않는
loginOrgCd(kakaobank, payco 등)로 요청 시 에러가 반환됩니다.
프론트엔드 구현
파일 구조
apps/app/src/app/(certified-user-only)/more/electronic-certificate/
├── page.tsx # 증명서 목록 (발급/제출 현황)
├── _components/
│ └── CertificateConsentBottomSheet.tsx
├── apply/
│ ├── page.tsx # 발급 퍼널 진입점
│ ├── funnel.ts # 퍼널 스텝 정의
│ ├── type.ts # 폼 타입 (BaseCertificateForm, ResidentRegistrationForm, FamilyRelationsForm)
│ ├── _component/
│ │ └── RegionSelect.tsx # 시/도, 시/군/구 선택
│ └── section/
│ ├── ElectronicCertificateApplyPageIntro.tsx # Step 1: 정보 입력
│ ├── ResidentRegistrationForm.tsx # 등본/초본 입력 폼
│ ├── FamilyRelationsForm.tsx # 가족관계 입력 폼
│ ├── ElectronicCertificateApplyPageSimpleVerification.tsx # Step 2: 간편인증 선택 (1차 API)
│ └── ElectronicCertificateApplyPageConfirm.tsx # Step 3: 인증 완료 (2차 API)퍼널 흐름
intro → simpleVerification → confirm| 스텝 | 역할 | API 호출 |
|---|---|---|
intro | 주민번호/주소/가족관계 입력, 암호화 | 없음 |
simpleVerification | 간편인증 서비스 선택 | 1차 요청 (간편인증 요청) |
confirm | 인증 안내 화면, 완료 버튼 | 2차 요청 (증명서 발급) |
폼 타입
// 공통 필드
type BaseCertificateForm = {
vendor: CertificateVendor;
loginOrgCd: EasyLoginCode;
preSignedUrl?: string | null;
stepData?: string | null; // HYPHEN 인증 데이터
twoWayInfo?: TwoWayInfo | null; // CODEF 인증 데이터
};
// 주민등록등본/초본
type ResidentRegistrationForm = BaseCertificateForm & {
birthDate: string; // 주민번호 앞 6자리 (평문)
identityEnc: string; // 주민번호 뒷 7자리 (암호화)
sido: string;
sigg: string;
};
// 가족관계증명서
type FamilyRelationsForm = BaseCertificateForm & {
rrn1: string; // 주민번호 앞 6자리 (평문)
rrn2Enc: string; // 주민번호 뒷 7자리 (암호화)
fthrNm: string;
mthrNm: string;
sposNm: string;
childNm: string;
};API 호출 예시
// 1차 요청 (simpleVerification 스텝)
const res = await UserAPI.createMyResidentCertificateV2({
vendor: form.vendor,
loginOrgCd: selectedLogin,
birthDate: form.birthDate,
identityEnc: form.identityEnc,
sido: form.sido,
sigg: form.sigg,
});
// → res.stepData 또는 res.twoWayInfo를 폼에 저장
// 2차 요청 (confirm 스텝)
const res = await UserAPI.createMyResidentCertificateV2({
vendor: form.vendor,
loginOrgCd: form.loginOrgCd,
birthDate: form.birthDate,
identityEnc: form.identityEnc,
sido: form.sido,
sigg: form.sigg,
stepData: form.stepData, // HYPHEN이면 값이 있음
twoWayInfo: form.twoWayInfo, // CODEF이면 값이 있음
});
// → res.preSignedUrl로 PDF 다운로드 가능환경 설정
| 환경변수 | 용도 | 비고 |
|---|---|---|
NEXT_PUBLIC_CERTIFICATE_AES_KEY | 얼마집 전용 AES-256 암호화 키 (32바이트) | 각 환경(dev/prod)마다 다른 값 |
주요 패키지/파일 참조
| 패키지/파일 | 역할 |
|---|---|
packages/api/lib/user/schema.ts | v2 요청/응답 타입 정의 |
packages/api/lib/user/index.ts | v2 API 엔드포인트 (createMyResidentCertificateV2 등) |
packages/api/lib/assignment/schema.ts | CertificateVendor enum, AssignmentDto.certificateVendor |
packages/utils-ts/aes-encryption.ts | encryptCertificateField() — AES-256-CBC 암호화 |
apps/app/src/routes/index.ts | electronicCertificateApply 라우트 (queryString: type, vendor) |
Last updated on