← 문서 목록

DB 초기 배포 SQL · db/deploy

새 서버 최초 구축 3단계 (데이터베이스·계정 → 표별 권한 → 배포 후 점검)

-- ═══════════════════════════════════════════════════════════
-- 01_create_database_and_accounts.sql
-- ═══════════════════════════════════════════════════════════
-- ═══════════════════════════════════════════════════════════════════
--  NestPay 초기 배포 ①  데이터베이스·계정 만들기        [앱을 켜기 전에 실행]
--
--  이 파일은 서버에 처음 한 번만 실행합니다. (DB 관리자 root 로 실행)
--  표(테이블)는 여기서 만들지 않습니다 — 앱이 켜질 때 Flyway 가 알아서 만듭니다.
--  여기서는 Flyway 가 할 수 없는 것만 합니다: 데이터베이스 그릇과 접속 계정.
--
--  ── 전체 순서 ────────────────────────────────────────────────
--    ① 이 파일 실행            ← 지금 여기
--    ② 앱 켜기                 (Flyway 가 표를 전부 만듭니다)
--    ③ 02_grant_table_privileges.sql 실행   (표가 생긴 뒤라야 줄 수 있는 권한)
--    ④ 03_post_deploy_check.sql 실행        (제대로 됐는지 확인)
--    자세한 설명은 같은 폴더의 README.md 를 보세요.
--
--  ── 실행 방법 ────────────────────────────────────────────────
--    1) 아래 ★ 표시된 비밀번호 3개를 반드시 바꿉니다(그대로 쓰면 안 됩니다).
--    2) 아래 ★ 표시된 접속 주소 3개를 실제 값으로 바꿉니다.
--    3) mysql -uroot -p < 01_create_database_and_accounts.sql
--
--  ※ 여기 적힌 비밀번호는 빈칸 표시일 뿐입니다. 실제 값은 이 파일에 적어 두지 말고
--    안전한 곳(비밀번호 관리 도구)에 보관하세요.
-- ═══════════════════════════════════════════════════════════════════

-- ───────────────────────────────────────────────────────────────────
-- 1. 데이터베이스 만들기
--
--    ★ 글자 방식(utf8mb4)과 비교 방식(utf8mb4_unicode_ci)을 반드시 적어 줍니다.
--      적지 않으면 서버 기본값을 따라가는데, MariaDB 기본값은 latin1 입니다.
--      그 상태로 마이그레이션을 돌리면 한글을 넣는 순간 멈춥니다
--      (순정 서버에서 V3 의 '최고 관리자' INSERT 가 ERROR 1366 으로 중단되는 것을 확인했습니다).
-- ───────────────────────────────────────────────────────────────────
CREATE DATABASE IF NOT EXISTS `nestpay`
  DEFAULT CHARACTER SET utf8mb4
  DEFAULT COLLATE utf8mb4_unicode_ci;


-- ───────────────────────────────────────────────────────────────────
-- 2. 계정 3개 만들기 — 하는 일이 다르면 계정도 나눕니다
--
--    ① nestpay_mig : 표를 만들고 바꾸는 계정   (Flyway 가 켜질 때만 사용)
--    ② nestpay_app : 서비스가 평소에 쓰는 계정 (표 구조는 못 건드림)
--    ③ nestpay_ro  : 보기만 하는 계정          (조사·통계·감사용)
--
--    왜 나누나요?
--      서비스 계정이 표를 지울 수 있으면, 프로그램 실수 하나로 장부가 통째로 날아갑니다.
--      돈을 다루는 시스템에서는 "할 수 있는 일"을 미리 줄여 두는 것이 가장 확실한 방어입니다.
--
--    ★ 접속 주소(뒤의 @'...')를 IP 로 묶습니다.
--      '%'(어디서든)로 두면 비밀번호만 새어도 외부에서 바로 붙습니다.
--      서버가 2대이므로 계정마다 2줄씩 만듭니다.
-- ───────────────────────────────────────────────────────────────────

-- ① 마이그레이션 계정 — 앱이 켜질 때 Flyway 가 이 계정으로 붙어 표를 만듭니다.
CREATE USER IF NOT EXISTS 'nestpay_mig'@'<API서버1_IP>' IDENTIFIED BY '★바꾸세요_마이그레이션_비밀번호';
CREATE USER IF NOT EXISTS 'nestpay_mig'@'<API서버2_IP>' IDENTIFIED BY '★바꾸세요_마이그레이션_비밀번호';

-- ② 서비스 계정 — 앱이 평소 업무에 쓰는 계정입니다.
CREATE USER IF NOT EXISTS 'nestpay_app'@'<API서버1_IP>' IDENTIFIED BY '★바꾸세요_서비스_비밀번호';
CREATE USER IF NOT EXISTS 'nestpay_app'@'<API서버2_IP>' IDENTIFIED BY '★바꾸세요_서비스_비밀번호';

-- ③ 읽기 전용 계정 — 조사·통계·감사에서 씁니다(고칠 수 없습니다).
CREATE USER IF NOT EXISTS 'nestpay_ro'@'<사내망_대역>'  IDENTIFIED BY '★바꾸세요_읽기전용_비밀번호';


-- ───────────────────────────────────────────────────────────────────
-- 3. 권한 주기 — 여기서는 "데이터베이스 전체" 단위 권한만 줍니다
--
--    표 하나하나에 주는 권한(UPDATE·DELETE)은 표가 실제로 있어야 줄 수 있습니다.
--    아직 표가 없으므로(앱을 켜기 전) 그것은 02 번 파일에서 줍니다.
--    (없는 표에 GRANT 하면 ERROR 1146 으로 거부됩니다 — 실측 확인)
-- ───────────────────────────────────────────────────────────────────

-- 3-A) 마이그레이션 계정 — 표·트리거·이벤트를 모두 다뤄야 하므로 전체 권한을 줍니다.
--      ★ EVENT 권한이 꼭 필요합니다: 오래된 기록을 달마다 잘라 내는 자동 작업이
--        이 계정 이름으로 등록되어 돌기 때문입니다(V35).
GRANT ALL PRIVILEGES ON `nestpay`.* TO 'nestpay_mig'@'<API서버1_IP>';
GRANT ALL PRIVILEGES ON `nestpay`.* TO 'nestpay_mig'@'<API서버2_IP>';

-- 3-B) 서비스 계정 — 읽기·추가만 전체 허용합니다.
--      · SELECT/INSERT 를 전체로 주는 이유: 나중에 표가 늘어도 서비스가 멈추지 않습니다.
--      · UPDATE 는 전체로 주지 않습니다 — 증거 표(장부·감사기록)를 고치지 못하게 하려는 것입니다.
--        업무 표에만 골라서 주는 일은 02 번 파일이 합니다.
--      · DELETE 도 전체로 주지 않습니다 — 실제로 지우는 표에만 02 번 파일이 골라서 줍니다.
--      · 표를 만들거나 지우는 권한(DDL)은 어떤 경우에도 주지 않습니다.
GRANT SELECT, INSERT ON `nestpay`.* TO 'nestpay_app'@'<API서버1_IP>';
GRANT SELECT, INSERT ON `nestpay`.* TO 'nestpay_app'@'<API서버2_IP>';

--      프로시저 실행 권한 — 무결성 점검·파티션 관리 프로시저를 부를 수 있어야 합니다.
GRANT EXECUTE ON `nestpay`.* TO 'nestpay_app'@'<API서버1_IP>';
GRANT EXECUTE ON `nestpay`.* TO 'nestpay_app'@'<API서버2_IP>';

-- 3-C) 읽기 전용 계정 — 보기만 됩니다.
GRANT SELECT ON `nestpay`.* TO 'nestpay_ro'@'<사내망_대역>';

FLUSH PRIVILEGES;


-- ───────────────────────────────────────────────────────────────────
-- 4. 서버 설정 두 가지 — my.cnf 에 넣고 재시작해야 합니다
--
--   ① event_scheduler = ON
--      오래된 기록을 달마다 잘라 내는 자동 작업이 이 스위치로 돕니다.
--      꺼져 있으면 작업이 등록만 되고 돌지 않아 기록이 계속 쌓이고,
--      보존 기한(개인정보 파기 등)을 지킬 수 없게 됩니다.
--
--   ② default_time_zone = '+09:00'
--      우리 시스템은 모든 시각을 한국 시간 하나로만 다룹니다.
--      서버가 세계표준시로 돌면 기록이 9시간 어긋나 정산·마감이 하루 밀립니다.
-- ───────────────────────────────────────────────────────────────────


-- ═══════════════════════════════════════════════════════════
-- 02_grant_table_privileges.sql
-- ═══════════════════════════════════════════════════════════
-- ═══════════════════════════════════════════════════════════════════
--  NestPay 초기 배포 ②  표별 권한 주기            [앱을 한 번 켠 뒤에 실행]
--
--  언제 실행하나요?
--    · 처음 배포할 때 : 앱을 켜서 Flyway 가 표를 다 만든 뒤에 한 번
--    · 그 뒤로는      : 표가 새로 생기는 마이그레이션을 배포한 뒤마다 다시 한 번
--                       (여러 번 실행해도 안전합니다 — 같은 권한을 다시 줄 뿐입니다)
--
--  왜 01 번과 나눴나요?
--    표 하나하나에 주는 권한은 그 표가 실제로 있어야 줄 수 있습니다.
--    없는 표에 GRANT 하면 ERROR 1146 으로 거부됩니다(실측 확인).
--    01 번은 앱을 켜기 전에 도는 파일이라, 그때는 아직 표가 하나도 없습니다.
--
--  실행:  mysql -uroot -p nestpay < 02_grant_table_privileges.sql
--         ★ 실행 전에 아래 ★ 표시된 접속 주소 2개를 실제 값으로 바꿉니다.
-- ═══════════════════════════════════════════════════════════════════


-- ───────────────────────────────────────────────────────────────────
-- 1. 고치기(UPDATE) 권한 — 증거 표를 뺀 나머지 업무 표에만 줍니다
--
--   증거 표란?
--     "무슨 일이 있었는지"를 남기는 장부입니다. 나중에 고칠 수 있으면 증거가 아닙니다.
--     그래서 이 9개 표에는 고칠 권한 자체를 주지 않습니다.
--
--       ledger_entries    돈이 오간 원장(차변·대변)
--       audit_logs        관리자가 무엇을 했는지
--       pii_access_logs   누가 개인정보를 열어 봤는지
--       login_histories   로그인 기록
--       reject_logs       거절·차단된 시도
--       external_api_logs 외부 회사와 주고받은 내역
--       app_error_logs    앱에서 난 오류
--       lot_allocations   포인트를 어느 뭉치에서 꺼내 썼는지
--       policy_agreements 약관 동의 기록
--
--     ※ 이 중 6개(ledger_entries·audit_logs·pii_access_logs·login_histories·
--       reject_logs·external_api_logs)는 DB 안에도 막는 장치(트리거)가 걸려 있어 두 겹으로 막힙니다.
--       나머지 3개(app_error_logs·lot_allocations·policy_agreements)는 트리거가 없어
--       이 권한 설정이 유일한 방어입니다 — 빠뜨리면 안 됩니다.
--
--   왜 표를 일일이 적지 않고 자동으로 도나요?
--     표가 70개가 넘습니다. 손으로 적어 두면 나중에 표가 하나 늘 때마다 이 파일을 고쳐야 하고,
--     잊으면 그 표만 권한이 없어 서비스가 조용히 실패합니다.
--     "증거 표만 빼고 전부"라고 규칙으로 적어 두면 표가 늘어도 그대로 맞습니다.
-- ───────────────────────────────────────────────────────────────────

DROP PROCEDURE IF EXISTS sp_grant_update_except_evidence;
DELIMITER $$
CREATE PROCEDURE sp_grant_update_except_evidence(IN p_user VARCHAR(64), IN p_host VARCHAR(255))
BEGIN
  DECLARE v_done INT DEFAULT 0;
  DECLARE v_table VARCHAR(64);
  -- 증거 표 9개와 Flyway 관리표를 뺀 나머지를 하나씩 훑습니다.
  DECLARE cur CURSOR FOR
    SELECT table_name FROM information_schema.tables
     WHERE table_schema = DATABASE()
       AND table_type <> 'VIEW'
       AND table_name NOT IN (
             'ledger_entries', 'audit_logs', 'pii_access_logs', 'login_histories',
             'reject_logs', 'external_api_logs', 'app_error_logs', 'lot_allocations',
             'policy_agreements',
             'flyway_schema_history')   -- 마이그레이션 기록표: 서비스가 건드릴 일이 없습니다
     ORDER BY table_name;
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;

  OPEN cur;
  each_table: LOOP
    FETCH cur INTO v_table;
    IF v_done = 1 THEN LEAVE each_table; END IF;
    SET @sql = CONCAT('GRANT UPDATE ON `', DATABASE(), '`.`', v_table,
                      '` TO ''', p_user, '''@''', p_host, '''');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  END LOOP;
  CLOSE cur;
END$$
DELIMITER ;

CALL sp_grant_update_except_evidence('nestpay_app', '<API서버1_IP>');
CALL sp_grant_update_except_evidence('nestpay_app', '<API서버2_IP>');

DROP PROCEDURE sp_grant_update_except_evidence;


-- ───────────────────────────────────────────────────────────────────
-- 2. 지우기(DELETE) 권한 — 실제로 지우는 표에만 하나씩 줍니다
--
--   아래 20개는 프로그램이 실제로 DELETE 하는 표입니다.
--   (SQL 파일에서 "DELETE FROM"을 찾아 뽑은 목록입니다 — 짐작이 아니라 코드 기준)
--   여기 없는 표는 지울 수 없습니다. 장부·거래 기록이 사고로 사라지는 것을 막습니다.
--
--   ※ 정책·설정 표 여러 개가 목록에 있는데, 이 표들은 "이력 관리(SYSTEM VERSIONED)" 표라
--     지워도 지난 내용이 이력으로 남습니다. 즉 지워도 흔적이 사라지지 않습니다.
-- ───────────────────────────────────────────────────────────────────

-- 관리자·인증 관련
GRANT DELETE ON `nestpay`.`admin_accounts`       TO 'nestpay_app'@'<API서버1_IP>';  -- 관리자 계정 삭제(이력 남음)
GRANT DELETE ON `nestpay`.`admin_allowed_ips`    TO 'nestpay_app'@'<API서버1_IP>';  -- 관리자 접속 허용 IP 해제(이력 남음)
GRANT DELETE ON `nestpay`.`auth_passkeys`        TO 'nestpay_app'@'<API서버1_IP>';  -- 패스키 삭제(회원·관리자 요청)
GRANT DELETE ON `nestpay`.`auth_pins`            TO 'nestpay_app'@'<API서버1_IP>';  -- PIN 초기화
GRANT DELETE ON `nestpay`.`used_challenges`      TO 'nestpay_app'@'<API서버1_IP>';  -- 다 쓴 일회용 값 정리
GRANT DELETE ON `nestpay`.`rate_limit_counters`  TO 'nestpay_app'@'<API서버1_IP>';  -- 호출 횟수 임시 계수기 정리
-- 정책·설정 (이력 관리 표)
GRANT DELETE ON `nestpay`.`fee_policies`         TO 'nestpay_app'@'<API서버1_IP>';  -- 수수료 정책
GRANT DELETE ON `nestpay`.`limit_policies`       TO 'nestpay_app'@'<API서버1_IP>';  -- 한도 정책
GRANT DELETE ON `nestpay`.`cancel_policies`      TO 'nestpay_app'@'<API서버1_IP>';  -- 취소 정책
GRANT DELETE ON `nestpay`.`card_quota_policies`  TO 'nestpay_app'@'<API서버1_IP>';  -- 카드 발급 수량 정책
GRANT DELETE ON `nestpay`.`payout_policies`      TO 'nestpay_app'@'<API서버1_IP>';  -- 정산 지급 정책
GRANT DELETE ON `nestpay`.`global_settings`      TO 'nestpay_app'@'<API서버1_IP>';  -- 전역 설정
-- 회사 계좌
GRANT DELETE ON `nestpay`.`deposit_accounts`     TO 'nestpay_app'@'<API서버1_IP>';  -- 입금 통장 등록 해제
GRANT DELETE ON `nestpay`.`payout_accounts`      TO 'nestpay_app'@'<API서버1_IP>';  -- 출금 통장 등록 해제
-- 게시물·파일
GRANT DELETE ON `nestpay`.`notices`              TO 'nestpay_app'@'<API서버1_IP>';  -- 공지
GRANT DELETE ON `nestpay`.`faqs`                 TO 'nestpay_app'@'<API서버1_IP>';  -- 자주 묻는 질문
GRANT DELETE ON `nestpay`.`banners`              TO 'nestpay_app'@'<API서버1_IP>';  -- 배너
GRANT DELETE ON `nestpay`.`files`                TO 'nestpay_app'@'<API서버1_IP>';  -- 업로드 파일 기록
GRANT DELETE ON `nestpay`.`merchant_documents`   TO 'nestpay_app'@'<API서버1_IP>';  -- 매장 제출 서류 교체
-- 점검 실행 기록
GRANT DELETE ON `nestpay`.`integrity_check_runs` TO 'nestpay_app'@'<API서버1_IP>';  -- 무결성 점검 실행분 정리

-- 두 번째 서버도 똑같이 줍니다.
GRANT DELETE ON `nestpay`.`admin_accounts`       TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`admin_allowed_ips`    TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`auth_passkeys`        TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`auth_pins`            TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`used_challenges`      TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`rate_limit_counters`  TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`fee_policies`         TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`limit_policies`       TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`cancel_policies`      TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`card_quota_policies`  TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`payout_policies`      TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`global_settings`      TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`deposit_accounts`     TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`payout_accounts`      TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`notices`              TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`faqs`                 TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`banners`              TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`files`                TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`merchant_documents`   TO 'nestpay_app'@'<API서버2_IP>';
GRANT DELETE ON `nestpay`.`integrity_check_runs` TO 'nestpay_app'@'<API서버2_IP>';

FLUSH PRIVILEGES;


-- ───────────────────────────────────────────────────────────────────
-- 3. 준 권한을 눈으로 확인합니다
--    · 증거 표 9개가 "고치기 권한 없음" 으로 나와야 정상입니다.
-- ───────────────────────────────────────────────────────────────────
SELECT
  table_name                                   AS `표이름`,
  CASE WHEN SUM(privilege_type = 'UPDATE') > 0 THEN '있음' ELSE '없음' END AS `고치기`,
  CASE WHEN SUM(privilege_type = 'DELETE') > 0 THEN '있음' ELSE '없음' END AS `지우기`
FROM information_schema.table_privileges
WHERE grantee LIKE '''nestpay_app''@%' AND table_schema = 'nestpay'
GROUP BY table_name
ORDER BY `고치기`, table_name;


-- ═══════════════════════════════════════════════════════════
-- 03_post_deploy_check.sql
-- ═══════════════════════════════════════════════════════════
-- ═══════════════════════════════════════════════════════════════════
--  NestPay 초기 배포 ②  제대로 올라갔는지 확인하기
--
--  언제 쓰나요?
--    01 번(데이터베이스·계정) → 앱 켜기(Flyway 가 표 생성) → 02 번(표별 권한)
--    까지 끝낸 뒤에 마지막으로 실행합니다.
--
--  어떻게 보나요?
--    각 검사는 판정 칸에 OK 또는 확인필요 를 냅니다.
--    "확인필요" 가 하나라도 나오면 그 항목을 해결한 뒤 서비스를 열어야 합니다.
--
--  실행:  mysql -uroot -p nestpay < 03_post_deploy_check.sql
-- ═══════════════════════════════════════════════════════════════════

SELECT '───── 1. 마이그레이션이 끝까지 적용됐는가 ─────' AS `검사`;
-- 실패한 단계가 하나라도 있으면 서비스를 열면 안 됩니다.
SELECT
  COUNT(*)                                   AS `적용단계수`,
  SUM(success = 1)                           AS `성공`,
  SUM(success = 0)                           AS `실패`,
  MAX(CAST(SUBSTRING(version, 1) AS UNSIGNED)) AS `최신버전`,
  CASE WHEN SUM(success = 0) = 0 THEN 'OK' ELSE '확인필요 — 실패 단계 있음' END AS `판정`
FROM flyway_schema_history
WHERE version IS NOT NULL;

SELECT '───── 2. 글자 비교 방식이 하나로 통일됐는가 ─────' AS `검사`;
-- 서로 다르면 표끼리 글자를 맞댈 때 오류가 납니다(ERROR 1267).
SELECT
  table_collation                            AS `비교방식`,
  COUNT(*)                                   AS `표수`,
  CASE WHEN table_collation = 'utf8mb4_unicode_ci' THEN 'OK'
       ELSE '확인필요 — utf8mb4_unicode_ci 로 맞춰야 함' END AS `판정`
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_collation IS NOT NULL
GROUP BY table_collation;

SELECT '───── 3. 자동 정리 작업이 실제로 도는가 ─────' AS `검사`;
-- event_scheduler 가 꺼져 있으면 오래된 기록을 잘라 내는 작업이 등록만 되고 돌지 않습니다.
SELECT
  @@global.event_scheduler                   AS `스케줄러`,
  (SELECT COUNT(*) FROM information_schema.events
     WHERE event_schema = DATABASE() AND status = 'ENABLED') AS `켜진작업수`,
  CASE WHEN @@global.event_scheduler = 'ON'
        AND (SELECT COUNT(*) FROM information_schema.events
               WHERE event_schema = DATABASE() AND status = 'ENABLED') = 2
       THEN 'OK' ELSE '확인필요 — event_scheduler=ON 필요' END AS `판정`;

SELECT '───── 4. 서버 시각이 한국 시간인가 ─────' AS `검사`;
-- 9시간 어긋나면 정산·마감이 하루 밀립니다.
SELECT
  NOW()                                      AS `DB가_보는_현재시각`,
  @@global.time_zone                         AS `설정값`,
  CASE WHEN TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), NOW()) BETWEEN 32000 AND 32800
       THEN 'OK (UTC+9)' ELSE '확인필요 — KST 가 아님' END AS `판정`;

SELECT '───── 5. 장부를 지키는 방어 장치가 걸려 있는가 ─────' AS `검사`;
-- 원장·감사기록을 고치거나 지우지 못하게 막는 트리거입니다.
SELECT
  COUNT(*)                                   AS `트리거수`,
  CASE WHEN COUNT(*) >= 14 THEN 'OK' ELSE '확인필요 — V35 적용 확인' END AS `판정`
FROM information_schema.triggers
WHERE trigger_schema = DATABASE();

SELECT '───── 6. 서비스에 꼭 필요한 기초 데이터가 들어갔는가 ─────' AS `검사`;
SELECT '은행 목록'      AS `항목`, COUNT(*) AS `건수`, CASE WHEN COUNT(*) >= 20 THEN 'OK' ELSE '확인필요' END AS `판정` FROM banks
UNION ALL SELECT '시스템 지갑',    COUNT(*), CASE WHEN COUNT(*) = 7 THEN 'OK' ELSE '확인필요' END FROM wallets WHERE owner_type = 'SYSTEM'
UNION ALL SELECT '관리자 계정',    COUNT(*), CASE WHEN COUNT(*) >= 1 THEN 'OK' ELSE '확인필요' END FROM admin_accounts
UNION ALL SELECT '이상거래 룰',    COUNT(*), CASE WHEN COUNT(*) = 8 THEN 'OK' ELSE '확인필요' END FROM fds_rules
UNION ALL SELECT '호출빈도 규칙',  COUNT(*), CASE WHEN COUNT(*) >= 18 THEN 'OK' ELSE '확인필요' END FROM rate_limit_rules
UNION ALL SELECT '알림 설정',      COUNT(*), CASE WHEN COUNT(*) >= 8 THEN 'OK' ELSE '확인필요' END FROM notification_settings
UNION ALL SELECT '약관(시행중)',   COUNT(*), CASE WHEN COUNT(*) >= 3 THEN 'OK' ELSE '확인필요' END FROM policy_documents WHERE status = 'ACTIVE'
UNION ALL SELECT '수수료 정책',    COUNT(*), CASE WHEN COUNT(*) >= 1 THEN 'OK' ELSE '확인필요' END FROM fee_policies
UNION ALL SELECT '한도 정책',      COUNT(*), CASE WHEN COUNT(*) >= 1 THEN 'OK' ELSE '확인필요' END FROM limit_policies
UNION ALL SELECT '입금통장',       COUNT(*), CASE WHEN COUNT(*) >= 1 THEN 'OK' ELSE '★ 확인필요 — 손님이 돈을 넣을 통장이 없습니다' END FROM deposit_accounts
UNION ALL SELECT '출금통장',       COUNT(*), CASE WHEN COUNT(*) >= 1 THEN 'OK' ELSE '★ 확인필요 — 정산·출금을 보낼 통장이 없습니다' END FROM payout_accounts;

SELECT '───── 7. ★ 운영 전 반드시 바꿔야 하는 것 ─────' AS `검사`;
-- 초기 관리자 비밀번호는 소스에 공개된 값(123456)입니다. 그대로 두면 누구나 들어옵니다.
SELECT
  login_id                                   AS `관리자아이디`,
  CASE WHEN otp_enabled = 1 THEN '등록됨' ELSE '미등록' END AS `OTP`,
  CASE WHEN otp_enabled = 1 THEN 'OK'
       ELSE '★ 확인필요 — 초기 비밀번호(123456) 상태. 먼저 접속하는 사람이 OTP 를 선점합니다' END AS `판정`
FROM admin_accounts
ORDER BY id;

SELECT '───── 8. 회사 계좌가 실제 운영 계좌인가 ─────' AS `검사`;
-- 개발용 예시 계좌가 그대로 남아 있으면 손님 돈이 엉뚱한 곳으로 갑니다.
-- ※ 두 표의 글자 비교 방식이 서로 달라 UNION 이 거부될 수 있어(ERROR 1267),
--   비교 방식을 명시적으로 맞춰 줍니다. (V70 적용 후에는 없어도 되지만 그대로 두어도 안전합니다)
SELECT '입금통장' AS `구분`,
       CONVERT(bank_name  USING utf8mb4) COLLATE utf8mb4_unicode_ci AS `은행`,
       CONVERT(account_no USING utf8mb4) COLLATE utf8mb4_unicode_ci AS `계좌번호`,
       CONVERT(holder     USING utf8mb4) COLLATE utf8mb4_unicode_ci AS `예금주`,
       '★ 운영 계좌가 맞는지 눈으로 확인' AS `판정`
FROM deposit_accounts WHERE status = 'ACTIVE'
UNION ALL
-- 새 서버는 출금통장이 아직 없을 수 있습니다. 없으면 이 줄이 나오지 않으므로,
-- 위 6번 검사의 '출금통장' 건수로 등록 여부를 함께 확인하세요.
SELECT '출금통장',
       CONVERT(bank_code  USING utf8mb4) COLLATE utf8mb4_unicode_ci,
       CONVERT(account_no USING utf8mb4) COLLATE utf8mb4_unicode_ci,
       CONVERT(holder     USING utf8mb4) COLLATE utf8mb4_unicode_ci,
       CASE WHEN status = 'ACTIVE' THEN '★ 운영 계좌가 맞는지 눈으로 확인' ELSE '사용 안 함' END
FROM payout_accounts;

SELECT '───── 9. 장부 균형(차변=대변) ─────' AS `검사`;
-- 새 서버라면 둘 다 0 이어야 정상입니다.
SELECT
  IFNULL(SUM(CASE WHEN direction = 'DR' THEN amount END), 0) AS `차변`,
  IFNULL(SUM(CASE WHEN direction = 'CR' THEN amount END), 0) AS `대변`,
  CASE WHEN IFNULL(SUM(CASE WHEN direction = 'DR' THEN amount END), 0)
          = IFNULL(SUM(CASE WHEN direction = 'CR' THEN amount END), 0)
       THEN 'OK' ELSE '★★ 즉시 중단 — 장부 불일치' END AS `판정`
FROM ledger_entries;

SELECT '───── 10. 서비스 계정 권한이 설계대로인가 ─────' AS `검사`;
-- 증거 표(장부·감사기록)에 고칠 권한이 남아 있으면 안 됩니다.
-- ※ 계정을 나누지 않고 한 계정으로만 쓰는 경우(로컬 개발)에는 결과가 0줄입니다 — 정상입니다.
SELECT
  t.table_name                                 AS `증거표`,
  CASE WHEN p.table_name IS NULL THEN 'OK (고칠 수 없음)'
       ELSE '★ 확인필요 — 고칠 권한이 남아 있음' END AS `판정`
FROM (SELECT 'ledger_entries' AS table_name UNION ALL SELECT 'audit_logs'
      UNION ALL SELECT 'pii_access_logs'  UNION ALL SELECT 'login_histories'
      UNION ALL SELECT 'reject_logs'      UNION ALL SELECT 'external_api_logs'
      UNION ALL SELECT 'app_error_logs'   UNION ALL SELECT 'lot_allocations'
      UNION ALL SELECT 'policy_agreements') t
LEFT JOIN (SELECT DISTINCT table_name FROM information_schema.table_privileges
            WHERE grantee LIKE '''nestpay_app''@%' AND table_schema = 'nestpay'
              AND privilege_type = 'UPDATE') p
  ON p.table_name = t.table_name
WHERE EXISTS (SELECT 1 FROM information_schema.table_privileges
               WHERE grantee LIKE '''nestpay_app''@%' AND table_schema = 'nestpay');

SELECT '───── 11. 표가 늘었는데 권한을 안 준 곳은 없는가 ─────' AS `검사`;
-- 02 번 파일을 다시 돌리지 않으면 새로 생긴 표만 고칠 권한이 없어 조용히 실패합니다.
-- ※ 계정을 나누지 않은 경우에는 결과가 0줄입니다 — 정상입니다.
SELECT
  t.table_name                                 AS `권한없는표`,
  '★ 확인필요 — 02_grant_table_privileges.sql 을 다시 실행하세요' AS `판정`
FROM information_schema.tables t
LEFT JOIN (SELECT DISTINCT table_name FROM information_schema.table_privileges
            WHERE grantee LIKE '''nestpay_app''@%' AND table_schema = 'nestpay'
              AND privilege_type = 'UPDATE') p
  ON p.table_name = t.table_name
WHERE t.table_schema = 'nestpay' AND t.table_type <> 'VIEW'
  AND t.table_name NOT IN ('ledger_entries','audit_logs','pii_access_logs','login_histories',
                           'reject_logs','external_api_logs','app_error_logs','lot_allocations',
                           'policy_agreements','flyway_schema_history')
  AND p.table_name IS NULL
  AND EXISTS (SELECT 1 FROM information_schema.table_privileges
                WHERE grantee LIKE '''nestpay_app''@%' AND table_schema = 'nestpay');