# 리포트 문서 번호 자동 채번 시스템 설계

## 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)

