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
+64
View File
@@ -0,0 +1,64 @@
import api, { unwrap } from './request';
export interface AccountingCloseResp extends Record<string, unknown> {
closeId: number;
closePeriod: string;
closeType: string;
closeTypeName?: string;
closeStatus: string;
closeStatusName?: string;
journalPrefix?: string;
journalCount?: number;
totalDebit?: number;
totalCredit?: number;
balanceDiff?: number;
closedAt?: string;
closedByName?: string;
reopenedAt?: string;
reopenedByName?: string;
reopenReason?: string;
createdAt?: string;
updatedAt?: string;
}
export interface AccountBalanceSnapshot extends Record<string, unknown> {
snapshotId: number;
closeId: number;
accountCode: string;
accountName?: string;
accountType?: string;
debitTotal: number;
creditTotal: number;
balance: number;
createdAt?: string;
}
export interface AccountingCloseSearchParam {
closePeriod?: string;
closeType?: string;
closeStatus?: string;
pageNum?: number;
pageSize?: number;
}
export interface AccountingCloseDraftReq {
closePeriod: string;
closeType: string;
}
export const accountingCloseApi = {
list: (params?: AccountingCloseSearchParam) =>
unwrap<{ list: AccountingCloseResp[]; total: number }>(
api.get('/api/accounting-closes', { params })
),
detail: (id: number) =>
unwrap<AccountingCloseResp>(api.get(`/api/accounting-closes/${id}`)),
snapshots: (id: number) =>
unwrap<AccountBalanceSnapshot[]>(api.get(`/api/accounting-closes/${id}/snapshots`)),
createDraft: (req: AccountingCloseDraftReq) =>
unwrap<number>(api.post('/api/accounting-closes/draft', req)),
close: (id: number) =>
unwrap<AccountingCloseResp>(api.post(`/api/accounting-closes/${id}/close`, {})),
reopen: (id: number, reopenReason: string) =>
unwrap<void>(api.post(`/api/accounting-closes/${id}/reopen`, { reopenReason })),
};
+40
View File
@@ -0,0 +1,40 @@
import api, { unwrap } from './request';
export interface ForecastKpiResp extends Record<string, unknown> {
forecastId: number;
forecastMonth: string;
targetOrgId?: number;
targetOrgName?: string;
metricCode: string;
metricName?: string;
forecastValue?: number;
lowerBound?: number;
upperBound?: number;
method?: string;
methodName?: string;
basePeriod?: string;
sampleCount?: number;
generatedAt?: string;
}
export interface ForecastKpiSearchParam {
forecastMonth?: string;
targetOrgId?: number;
metricCode?: string;
pageNum?: number;
pageSize?: number;
}
export interface ForecastKpiRegenerateReq {
forecastMonth: string;
method?: string;
}
export const forecastKpiApi = {
list: (params?: ForecastKpiSearchParam) =>
unwrap<{ list: ForecastKpiResp[]; total: number }>(
api.get('/api/forecast-kpi', { params })
),
regenerate: (req: ForecastKpiRegenerateReq) =>
unwrap<number>(api.post('/api/forecast-kpi/regenerate', req)),
};
+51
View File
@@ -0,0 +1,51 @@
import api, { unwrap } from './request';
export interface RetentionAnomalyResp extends Record<string, unknown> {
alertId: number;
targetType: string;
targetTypeName?: string;
targetId?: number;
targetName?: string;
alertMonth: string;
retentionBand: string;
expectedRate?: number;
actualRate?: number;
deviation?: number;
severity: string;
severityName?: string;
status: string;
statusName?: string;
note?: string;
detectedAt?: string;
reviewedAt?: string;
reviewedByName?: string;
}
export interface RetentionAnomalySearchParam {
alertMonth?: string;
targetType?: string;
retentionBand?: string;
severity?: string;
status?: string;
pageNum?: number;
pageSize?: number;
}
export interface RetentionAnomalyReviewReq {
ids: number[];
status: string;
note?: string;
}
export const retentionAnomalyApi = {
list: (params?: RetentionAnomalySearchParam) =>
unwrap<{ list: RetentionAnomalyResp[]; total: number }>(
api.get('/api/retention-anomalies', { params })
),
detail: (id: number) =>
unwrap<RetentionAnomalyResp>(api.get(`/api/retention-anomalies/${id}`)),
detect: (alertMonth: string) =>
unwrap<number>(api.post('/api/retention-anomalies/detect', { alertMonth })),
review: (req: RetentionAnomalyReviewReq) =>
unwrap<void>(api.put('/api/retention-anomalies/review', req)),
};
+61
View File
@@ -0,0 +1,61 @@
import api, { unwrap } from './request';
export interface YearEndStatementResp extends Record<string, unknown> {
statementId: number;
agentId: number;
agentName?: string;
agentCode?: string;
businessNo?: string;
orgName?: string;
statementYear: number;
statementType?: string;
statementTypeName?: string;
totalIncome: number;
totalWithheld: number;
totalLocalTax: number;
filePath?: string;
fileFormat?: string;
fileStatus?: string;
fileStatusName?: string;
generatedAt?: string;
sentAt?: string;
sentTo?: string;
createdAt?: string;
updatedAt?: string;
}
export interface YearEndStatementSearchParam {
statementYear?: number;
agentId?: number;
orgId?: number;
fileStatus?: string;
fileFormat?: string;
pageNum?: number;
pageSize?: number;
}
export interface YearEndStatementRegenerateReq {
statementYear: number;
agentIds?: number[];
statementType?: string;
}
export interface YearEndStatementSendReq {
ids: number[];
sentTo: string;
}
export const yearEndStatementApi = {
list: (params?: YearEndStatementSearchParam) =>
unwrap<{ list: YearEndStatementResp[]; total: number }>(
api.get('/api/year-end-statements', { params })
),
detail: (id: number) =>
unwrap<YearEndStatementResp>(api.get(`/api/year-end-statements/${id}`)),
regenerate: (req: YearEndStatementRegenerateReq) =>
unwrap<number>(api.post('/api/year-end-statements/regenerate', req)),
generate: (id: number, fileFormat?: string) =>
unwrap<void>(api.post(`/api/year-end-statements/${id}/generate`, { fileFormat })),
send: (req: YearEndStatementSendReq) =>
unwrap<void>(api.put('/api/year-end-statements/send', req)),
};