ERP-node/docs/리포트_문서번호_채번_시스템_설계.md

10 KiB

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

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 문서 번호 규칙 테이블

-- 문서 번호 규칙 정의
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 문서 번호 시퀀스 테이블

-- 문서 번호 시퀀스 관리 (연도/부서별 현재 번호)
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 리포트 테이블 수정

-- 기존 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 문서 번호 이력 테이블 (감사용)

-- 문서 번호 생성 이력
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)

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)

// 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 핵심 로직 (번호 생성)

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 함수

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

위치: 디자이너 헤더

<div className="document-number">
  <Label>문서 번호</Label>
  <Badge variant="outline">{documentNumber || "저장 시 자동 생성"}</Badge>
</div>

6. 동시성 제어

6.1 문제점

여러 사용자가 동시에 문서 번호를 생성할 때 중복 발생 가능성

6.2 해결 방법

PostgreSQL의 FOR UPDATE 사용

-- 시퀀스 조회 시 행 락 걸기
SELECT * FROM report_number_sequences
WHERE rule_id = $1 AND year = $2
FOR UPDATE;

트랜잭션 격리 수준

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)