주민번호 복호화를 붙인다 — 이 DB 는 실제로 암호화되어 있었다

문서(docs/RESNUM-DECRYPT.md)의 1→2→3 을 실행. 태그 7종 → 11종.

## 이 DB 가 암호화 병원이다

진단이 읽은 설정: 전체=Y 주민번호=Y.
앞 커밋에서 PAT_주민번호 를 뺀 판단이 맞았다 —
PatResNum 을 그대로 썼으면 <b>암호문이 종이에 찍혔다</b>.

## 앱은 키를 갖지 않는다

복호화는 오라클 패키지가 한다.

  SELECT SUBSTR(:e,1,7) || CRYPTO_AES256.DEC_AES(SUBSTR(:e,8,256)) FROM DUAL

바인드 변수이고 접근 통제는 DB 권한이다. SCHILDREN 갈래(UDF_GETDECRESNUM_damo)도 함께 옮겼다.
앞 7자리가 평문이라 SUBSTR(...,1,7) 을 이어 붙이는 것을 빠뜨리면 앞자리가 사라진다.

## 설정을 못 읽으면 값을 만들지 않는다

"암호화 아님"으로 가정하면 PatResNum 을 평문처럼 써서 암호문을 찍는다.
PatientIdentity 는 예외 시 빈 문자열을 돌려주고 사유만 로그에 남긴다 — 값은 남기지 않는다.
주민번호를 만드는 자리를 한 곳으로 좁혀 뒀다(만드는 경로가 여럿이면 어디서 새는지 추적할 수 없다).

## 순수 계산은 표로 고정했다 (테스트 13건)

보정 ①(기관기호, SRCH·JINJU): 앞 6자리를 PatBthDay 로 갈아 끼운다.

  원본은 If Not DateValidCheck(Substring(1,6)) 으로 감싸 있지만 <b>그 조건은 항상 참이다</b> —
  DateValidCheck 는 길이가 8·12·14·17 이 아니면 무조건 False 인데 6자리를 넘긴다.
  여기서 "고쳐서" 진짜 날짜 검사를 넣으면 레거시와 다른 번호가 나온다(이 DB 가 SRCH 다).
  이식은 동작을 옮기는 일이지 바로잡는 일이 아니다 — 그 사실을 테스트로 못 박았다.

보정 ②(외국인): 7번째 자리 0/9 를 PatBthDay 세기로 6/8·5/7 로. 국적코드가 있어야 걸린다.
성별: 7번째 자리 mod 2. PAT_성별 은 CoiCalCod 가 "24" 로 시작하면 PatSexTyp 을 먼저 쓴다.

## 추측하면 걸린다

M_DtsMst 컬럼을 DtsTabCod·DtlCod 로 추측해 ORA-00904 를 맞았다.
실제는 DtsTblCod·DtsDtlCod·DtsCod 다(dtCommonLib.vb:1136).

## 확인한 것과 못 한 것을 갈라 적는다

진단은 HIS 사용자 문맥 없이 도므로 병원코드가 비고, SRCH 기관기호 보정 갈래는
이 경로로 확인되지 않는다. 통과로 세지 않고 [미확인] 줄로 적는다 —
그 갈래는 순수 함수 테스트가 덮는다.

진단 리포트에 <b>값을 쓰지 않는다</b> — 길이와 "전부 숫자인가"만 적는다. 리포트는 파일로 남는다.

## 아직 안 한 것

나이(AgeCheck)는 병원별 규칙과 의료급여 전산관리번호 규약이 붙어 별도 작업이다.
개인정보 접근 로그(C_PdrInf)에 쓸지는 운영 DB 쓰기라 승인 전까지 하지 않는다.

## 게이트

- dotnet test 336/336 (주민번호 판정 13건 신규)
- --db-patient ①~㉑ 전건 통과 (⑲ 설정 · ⑳ 13자리 · ㉑ 성별)
- --edit-smoke 실패 0 (이름 검사가 파생 태그 4종까지 본다)
- --dialog-shots FAIL 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 11:15:24 +09:00
co-authored by Claude Opus 5
parent 34c53e3d69
commit 4bdd583adb
7 changed files with 555 additions and 4 deletions
@@ -0,0 +1,144 @@
using SheetMe.Core.Catalog;
namespace SheetMe.Core.Tests;
/// <summary>
/// 주민번호 보정 고정.
///
/// 이 값이 틀리면 종이에 <b>다른 사람의 번호</b>가 찍힌다. 그리고 그건 그럴듯해 보여서
/// 아무도 의심하지 않는다 — 빈칸으로 나오는 고장보다 위험하다.
/// </summary>
[TestClass]
public sealed class ResidentNumberTests
{
#region Methods
[TestMethod]
public void _병원은_앞_여섯자리를_생년월일로_바꾼다()
{
// 레거시 예시: 9750012203265 (앞자리가 날짜가 아니라 보장기관기호)
// 1998-03-04 생 → 앞 6자리가 980304 로 갈린다. 뒤 7자리는 그대로.
Assert.AreEqual("9803042203265",
ResidentNumber.Normalize("9750012203265", "19980304", string.Empty, "SRCH"));
Assert.AreEqual("9803042203265",
ResidentNumber.Normalize("9750012203265", "19980304", string.Empty, "JINJU"));
}
[TestMethod]
public void _유효한_날짜여도_바꾼다()
{
// 원본의 If Not DateValidCheck(Substring(1,6)) 는 <b>항상 참</b>이다 —
// DateValidCheck 는 길이가 8·12·14·17 이 아니면 무조건 False 이고 6자리를 넘긴다.
// 즉 조건이 아무것도 걸러 내지 않는다. "고쳐서" 진짜 날짜 검사를 넣으면
// 여기서 레거시와 다른 번호가 나온다. 그 사실을 이 판정으로 못 박는다.
Assert.AreEqual("8801011234567",
ResidentNumber.Normalize("9001011234567", "19880101", string.Empty, "SRCH"),
"앞자리 900101 은 유효한 날짜지만 레거시는 그래도 갈아 끼운다");
}
[TestMethod]
public void _병원은_기관기호_보정을_하지_않는다()
{
Assert.AreEqual("9750012203265",
ResidentNumber.Normalize("9750012203265", "19980304", string.Empty, "HANKOOK"));
}
[TestMethod]
public void _없으면_기관기호_보정을_건너뛴다()
{
// 원본도 PatBthDay 가 비면 갈아 끼우지 않는다 — 바꿀 재료가 없다
Assert.AreEqual("9750012203265",
ResidentNumber.Normalize("9750012203265", string.Empty, string.Empty, "SRCH"));
}
[TestMethod]
public void _일곱번째_자리를_세기로_바꾼다()
{
// 0 → 19xx 면 6, 그 밖이면 8
Assert.AreEqual("8801016234567",
ResidentNumber.Normalize("8801010234567", "19880101", "USA", "HANKOOK"));
Assert.AreEqual("0501018234567",
ResidentNumber.Normalize("0501010234567", "20050101", "USA", "HANKOOK"));
// 9 → 19xx 면 5, 그 밖이면 7
Assert.AreEqual("8801015234567",
ResidentNumber.Normalize("8801019234567", "19880101", "USA", "HANKOOK"));
Assert.AreEqual("0501017234567",
ResidentNumber.Normalize("0501019234567", "20050101", "USA", "HANKOOK"));
}
[TestMethod]
public void _없으면_외국인_보정을_하지_않는다()
{
// 이 조건이 빠지면 내국인의 7번째 자리까지 바꿔 버린다
Assert.AreEqual("8801010234567",
ResidentNumber.Normalize("8801010234567", "19880101", string.Empty, "HANKOOK"));
}
[TestMethod]
public void _자리가_0_9_가_아니면_손대지_않는다()
{
foreach (var digit in new[] { '1', '2', '3', '4', '5', '6', '7', '8' })
{
var input = "880101" + digit + "234567";
Assert.AreEqual(input,
ResidentNumber.Normalize(input, "19880101", "USA", "HANKOOK"),
$"7번째 자리 {digit} 는 보정 대상이 아니다");
}
}
[TestMethod]
public void _보정이_함께_걸린다()
{
// SRCH 에서 앞자리를 갈고, 그 결과의 7번째 자리가 0 이면 외국인 보정까지 이어진다.
// 순서가 뒤바뀌면 다른 값이 나온다 — 기관기호 보정이 7번째 자리를 새로 정하기 때문이다.
Assert.AreEqual("9803046203265",
ResidentNumber.Normalize("9750010203265", "19980304", "USA", "SRCH"));
}
[TestMethod]
public void _안_되면_그대로_돌려준다()
{
// 원본은 Substring 에서 예외가 나고 Try 가 삼킨다 — 손대지 않는 것이 같은 결과다
Assert.AreEqual("880101", ResidentNumber.Normalize("880101", "19880101", "USA", "SRCH"));
Assert.AreEqual(string.Empty, ResidentNumber.Normalize(string.Empty, "19880101", "USA", "SRCH"));
Assert.AreEqual(string.Empty, ResidentNumber.Normalize(null, null, null, null));
}
[TestMethod]
public void _일곱번째_자리_홀짝이다()
{
Assert.AreEqual("M", ResidentNumber.SexOf("8801011234567"));
Assert.AreEqual("F", ResidentNumber.SexOf("8801012234567"));
Assert.AreEqual("M", ResidentNumber.SexOf("0501013234567"));
Assert.AreEqual("F", ResidentNumber.SexOf("0501014234567"));
// 외국인 자리(5~8)도 같은 규칙이다
Assert.AreEqual("M", ResidentNumber.SexOf("8801015234567"));
Assert.AreEqual("F", ResidentNumber.SexOf("8801016234567"));
}
[TestMethod]
public void _만들_수_없으면_빈_값이다()
{
// 원본은 CLong 에서 예외가 나고 삼켜져 값이 빈다 — 추측해서 채우지 않는다
Assert.AreEqual(string.Empty, ResidentNumber.SexOf("880101"));
Assert.AreEqual(string.Empty, ResidentNumber.SexOf("880101X234567"));
Assert.AreEqual(string.Empty, ResidentNumber.SexOf(null));
}
[TestMethod]
public void _성별은_남녀로_바꾼다()
{
Assert.AreEqual("남", ResidentNumber.SexKoreanOf("8801011234567"));
Assert.AreEqual("여", ResidentNumber.SexKoreanOf("8801012234567"));
Assert.AreEqual(string.Empty, ResidentNumber.SexKoreanOf("880101"));
}
[TestMethod]
public void _양쪽_공백을_지킨다()
{
// 레거시가 " - " 다. 공백을 빼면 서식에서 폭이 달라진다
Assert.AreEqual("880101 - 1234567", ResidentNumber.WithDash("8801011234567"));
Assert.AreEqual("880101", ResidentNumber.WithDash("880101"));
}
#endregion
}