---
template: policy-doc@1
doc_id: 02-privacy-and-masking
title: 개인정보와 마스킹 — 규칙과 그 규칙이 통과시키는 입력
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: privacy:mask-canonical
    status: active-prod
    evidence: [S-guard, A-comment]
    limitations:
      - 통합 함수의 기본 분기가 값을 그대로 돌려준다. 알려진 다섯 종류 밖의 값이 들어오면 마스킹 없이 통과하며, 그 통과는 예외를 던지지 않아 호출부에서 구별되지 않는다
    anchors:
      - insuniverse-server/apps/api/src/helpers/mask.helper.ts:75
  - id: privacy:mask-passthrough
    status: active-prod
    evidence: [S-guard, A-comment]
    limitations:
      - 주석이 적은 출력 형태와 코드가 만드는 출력 형태가 다르다. 인덱스가 이 자리를 comment_matches_code false 로 기록했다. 이 문서는 코드가 만드는 형태만 적고 주석을 적지 않으며 정정을 제안하지 않는다 — 그 판단은 저장소 소유자 소관이고 이 창은 이월로 올린다
      - 함수 하나는 이름과 동작이 어긋난다. 이름은 끝 두 글자를 가린다고 읽히지만 코드는 앞 세 글자를 남기고 나머지 전부를 가린다. 어긋남을 만든 커밋은 커밋 인덱스에 있고 운영에 도달했다
    anchors:
      - insuniverse-server/apps/api/src/helpers/mask.helper.ts:39
      - insuniverse-server/apps/api/src/helpers/mask.helper.ts:13
  - id: privacy:mask-parallel
    status: active-prod
    evidence: [S-guard, A-comment]
    limitations:
      - 정본 밖 구현의 규칙은 이 문서가 파싱하지 않는다. 인덱스가 rules_unparsed 9 로 미조사를 값으로 적었고 이 문서는 그 값을 승계한다 — 파싱은 12-index-runtime 소유 축이다
    anchors:
      - insuniverse-server/libs/webclient/src/webclient.service.ts:1
      - insuniverse-admin/src/shared/helpers/common.helper.ts:1
  - id: privacy:processor-disclosure
    status: staged-only
    evidence: [S-component, A-legal]
    limitations:
      - 두 앵커 중 서버 시드를 세운 커밋 `c577fd29` (2026-07-16) 이 동결 stg 기준 운영 미도달이다. 운영에 도달한 소재는 가입 화면 상수 쪽 하나뿐이다
      - 이 표면의 소재 일부가 manifest src_roots 밖(정적 배포 디렉터리)에 있다. 앵커로 인용은 되지만 코드 스캔 우주 밖이며 인덱스도 publish_covered false 로 그 사실을 적는다
    anchors:
      - insuniverse-site/front/src/features/agreementForm/constants/joinAgreementContents.const.tsx:1
      - insuniverse-server/drizzle/0024_company_terms_defaults.sql:1
---

# 개인정보와 마스킹 — 규칙과 그 규칙이 통과시키는 입력

이 문서는 마스킹 두 표면(`마스킹` · `수탁업체`)이 코드에서 어떤 형태를 갖는가를 적는다.
**개인정보 실제 값은 열지 않는다** — 규칙과 그 규칙이 만드는 출력 형태만이 대상이고, 예시가
필요한 자리에는 합성값을 쓴다.

마스킹 규칙표는 이 문서가 적지 않는다. 동결 stg `4d798f55` 의 정본 파일을 게이트가 재파싱해
함수별 분기 수와 출력 형태를 만들고, 그 결과를 런타임 법적 표면 인덱스의 같은 필드와 대조한 뒤
표로 낸다. **두 값이 갈리면 어느 쪽이 옳은지 이 문서가 정하지 않는다** — 갈림 자체가 보고
대상이며 게이트는 두 값을 함께 찍고 실패한다.

## 표면 대조

첫 열은 법적 표면 인덱스 `surface_traceability` 의 키를 **그대로** 옮긴 것이다.

| 인덱스 키 | 정책 id | state | 앵커 도달성 | status | 배치 |
|---|---|---|---|---|---|
| `마스킹` | `privacy:mask-canonical` | `partial` | `reached-prod` | `active-prod` | 본문 |
| `마스킹` | `privacy:mask-passthrough` | `partial` | `reached-prod` | `active-prod` | 본문 |
| `마스킹` | `privacy:mask-parallel` | `partial` | `reached-prod` | `active-prod` | 본문 |
| `수탁업체` | `privacy:processor-disclosure` | `partial` | `mixed` | `staged-only` | 부록 |

`수탁업체` 행의 도달성이 `mixed` 인 것은 두 앵커의 정의 커밋이 서로 다른 쪽에 있기 때문이다.
하나는 운영에 도달했고 다른 하나는 도달하지 않았다.

## 마스킹 규칙

전건이 게이트 유도값이다. `분기 수` 는 함수 본문의 조건 분기 개수이고, `출력 형태` 는 그 함수가
결과를 만들 때 쓰는 구성 요소를 정렬한 유일 집합이며, `앵커` 는 함수 선언부터 본문 끝까지의 행
범위다. 저자가 세지 않는다.

| 함수 | 분기 수 | 출력 형태 | 앵커 |
|---|---|---|---|
| `maskName` | 3 | `literal(1)` · `repeat(length-2)` | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:2-9` |
| `maskPhone` | 2 | `slice(0,3)` · `slice(7)` | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:13-27` |
| `maskBirth` | 2 | `literal(4)` · `slice(0,2)` · `slice(0,4)` | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:31-35` |
| `maskEmail` | 1 | `literal(1)` · `repeat(Math.max(local.length-3,0))` · `slice(0,3)` | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:39-45` |
| `maskLastTwoChars` | 2 | `literal(1)` · `repeat(idString.length-visiblePrefixLength)` · `repeat(idString.length‖1)` | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:48-66` |

> 마지막 행의 `‖` 는 코드의 이중 세로선을 옮긴 것이다. 마크다운 표 셀은 세로선을 담을 수 없어
> 그 자리에서 열이 쪼개지므로 문서는 이중 세로선 문자로 싣고 게이트가 되돌려 대조한다.

## 규칙이 통과시키는 입력

마스킹 함수는 순수 함수라 입력에서 출력을 결정적으로 되짚을 수 있다. 아래는 그 되짚음의 결과이며
전부 합성 입력이다.

**짧은 이메일 로컬파트는 마스킹 없이 통과한다.** 별표 개수가 로컬파트 길이에서 셋을 뺀 값이고
음수가 되지 않도록 0 으로 잘리므로, 로컬파트가 세 글자 이하이면 별표가 하나도 붙지 않는다. 세
글자 로컬파트를 넣으면 입력과 출력이 같다.

**이메일 도메인은 어떤 경우에도 그대로 남는다.** 이 함수는 로컬파트만 다루고 도메인은 결과에
그대로 이어 붙인다.

**연락처는 자릿수가 맞아야 가려진다.** 숫자만 남긴 길이가 열한 자리이거나 열 자리일 때만 앞 세
자리와 여덟째 자리부터의 나머지를 남기고 그 사이를 별표로 채우며, 그 밖의 길이는 **입력을 그대로
돌려준다.** 열 자리 분기는 조건만 다를 뿐 열한 자리 분기와 같은 자리를 자른다.

**생년월일 여덟 자리는 연도 네 자리가 그대로 남는다.** 여섯 자리 입력은 앞 두 자리가 남고, 그
밖의 길이는 전부 가려진다.

**통합 함수의 기본 분기는 값을 그대로 낸다.** 알려진 다섯 종류(이름 · 연락처 · 생년월일 ·
이메일 · 식별자) 밖의 종류가 들어오면 마스킹 없이 통과하며 예외도 던지지 않는다.

**주석과 코드가 갈린다.** 연락처 함수의 열 자리 분기 주석은 코드가 만드는 것과 다른 형태를
적는다. 인덱스가 이 자리를 정본의 `comment_matches_code` 값으로 이미 기록했다. **이 절은 코드가
만드는 형태만 적고 주석의 형태를 적지 않는다** — 어긋남 자체는 프론트매터 `limitations` 가 지고,
정정 제안은 하지 않는다. 그 판단은 저장소 소유자 소관이며 이 창은 이월로 올린다.

## 정본과 병렬 구현

**두 축은 원소가 다르다.** 인덱스가 센 것은 마스킹 신호를 가진 파일의 집합(구현 축)이고, 정본
통합 함수를 실제로 부르는 파일의 집합(소비 축)은 그것과 겹치지 않는다. 두 집합의 크기가 같은
것은 우연이며 한 수로 접으면 서로 다른 질문의 답이 하나로 보인다. 게이트가 두 집합을 각각
재열거하고 교집합을 매 실행 출력에 낸다.

구현 축 — 인덱스 `parallel_implementations` 재열거:

- `insuniverse-admin/src/api/invoiceApi.ts`
- `insuniverse-admin/src/shared/helpers/common.helper.ts`
- `insuniverse-admin/src/shared/helpers/invoice.helper.ts`
- `insuniverse-server/apps/api/src/modules/invoice/invoice-admin.dto.ts`
- `insuniverse-server/apps/api/src/modules/invoice/invoice.drawer.ts`
- `insuniverse-server/apps/api/src/modules/treat-test/vendors/types.ts`
- `insuniverse-server/libs/webclient/src/webclient.service.ts`
- `insuniverse-site/front/src/api/invoicesApi.ts`
- `insuniverse-site/front/src/shared/helpers/invoice.helper.ts`

소비 축 — 동결 stg 트리에서 정본 통합 함수 호출을 재열거:

- `insuniverse-server/apps/api/src/modules/courses/reviews/courses-reviews.service.ts`
- `insuniverse-server/apps/api/src/modules/fc/agency/agency-fc-admin.controller.ts`
- `insuniverse-server/apps/api/src/modules/fc/agency/fc-agency.service.ts`
- `insuniverse-server/apps/api/src/modules/fc/fc-admin.controller.ts`
- `insuniverse-server/apps/api/src/modules/fc/fc.service.ts`
- `insuniverse-server/apps/api/src/modules/order/order-detail/order-detail-admin.controller.ts`
- `insuniverse-server/apps/api/src/modules/order/order-detail/order-detail.service.ts`
- `insuniverse-server/apps/api/src/modules/user/user-admin.controller.ts`
- `insuniverse-server/apps/api/src/modules/user/user.service.ts`

**구현 축의 규칙은 이 문서가 파싱하지 않는다.** 인덱스가 미조사를 값으로 적었고 이 문서는 그
값을 승계한다. 따라서 정본 밖 구현이 정본과 같은 형태를 만드는지는 여기서 답해지지 않는다.

## 수탁업체

**코드와 스키마에 구조가 없다.** 처리 위탁 관계를 담는 표도, 그 관계를 다루는 코드 경로도 세
저장소에서 관측되지 않는다. 관측되는 것은 약관 본문 문구뿐이다.

인덱스가 이 표면의 근거를 `terms-text-only` 로 적었고, 게이트가 같은 스캐너로 동결 3트리를 다시
훑어 그 계수를 재현한다. 재현이 성립하는 것은 같은 스캔이 양성 프로브를 먼저 통과했기 때문이다 —
이 표면의 어휘가 코퍼스에서 인덱스와 같은 수로 다시 나온다는 것을 확인한 뒤에야, 이웃 표면의
0건을 부재로 받는다.

**소재의 일부가 코드 스캔 우주 밖에 있다.** 정적 배포 디렉터리 아래 세 파일이 이 표면의 소재로
인덱스에 실려 있는데, 그 디렉터리는 manifest 의 소스 루트 선언 밖이다. 인덱스도 같은 사실을
`publish_covered` 값으로 적는다. **앵커로 인용은 되지만 코드 축의 스캔 결과에는 반영되지 않는다** —
이 차이를 적지 않으면 "코드 히트 0" 이 우주 전체의 0 으로 읽힌다.

## 정책 (기획)

**이 절에 본문 정책이 실린다.** 마스킹 규칙은 순수 함수이고 그 정의 커밋이 운영에 도달해 있으므로
지금 무엇이 어떻게 가려지는가가 저장소 안에서 확정된다. 판정을 미룰 자리가 아니다.

### privacy:mask-canonical

개인정보를 화면과 응답에 실을 때 가리는 정본 경로가 하나 있다. 종류를 받아 다섯 갈래로 나눠 각
전용 함수에 넘기는 통합 함수이며, 종류는 이름 · 연락처 · 생년월일 · 이메일 · 식별자다.

**기본 분기가 값을 그대로 낸다.** 다섯 갈래 밖의 종류가 오면 가리지 않고 통과시키며 예외도 던지지
않는다. 호출부에서는 가려진 값과 가려지지 않은 값이 같은 형태로 돌아온다.

### privacy:mask-passthrough

정본 함수 다섯 중 **둘**이 입력에 따라 아무것도 가리지 않고 통과시킨다 — 이메일은 로컬파트가
짧으면 별표가 0개라 입력과 출력이 같고, 연락처는 자릿수가 맞지 않으면 입력을 그대로 돌려준다.
셋째 갈래는 통과가 아니라 **부분 노출**이다: 생년월일 여덟 자리는 연도 네 자리가 남는다. 그리고
이메일 도메인은 어떤 경우에도 노출된다.

이것은 결함의 주장이 아니라 **규칙이 만드는 형태의 기술**이다. 어떤 입력이 어떤 출력을 만드는가는
위 `## 규칙이 통과시키는 입력` 이 지고, 그 형태가 의도된 것인지는 이 문서가 판정하지 않는다.

### privacy:mask-parallel

정본 밖에도 마스킹 신호를 가진 구현이 여럿 있다. 인덱스가 그 집합을 열거했고 규칙은 미조사로
적었다. 정본을 부르는 파일의 집합은 그것과 별개이며 두 집합은 겹치지 않는다.

**그래서 "마스킹이 어떻게 되는가" 의 답은 경로마다 갈릴 수 있다.** 정본을 지나는 경로는 위 규칙표가
답하고, 그 밖의 경로는 이 문서가 답하지 않는다.

## 강제 지점 (개발)

정책 내용을 다시 쓰지 않는다. 각 정책이 저장소 안 **어디에서** 강제되는가와 그 기제만 적는다.
배치가 부록인 정책도 이 절에 있다. 기준점은 전건 동결 stg `4d798f55` 다.

### privacy:mask-canonical

- 강제 방식: 종류별 분기. 다섯 갈래 각각을 전용 함수에 넘기고 그 밖은 기본 분기로 떨어진다.
- 앵커: `insuniverse-server/apps/api/src/helpers/mask.helper.ts:75` (분기 시작)
- 게이트: 없음 — 순수 함수이고 조건에 걸리지 않는다.

### privacy:mask-passthrough

- 강제 방식: 각 전용 함수의 조건 분기. 조건에 맞지 않는 입력은 원본을 그대로 반환한다.
- 앵커: `insuniverse-server/apps/api/src/helpers/mask.helper.ts:39` (이메일) · `:13` (연락처)
- 게이트: 없음.

### privacy:mask-parallel

- 강제 방식: 없음. 정본 밖 구현이 각자 자기 규칙을 갖는다는 관측이며, 앵커는 그 구현이 사는 두 파일의 진입 지점이다.
- 앵커: `insuniverse-server/libs/webclient/src/webclient.service.ts:1` · `insuniverse-admin/src/shared/helpers/common.helper.ts:1`
- 게이트: 없음.

### privacy:processor-disclosure

- 강제 방식: 없음. 약관 본문 문구가 소재이고 코드 경로가 없다. 앵커는 가입 동의 문구 상수와 기업별 약관 기본값 시드다.
- 앵커: `insuniverse-site/front/src/features/agreementForm/constants/joinAgreementContents.const.tsx:1` · `insuniverse-server/drizzle/0024_company_terms_defaults.sql:1`
- 게이트: 없음 — 다만 시드 쪽 정의 커밋이 운영에 도달하지 않았다.

## 적용 범위 (actor × 서비스)

`적용` 은 그 표면이 해당 축에서 실제로 관측되는 경우에만 쓴다. 그 밖은 `—` 이며 빈 칸이 아니라
관측된 부재다.

마스킹 정본은 특정 로그인 주체를 겨냥해 세워졌다 — 정본을 세운 커밋의 제목이 대리점 로그인
응답의 개인정보 마스킹을 적는다. 그래서 `영업대리점` 은 `적용` 이고 나머지 주체는 `—` 다.
**다만 소비 축 전건이 그 주체에 걸리는 것은 아니다** — 아홉 경로 중 하나는 수강평 목록이고 그
경로는 다른 커밋이 붙였다(아래 `## 연혁`). 이 표는 그 하나를 근거로 다른 주체를 세우지 않는다.
병렬 구현은 세 저장소에 흩어져 있으므로 서비스 축 전건이 `적용` 이다.
수탁업체 표면은 가입 화면에서 노출되므로 로그인하지 않는 표면이고, 이 축의 원소를 지목하지 않는다.

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| privacy:mask-canonical | — | 적용 | — | — | — | 적용 | — | — |
| privacy:mask-passthrough | — | 적용 | — | — | — | 적용 | — | — |
| privacy:mask-parallel | — | 적용 | — | — | — | 적용 | 적용 | 적용 |
| privacy:processor-disclosure | — | — | — | — | — | 적용 | 적용 | — |

## 예외·폴백

예외가 없는 정책도 행을 둔다 — 없다는 것도 확인의 결과이며 그 확인 근거를 함께 적는다.

| 정책 id | 예외 | 폴백 | 확인 근거 |
|---|---|---|---|
| privacy:mask-canonical | 알려진 다섯 종류 밖의 종류 | 없음 — 값을 그대로 반환하고 예외를 던지지 않는다 | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:75` 의 분기 열거 전수 |
| privacy:mask-passthrough | 이메일 로컬파트가 짧을 때 · 연락처 자릿수가 맞지 않을 때 | 없음 — 입력을 그대로 반환한다 | `insuniverse-server/apps/api/src/helpers/mask.helper.ts:39` · `:13` 의 조건 분기 |
| privacy:mask-parallel | 없음 | 없음 — 정본 밖 구현의 폴백은 이 문서가 파싱하지 않는다 | 인덱스가 그 축을 미조사로 값에 적었다 |
| privacy:processor-disclosure | 운영에서는 시드 쪽 소재가 존재하지 않는다 | 없음 | 시드 정의 커밋이 릴리스 인덱스의 미도달 집합에 있다 |

## 부록

이 절의 정책은 **현행 발효 여부가 판정되지 않아** 본문에서 격리한다. 이 문서의 격리 사유는
하나다 — 소재의 절반이 운영에 도달하지 않았다. 그 상태로 본문에 두면 독자가 지금 걸려 있는
공시로 읽는다.

### privacy:processor-disclosure

개인정보 처리 위탁의 공시가 어디에 있는가의 표면이다. 저장 계층에는 그 관계를 담는 구조가 없고,
관측되는 소재는 약관 본문 문구뿐이다.

소재는 두 갈래로 나뉜다. 하나는 가입 동의 화면이 쓰는 문구 상수이고 운영에 도달해 있다. 다른
하나는 기업별 약관의 기본값 시드이며 **운영에 도달하지 않았다.** 두 갈래를 한 상태로 접으면
운영에 없는 소재가 운영 공시로 읽힌다.

**소재의 일부는 코드 스캔 우주 밖에 있다.** 정적 배포 디렉터리 아래 파일들이 그렇고, 인덱스도
같은 사실을 값으로 적는다. 그 파일들은 앵커로 인용되지만 코드 축 계수에는 들어가지 않는다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋 SHA 로 추적한다. 행은 커밋 인덱스가
이 문서의 앵커 경로 접촉으로 기록한 커밋 집합 안에서 **마스킹 규칙이나 공시 소재가 바뀐 시점**만
골랐다. 소재 파일 하나는 접촉이 열 건을 넘으므로 전건을 싣지 않으며, 그 선별은 사람 판단이고
게이트는 앵커 경로 접촉까지만 본다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2024-11-18 | `253e78ee` | 가입 동의 문구 상수가 처음 선다 — 처리 위탁 공시의 유일한 운영 소재가 여기서 시작한다 |
| — | 2024-11-30 | `8b134b78` | admin 공용 헬퍼가 선다 — 정본 밖 마스킹 구현이 사는 자리 |
| — | 2024-12-19 | `d0f37978` | 가입 동의 문구 구조가 정리된다 |
| — | 2025-06-25 | `931812c2` | 대리점 권한 화면의 개인정보 마스킹이 admin 쪽에 붙는다 — 병렬 구현이 늘어나는 첫 시점 |
| — | 2025-07-07 | `359be4cd` | 서버 정본 마스킹 헬퍼가 선다 — 종류별 다섯 갈래와 통합 분기가 이때 형태를 갖는다 |
| — | 2025-07-14 | `d6f69607` | 식별자 종류가 통합 분기에 더해진다 |
| — | 2025-07-15 | `712f6fd4` | 식별자 마스킹 조건이 앞 세 글자만 남기는 형태로 바뀐다 — **함수 이름과 동작이 어긋나기 시작하는 지점이며 동결 시점까지 다시 바뀌지 않았다** |
| — | 2026-06-30 | `bcb456af` | 외부 호출 로그의 개인정보·키 노출 차단이 붙는다 — 정본과 다른 규칙을 갖는 구현이 하나 더 선다 |
| — | 2026-07-16 | `c577fd29` | 기업별 약관 기본값 시드가 선다 — 처리 위탁 문구의 둘째 소재이며 **운영 미도달** |
