feat: 지정판매소 개인정보(PII) 보호 — 마스킹·암호화·열람 게이트 (기본 OFF)

종량제3 세션 작업. 마스터 플래그(Config\Pii.designatedShopProtection)가 OFF면
현재 동작(원문 그대로) 유지, ON일 때만 마스킹·게이트 적용.

- 표시 계층 단일 지점 마스킹: 대표자명/전화/이메일/계좌/가상계좌 (목록·상세·엑셀·판매대장·전화주문)
- 원문 열람 게이트: 지자체 허용 IP 자동열람 or 2단계 인증(TOTP) step-up + 감사기록(revealPii)
- 정확일치 검색: 블라인드 인덱스(HMAC) 컬럼 있으면 사용, 없으면 평문 폴백
- 저장계층 암호화(encryptAtRest, 기본 OFF): 모델 콜백 + spark 명령 pii:protect-designated-shops
- SQL(추가·멱등): designated_shop_add_pii_bidx / local_government_add_allow_ips
- Auth: TOTP 실제 통과 시에만 세션 auth_2fa_verified=true

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-07-16 15:45:28 +09:00
parent c5df784f91
commit 5e8e81f404
12 changed files with 726 additions and 19 deletions

View File

@@ -7616,16 +7616,25 @@ SQL;
$shopMap = [];
$dsIdxs = array_values(array_unique(array_filter(array_map(static fn ($o): int => (int) ($o->so_ds_idx ?? 0), $orders), static fn ($v): bool => $v > 0)));
if ($dsIdxs !== [] && $db->tableExists('designated_shop')) {
helper('pii_encryption');
$canViewPii = can_view_shop_pii((int) $lgIdx); // 이 화면은 현재 지자체 판매소만
$shopRows = $db->table('designated_shop')
->select('ds_idx, ds_shop_no, ds_name, ds_rep_name, ds_tel, ds_addr, ds_addr_detail')
->whereIn('ds_idx', $dsIdxs)
->get()->getResultArray();
foreach ($shopRows as $s) {
// 암호화 저장 대비 복호화 후, 권한 없으면 마스킹
$rep = pii_decrypt((string) ($s['ds_rep_name'] ?? ''));
$tel = pii_decrypt((string) ($s['ds_tel'] ?? ''));
if (! $canViewPii) {
$rep = mask_shop_field('ds_rep_name', $rep);
$tel = mask_shop_field('ds_tel', $tel);
}
$shopMap[(int) $s['ds_idx']] = [
'shop_no' => (string) ($s['ds_shop_no'] ?? ''),
'name' => (string) ($s['ds_name'] ?? ''),
'rep_name' => (string) ($s['ds_rep_name'] ?? ''),
'tel' => (string) ($s['ds_tel'] ?? ''),
'rep_name' => $rep,
'tel' => $tel,
'addr' => trim((string) ($s['ds_addr'] ?? '') . ' ' . (string) ($s['ds_addr_detail'] ?? '')),
];
}