feat: P5-W3 본론 완료 — 회계결산/이상치예측/연말명세서 (V74~V78)

P5-W2 원천세·부가세 분기신고 미커밋분 + P5-W3 메뉴/권한 보강(V74) + P5-W3 본론(V75~V78)을 일괄 정리.

- V75 accounting_close / account_balance_snapshot (회계 결산)
- V76 retention_anomaly_alert / forecast_kpi_monthly (이상치·예측 KPI)
- V77 year_end_statement (연말 지급명세서)
- V78 P5-W3 본론 메뉴 4 + 권한 + 공통코드 7그룹 + 테스트데이터
- api: AccountingClose/YearEndStatement/ForecastKpi/RetentionAnomaly Service+Controller
- batch: BatchConfig Step 11/12 (원천세·부가세 분기) 연결
- frontend: 4화면(AccountingClose/YearEndStatement/RetentionAnomaly/ForecastKpi) + API 모듈 4 + 라우트
- frontend: usePermission/PermissionButton EXECUTE 권한 누락 보강
- fix: YearEndStatementMapper.xml 미존재 컬럼 a.agent_code 참조 제거

통합검증: Flyway V74~V78 운영DB 적용(v78), 4모듈 compileJava + 프론트 build PASS,
스모크 GET 8/8 + 쓰기 4/4 PASS, verify_v74.py 20/20 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-21 22:22:34 +09:00
parent d5ed642c80
commit 49f08f001d
80 changed files with 4506 additions and 62 deletions
@@ -0,0 +1,30 @@
-- V73: P5-W2 WITHHOLDING_TAX / VAT_REPORT 메뉴 EXECUTE 권한 누락 보강
-- V69에서 READ/CREATE/UPDATE/EXPORT만 등록되어 regenerate/complete 엔드포인트 403 발생
-- (P5-W1 ANNUAL_INCOME 의 V72 와 동일 패턴)
-- SUPER_ADMIN / ADMIN 역할에 EXECUTE 권한 부여
-- 1. menu_permission에 EXECUTE 항목 추가
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, 'EXECUTE', '실행', 70
FROM menu m
WHERE m.menu_code IN ('WITHHOLDING_TAX', 'VAT_REPORT')
AND NOT EXISTS (
SELECT 1 FROM menu_permission mp2
WHERE mp2.menu_id = m.menu_id AND mp2.perm_code = 'EXECUTE'
);
-- 2. SUPER_ADMIN / ADMIN에 EXECUTE 권한 부여
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
AND mp.perm_code = 'EXECUTE'
AND m.menu_code IN ('WITHHOLDING_TAX', 'VAT_REPORT')
AND NOT EXISTS (
SELECT 1 FROM role_menu_permission rmp2
WHERE rmp2.role_id = r.role_id
AND rmp2.menu_id = mp.menu_id
AND rmp2.perm_code = 'EXECUTE'
);
@@ -0,0 +1,169 @@
-- V74: 메뉴/권한 전수보강 (AUDIT_P5W3.md §4 갭 리포트 기반)
-- 작성: 2026-05-17
--
-- 작업 범위 (총 20건):
-- (b) menu_permission INSERT 3건: NOTICE/DELETE, SYSTEM_DATA_DICT/DELETE, TERM_SETTLE/APPROVE
-- (c) role_menu_permission INSERT 6건: 위 3 perm × {SUPER_ADMIN, ADMIN}
-- (route) menu UPDATE menu_path + component_path 11건: §4-4 표 기준 App.tsx 라우트 목표값
--
-- ※ 의도적 제외: ADMIN/SYSTEM_ROLE(READ/CREATE/UPDATE/DELETE 4건) +
-- ADMIN/SYSTEM_USER(READ/CREATE/UPDATE 3건) = 7건은 V74에서 보강하지 않는다.
-- 사유: V16에서 ADMIN 역할은 GRP_SYSTEM 계열 역할/사용자 관리 메뉴로부터 의도적으로 배제됨.
-- (역할·사용자 관리는 SUPER_ADMIN 전용 보안 경계)
-- 이를 "정합 보강"으로 ADMIN에 부여하면 권한상승 회귀에 해당한다.
-- SUPER_ADMIN은 §4-3 기준 이미 해당 grant를 보유하여 기능상 문제 없음.
-- ============================================================
-- 1. menu_permission INSERT — 누락된 perm 3건
-- 패턴: V72/V73 과 동일한 NOT EXISTS 서브쿼리
-- ============================================================
-- NOTICE / DELETE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, 'DELETE', '삭제', 40
FROM menu m
WHERE m.menu_code = 'NOTICE'
AND NOT EXISTS (
SELECT 1 FROM menu_permission mp
WHERE mp.menu_id = m.menu_id AND mp.perm_code = 'DELETE'
);
-- SYSTEM_DATA_DICT / DELETE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, 'DELETE', '삭제', 40
FROM menu m
WHERE m.menu_code = 'SYSTEM_DATA_DICT'
AND NOT EXISTS (
SELECT 1 FROM menu_permission mp
WHERE mp.menu_id = m.menu_id AND mp.perm_code = 'DELETE'
);
-- TERM_SETTLE / APPROVE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, 'APPROVE', '승인', 50
FROM menu m
WHERE m.menu_code = 'TERM_SETTLE'
AND NOT EXISTS (
SELECT 1 FROM menu_permission mp
WHERE mp.menu_id = m.menu_id AND mp.perm_code = 'APPROVE'
);
-- ============================================================
-- 2. role_menu_permission INSERT — 위 3 perm × {SUPER_ADMIN, ADMIN} = 6건
-- 패턴: ON CONFLICT DO NOTHING (role_id, menu_id, perm_code UNIQUE)
-- ============================================================
-- NOTICE / DELETE — SUPER_ADMIN + ADMIN
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
AND m.menu_code = 'NOTICE'
AND mp.perm_code = 'DELETE'
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- SYSTEM_DATA_DICT / DELETE — SUPER_ADMIN + ADMIN
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
AND m.menu_code = 'SYSTEM_DATA_DICT'
AND mp.perm_code = 'DELETE'
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- TERM_SETTLE / APPROVE — SUPER_ADMIN + ADMIN
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
AND m.menu_code = 'TERM_SETTLE'
AND mp.perm_code = 'APPROVE'
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 3. menu UPDATE — menu_path / component_path 불일치 11건
-- App.tsx 실제 라우트를 정본으로 UPDATE
-- ============================================================
-- commission/reinstatement (REINSTATE_COMM)
UPDATE menu
SET menu_path = '/commission/reinstatement',
component_path = 'commission/ReinstatementCommission'
WHERE menu_code = 'REINSTATE_COMM'
AND menu_path != '/commission/reinstatement';
-- commission/persistency (PERSIST_BONUS)
UPDATE menu
SET menu_path = '/commission/persistency',
component_path = 'commission/PersistencyBonus'
WHERE menu_code = 'PERSIST_BONUS'
AND menu_path != '/commission/persistency';
-- commission/override-ledger (OVERRIDE_LEDGER)
UPDATE menu
SET menu_path = '/commission/override-ledger',
component_path = 'commission/OverrideLedger'
WHERE menu_code = 'OVERRIDE_LEDGER'
AND menu_path != '/commission/override-ledger';
-- agent/contracts (AGENT_CONTRACT)
UPDATE menu
SET menu_path = '/agent/contracts',
component_path = 'agent/AgentContracts'
WHERE menu_code = 'AGENT_CONTRACT'
AND menu_path != '/agent/contracts';
-- agent/licenses (AGENT_LICENSE)
UPDATE menu
SET menu_path = '/agent/licenses',
component_path = 'agent/AgentLicenses'
WHERE menu_code = 'AGENT_LICENSE'
AND menu_path != '/agent/licenses';
-- agent/trainings (AGENT_TRAINING)
UPDATE menu
SET menu_path = '/agent/trainings',
component_path = 'agent/AgentTrainings'
WHERE menu_code = 'AGENT_TRAINING'
AND menu_path != '/agent/trainings';
-- contracts/endorsements (ENDORSE)
UPDATE menu
SET menu_path = '/contracts/endorsements',
component_path = 'contracts/ContractEndorsements'
WHERE menu_code = 'ENDORSE'
AND menu_path != '/contracts/endorsements';
-- contracts/complaints (COMPLAINT)
UPDATE menu
SET menu_path = '/contracts/complaints',
component_path = 'contracts/Complaints'
WHERE menu_code = 'COMPLAINT'
AND menu_path != '/contracts/complaints';
-- system/approvals (APPROVAL)
UPDATE menu
SET menu_path = '/system/approvals',
component_path = 'system/Approvals'
WHERE menu_code = 'APPROVAL'
AND menu_path != '/system/approvals';
-- system/notices (NOTICE)
UPDATE menu
SET menu_path = '/system/notices',
component_path = 'system/Notices'
WHERE menu_code = 'NOTICE'
AND menu_path != '/system/notices';
-- payments/withdraw (WITHDRAW)
UPDATE menu
SET menu_path = '/payments/withdraw',
component_path = 'payments/WithdrawRequest'
WHERE menu_code = 'WITHDRAW'
AND menu_path != '/payments/withdraw';
@@ -0,0 +1,58 @@
-- V75: 회계 결산 (Accounting Close)
-- period_close(V29 운영 마감)와 별도로 월/분기/연 단위 회계 마감을 관리한다.
-- 결산 시 contract_accounting_entry.journal_no 를 채번해 미전기→전기 처리하고,
-- 결산 시점의 계정과목별 잔액을 account_balance_snapshot 으로 보존한다.
-- ============================================================
-- 1. accounting_close: 결산 헤더
-- ============================================================
CREATE TABLE accounting_close (
close_id BIGSERIAL PRIMARY KEY,
close_period VARCHAR(10) NOT NULL, -- YYYY-MM / YYYY-Qn / YYYY
close_type VARCHAR(10) NOT NULL, -- MONTHLY / QUARTERLY / YEARLY
close_status VARCHAR(10) NOT NULL DEFAULT 'DRAFT', -- DRAFT / CLOSED / REOPENED
journal_prefix VARCHAR(20) NOT NULL, -- 분개번호 채번 prefix (예: 2025-03)
journal_count INTEGER NOT NULL DEFAULT 0, -- 채번된 분개 수
total_debit NUMERIC(18,2) NOT NULL DEFAULT 0,
total_credit NUMERIC(18,2) NOT NULL DEFAULT 0,
closed_at TIMESTAMP,
closed_by BIGINT,
reopened_at TIMESTAMP,
reopened_by BIGINT,
reopen_reason VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT now(),
created_by BIGINT,
updated_at TIMESTAMP NOT NULL DEFAULT now(),
updated_by BIGINT,
UNIQUE (close_period, close_type),
CONSTRAINT chk_acc_type CHECK (close_type IN ('MONTHLY','QUARTERLY','YEARLY')),
CONSTRAINT chk_acc_status CHECK (close_status IN ('DRAFT','CLOSED','REOPENED'))
);
COMMENT ON TABLE accounting_close IS '회계 결산 헤더';
COMMENT ON COLUMN accounting_close.close_period IS '결산 기간 키 (월: YYYY-MM, 분기: YYYY-Q1, 연: YYYY)';
COMMENT ON COLUMN accounting_close.close_status IS 'DRAFT=초안, CLOSED=마감완료, REOPENED=재오픈';
COMMENT ON COLUMN accounting_close.journal_prefix IS '분개 채번 prefix (월결산 시 YYYY-MM)';
CREATE INDEX idx_ac_period ON accounting_close(close_period);
CREATE INDEX idx_ac_status ON accounting_close(close_status);
-- ============================================================
-- 2. account_balance_snapshot: 결산 시점 잔액 스냅샷
-- ============================================================
CREATE TABLE account_balance_snapshot (
snapshot_id BIGSERIAL PRIMARY KEY,
close_id BIGINT NOT NULL REFERENCES accounting_close(close_id) ON DELETE CASCADE,
account_code VARCHAR(20) NOT NULL, -- account_code FK 미강제(미시드 코드도 허용)
account_name VARCHAR(100),
account_type VARCHAR(10), -- DEBIT/CREDIT/BOTH 캐시
debit_total NUMERIC(18,2) NOT NULL DEFAULT 0,
credit_total NUMERIC(18,2) NOT NULL DEFAULT 0,
balance NUMERIC(18,2) NOT NULL DEFAULT 0, -- debit_total - credit_total
created_at TIMESTAMP NOT NULL DEFAULT now(),
UNIQUE (close_id, account_code)
);
COMMENT ON TABLE account_balance_snapshot IS '결산 시점 계정과목별 잔액 스냅샷';
COMMENT ON COLUMN account_balance_snapshot.balance IS '차변합 - 대변합';
CREATE INDEX idx_abs_close ON account_balance_snapshot(close_id);
CREATE INDEX idx_abs_account ON account_balance_snapshot(account_code);
@@ -0,0 +1,68 @@
-- V76: 13M/25M 유지율 이상치 탐지 + 차월 KPI 예측
-- mv_retention_rate (V37) 값을 input 으로 직전 N개월 평균 대비 편차를 계산한다.
-- ============================================================
-- 1. retention_anomaly_alert: 유지율 이상 알림
-- ============================================================
CREATE TABLE retention_anomaly_alert (
alert_id BIGSERIAL PRIMARY KEY,
target_type VARCHAR(10) NOT NULL, -- AGENT / ORG / ALL
target_id BIGINT, -- target_type=ALL 이면 NULL
alert_month VARCHAR(7) NOT NULL, -- YYYY-MM
retention_band VARCHAR(5) NOT NULL, -- 13M / 25M
expected_rate NUMERIC(7,4), -- 직전 3개월 평균
actual_rate NUMERIC(7,4), -- 현재 월
deviation NUMERIC(7,4), -- actual - expected
severity VARCHAR(5) NOT NULL, -- LOW / MED / HIGH
status VARCHAR(10) NOT NULL DEFAULT 'NEW', -- NEW / REVIEWED / RESOLVED
note VARCHAR(300),
detected_at TIMESTAMP NOT NULL DEFAULT now(),
reviewed_at TIMESTAMP,
reviewed_by BIGINT,
CONSTRAINT chk_raa_type CHECK (target_type IN ('AGENT','ORG','ALL')),
CONSTRAINT chk_raa_band CHECK (retention_band IN ('13M','25M')),
CONSTRAINT chk_raa_severity CHECK (severity IN ('LOW','MED','HIGH')),
CONSTRAINT chk_raa_status CHECK (status IN ('NEW','REVIEWED','RESOLVED'))
);
COMMENT ON TABLE retention_anomaly_alert IS '13M/25M 유지율 이상치 알림';
-- target_id NULL 허용을 위한 부분 UNIQUE (ALL 스코프) / 일반 UNIQUE
CREATE UNIQUE INDEX uq_raa_target
ON retention_anomaly_alert(target_type, target_id, alert_month, retention_band)
WHERE target_id IS NOT NULL;
CREATE UNIQUE INDEX uq_raa_all
ON retention_anomaly_alert(target_type, alert_month, retention_band)
WHERE target_id IS NULL;
CREATE INDEX idx_raa_month ON retention_anomaly_alert(alert_month);
CREATE INDEX idx_raa_severity ON retention_anomaly_alert(severity, status);
-- ============================================================
-- 2. forecast_kpi_monthly: 차월 KPI 예측 (단순 이동평균)
-- ============================================================
CREATE TABLE forecast_kpi_monthly (
forecast_id BIGSERIAL PRIMARY KEY,
forecast_month VARCHAR(7) NOT NULL, -- 예측 대상 월 YYYY-MM
target_org_id BIGINT, -- NULL = 전사
metric_code VARCHAR(30) NOT NULL, -- MONTHLY_PREMIUM / MONTHLY_PAYOUT / RETENTION_13M / RETENTION_25M
forecast_value NUMERIC(18,4),
lower_bound NUMERIC(18,4), -- 평균 - 1σ
upper_bound NUMERIC(18,4), -- 평균 + 1σ
method VARCHAR(20) NOT NULL DEFAULT 'SMA3', -- SMA3 / SMA6 / NAIVE
base_period VARCHAR(30), -- 산출 기준 (예: 2025-01~2025-03)
sample_count INTEGER, -- 산출에 사용된 표본수
generated_at TIMESTAMP NOT NULL DEFAULT now()
);
COMMENT ON TABLE forecast_kpi_monthly IS '차월 KPI 예측 (이동평균 기반)';
COMMENT ON COLUMN forecast_kpi_monthly.metric_code IS 'MONTHLY_PREMIUM/MONTHLY_PAYOUT/RETENTION_13M/RETENTION_25M';
CREATE INDEX idx_fkm_month ON forecast_kpi_monthly(forecast_month);
CREATE INDEX idx_fkm_metric ON forecast_kpi_monthly(metric_code);
-- 부분 UNIQUE: 전사(NULL) / 조직별
CREATE UNIQUE INDEX uq_fkm_org
ON forecast_kpi_monthly(forecast_month, target_org_id, metric_code)
WHERE target_org_id IS NOT NULL;
CREATE UNIQUE INDEX uq_fkm_all
ON forecast_kpi_monthly(forecast_month, metric_code)
WHERE target_org_id IS NULL;
@@ -0,0 +1,33 @@
-- V77: 설계사 연말 지급명세서 (사업소득 지급명세서)
-- agent_annual_income(V63) 의 연 집계 데이터를 기반으로 설계사별 PDF/Excel 발급 마스터.
CREATE TABLE year_end_statement (
statement_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
statement_year INTEGER NOT NULL, -- 신고 연도 (예: 2025)
statement_type VARCHAR(20) NOT NULL DEFAULT 'INCOME_DEDUCTION',
-- INCOME_DEDUCTION=사업소득 지급명세서
total_income NUMERIC(18,2) NOT NULL DEFAULT 0, -- agent_annual_income.total_income 캐시
total_withheld NUMERIC(18,2) NOT NULL DEFAULT 0, -- agent_annual_income.total_withheld 캐시
total_local_tax NUMERIC(18,2) NOT NULL DEFAULT 0,
file_path VARCHAR(500), -- 생성된 PDF/Excel 경로
file_format VARCHAR(10) NOT NULL DEFAULT 'XLSX', -- XLSX / PDF
file_status VARCHAR(15) NOT NULL DEFAULT 'DRAFT', -- DRAFT / GENERATED / SENT
generated_at TIMESTAMP,
sent_at TIMESTAMP,
sent_to VARCHAR(200), -- 발송처(이메일/내부공유 등)
created_at TIMESTAMP NOT NULL DEFAULT now(),
created_by BIGINT,
updated_at TIMESTAMP NOT NULL DEFAULT now(),
updated_by BIGINT,
UNIQUE (agent_id, statement_year, statement_type),
CONSTRAINT chk_yes_format CHECK (file_format IN ('XLSX','PDF')),
CONSTRAINT chk_yes_status CHECK (file_status IN ('DRAFT','GENERATED','SENT'))
);
COMMENT ON TABLE year_end_statement IS '설계사 연말 지급명세서 발급 마스터';
COMMENT ON COLUMN year_end_statement.statement_type IS 'INCOME_DEDUCTION=사업소득 지급명세서';
COMMENT ON COLUMN year_end_statement.file_status IS 'DRAFT=대기, GENERATED=파일생성, SENT=발송완료';
CREATE INDEX idx_yes_year ON year_end_statement(statement_year);
CREATE INDEX idx_yes_status ON year_end_statement(file_status);
CREATE INDEX idx_yes_agent ON year_end_statement(agent_id, statement_year);
@@ -0,0 +1,160 @@
-- V78: P5-W3 본론(결산 / 이상치·예측 / 연말명세서) 메뉴·권한·공통코드 일괄 등록
-- 신규 메뉴 4개:
-- ACCOUNTING_CLOSE (GRP_COMMISSION 하위) — 회계 결산
-- YEAR_END_STMT (GRP_COMMISSION 하위) — 연말 지급명세서
-- RETENTION_ANOMALY (GRP_KPI 하위) — 유지율 이상치
-- FORECAST_KPI (GRP_KPI 하위) — 차월 KPI 예측
-- ============================================================
-- 1. 메뉴 INSERT
-- ============================================================
INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order, is_visible, is_active)
SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort, 'Y', 'Y'
FROM menu p
JOIN (VALUES
('ACCOUNTING_CLOSE', '회계 결산', '/commission/accounting-close', 'accounting/AccountingCloseList', 130),
('YEAR_END_STMT', '연말 지급명세서', '/commission/year-end-statement', 'income/YearEndStatementList', 140)
) AS x(code, name, path, cmpt, sort) ON p.menu_code = 'GRP_COMMISSION'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order, is_visible, is_active)
SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort, 'Y', 'Y'
FROM menu p
JOIN (VALUES
('RETENTION_ANOMALY', '유지율 이상치', '/kpi/retention-anomaly', 'kpi/RetentionAnomalyList', 50),
('FORECAST_KPI', '차월 KPI 예측', '/kpi/forecast', 'kpi/ForecastKpiList', 60)
) AS x(code, name, path, cmpt, sort) ON p.menu_code = 'GRP_KPI'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
-- ============================================================
-- 2. 권한 항목 등록 (perm)
-- ============================================================
-- ACCOUNTING_CLOSE: READ / CREATE / EXECUTE / EXPORT / APPROVE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, p.code, p.name, p.sort
FROM menu m
CROSS JOIN (VALUES
('READ', '조회', 10),
('CREATE', '생성', 20),
('EXECUTE', '실행', 30),
('EXPORT', '엑셀', 40),
('APPROVE', '승인', 50)
) AS p(code, name, sort)
WHERE m.menu_code = 'ACCOUNTING_CLOSE'
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- YEAR_END_STMT: READ / EXECUTE / EXPORT / UPDATE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, p.code, p.name, p.sort
FROM menu m
CROSS JOIN (VALUES
('READ', '조회', 10),
('UPDATE', '수정', 30),
('EXECUTE', '발급', 40),
('EXPORT', '엑셀', 60)
) AS p(code, name, sort)
WHERE m.menu_code = 'YEAR_END_STMT'
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- RETENTION_ANOMALY: READ / UPDATE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, p.code, p.name, p.sort
FROM menu m
CROSS JOIN (VALUES
('READ', '조회', 10),
('UPDATE', '검토', 30)
) AS p(code, name, sort)
WHERE m.menu_code = 'RETENTION_ANOMALY'
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- FORECAST_KPI: READ / EXECUTE
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, p.code, p.name, p.sort
FROM menu m
CROSS JOIN (VALUES
('READ', '조회', 10),
('EXECUTE', '재산출', 30)
) AS p(code, name, sort)
WHERE m.menu_code = 'FORECAST_KPI'
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 3. 역할별 권한 부여
-- ============================================================
-- SUPER_ADMIN / ADMIN: 전체
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
AND m.menu_code IN ('ACCOUNTING_CLOSE', 'YEAR_END_STMT', 'RETENTION_ANOMALY', 'FORECAST_KPI')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- MANAGER: READ + EXPORT만
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
FROM role r
CROSS JOIN menu_permission mp
JOIN menu m ON m.menu_id = mp.menu_id
WHERE r.role_code = 'MANAGER'
AND mp.perm_code IN ('READ', 'EXPORT')
AND m.menu_code IN ('ACCOUNTING_CLOSE', 'YEAR_END_STMT', 'RETENTION_ANOMALY', 'FORECAST_KPI')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 4. 공통코드 그룹 + 상세 등록
-- ============================================================
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
('ACC_CLOSE_TYPE', '회계결산구분', 'MONTHLY/QUARTERLY/YEARLY', 'Y', 400),
('ACC_CLOSE_STATUS', '회계결산상태', 'DRAFT/CLOSED/REOPENED', 'Y', 410),
('YES_STATUS', '연말명세서상태', 'DRAFT/GENERATED/SENT', 'Y', 420),
('YES_FORMAT', '연말명세서포맷', 'XLSX/PDF', 'Y', 430),
('ANOMALY_SEVERITY', '이상치심각도', 'LOW/MED/HIGH', 'Y', 440),
('ANOMALY_STATUS', '이상치상태', 'NEW/REVIEWED/RESOLVED', 'Y', 450),
('FORECAST_METHOD', '예측기법', 'SMA3/SMA6/NAIVE', 'Y', 460)
ON CONFLICT (group_code) DO NOTHING;
INSERT INTO common_code (group_code, code, code_name, sort_order, is_active)
VALUES
('ACC_CLOSE_TYPE', 'MONTHLY', '월결산', 10, 'Y'),
('ACC_CLOSE_TYPE', 'QUARTERLY', '분기결산', 20, 'Y'),
('ACC_CLOSE_TYPE', 'YEARLY', '연결산', 30, 'Y'),
('ACC_CLOSE_STATUS', 'DRAFT', '초안', 10, 'Y'),
('ACC_CLOSE_STATUS', 'CLOSED', '마감완료', 20, 'Y'),
('ACC_CLOSE_STATUS', 'REOPENED', '재오픈', 30, 'Y'),
('YES_STATUS', 'DRAFT', '대기', 10, 'Y'),
('YES_STATUS', 'GENERATED', '파일생성', 20, 'Y'),
('YES_STATUS', 'SENT', '발송완료', 30, 'Y'),
('YES_FORMAT', 'XLSX', 'Excel', 10, 'Y'),
('YES_FORMAT', 'PDF', 'PDF', 20, 'Y'),
('ANOMALY_SEVERITY', 'LOW', '낮음', 10, 'Y'),
('ANOMALY_SEVERITY', 'MED', '보통', 20, 'Y'),
('ANOMALY_SEVERITY', 'HIGH', '높음', 30, 'Y'),
('ANOMALY_STATUS', 'NEW', '신규', 10, 'Y'),
('ANOMALY_STATUS', 'REVIEWED', '검토중', 20, 'Y'),
('ANOMALY_STATUS', 'RESOLVED', '완료', 30, 'Y'),
('FORECAST_METHOD', 'SMA3', '3개월 이동평균', 10, 'Y'),
('FORECAST_METHOD', 'SMA6', '6개월 이동평균', 20, 'Y'),
('FORECAST_METHOD', 'NAIVE', '단순 직전월', 30, 'Y')
ON CONFLICT (group_code, code) DO NOTHING;
-- ============================================================
-- 5. 테스트 데이터 (소량) — accounting_close 1건 / year_end_statement 1건
-- ============================================================
INSERT INTO accounting_close (close_period, close_type, close_status, journal_prefix, journal_count, total_debit, total_credit, created_at)
VALUES ('2025-03', 'MONTHLY', 'DRAFT', '2025-03', 0, 0, 0, now())
ON CONFLICT (close_period, close_type) DO NOTHING;
INSERT INTO year_end_statement (agent_id, statement_year, statement_type, total_income, total_withheld, total_local_tax, file_status)
SELECT a.agent_id, 2025, 'INCOME_DEDUCTION',
COALESCE(ai.total_commission, 0),
COALESCE(ai.total_tax_withheld, 0),
COALESCE(ai.total_local_tax, 0),
'DRAFT'
FROM agent a
LEFT JOIN agent_annual_income ai ON ai.agent_id = a.agent_id AND ai.settle_year = 2025
WHERE NOT EXISTS (
SELECT 1 FROM year_end_statement yes
WHERE yes.agent_id = a.agent_id AND yes.statement_year = 2025 AND yes.statement_type = 'INCOME_DEDUCTION'
);