372 lines
10 KiB
Markdown
372 lines
10 KiB
Markdown
# 리포트 문서 번호 자동 채번 시스템 설계
|
|
|
|
## 1. 개요
|
|
|
|
리포트 관리 시스템에 체계적인 문서 번호 자동 채번 시스템을 추가하여, 기업 환경에서 문서를 추적하고 관리할 수 있도록 합니다.
|
|
|
|
## 2. 문서 번호 형식
|
|
|
|
### 2.1 기본 형식
|
|
|
|
```
|
|
{PREFIX}-{YEAR}-{SEQUENCE}
|
|
예: RPT-2024-0001, INV-2024-0123
|
|
```
|
|
|
|
### 2.2 확장 형식 (선택 사항)
|
|
|
|
```
|
|
{PREFIX}-{DEPT_CODE}-{YEAR}-{SEQUENCE}
|
|
예: RPT-SALES-2024-0001, INV-FIN-2024-0123
|
|
```
|
|
|
|
### 2.3 구성 요소
|
|
|
|
- **PREFIX**: 문서 유형 접두사 (예: RPT, INV, PO, QT)
|
|
- **DEPT_CODE**: 부서 코드 (선택 사항)
|
|
- **YEAR**: 연도 (4자리)
|
|
- **SEQUENCE**: 순차 번호 (0001부터 시작, 자릿수 설정 가능)
|
|
|
|
## 3. 데이터베이스 스키마
|
|
|
|
### 3.1 문서 번호 규칙 테이블
|
|
|
|
```sql
|
|
-- 문서 번호 규칙 정의
|
|
CREATE TABLE report_number_rules (
|
|
rule_id SERIAL PRIMARY KEY,
|
|
rule_name VARCHAR(100) NOT NULL, -- 규칙 이름
|
|
prefix VARCHAR(20) NOT NULL, -- 접두사 (RPT, INV 등)
|
|
use_dept_code BOOLEAN DEFAULT FALSE, -- 부서 코드 사용 여부
|
|
use_year BOOLEAN DEFAULT TRUE, -- 연도 사용 여부
|
|
sequence_length INTEGER DEFAULT 4, -- 순차 번호 자릿수
|
|
reset_period VARCHAR(20) DEFAULT 'YEARLY', -- 초기화 주기 (YEARLY, MONTHLY, NEVER)
|
|
separator VARCHAR(5) DEFAULT '-', -- 구분자
|
|
description TEXT, -- 설명
|
|
is_active BOOLEAN DEFAULT TRUE, -- 활성화 여부
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
created_by VARCHAR(50),
|
|
updated_by VARCHAR(50)
|
|
);
|
|
|
|
-- 기본 데이터 삽입
|
|
INSERT INTO report_number_rules (rule_name, prefix, description)
|
|
VALUES ('리포트 문서 번호', 'RPT', '일반 리포트 문서 번호 규칙');
|
|
```
|
|
|
|
### 3.2 문서 번호 시퀀스 테이블
|
|
|
|
```sql
|
|
-- 문서 번호 시퀀스 관리 (연도/부서별 현재 번호)
|
|
CREATE TABLE report_number_sequences (
|
|
sequence_id SERIAL PRIMARY KEY,
|
|
rule_id INTEGER NOT NULL REFERENCES report_number_rules(rule_id),
|
|
dept_code VARCHAR(20), -- 부서 코드 (NULL 가능)
|
|
year INTEGER NOT NULL, -- 연도
|
|
current_number INTEGER DEFAULT 0, -- 현재 번호
|
|
last_generated_at TIMESTAMP, -- 마지막 생성 시각
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE (rule_id, dept_code, year) -- 규칙+부서+연도 조합 유니크
|
|
);
|
|
```
|
|
|
|
### 3.3 리포트 테이블 수정
|
|
|
|
```sql
|
|
-- 기존 report_layout 테이블에 컬럼 추가
|
|
ALTER TABLE report_layout
|
|
ADD COLUMN document_number VARCHAR(100), -- 생성된 문서 번호
|
|
ADD COLUMN number_rule_id INTEGER REFERENCES report_number_rules(rule_id), -- 사용된 규칙
|
|
ADD COLUMN number_generated_at TIMESTAMP; -- 번호 생성 시각
|
|
|
|
-- 문서 번호 인덱스 (검색 성능)
|
|
CREATE INDEX idx_report_layout_document_number ON report_layout(document_number);
|
|
```
|
|
|
|
### 3.4 문서 번호 이력 테이블 (감사용)
|
|
|
|
```sql
|
|
-- 문서 번호 생성 이력
|
|
CREATE TABLE report_number_history (
|
|
history_id SERIAL PRIMARY KEY,
|
|
report_id INTEGER REFERENCES report_layout(id),
|
|
document_number VARCHAR(100) NOT NULL,
|
|
rule_id INTEGER REFERENCES report_number_rules(rule_id),
|
|
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
generated_by VARCHAR(50),
|
|
is_voided BOOLEAN DEFAULT FALSE, -- 번호 무효화 여부
|
|
void_reason TEXT, -- 무효화 사유
|
|
voided_at TIMESTAMP,
|
|
voided_by VARCHAR(50)
|
|
);
|
|
|
|
-- 문서 번호로 검색 인덱스
|
|
CREATE INDEX idx_report_number_history_doc_number ON report_number_history(document_number);
|
|
```
|
|
|
|
## 4. 백엔드 구현
|
|
|
|
### 4.1 서비스 레이어 (`reportNumberService.ts`)
|
|
|
|
```typescript
|
|
export class ReportNumberService {
|
|
// 문서 번호 생성
|
|
static async generateNumber(
|
|
ruleId: number,
|
|
deptCode?: string
|
|
): Promise<string>;
|
|
|
|
// 문서 번호 형식 검증
|
|
static async validateNumber(documentNumber: string): Promise<boolean>;
|
|
|
|
// 문서 번호 중복 체크
|
|
static async isDuplicate(documentNumber: string): Promise<boolean>;
|
|
|
|
// 문서 번호 무효화
|
|
static async voidNumber(
|
|
documentNumber: string,
|
|
reason: string,
|
|
userId: string
|
|
): Promise<void>;
|
|
|
|
// 특정 규칙의 다음 번호 미리보기
|
|
static async previewNextNumber(
|
|
ruleId: number,
|
|
deptCode?: string
|
|
): Promise<string>;
|
|
}
|
|
```
|
|
|
|
### 4.2 컨트롤러 (`reportNumberController.ts`)
|
|
|
|
```typescript
|
|
// GET /api/report/number-rules - 규칙 목록
|
|
// GET /api/report/number-rules/:id - 규칙 상세
|
|
// POST /api/report/number-rules - 규칙 생성
|
|
// PUT /api/report/number-rules/:id - 규칙 수정
|
|
// DELETE /api/report/number-rules/:id - 규칙 삭제
|
|
|
|
// POST /api/report/:reportId/generate-number - 문서 번호 생성
|
|
// POST /api/report/number/preview - 다음 번호 미리보기
|
|
// POST /api/report/number/void - 문서 번호 무효화
|
|
// GET /api/report/number/history/:documentNumber - 문서 번호 이력
|
|
```
|
|
|
|
### 4.3 핵심 로직 (번호 생성)
|
|
|
|
```typescript
|
|
async generateNumber(ruleId: number, deptCode?: string): Promise<string> {
|
|
// 1. 트랜잭션 시작
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// 2. 규칙 조회
|
|
const rule = await this.getRule(ruleId);
|
|
|
|
// 3. 현재 연도/월
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
|
|
// 4. 시퀀스 조회 또는 생성 (FOR UPDATE로 락)
|
|
let sequence = await this.getSequence(ruleId, deptCode, year, true);
|
|
|
|
if (!sequence) {
|
|
sequence = await this.createSequence(ruleId, deptCode, year);
|
|
}
|
|
|
|
// 5. 다음 번호 계산
|
|
const nextNumber = sequence.current_number + 1;
|
|
|
|
// 6. 문서 번호 생성
|
|
const documentNumber = this.formatNumber(rule, deptCode, year, nextNumber);
|
|
|
|
// 7. 시퀀스 업데이트
|
|
await this.updateSequence(sequence.sequence_id, nextNumber);
|
|
|
|
// 8. 커밋
|
|
await client.query('COMMIT');
|
|
|
|
return documentNumber;
|
|
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// 번호 포맷팅
|
|
private formatNumber(
|
|
rule: NumberRule,
|
|
deptCode: string | undefined,
|
|
year: number,
|
|
sequence: number
|
|
): string {
|
|
const parts = [rule.prefix];
|
|
|
|
if (rule.use_dept_code && deptCode) {
|
|
parts.push(deptCode);
|
|
}
|
|
|
|
if (rule.use_year) {
|
|
parts.push(year.toString());
|
|
}
|
|
|
|
// 0 패딩
|
|
const paddedSequence = sequence.toString().padStart(rule.sequence_length, '0');
|
|
parts.push(paddedSequence);
|
|
|
|
return parts.join(rule.separator);
|
|
}
|
|
```
|
|
|
|
## 5. 프론트엔드 구현
|
|
|
|
### 5.1 문서 번호 규칙 관리 화면
|
|
|
|
**경로**: `/admin/report/number-rules`
|
|
|
|
**기능**:
|
|
|
|
- 규칙 목록 조회
|
|
- 규칙 생성/수정/삭제
|
|
- 규칙 미리보기 (다음 번호 확인)
|
|
- 규칙 활성화/비활성화
|
|
|
|
### 5.2 리포트 목록 화면 수정
|
|
|
|
**변경 사항**:
|
|
|
|
- 문서 번호 컬럼 추가
|
|
- 문서 번호로 검색 기능
|
|
|
|
### 5.3 리포트 저장 시 번호 생성
|
|
|
|
**위치**: `ReportDesignerContext.tsx` - `saveLayout` 함수
|
|
|
|
```typescript
|
|
const saveLayout = async () => {
|
|
// 1. 새 리포트인 경우 문서 번호 자동 생성
|
|
if (reportId === "new" && !documentNumber) {
|
|
const response = await fetch(`/api/report/generate-number`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ ruleId: 1 }), // 기본 규칙
|
|
});
|
|
const { documentNumber: newNumber } = await response.json();
|
|
setDocumentNumber(newNumber);
|
|
}
|
|
|
|
// 2. 리포트 저장 (문서 번호 포함)
|
|
await saveReport({ ...reportData, documentNumber });
|
|
};
|
|
```
|
|
|
|
### 5.4 문서 번호 표시 UI
|
|
|
|
**위치**: 디자이너 헤더
|
|
|
|
```tsx
|
|
<div className="document-number">
|
|
<Label>문서 번호</Label>
|
|
<Badge variant="outline">{documentNumber || "저장 시 자동 생성"}</Badge>
|
|
</div>
|
|
```
|
|
|
|
## 6. 동시성 제어
|
|
|
|
### 6.1 문제점
|
|
|
|
여러 사용자가 동시에 문서 번호를 생성할 때 중복 발생 가능성
|
|
|
|
### 6.2 해결 방법
|
|
|
|
**PostgreSQL의 `FOR UPDATE` 사용**
|
|
|
|
```sql
|
|
-- 시퀀스 조회 시 행 락 걸기
|
|
SELECT * FROM report_number_sequences
|
|
WHERE rule_id = $1 AND year = $2
|
|
FOR UPDATE;
|
|
```
|
|
|
|
**트랜잭션 격리 수준**
|
|
|
|
```typescript
|
|
await client.query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
|
|
```
|
|
|
|
## 7. 테스트 시나리오
|
|
|
|
### 7.1 기본 기능 테스트
|
|
|
|
- [ ] 규칙 생성 → 문서 번호 생성 → 포맷 확인
|
|
- [ ] 연속 생성 시 순차 번호 증가 확인
|
|
- [ ] 연도 변경 시 시퀀스 초기화 확인
|
|
|
|
### 7.2 동시성 테스트
|
|
|
|
- [ ] 10명이 동시에 문서 번호 생성 → 중복 없음 확인
|
|
- [ ] 동일 규칙으로 100개 생성 → 순차 번호 연속성 확인
|
|
|
|
### 7.3 에러 처리
|
|
|
|
- [ ] 존재하지 않는 규칙 ID → 에러 메시지
|
|
- [ ] 비활성화된 규칙 사용 → 경고 메시지
|
|
- [ ] 시퀀스 최대값 초과 → 관리자 알림
|
|
|
|
## 8. 구현 순서
|
|
|
|
### Phase 1: 데이터베이스 (1단계)
|
|
|
|
1. 테이블 생성 SQL 작성
|
|
2. 마이그레이션 실행
|
|
3. 기본 데이터 삽입
|
|
|
|
### Phase 2: 백엔드 (2단계)
|
|
|
|
1. `reportNumberService.ts` 구현
|
|
2. `reportNumberController.ts` 구현
|
|
3. 라우트 추가
|
|
4. 단위 테스트
|
|
|
|
### Phase 3: 프론트엔드 (3단계)
|
|
|
|
1. 문서 번호 규칙 관리 화면
|
|
2. 리포트 목록 화면 수정
|
|
3. 디자이너 문서 번호 표시
|
|
4. 저장 시 자동 생성 연동
|
|
|
|
### Phase 4: 테스트 및 최적화 (4단계)
|
|
|
|
1. 통합 테스트
|
|
2. 동시성 테스트
|
|
3. 성능 최적화
|
|
4. 사용자 가이드 작성
|
|
|
|
## 9. 향후 확장
|
|
|
|
### 9.1 고급 기능
|
|
|
|
- 문서 번호 예약 기능
|
|
- 번호 건너뛰기 허용 설정
|
|
- 커스텀 포맷 지원 (정규식 기반)
|
|
- 연/월/일 단위 초기화 선택
|
|
|
|
### 9.2 통합
|
|
|
|
- 승인 완료 시점에 최종 번호 확정
|
|
- 외부 시스템과 번호 동기화
|
|
- 바코드/QR 코드 자동 생성
|
|
|
|
## 10. 보안 고려사항
|
|
|
|
- 문서 번호 생성 권한 제한
|
|
- 번호 무효화 감사 로그
|
|
- 시퀀스 직접 수정 방지
|
|
- API 호출 횟수 제한 (Rate Limiting)
|
|
|