Skip to Content
증명서 발급 (전자증명서)

증명서 발급 (전자증명서)

주민등록등본, 주민등록초본, 가족관계증명서를 간편인증으로 발급하는 기능입니다. HYPHEN과 CODEF 두 벤더를 선택적으로 사용할 수 있는 멀티 벤더 구조(v2)로 운영됩니다.

전체 흐름

사용자 입력 → 프론트 암호화 → 서버 복호화 → 벤더 규격 재암호화 → 벤더 API
  1. 사용자가 주민등록번호, 주소 등 민감 정보를 입력
  2. 프론트에서 얼마집 전용 AES-256 키로 암호화하여 서버에 전송
  3. 서버에서 복호화 후, 선택된 벤더(HYPHEN 또는 CODEF) API 규격에 맞게 재암호화
  4. 벤더를 통해 간편인증 요청 → 사용자 승인 → 증명서 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차 요청에 포함
HYPHENstepData (string)stepData
CODEFtwoWayInfo (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 암호화 차이

v1v2
알고리즘AES-128-CBCAES-256-CBC
ENCRYPTION_KEY (16바이트)NEXT_PUBLIC_CERTIFICATE_AES_KEY (32바이트)
IVuserId 기반 고정 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 → queryString vendor → apply page
  • 전자증명서 메뉴에서 직접 진입: 기본값 CertificateVendor.HYPHEN

간편인증 서비스 (loginOrgCd)

코드서비스HYPHENCODEF
kakao카카오톡OO
naver네이버OO
toss토스OO
kbKB인증서OO
shinhan신한인증서OO

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.tsv2 요청/응답 타입 정의
packages/api/lib/user/index.tsv2 API 엔드포인트 (createMyResidentCertificateV2 등)
packages/api/lib/assignment/schema.tsCertificateVendor enum, AssignmentDto.certificateVendor
packages/utils-ts/aes-encryption.tsencryptCertificateField() — AES-256-CBC 암호화
apps/app/src/routes/index.tselectronicCertificateApply 라우트 (queryString: type, vendor)

Native (Expo)

native 앱(apps/native-app)은 web 웹뷰를 쓰지 않고 증명서 발급 플로우를 RN으로 자체 구현합니다. API 규격(createMy*V2, stepData/twoWayInfo, preSignedUrl)과 2단계 인증 흐름은 web과 동일하며, 아래는 native에서만 달라지는 점입니다.

관련 파일: apps/native-app/src/features/electronic-certificate/

암호화 구현 차이 (node-forge)

web은 @howmuchhome-web/utils-tsencryptCertificateField()(Node crypto)를 사용하지만, native는 동일 알고리즘/출력 포맷을 node-forge로 재구현한 별도 함수를 씁니다.

  • 파일: apps/native-app/src/features/electronic-certificate/lib/encryptCertificateField.ts
  • 왜 utils-ts 대신인가: native(Hermes)에서는 metro가 Node crypto 모듈을 빈 모듈로 stub하므로 utils-ts의 crypto 기반 구현을 그대로 실행할 수 없습니다. (native-app/metro-and-dependencies 의 crypto stub 참고, utils-ts)
  • 동작: 순수 JS(node-forge, 이미 번들에 존재해 추가 용량 0)로 AES-256-CBC · 랜덤 16byte IV · PKCS#7 패딩 · 출력 base64(IV ‖ 암호문) 을 구현합니다. web(Node crypto = 서버 복호화)과 양방향 호환됨을 검증했습니다.
  • IV 난수원: global.crypto.getRandomValues(react-native-get-random-values 폴리필, index.js 최상단 import).
  • : web의 NEXT_PUBLIC_CERTIFICATE_AES_KEY 와 동일한 공개값을 @/config/envCERTIFICATE_AES_KEY(JS 번들 인라인, 노출 수준 web과 동일)에서 가져옵니다. 필요 시 EXPO_PUBLIC_CERTIFICATE_AES_KEY 로 override.

앞자리(생년월일) 평문 + 뒷 7자리만 암호화하는 v2 규약은 web과 동일합니다. ApplyFunnel.tsx 의 intro 스텝에서 native encryptCertificateField() 로 뒷자리를 암호화해 폼 상태에 저장합니다.

간편인증 화면 흐름

web과 동일하게 intro → simpleVerification → confirm 퍼널로 구성됩니다 (@howmuchhome-web/funnel/nativeuseFunnel/Funnel, apps/native-app/.../ApplyFunnel.tsx).

스텝native 파일역할API
introResidentRegistrationForm.tsx / FamilyRelationsForm.tsx주민번호·주소·가족관계 입력, 암호화없음
simpleVerificationSimpleVerification.tsx간편인증 provider 선택 그리드1차 createMy*V2
confirmConfirm.tsx인증 안내 → 완료 버튼 → 제출2차 createMy*V2 (with stepData/twoWayInfo)
  • provider 선택: providers.tsEASY_LOGIN_PROVIDERS(카카오톡·네이버·토스·KB인증서·신한인증서)를 단일 소스로 관리합니다. 노출 순서·라벨·배경색은 web과 동일하며, web에서 주석 처리된 PASS는 동일하게 제외됩니다.
  • 백엔드 중개: native에는 provider 인증 SDK가 없습니다. UserAPI.createMy*V2 호출로 백엔드가 provider 인증을 중개하고 preSignedUrl/stepData(HYPHEN)/twoWayInfo(CODEF)를 반환합니다. 이후 사용자가 provider 앱의 푸시 알림으로 2-way 승인하는 흐름입니다.
  • 인증 대기 → 제출 활성화: web은 브라우저 포커스(useWindowFocused)로 “인증 후 눌러주세요” 버튼을 활성화하지만, native는 AppStatebackground/inactive 로 바뀌면(= 인증 앱으로 이탈) 버튼을 활성화합니다(Confirm.tsx).
  • 에러 처리: 잘못된 정보/인증 미완료/발급 실패를 useAlert 로 처리하며, 에러 step은 두지 않습니다.
  • 완료 후 캐시 무효화: native 홈은 focus refetch가 없어, 제출 성공 후 Confirm.tsx 에서 증명서 목록·과제 카운트/상세·대기 문서 수 관련 쿼리를 명시적으로 invalidateQueries 합니다.

외부 앱 열기 스킴 선등록 (withExternalAppSchemes)

향후 provider 앱을 직접 여는 동작에 대비해, config plugin으로 외부 앱 스킴을 선등록합니다.

  • 파일: apps/native-app/plugins/withExternalAppSchemes.js (app.config.ts 에서 등록)
  • 등록 스킴: kakaotalk, kakaoplus, supertoss, naversearchapp (Android <queries> + iOS LSApplicationQueriesSchemesopenURL/canOpenURL 재빌드 선반영)

주의(코드 기준 현황): 증명서 flow에서 provider 앱을 여는 일반 동작은 아직 정식 구현되어 있지 않습니다. Confirm.tsx 에 카카오 선택 시 카카오톡 채널 채팅방(kakaoplus://..., 실패 시 kakaotalk:// 폴백)으로 여는 [TEST] 실험 블록만 존재하며, 카카오 외 provider나 안정화된 앱 열기 UX는 미구현입니다. 스킴 선등록은 이 실험/향후 확장을 위한 인프라 성격입니다.

Last updated on