← 문서 목록

DB 초기 배포 SQL · db/deploy

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

-- ═══════════════════════════════════════════════════════════
-- 01_create_database.sql
-- ═══════════════════════════════════════════════════════════
-- ═══════════════════════════════════════════════════════════════════
--  NPWP 초기 배포 ①  데이터베이스 만들기            [앱을 켜기 전에 실행]
--
--  이 파일은 서버에 처음 한 번만 실행합니다. (DB 관리자 권한으로 실행)
--  표(테이블)는 여기서 만들지 않습니다 — 앱이 켜질 때 Flyway 가 알아서 만듭니다.
--
--  ── 이 프로젝트의 계정 정책 (발주사 확정, 2026-08-12) ─────────────
--    발주사 DB 서버는 <b>단일 계정</b>(npwp_admin)에 NPWP 전체 권한을 줍니다.
--    그래서 우리가 계정을 여러 개(마이그레이션·서비스·읽기전용) 나누지 않습니다.
--    npwp_admin 하나로 표 생성(Flyway)과 서비스 접속을 모두 처리합니다.
--
--    ★ 그 대신 알아 둘 것: 서비스 계정이 표를 지울 수 있는 권한도 갖게 됩니다.
--      원장·감사기록의 위·변조는 DB 안의 append-only 트리거(V35)가 여전히 막지만,
--      "실수로 표 자체를 지우는 사고"에 대한 계정-레벨 방어는 없습니다.
--      계정 분리가 필요해지면 발주사에 마이그레이션/서비스/읽기전용 3계정을 요청하고,
--      git 이력의 옛 02_grant_table_privileges.sql(권한 분리)을 되살려 쓰면 됩니다.
--
--  ── 실행 방법 ────────────────────────────────────────────────
--    보통 발주사가 DB·계정을 이미 만들어 둡니다(NPWP + npwp_admin).
--    그 경우 이 파일은 실행할 필요가 없습니다 — 아래는 "직접 만들어야 할 때"의 참고입니다.
--      mysql -uroot -p < 01_create_database.sql
-- ═══════════════════════════════════════════════════════════════════

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


-- ───────────────────────────────────────────────────────────────────
-- 2. 접속 계정 (발주사가 이미 만들어 둔 경우 이 절은 건너뜁니다)
--
--    단일 계정 정책이므로 npwp_admin 하나만 만들고 NPWP 전체 권한을 줍니다.
--    ★ 접속 주소(@'...')는 실제 API 서버 IP 로 묶는 것이 안전합니다.
--      '%'(어디서든)로 두면 비밀번호만 새어도 외부에서 붙을 수 있습니다.
--      (아래는 참고용 예시입니다 — 발주사 계정을 쓰면 실행할 필요가 없습니다)
-- ───────────────────────────────────────────────────────────────────
-- CREATE USER IF NOT EXISTS 'npwp_admin'@'<API서버_IP>' IDENTIFIED BY '★비밀번호';
-- GRANT ALL PRIVILEGES ON `NPWP`.* TO 'npwp_admin'@'<API서버_IP>';
-- FLUSH PRIVILEGES;


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


-- ═══════════════════════════════════════════════════════════
-- 03_post_deploy_check.sql
-- ═══════════════════════════════════════════════════════════
-- ═══════════════════════════════════════════════════════════════════
--  NPWP 초기 배포 ②  제대로 올라갔는지 확인하기
--
--  언제 쓰나요?
--    DB(NPWP)·계정(npwp_admin) 준비 → 앱 켜기(Flyway 가 표 생성)
--    까지 끝낸 뒤에 마지막으로 실행합니다.
--
--    ※ 이 프로젝트는 단일 계정 정책(발주사 확정)이라 "표별 권한 부여" 단계가 없습니다.
--      npwp_admin 하나가 NPWP 전체 권한을 가집니다(01_create_database.sql 참고).
--
--  어떻게 보나요?
--    각 검사는 판정 칸에 OK 또는 확인필요 를 냅니다.
--    "확인필요" 가 하나라도 나오면 그 항목을 해결한 뒤 서비스를 열어야 합니다.
--    ※ 아래 검사들은 DATABASE()(지금 접속한 DB)를 기준으로 도므로,
--      NPWP 에 접속해 실행하면 자동으로 NPWP 를 점검합니다.
--
--  실행:  mysql -h <DB호스트> -P <포트> -u npwp_admin -p NPWP < 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 '───── 1-2. 표가 빠짐없이 만들어졌는가 ─────' AS `검사`;
-- ★ 세는 방법에 함정이 있습니다.
--   이력을 보존하는 표(SYSTEM VERSIONED)는 table_type 이 'BASE TABLE' 이 아닙니다.
--   그래서 BASE TABLE 만 세면 16개가 빠진 것처럼 보여 "마이그레이션이 덜 됐다"고 오판합니다.
--   아래 기준값은 <빈 데이터베이스에 처음부터 78단계를 적용해> 실제로 세어 본 값입니다(2026-08-18).
SELECT
  SUM(table_type = 'BASE TABLE')             AS `일반표`,
  SUM(table_type = 'SYSTEM VERSIONED')       AS `이력보존표`,
  COUNT(*)                                   AS `합계`,
  CASE WHEN SUM(table_type = 'BASE TABLE') = 56
        AND SUM(table_type = 'SYSTEM VERSIONED') = 16
       THEN 'OK (기준: 일반 56 · 이력보존 16 · 합계 72)'
       ELSE '확인필요 — 기준(56/16/72)과 다릅니다. 마이그레이션이 끝까지 적용됐는지 보세요' END AS `판정`
FROM information_schema.tables
WHERE table_schema = DATABASE();

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;