diff --git a/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
new file mode 100644
index 0000000..a56cdc5
--- /dev/null
+++ b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
@@ -0,0 +1,194 @@
+package com.eactive.apim.portal.common.migration;
+
+import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.HttpStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.sql.DataSource;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * [임시] 레거시 평문 데이터를 {@link PersonalDataEncryptConverter} 규칙으로 일괄 정규화(암호화)하는 운영 도구.
+ *
+ *
배경: {@code @Convert} 컬럼이 평문으로 저장된 레거시 행은, derived query가 검색값을 암호화하면서
+ * 평문 DB값과 불일치해 검색/로그인이 실패한다. 컨버터는 읽기에서 평문/암호문을 자동 구분하고
+ * 쓰기에서 무조건 인코딩하므로, {@code convertToDatabaseColumn(convertToEntityAttribute(x))}는
+ * 평문→인코딩, 인코딩→동일값(멱등)으로 정규화된다. 이 값이 기존과 다를 때만 UPDATE 한다.
+ *
+ * 보안: 오직 127.0.0.1(localhost)에서 직접 호출한 요청만 허용한다. 기본은 dry-run(미변경)이며,
+ * 실제 실행은 {@code dryRun=false}를 명시해야 한다. 작업 완료 후 이 클래스는 제거한다.
+ *
+ *
+ * # 미리보기(변경 안 함)
+ * curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy'
+ * # 실제 실행 (PII 컬럼)
+ * curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false'
+ * # audit 컬럼(created_by/last_modified_by, 15개 테이블)까지 포함
+ * curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false&includeAudit=true'
+ *
+ */
+@Slf4j
+@RestController
+@RequestMapping("/internal/migration")
+public class LegacyEncryptionMigrationController {
+
+ /** PII 직접 컬럼 (로그인/검색에 직접 영향) */
+ private static final List PII_TARGETS = Arrays.asList(
+ new TargetTable("PTL_USER", Arrays.asList("login_id", "email_addr", "phone_number", "mobile_number")),
+ new TargetTable("PTL_MESSAGE_REQUEST", Arrays.asList("email", "phone")),
+ new TargetTable("tseairm02", Arrays.asList("cphnno", "emad")),
+ new TargetTable("PTL_USER_LOG", Arrays.asList("login_id")),
+ new TargetTable("PTL_TWO_FACTOR_AUTH", Arrays.asList("recipient"))
+ );
+
+ /** Auditable(@MappedSuperclass) 상속 테이블의 감사 컬럼 (옵션) */
+ private static final List AUDIT_TABLES = Arrays.asList(
+ "PTL_USER", "ptl_faq", "ptl_org",
+ "DJB_APISTATUS_INCIDENT", "DJB_APISTATUS_INCIDENT_TIMELINE", "DJB_APISTATUS_INCIDENT_API",
+ "ptl_file", "PTL_MESSAGE_TEMPLATE", "ptl_notice", "ptl_terms",
+ "ptl_user_privacy_policy_agreement", "ptl_approval_line",
+ "PTL_INQUIRY_COMMENT", "ptl_inquiry", "ptl_partnership_application"
+ );
+ private static final List AUDIT_COLUMNS = Arrays.asList("created_by", "last_modified_by");
+
+ private final JdbcTemplate jdbcTemplate;
+ private final PersonalDataEncryptConverter converter = new PersonalDataEncryptConverter();
+
+ public LegacyEncryptionMigrationController(@Qualifier("portalDataSource") DataSource emsDataSource) {
+ // EMS(EMSAPP) 스키마 데이터소스. 컨버터 적용 테이블은 모두 EMS에 존재한다.
+ this.jdbcTemplate = new JdbcTemplate(emsDataSource);
+ }
+
+ @PostMapping("/encrypt-legacy")
+ @Transactional("transactionManager")
+ public Map encryptLegacy(HttpServletRequest request,
+ @RequestParam(defaultValue = "true") boolean dryRun,
+ @RequestParam(defaultValue = "false") boolean includeAudit) {
+ assertLocalOnly(request);
+ assertNotBypass();
+
+ List targets = new ArrayList<>(PII_TARGETS);
+ if (includeAudit) {
+ for (String table : AUDIT_TABLES) {
+ targets.add(new TargetTable(table, AUDIT_COLUMNS));
+ }
+ }
+
+ List