<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('컬럼 마스킹')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
  <div class="page-title">
    <h1>컬럼 마스킹</h1>
    <details class="explanation-details">
      <summary>도움말</summary>
      <p>마스킹 방법을 사전 정의 템플릿으로 등록하고, 민감 컬럼마다 하나의 기본 규칙을 연결합니다. 컬럼 연결 목록은 <strong>마스킹 대상 블랙리스트</strong>이며, 연결하지 않은 컬럼은 ASO 마스킹 대상이 아닙니다. 행 접근은 행 접근 정책이 계속 담당하며 이 화면은 반환값 표시 방식만 다룹니다.</p>
      <p>연결된 컬럼은 행 접근 정책이 조회를 허용한 사용자에게도 기본적으로 마스킹됩니다. <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에 등록한 사용자만 원문을 봅니다. 이 설정은 행 접근 권한을 추가하지 않습니다. 컬럼 연결·해제 또는 연결된 규칙의 활성 상태 변경 시 DBMS_REDACT 정책도 자동 동기화됩니다.</p>
      <section class="content-band mt-3 mb-0">
        <h2>ASO 마스킹 적용 흐름</h2>
        <p><strong>ASO/Data Redaction은 권한 테이블을 직접 조회하지 않습니다.</strong> 토큰 처리 중 <code>set_masking_rule_context(user_id)</code>가 사용자별 마스킹 예외를 세션 context로 만들고, ASO 정책은 그 context만 읽습니다.</p>
        <div class="diagram-canvas">
          <svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="aso-flow-title aso-flow-desc">
            <title id="aso-flow-title">Bearer 토큰 기반 ASO 마스킹 적용 흐름</title>
            <desc id="aso-flow-desc">Bearer 토큰으로 사용자 컨텍스트와 컬럼별 MR context를 만들고, Oracle Data Redaction이 MR context 값에 따라 원문 또는 마스킹값을 반환한다.</desc>
            <defs>
              <marker id="aso-flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"/></marker>
            </defs>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M185 92 H225"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M385 92 H425"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M585 92 H625"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M785 92 H825"/>
            <path class="diagram-link diagram-link-secondary" marker-end="url(#aso-flow-arrow)" d="M705 212 V144"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M910 136 V205"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M810 248 H850"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M930 291 V315"/>
            <path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M850 248 H720 V315"/>
            <g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">요청 주체 식별</text></g>
            <g class="diagram-node"><rect x="225" y="48" width="160" height="88" rx="12"/><text x="305" y="76" class="diagram-node-title">토큰 검증</text><text x="305" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="305" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
            <g class="diagram-node"><rect x="425" y="48" width="160" height="88" rx="12"/><text x="505" y="76" class="diagram-node-title">사용자 context</text><text x="505" y="103" class="diagram-node-detail">CB_AGENT_CTX.USER_ID</text><text x="505" y="120" class="diagram-node-note">EMP_NO · DEPT_CODE</text></g>
            <g class="diagram-node diagram-node-accent"><rect x="625" y="48" width="160" height="88" rx="12"/><text x="705" y="76" class="diagram-node-title">마스킹 context</text><text x="705" y="103" class="diagram-node-detail">set_masking_rule_</text><text x="705" y="120" class="diagram-node-detail">context(user_id)</text></g>
            <g class="diagram-node diagram-node-accent"><rect x="825" y="48" width="245" height="88" rx="12"/><text x="947" y="76" class="diagram-node-title">컬럼별 세션 변수</text><text x="947" y="103" class="diagram-node-detail">MR_&lt;column_id&gt; = Y / N</text><text x="947" y="120" class="diagram-node-note">현재 DB 세션에만 설정</text></g>
            <g class="diagram-node diagram-node-data"><rect x="525" y="212" width="280" height="72" rx="12"/><text x="665" y="240" class="diagram-node-title">마스킹 예외·규칙 메타데이터</text><text x="665" y="263" class="diagram-node-detail">cb_user_masking_rule · cb_column_masking_rule</text></g>
            <g class="diagram-node diagram-node-accent"><rect x="850" y="205" width="160" height="86" rx="12"/><text x="930" y="235" class="diagram-node-title">DBMS_REDACT</text><text x="930" y="260" class="diagram-node-detail">SYS_CONTEXT로 MR 읽음</text></g>
            <g class="diagram-node diagram-node-data"><rect x="850" y="315" width="160" height="64" rx="12"/><text x="930" y="341" class="diagram-node-title">MR = Y</text><text x="930" y="363" class="diagram-node-detail">원문 표시</text></g>
            <g class="diagram-node"><rect x="640" y="315" width="160" height="64" rx="12"/><text x="720" y="341" class="diagram-node-title">MR = N / NULL</text><text x="720" y="363" class="diagram-node-detail">마스킹 적용</text></g>
          </svg>
        </div>
        <p class="form-hint mb-0">연결 고리: 토큰 검증 후 <code>set_masking_rule_context(v_user_id)</code>가 <code>MR_&lt;column_id&gt;</code>를 설정하고, Data Redaction 조건식은 <code>SYS_CONTEXT('CB_AGENT_CTX', 'MR_&lt;column_id&gt;')</code>만 평가합니다. 자세한 SQL은 <a href="/security-sql-scripts?script=aso-masking-runtime">ASO 마스킹 런타임 스크립트</a>에서 확인하세요.</p>
      </section>
    </details>
  </div>

  <div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
  <div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>

  <section class="content-band">
    <div class="section-heading">
      <div>
        <span class="architecture-kicker">행 접근과 분리된 컬럼 제어</span>
        <h2>ASO 마스킹 설정 순서</h2>
        <p class="section-subtitle">행 접근은 행 접근 규칙과 Oracle VPD 정책이 결정합니다. 이 화면은 허용된 행 안에서 컬럼 값을 원문으로 줄지, 마스킹해서 줄지만 관리합니다.</p>
      </div>
    </div>
    <div class="effective-preview">
      <dl>
        <div>
          <dt>대상 컬럼 등록</dt>
          <dd>마스킹 후보 블랙리스트에 DB 컬럼을 올립니다. 이 단계만으로는 값이 마스킹되지 않습니다.</dd>
        </div>
        <div>
          <dt>기본 규칙 연결</dt>
          <dd>컬럼에 활성 컬럼 마스킹 규칙을 연결하면 DBMS_REDACT 정책이 동기화됩니다.</dd>
        </div>
        <div>
          <dt>원문 표시 허용</dt>
          <dd>예외 사용자는 <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에서 지정합니다.</dd>
        </div>
        <div>
          <dt>DB 적용 상태 확인</dt>
          <dd>아래 DB ASO 정책 동기화 표에서 백오피스 활성 컬럼과 실제 Redaction 컬럼이 일치하는지 확인합니다.</dd>
        </div>
      </dl>
    </div>
    <div class="effective-preview mt-3">
      <div class="field-block-title">사용자별 실제 결과 해석</div>
      <dl>
        <div>
          <dt>행 접근 권한 없음</dt>
          <dd>VPD에서 행이 제외되므로 컬럼 마스킹 여부와 무관하게 조회 결과가 없습니다.</dd>
        </div>
        <div>
          <dt>행 접근 허용 + 원문 허용 없음</dt>
          <dd>행은 보이지만, 이 화면에서 연결한 민감 컬럼은 ASO/Data Redaction 정책에 따라 마스킹됩니다.</dd>
        </div>
        <div>
          <dt>행 접근 허용 + 원문 허용 있음</dt>
          <dd>행은 VPD 기준으로 제한되고, 원문 허용된 컬럼만 마스킹 없이 반환됩니다.</dd>
        </div>
      </dl>
    </div>
  </section>

  <section class="content-band">
    <h2>DB ASO 정책 동기화</h2>
    <p class="section-subtitle">이제 컬럼 기본 규칙을 연결·해제하거나 연결된 규칙을 활성·비활성화하면 DB 정책도 자동으로 반영됩니다. 아래 버튼은 자동 동기화 도입 전의 기존 설정을 한 번에 보정하거나, 운영 점검 시 현재 설정을 다시 반영할 때만 사용하세요.</p>
    <form method="post" action="/masking-rules/synchronize" class="inline-form">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
      <button class="btn btn-outline-primary" type="submit">현재 DB 정책 동기화</button>
    </form>

    <div class="table-responsive mt-3">
      <table class="table table-sm align-middle">
        <thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
        <tbody>
        <tr th:each="status : ${policyStatuses}">
          <td><code th:text="${status.targetLabel()}">OWNER.OBJECT_NAME</code></td>
          <td><code th:text="${status.policyName()}">REDACTION_POLICY</code></td>
          <td th:text="${status.configuredColumnCount()}">0</td>
          <td th:text="${status.appliedColumnCount()}">0</td>
          <td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
          <td><span class="badge" th:classappend="${status.policyEnabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${status.policyEnabled()} ? 'DB 적용 ON' : 'DB 적용 OFF'">DB 적용 OFF</span></td>
          <td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">미적용</span><br><small class="text-muted" th:text="${status.detail()}">설명</small></td>
        </tr>
        <tr th:if="${#lists.isEmpty(policyStatuses)}"><td colspan="7" class="text-muted">조회할 ASO 정책이 없습니다.</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <section class="content-band">
    <h2>사전 정의 마스킹 방식</h2>
    <p class="section-subtitle">임의 SQL이나 정규식을 직접 입력하지 않고 검증된 템플릿만 선택합니다.</p>
    <div class="table-responsive">
      <table class="table table-sm align-middle">
        <thead><tr><th>템플릿</th><th>방식</th><th>동작</th><th>ASO 구현</th></tr></thead>
        <tbody>
        <tr th:each="template : ${templates}">
          <td><code th:text="${template.code()}">NULLIFY</code></td>
          <td th:text="${template.label()}">값 숨김(NULL)</td>
          <td th:text="${template.description()}">설명</td>
          <td><code th:text="${template.asoFunction()}">DBMS_REDACT.NULLIFY</code></td>
        </tr>
        </tbody>
      </table>
    </div>
  </section>

  <section class="content-band">
    <h2>컬럼 마스킹 규칙 등록</h2>
    <p class="section-subtitle">게임 데이터 기준의 업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>GAME_USER_ID_MASK</code>.</p>
    <form method="post" action="/masking-rules" class="form-grid">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
      <label>
        규칙 코드
        <input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="GAME_USER_ID_MASK">
        <span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
      </label>
      <label>
        규칙명
        <input class="form-control" name="ruleName" maxlength="100" required placeholder="게임 사용자 식별자 기본 마스킹">
      </label>
      <label>
        마스킹 템플릿
        <select class="form-select" name="templateCode" required>
          <option th:each="template : ${templates}" th:value="${template.code()}"
                  th:text="${template.label()}">값 숨김(NULL)</option>
        </select>
      </label>
      <label>
        설명
        <input class="form-control" name="description" maxlength="400" placeholder="적용 목적 또는 업무 기준">
      </label>
      <button class="btn btn-primary" type="submit">규칙 등록</button>
    </form>
  </section>

  <section class="content-band">
    <h2>등록된 컬럼 마스킹 규칙</h2>
    <div class="table-responsive">
      <table class="table table-sm align-middle">
        <thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
        <tbody>
        <tr th:each="rule : ${rules}">
          <td><code th:text="${rule.ruleCode()}">GAME_USER_ID_MASK</code></td>
          <td th:text="${rule.ruleName()}">게임 사용자 식별자 기본 마스킹</td>
          <td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
          <td th:text="${rule.description() ?: '-'}">설명</td>
          <td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
          <td>
            <form method="post" action="/masking-rules/active" class="inline-form">
              <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
              <input type="hidden" name="ruleId" th:value="${rule.ruleId()}">
              <input type="hidden" name="active" th:value="${!rule.enabled()}">
              <button class="btn btn-sm btn-outline-secondary" type="submit" th:text="${rule.enabled()} ? '비활성화' : '활성화'">변경</button>
            </form>
          </td>
        </tr>
        <tr th:if="${#lists.isEmpty(rules)}"><td colspan="6" class="text-muted">등록된 컬럼 마스킹 규칙이 없습니다.</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <section class="content-band">
    <h2>마스킹 대상 컬럼 추가</h2>
    <p class="section-subtitle">DB 실제 컬럼을 ASO 마스킹 후보 블랙리스트에 등록합니다. 이 단계는 컬럼을 선택 목록에 올리는 작업이며, 실제 마스킹 적용은 아래에서 기본 규칙을 연결할 때 수행됩니다.</p>
    <form method="post" action="/masking-rules/target-columns" class="form-grid">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
      <label>
        대상 객체 / 컬럼
        <select class="form-select" name="target" required>
          <option value="">선택하세요</option>
          <th:block th:each="object : ${maskingTargetObjects}">
            <optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
              <option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
                      th:value="|${object.objectId()}:${columnName}|"
                      th:text="${object.displayName() + '.' + columnName}">OWNER.OBJECT_NAME.COLUMN_NAME</option>
            </optgroup>
          </th:block>
        </select>
        <span class="form-hint">현재 관리 대상 ASO 정책이 있는 등록 업무 데이터 객체만 표시됩니다. 예: <code>OWNER.OBJECT_NAME.COLUMN_NAME</code>.</span>
      </label>
      <button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
    </form>
  </section>

  <section class="content-band">
    <h2>민감 컬럼에 기본 규칙 연결</h2>
    <p class="section-subtitle">이 목록이 마스킹 대상 블랙리스트입니다. 컬럼 하나에는 활성 규칙 하나만 연결할 수 있으며, 컬럼 원문 표시 허용 사용자는 별도 화면에서 지정합니다. 오른쪽 DB 상태는 컬럼이 속한 객체의 실제 Redaction 정책 상태입니다.</p>
    <form method="post" action="/masking-rules/columns" class="form-grid">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
      <label>
        민감 컬럼
        <select class="form-select" name="columnId" required>
          <option value="">선택하세요</option>
          <th:block th:each="object : ${objects}">
            <optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(sensitiveColumnsByObject[object.objectId()])}">
              <option th:each="column : ${sensitiveColumnsByObject[object.objectId()]}"
                      th:value="${column.columnId()}"
                      th:text="${column.columnName() + ' [' + column.displayPolicyLabel() + ']'}">RRN_MASKED</option>
            </optgroup>
          </th:block>
        </select>
      </label>
      <label>
        적용할 컬럼 마스킹 규칙
        <select class="form-select" name="ruleId" required>
          <option value="">선택하세요</option>
          <option th:each="rule : ${rules}" th:if="${rule.enabled()}" th:value="${rule.ruleId()}"
                  th:text="${rule.ruleName() + ' · ' + rule.templateLabel()}">주민번호 기본 마스킹</option>
        </select>
      </label>
      <button class="btn btn-primary" type="submit">컬럼에 연결</button>
    </form>

    <div class="table-responsive mt-3">
      <table class="table table-sm align-middle">
        <thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
        <tbody>
        <tr th:each="columnRule : ${columnRules}">
          <td><code th:text="${columnRule.targetLabel()}">OWNER.OBJECT_NAME.COLUMN_NAME</code></td>
          <td th:text="${columnRule.ruleName()}">게임 사용자 식별자 기본 마스킹</td>
          <td th:text="${columnRule.template().label()}">식별자 부분 마스킹</td>
          <td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
          <td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
            <span class="badge"
                  th:if="${policyStatus}"
                  th:classappend="${' ' + policyStatus.badgeClass()}"
                  th:text="${policyStatus.statusLabel()}">적용됨</span>
            <span class="badge text-bg-secondary" th:unless="${policyStatus}">관리 정책 없음</span>
            <div class="text-muted small" th:if="${policyStatus}" th:text="${policyStatus.detail()}">상세</div>
          </td>
          <td>
            <form method="post" action="/masking-rules/columns/delete" class="inline-form">
              <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
              <input type="hidden" name="columnId" th:value="${columnRule.columnId()}">
              <button class="btn btn-sm btn-outline-danger" type="submit">연결 해제</button>
            </form>
          </td>
        </tr>
        <tr th:if="${#lists.isEmpty(columnRules)}"><td colspan="6" class="text-muted">민감 컬럼에 연결된 컬럼 마스킹 규칙이 없습니다.</td></tr>
        </tbody>
      </table>
    </div>
  </section>
</main>
</body>
</html>
