ERP-node/docs/권한_체계_가이드.md

16 KiB
Raw Permalink Blame History

3단계 권한 체계 가이드

📋 목차

  1. 권한 체계 개요
  2. 권한 레벨 상세
  3. 데이터베이스 설정
  4. 백엔드 구현
  5. 프론트엔드 구현
  6. 실무 예제
  7. FAQ

권한 체계 개요

3단계 권한 구조

┌────────────────────┬──────────────┬─────────────────┬────────────────────────┐
│ 권한 레벨          │ company_code │ user_type       │ 접근 범위              │
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
│ 최고 관리자        │ *            │ SUPER_ADMIN     │ ✅ 전체 회사 데이터    │
│ (Super Admin)      │              │                 │ ✅ DDL 실행 권한       │
│                    │              │                 │ ✅ 회사 생성/삭제      │
│                    │              │                 │ ✅ 시스템 설정         │
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
│ 회사 관리자        │ 20           │ COMPANY_ADMIN   │ ✅ 자기 회사 데이터    │
│ (Company Admin)    │              │                 │ ✅ 회사 사용자 관리    │
│                    │              │                 │ ✅ 회사 설정 변경      │
│                    │              │                 │ ❌ DDL 실행 불가       │
│                    │              │                 │ ❌ 타회사 접근 불가    │
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
│ 일반 사용자        │ 20           │ USER            │ ✅ 자기 회사 데이터    │
│ (User)             │              │                 │ ❌ 사용자 관리 불가    │
│                    │              │                 │ ❌ 설정 변경 불가      │
└────────────────────┴──────────────┴─────────────────┴────────────────────────┘

핵심 원칙

  1. company_code = "*" → 전체 시스템 접근 (슈퍼관리자 전용)
  2. company_code = "특정코드" → 해당 회사만 접근
  3. user_type → 회사 내 권한 레벨 결정

권한 레벨 상세

1 슈퍼관리자 (SUPER_ADMIN)

조건:

  • company_code = '*'
  • user_type = 'SUPER_ADMIN'

권한:

  • 모든 회사 데이터 조회/수정
  • DDL 실행 (CREATE TABLE, ALTER TABLE 등)
  • 회사 생성/삭제
  • 시스템 설정 변경
  • 모든 사용자 관리
  • 코드 관리, 템플릿 관리 등 전역 설정

사용 사례:

  • 시스템 전체 관리자
  • 데이터베이스 스키마 변경
  • 새로운 회사 추가
  • 전사 공통 설정 관리

계정 예시:

INSERT INTO user_info (user_id, user_name, company_code, user_type)
VALUES ('super_admin', '시스템 관리자', '*', 'SUPER_ADMIN');

2 회사 관리자 (COMPANY_ADMIN)

조건:

  • company_code = '특정 회사 코드' (예: '20')
  • user_type = 'COMPANY_ADMIN'

권한:

  • 자기 회사 데이터 조회/수정
  • 자기 회사 사용자 관리 (추가/수정/삭제)
  • 자기 회사 설정 변경
  • 자기 회사 대시보드/화면 관리
  • DDL 실행 불가
  • 타 회사 데이터 접근 불가
  • 시스템 전역 설정 변경 불가

사용 사례:

  • 각 회사의 IT 관리자
  • 회사 내 사용자 계정 관리
  • 회사별 커스터마이징 설정

계정 예시:

INSERT INTO user_info (user_id, user_name, company_code, user_type)
VALUES ('company_admin_20', '회사20 관리자', '20', 'COMPANY_ADMIN');

3 일반 사용자 (USER)

조건:

  • company_code = '특정 회사 코드' (예: '20')
  • user_type = 'USER'

권한:

  • 자기 회사 데이터 조회/수정
  • 자신이 만든 화면/대시보드 관리
  • 사용자 관리 불가
  • 회사 설정 변경 불가
  • 타 회사 데이터 접근 불가

사용 사례:

  • 일반 업무 사용자
  • 데이터 입력/조회
  • 개인 대시보드 생성

계정 예시:

INSERT INTO user_info (user_id, user_name, company_code, user_type)
VALUES ('user_kim', '김철수', '20', 'USER');

데이터베이스 설정

마이그레이션 실행

# 권한 체계 마이그레이션 실행
psql -U postgres -d your_database -f db/migrations/026_add_user_type_hierarchy.sql

주요 변경사항

  1. 코드 테이블 업데이트:

    • ADMINCOMPANY_ADMIN 으로 변경
    • SUPER_ADMIN 신규 추가
  2. PostgreSQL 함수 추가:

    • is_super_admin(user_id) - 슈퍼관리자 확인
    • is_company_admin(user_id, company_code) - 회사 관리자 확인
    • can_access_company_data(user_id, company_code) - 데이터 접근 권한
  3. 권한 뷰 생성:

    • v_user_permissions - 사용자별 권한 요약

백엔드 구현

1. 권한 체크 유틸리티 사용

import {
  isSuperAdmin,
  isCompanyAdmin,
  isAdmin,
  canExecuteDDL,
  canAccessCompanyData,
  canManageUsers,
} from "../utils/permissionUtils";

// 슈퍼관리자 확인
if (isSuperAdmin(req.user)) {
  // 전체 데이터 조회
}

// 회사 데이터 접근 권한 확인
if (canAccessCompanyData(req.user, targetCompanyCode)) {
  // 해당 회사 데이터 조회
}

// 사용자 관리 권한 확인
if (canManageUsers(req.user, targetCompanyCode)) {
  // 사용자 추가/수정/삭제
}

2. 미들웨어 사용

import {
  requireSuperAdmin,
  requireAdmin,
  requireCompanyAccess,
  requireUserManagement,
  requireDDLPermission,
} from "../middleware/permissionMiddleware";

// 슈퍼관리자 전용 엔드포인트
router.post(
  "/api/admin/ddl/execute",
  authenticate,
  requireDDLPermission,
  ddlController.execute
);

// 관리자 전용 엔드포인트 (슈퍼관리자 + 회사관리자)
router.get(
  "/api/admin/users",
  authenticate,
  requireAdmin,
  userController.getUserList
);

// 회사 데이터 접근 체크
router.get(
  "/api/data/:companyCode/orders",
  authenticate,
  requireCompanyAccess,
  orderController.getOrders
);

// 사용자 관리 권한 체크
router.post(
  "/api/admin/users/:companyCode",
  authenticate,
  requireUserManagement,
  userController.createUser
);

3. 서비스 레이어 구현

// ❌ 잘못된 방법 - 하드코딩된 회사 코드
async getOrders(companyCode: string) {
  return query("SELECT * FROM orders WHERE company_code = $1", [companyCode]);
}

// ✅ 올바른 방법 - 권한 체크 포함
async getOrders(user: PersonBean, companyCode: string) {
  // 권한 확인
  if (!canAccessCompanyData(user, companyCode)) {
    throw new Error("해당 회사 데이터에 접근할 권한이 없습니다.");
  }

  // 슈퍼관리자는 모든 데이터 조회 가능
  if (isSuperAdmin(user)) {
    if (companyCode === "*") {
      return query("SELECT * FROM orders"); // 전체 조회
    }
  }

  // 일반 사용자/회사 관리자는 자기 회사만
  return query("SELECT * FROM orders WHERE company_code = $1", [companyCode]);
}

프론트엔드 구현

1. 사용자 타입 정의

// frontend/types/user.ts
export interface UserInfo {
  userId: string;
  userName: string;
  companyCode: string;
  userType: string; // 'SUPER_ADMIN' | 'COMPANY_ADMIN' | 'USER'
  isSuperAdmin?: boolean;
  isCompanyAdmin?: boolean;
  isAdmin?: boolean;
}

2. 권한 기반 UI 렌더링

import { useAuth } from "@/hooks/useAuth";

function AdminPanel() {
  const { user } = useAuth();

  return (
    <div>
      {/* 슈퍼관리자만 표시 */}
      {user?.isSuperAdmin && (
        <Button onClick={handleDDLExecution}>DDL 실행</Button>
      )}

      {/* 관리자만 표시 (슈퍼관리자 + 회사관리자) */}
      {user?.isAdmin && (
        <Button onClick={handleUserManagement}>사용자 관리</Button>
      )}

      {/* 모든 사용자 표시 */}
      <Button onClick={handleDataView}>데이터 조회</Button>
    </div>
  );
}

3. 권한 체크 Hook

// frontend/hooks/usePermissions.ts
export function usePermissions() {
  const { user } = useAuth();

  return {
    isSuperAdmin: user?.isSuperAdmin ?? false,
    isCompanyAdmin: user?.isCompanyAdmin ?? false,
    isAdmin: user?.isAdmin ?? false,
    canExecuteDDL: user?.isSuperAdmin ?? false,
    canManageUsers: user?.isAdmin ?? false,
    canAccessCompany: (companyCode: string) => {
      if (user?.isSuperAdmin) return true;
      return user?.companyCode === companyCode;
    },
  };
}

// 사용 예시
function DataTable({ companyCode }: { companyCode: string }) {
  const { canAccessCompany } = usePermissions();

  if (!canAccessCompany(companyCode)) {
    return <div>접근 권한이 없습니다.</div>;
  }

  return <Table data={data} />;
}

실무 예제

예제 1: 주문 데이터 조회

시나리오:

  • 슈퍼관리자: 모든 회사의 주문 조회
  • 회사20 관리자: 회사20의 주문만 조회
  • 회사20 사용자: 회사20의 주문만 조회

백엔드 구현:

// orders.service.ts
export class OrderService {
  async getOrders(user: PersonBean, companyCode?: string) {
    let sql = "SELECT * FROM orders WHERE 1=1";
    const params: any[] = [];

    // 슈퍼관리자가 아닌 경우 회사 필터 적용
    if (!isSuperAdmin(user)) {
      sql += " AND company_code = $1";
      params.push(user.companyCode);
    } else if (companyCode && companyCode !== "*") {
      // 슈퍼관리자가 특정 회사를 지정한 경우
      sql += " AND company_code = $1";
      params.push(companyCode);
    }

    return query(sql, params);
  }
}

프론트엔드 구현:

function OrderList() {
  const { user } = useAuth();
  const [selectedCompany, setSelectedCompany] = useState(user?.companyCode);

  // 슈퍼관리자는 회사 선택 가능
  const showCompanySelector = user?.isSuperAdmin;

  return (
    <div>
      {showCompanySelector && (
        <Select value={selectedCompany} onChange={setSelectedCompany}>
          <option value="*">전체 회사</option>
          <option value="20">회사 20</option>
          <option value="30">회사 30</option>
        </Select>
      )}

      <OrderTable companyCode={selectedCompany} />
    </div>
  );
}

예제 2: 사용자 관리

시나리오:

  • 슈퍼관리자: 모든 회사의 사용자 관리
  • 회사20 관리자: 회사20 사용자만 관리
  • 회사20 사용자: 사용자 관리 불가

백엔드 구현:

// users.controller.ts
router.post("/api/admin/users", authenticate, async (req, res) => {
  const { companyCode, userId, userName } = req.body;

  // 권한 확인
  if (!canManageUsers(req.user, companyCode)) {
    return res.status(403).json({
      success: false,
      error: "사용자 관리 권한이 없습니다.",
    });
  }

  // 슈퍼관리자가 아닌 경우, 자기 회사만 가능
  if (!isSuperAdmin(req.user) && companyCode !== req.user.companyCode) {
    return res.status(403).json({
      success: false,
      error: "다른 회사의 사용자를 생성할 수 없습니다.",
    });
  }

  // 사용자 생성
  await UserService.createUser({ companyCode, userId, userName });

  res.json({ success: true });
});

예제 3: DDL 실행 (테이블 생성)

시나리오:

  • 슈퍼관리자만 DDL 실행 가능
  • 다른 모든 사용자는 차단

백엔드 구현:

// ddl.controller.ts
router.post(
  "/api/admin/ddl/execute",
  authenticate,
  requireDDLPermission, // 슈퍼관리자 체크 미들웨어
  async (req, res) => {
    const { sql } = req.body;

    // 추가 보안 검증
    if (!canExecuteDDL(req.user)) {
      return res.status(403).json({
        success: false,
        error: "DDL 실행 권한이 없습니다.",
      });
    }

    // DDL 실행
    await query(sql);

    // 감사 로그 기록
    await AuditService.logDDL({
      userId: req.user.userId,
      sql,
      timestamp: new Date(),
    });

    res.json({ success: true });
  }
);

프론트엔드 구현:

function DDLExecutor() {
  const { user } = useAuth();

  // 슈퍼관리자가 아니면 컴포넌트 자체를 숨김
  if (!user?.isSuperAdmin) {
    return null;
  }

  return (
    <div>
      <h2>DDL 실행 (슈퍼관리자 전용)</h2>
      <textarea placeholder="SQL 입력" />
      <Button onClick={handleExecute}>실행</Button>
    </div>
  );
}

FAQ

Q1: 기존 ADMIN 계정은 어떻게 되나요?

A: 마이그레이션 스크립트가 자동으로 처리합니다:

  • company_code = '*'인 ADMIN → SUPER_ADMIN으로 변경
  • company_code = '특정코드'인 ADMIN → COMPANY_ADMIN으로 변경

Q2: 슈퍼관리자 계정은 몇 개가 적절한가요?

A: 보안상 최소 1개, 최대 2-3개를 권장합니다. 모든 DDL 실행이 감사 로그에 기록되므로 책임 추적이 가능합니다.

Q3: 회사 관리자가 다른 회사 데이터를 조회하려면?

A: 불가능합니다. 회사 간 데이터 격리가 필수입니다. 필요시 슈퍼관리자에게 요청하거나, API 통합 기능을 사용해야 합니다.

Q4: USER가 COMPANY_ADMIN으로 승격하려면?

A:

  1. 슈퍼관리자 또는 해당 회사의 관리자가 처리
  2. UPDATE user_info SET user_type = 'COMPANY_ADMIN' WHERE user_id = 'xxx'
  3. 사용자 재로그인 필요

Q5: 회사 코드 '*'의 의미는?

A: 와일드카드로, "모든 회사"를 의미합니다. 슈퍼관리자 전용 코드이며, 일반 회사 코드로는 사용할 수 없습니다.

Q6: 권한 체크는 어디서 해야 하나요?

A:

  • 백엔드 (필수): 미들웨어 + 서비스 레이어 모두
  • 프론트엔드 (선택): UI 렌더링 최적화용 (보안 목적 아님)

Q7: 테이블에 회사 필터링을 추가하려면?

A:

  1. 테이블에 company_code 컬럼 추가
  2. backend-node/src/services/dataService.tsCOMPANY_FILTERED_TABLES 배열에 테이블명 추가
  3. 자동으로 회사 필터링 적용됨

체크리스트

새로운 엔드포인트 추가 시

  • 적절한 권한 미들웨어 적용 (requireSuperAdmin, requireAdmin 등)
  • 서비스 레이어에서 canAccessCompanyData() 체크
  • 감사 로그 기록 (중요 작업의 경우)
  • 프론트엔드 UI에 권한 기반 렌더링 적용
  • 에러 메시지에 필요한 권한 레벨 명시

새로운 테이블 생성 시

  • company_code 컬럼 추가 (회사별 데이터인 경우)
  • COMPANY_FILTERED_TABLES 배열에 등록
  • 인덱스 생성: CREATE INDEX ON table_name(company_code)
  • Row Level Security 정책 고려 (선택사항)

참고 파일

  • 마이그레이션: /db/migrations/026_add_user_type_hierarchy.sql
  • 권한 유틸: /backend-node/src/utils/permissionUtils.ts
  • 미들웨어: /backend-node/src/middleware/permissionMiddleware.ts
  • 타입 정의: /backend-node/src/types/auth.ts
  • 인증 서비스: /backend-node/src/services/authService.ts