ERP-node/docs/권한_그룹_관리_상세_가이드.md

22 KiB

권한 그룹 관리 시스템 상세 가이드

작성일: 2025-01-27
파일 위치: backend-node/src/services/roleService.ts, backend-node/src/controllers/roleController.ts


📋 목차

  1. 권한 그룹 관리 구조
  2. 최고 관리자 권한
  3. 회사 관리자 권한
  4. 메뉴 권한 설정
  5. 멤버 관리
  6. 권한 체크 로직

권한 그룹 관리 구조

데이터베이스 구조

-- 권한 그룹 마스터 테이블
authority_master (
  objid SERIAL PRIMARY KEY,           -- 권한 그룹 ID
  auth_name VARCHAR(200),             -- 권한 그룹명
  auth_code VARCHAR(100),             -- 권한 그룹 코드
  company_code VARCHAR(50),           -- 회사 코드 ⭐
  status VARCHAR(20),                 -- 상태 (active/inactive)
  writer VARCHAR(50),                 -- 작성자
  regdate TIMESTAMP                   -- 등록일
)

-- 권한 그룹 멤버 테이블
authority_sub_user (
  objid SERIAL PRIMARY KEY,           -- 멤버 ID
  master_objid INTEGER,               -- 권한 그룹 ID (FK)
  user_id VARCHAR(50),                -- 사용자 ID
  writer VARCHAR(50),                 -- 작성자
  regdate TIMESTAMP                   -- 등록일
)

-- 메뉴 권한 테이블
rel_menu_auth (
  objid SERIAL PRIMARY KEY,           -- 권한 ID
  menu_objid INTEGER,                 -- 메뉴 ID (FK)
  auth_objid INTEGER,                 -- 권한 그룹 ID (FK)
  create_yn VARCHAR(1),               -- 생성 권한 (Y/N)
  read_yn VARCHAR(1),                 -- 조회 권한 (Y/N)
  update_yn VARCHAR(1),               -- 수정 권한 (Y/N)
  delete_yn VARCHAR(1),               -- 삭제 권한 (Y/N)
  execute_yn VARCHAR(1),              -- 실행 권한 (Y/N) ⭐
  export_yn VARCHAR(1),               -- 내보내기 권한 (Y/N) ⭐
  writer VARCHAR(50),                 -- 작성자
  regdate TIMESTAMP                   -- 등록일
)

-- 메뉴 정보 테이블
menu_info (
  objid SERIAL PRIMARY KEY,           -- 메뉴 ID
  menu_name_kor VARCHAR(200),         -- 메뉴명 (한글)
  menu_name_eng VARCHAR(200),         -- 메뉴명 (영문)
  menu_code VARCHAR(100),             -- 메뉴 코드
  menu_url VARCHAR(500),              -- 메뉴 URL
  menu_type INTEGER,                  -- 메뉴 타입
  parent_obj_id INTEGER,              -- 부모 메뉴 ID
  seq INTEGER,                        -- 정렬 순서
  company_code VARCHAR(50),           -- 회사 코드 ⭐
  status VARCHAR(20)                  -- 상태 (active/inactive)
)

권한 계층 구조

최고 관리자 (SUPER_ADMIN, company_code = "*")
  ├─ 모든 회사의 권한 그룹 조회/생성/수정/삭제
  ├─ 모든 회사의 메뉴에 대해 권한 부여 가능
  └─ 다른 회사 사용자를 권한 그룹에 추가 가능

회사 관리자 (COMPANY_ADMIN, company_code = "20", "30", etc.)
  ├─ 자기 회사의 권한 그룹만 조회/생성/수정/삭제
  ├─ 자기 회사의 메뉴에 대해서만 권한 부여 가능
  └─ 자기 회사 사용자만 권한 그룹에 추가 가능

일반 사용자 (USER)
  └─ 권한 그룹 관리 불가 (읽기 전용)

최고 관리자 권한

1. 권한 그룹 목록 조회

API: GET /api/roles

로직:

export const getRoleGroups = async (req, res) => {
  const companyCode = req.query.companyCode as string | undefined;

  if (isSuperAdmin(req.user)) {
    // 최고 관리자: companyCode 파라미터가 있으면 해당 회사만, 없으면 전체 조회
    targetCompanyCode = companyCode; // undefined면 전체 조회
  } else {
    // 회사 관리자: 자기 회사만 조회
    targetCompanyCode = req.user?.companyCode;
  }

  const roleGroups = await RoleService.getRoleGroups(targetCompanyCode, search);
};

데이터베이스 쿼리:

// RoleService.getRoleGroups()
let sql = `SELECT * FROM authority_master WHERE 1=1`;

if (companyCode) {
  sql += ` AND company_code = $1`; // 특정 회사만
} else {
  // 조건 없음 -> 전체 조회
}

결과:

  • companyCode 파라미터 없음 → 모든 회사의 권한 그룹 조회
  • companyCode = "20"회사 "20"의 권한 그룹만 조회
  • companyCode = "*"공통 권한 그룹만 조회 (있다면)

2. 권한 그룹 생성

API: POST /api/roles

로직:

export const createRoleGroup = async (req, res) => {
  const { authName, authCode, companyCode } = req.body;

  // 최고 관리자가 아닌 경우, 자기 회사에만 생성 가능
  if (!isSuperAdmin(req.user) && companyCode !== req.user?.companyCode) {
    return res.status(403).json({
      message: "자신의 회사에만 권한 그룹을 생성할 수 있습니다",
    });
  }

  await RoleService.createRoleGroup({
    authName,
    authCode,
    companyCode,
    writer,
  });
};

결과:

  • 최고 관리자는 어떤 회사에도 권한 그룹 생성 가능
  • companyCode = "*" 로 공통 권한 그룹도 생성 가능
  • 회사 관리자는 자기 회사에만 생성 가능

3. 메뉴 목록 조회 (권한 부여용)

API: GET /api/roles/menus/all?companyCode=20

로직:

export const getAllMenus = async (req, res) => {
  const requestedCompanyCode = req.query.companyCode as string | undefined;

  let companyCode: string | undefined;
  if (isSuperAdmin(req.user)) {
    // 최고 관리자: 요청한 회사 코드 사용 (없으면 전체)
    companyCode = requestedCompanyCode;
  } else {
    // 회사 관리자: 자기 회사 코드만 사용
    companyCode = req.user?.companyCode;
  }

  const menus = await RoleService.getAllMenus(companyCode);
};

데이터베이스 쿼리:

// RoleService.getAllMenus()
let whereConditions = ["status = 'active'"];

if (companyCode) {
  // 특정 회사 메뉴만 조회 (공통 메뉴 제외)
  whereConditions.push(`company_code = $1`);
} else {
  // 조건 없음 -> 전체 메뉴 조회
}

const sql = `
  SELECT * FROM menu_info
  WHERE ${whereConditions.join(" AND ")}
  ORDER BY seq, menu_name_kor
`;

결과:

  • companyCode 없음 → 모든 회사의 모든 메뉴 조회
  • companyCode = "20"회사 "20"의 메뉴만 조회
  • companyCode = "*"공통 메뉴만 조회

⚠️ 프론트엔드 구현 주의사항:

프론트엔드에서 권한 그룹 상세 화면 (메뉴 권한 설정)에서는 최고 관리자가 모든 메뉴를 볼 수 있도록 companyCode 파라미터를 전달하지 않아야 합니다:

// MenuPermissionsTable.tsx
const { user: currentUser } = useAuth();
const isSuperAdmin =
  currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";

// 최고 관리자: companyCode 없이 모든 메뉴 조회
// 회사 관리자: 자기 회사 메뉴만 조회
const targetCompanyCode = isSuperAdmin ? undefined : roleGroup.companyCode;

const response = await roleAPI.getAllMenus(targetCompanyCode);

이렇게 하지 않으면:

  • 최고 관리자가 회사 "20"의 권한 그룹에 들어가도
  • roleGroup.companyCode (= "20")를 API로 전달하면
  • 회사 "20"의 메뉴만 보이게 됨

올바른 동작:

  • 최고 관리자가 회사 "20"의 권한 그룹에 들어가면
  • undefined를 API로 전달하여
  • 모든 회사의 모든 메뉴를 볼 수 있어야 함

4. 메뉴 권한 설정

API: POST /api/roles/:id/menu-permissions

Request Body:

{
  "permissions": [
    {
      "menuObjid": 101,
      "createYn": "Y",
      "readYn": "Y",
      "updateYn": "Y",
      "deleteYn": "Y",
      "executeYn": "Y",
      "exportYn": "Y"
    },
    {
      "menuObjid": 102,
      "createYn": "N",
      "readYn": "Y",
      "updateYn": "N",
      "deleteYn": "N",
      "executeYn": "N",
      "exportYn": "N"
    }
  ]
}

로직:

export const setMenuPermissions = async (req, res) => {
  const authObjid = parseInt(req.params.id, 10);
  const { permissions } = req.body;

  // 권한 그룹 조회
  const roleGroup = await RoleService.getRoleGroupById(authObjid);

  // 권한 체크
  if (
    !isSuperAdmin(req.user) &&
    !canAccessCompanyData(req.user, roleGroup.companyCode)
  ) {
    return res.status(403).json({ message: "메뉴 권한 설정 권한이 없습니다" });
  }

  await RoleService.setMenuPermissions(authObjid, permissions, writer);
};

데이터베이스 쿼리:

// RoleService.setMenuPermissions()

// 1. 기존 권한 삭제
await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [authObjid]);

// 2. 새로운 권한 삽입
const sql = `
  INSERT INTO rel_menu_auth 
  (objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, execute_yn, export_yn, writer, regdate)
  VALUES 
  ${values} -- (nextval('seq'), $1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()), ...
`;

결과:

  • 최고 관리자는 어떤 권한 그룹에도 메뉴 권한 설정 가능
  • 모든 회사의 메뉴에 대해 권한 부여 가능
  • CRUD + Execute + Export 총 6가지 권한 설정

5. 멤버 관리

API: PUT /api/roles/:id/members

Request Body:

{
  "userIds": ["user001", "user002", "user003"]
}

로직:

export const updateRoleMembers = async (req, res) => {
  const masterObjid = parseInt(req.params.id, 10);
  const { userIds } = req.body;

  // 권한 그룹 조회
  const roleGroup = await RoleService.getRoleGroupById(masterObjid);

  // 권한 체크
  if (
    !isSuperAdmin(req.user) &&
    !canAccessCompanyData(req.user, roleGroup.companyCode)
  ) {
    return res
      .status(403)
      .json({ message: "권한 그룹 멤버 수정 권한이 없습니다" });
  }

  // 기존 멤버 조회
  const existingMembers = await RoleService.getRoleMembers(masterObjid);
  const existingUserIds = existingMembers.map((m) => m.userId);

  // 추가할 멤버 (새로 추가된 것들)
  const toAdd = userIds.filter((id) => !existingUserIds.includes(id));

  // 제거할 멤버 (기존에 있었는데 없어진 것들)
  const toRemove = existingUserIds.filter((id) => !userIds.includes(id));

  // 추가
  if (toAdd.length > 0) {
    await RoleService.addRoleMembers(masterObjid, toAdd, writer);
  }

  // 제거
  if (toRemove.length > 0) {
    await RoleService.removeRoleMembers(masterObjid, toRemove, writer);
  }
};

결과:

  • 최고 관리자는 어떤 회사 사용자도 권한 그룹에 추가 가능
  • 다른 회사 권한 그룹에도 멤버 추가 가능
  • ⚠️ 단, 사용자 목록 API에서 최고 관리자는 필터링되므로 추가 불가

회사 관리자 권한

1. 권한 그룹 목록 조회

로직:

if (!isSuperAdmin(req.user)) {
  // 회사 관리자: 자기 회사만 조회
  targetCompanyCode = req.user?.companyCode; // 강제로 자기 회사
}

결과:

  • 자기 회사 (company_code = "20") 권한 그룹만 조회
  • 다른 회사 권한 그룹은 절대 볼 수 없음

2. 권한 그룹 생성

로직:

if (!isSuperAdmin(req.user) && companyCode !== req.user?.companyCode) {
  return res.status(403).json({
    message: "자신의 회사에만 권한 그룹을 생성할 수 있습니다",
  });
}

결과:

  • 자기 회사에만 권한 그룹 생성 가능
  • 다른 회사나 공통(*)에는 생성 불가

3. 메뉴 목록 조회

로직:

if (!isSuperAdmin(req.user)) {
  // 회사 관리자: 자기 회사 코드만 사용
  companyCode = req.user?.companyCode; // 강제로 자기 회사
}

데이터베이스 쿼리:

SELECT * FROM menu_info
WHERE status = 'active'
  AND company_code = '20' -- 자기 회사만
ORDER BY seq, menu_name_kor

결과:

  • 자기 회사 메뉴만 조회
  • 공통 메뉴(company_code = "*")는 조회되지 않음
  • 다른 회사 메뉴는 절대 볼 수 없음

4. 메뉴 권한 설정

로직:

// 권한 그룹의 회사 코드 체크
if (
  !isSuperAdmin(req.user) &&
  !canAccessCompanyData(req.user, roleGroup.companyCode)
) {
  return res.status(403).json({ message: "메뉴 권한 설정 권한이 없습니다" });
}

canAccessCompanyData 함수:

export function canAccessCompanyData(
  user: UserInfo | undefined,
  targetCompanyCode: string
): boolean {
  if (!user) return false;
  if (isSuperAdmin(user)) return true; // 슈퍼관리자는 모든 회사 접근 가능
  return user.companyCode === targetCompanyCode; // 자기 회사만 접근 가능
}

결과:

  • 자기 회사 권한 그룹에만 메뉴 권한 설정 가능
  • 자기 회사 메뉴에 대해서만 권한 부여 가능
  • 다른 회사 권한 그룹이나 메뉴는 접근 불가

5. 멤버 관리

로직:

// 권한 그룹의 회사 코드 체크
if (
  !isSuperAdmin(req.user) &&
  !canAccessCompanyData(req.user, roleGroup.companyCode)
) {
  return res
    .status(403)
    .json({ message: "권한 그룹 멤버 수정 권한이 없습니다" });
}

사용자 목록 조회 (Dual List Box):

// adminController.getUserList()
if (req.user && req.user.companyCode !== "*") {
  whereConditions.push(`company_code != '*'`); // 최고 관리자 필터링
  whereConditions.push(`company_code = $1`); // 자기 회사만
}

결과:

  • 자기 회사 권한 그룹에만 멤버 추가/제거 가능
  • 자기 회사 사용자만 멤버로 추가 가능
  • 최고 관리자는 목록에서 절대 보이지 않음
  • 다른 회사 사용자는 절대 볼 수 없음

메뉴 권한 설정

권한 종류 (6가지)

권한 컬럼명 설명 예시
생성 create_yn 데이터 생성 가능 여부 사용자 추가, 주문 생성
조회 read_yn 데이터 조회 가능 여부 목록 보기, 상세 보기
수정 update_yn 데이터 수정 가능 여부 정보 변경, 상태 변경
삭제 delete_yn 데이터 삭제 가능 여부 항목 삭제, 데이터 제거
실행 execute_yn 특수 기능 실행 가능 여부 플로우 실행, 배치 실행
내보내기 export_yn 데이터 내보내기 가능 여부 Excel 다운로드, PDF 출력

권한 설정 예시

1. 읽기 전용 권한

{
  "createYn": "N",
  "readYn": "Y",
  "updateYn": "N",
  "deleteYn": "N",
  "executeYn": "N",
  "exportYn": "N"
}

→ 조회만 가능, 수정/삭제 불가

2. 전체 권한

{
  "createYn": "Y",
  "readYn": "Y",
  "updateYn": "Y",
  "deleteYn": "Y",
  "executeYn": "Y",
  "exportYn": "Y"
}

→ 모든 작업 가능

3. 운영자 권한

{
  "createYn": "Y",
  "readYn": "Y",
  "updateYn": "Y",
  "deleteYn": "N",
  "executeYn": "Y",
  "exportYn": "Y"
}

→ 삭제 제외 모든 작업 가능

메뉴 권한 설정 프로세스

graph TD
    A[권한 그룹 선택] --> B[메뉴 목록 조회]
    B --> C{최고 관리자?}
    C -->|Yes| D[모든 회사 메뉴 조회]
    C -->|No| E[자기 회사 메뉴만 조회]
    D --> F[메뉴별 권한 설정]
    E --> F
    F --> G[기존 권한 삭제]
    G --> H[새 권한 일괄 삽입]
    H --> I[완료]

멤버 관리

Dual List Box 구조

┌─────────────────────┐        ┌─────────────────────┐
│   사용 가능한 사용자   │   →   │   권한 그룹 멤버      │
│                     │   ←   │                     │
│  [ ] user001        │        │  [x] user005        │
│  [ ] user002        │        │  [x] user006        │
│  [ ] user003        │        │  [x] user007        │
│  [ ] user004        │        │                     │
└─────────────────────┘        └─────────────────────┘

멤버 추가/제거 로직

// 프론트엔드에서 전송
PUT /api/roles/123/members
{
  "userIds": ["user005", "user006", "user008"] // 최종 멤버 목록
}

// 백엔드 처리
const existingUserIds = ["user005", "user006", "user007"]; // 기존 멤버
const newUserIds = ["user005", "user006", "user008"];     // 요청된 멤버

const toAdd = ["user008"];    // 새로 추가 (기존에 없던 것)
const toRemove = ["user007"]; // 제거 (기존에 있었는데 없어진 것)

// 추가
INSERT INTO authority_sub_user (master_objid, user_id, writer, regdate)
VALUES (123, 'user008', 'admin', NOW())

// 제거
DELETE FROM authority_sub_user
WHERE master_objid = 123 AND user_id = 'user007'

사용자 목록 필터링

API: GET /api/admin/users?companyCode=20&size=1000

로직:

// 회사 코드 필터
if (companyCode && companyCode.trim()) {
  whereConditions.push(`company_code = $1`);
  queryParams.push(companyCode.trim());
}

// 최고 관리자 필터링 (중요!)
if (req.user && req.user.companyCode !== "*") {
  whereConditions.push(`company_code != '*'`); // 최고 관리자 숨김
}

// 검색 조건
if (search && search.trim()) {
  whereConditions.push(`(
    user_id ILIKE $2 OR
    user_name ILIKE $2 OR
    dept_name ILIKE $2
  )`);
  queryParams.push(`%${search.trim()}%`);
}

결과:

  • 최고 관리자: 요청한 회사의 사용자 목록
  • 회사 관리자: 자기 회사 사용자 목록
  • 최고 관리자(company_code = "*")는 절대 목록에 표시되지 않음

권한 체크 로직

1. isSuperAdmin()

파일: backend-node/src/utils/permissionUtils.ts

export function isSuperAdmin(user: UserInfo | undefined): boolean {
  if (!user) return false;
  return user.companyCode === "*";
}

사용:

  • 권한 그룹 목록 전체 조회 여부
  • 다른 회사 데이터 접근 여부
  • 모든 메뉴 조회 여부

2. isCompanyAdmin()

export function isCompanyAdmin(user: UserInfo | undefined): boolean {
  if (!user) return false;
  return user.userType === "COMPANY_ADMIN";
}

사용:

  • 권한 그룹 관리 접근 여부
  • 메뉴 권한 설정 접근 여부

3. canAccessCompanyData()

export function canAccessCompanyData(
  user: UserInfo | undefined,
  targetCompanyCode: string
): boolean {
  if (!user) return false;
  if (isSuperAdmin(user)) return true; // 슈퍼관리자는 모든 회사 접근 가능
  return user.companyCode === targetCompanyCode; // 자기 회사만 접근 가능
}

사용:

  • 권한 그룹 상세 조회 접근 여부
  • 권한 그룹 수정/삭제 접근 여부
  • 멤버 관리 접근 여부
  • 메뉴 권한 설정 접근 여부

권한 체크 플로우

graph TD
    A[API 요청] --> B{로그인 확인}
    B -->|No| C[401 Unauthorized]
    B -->|Yes| D{최고 관리자?}
    D -->|Yes| E[모든 데이터 접근 허용]
    D -->|No| F{회사 관리자?}
    F -->|No| G[403 Forbidden]
    F -->|Yes| H{자기 회사 데이터?}
    H -->|No| I[403 Forbidden]
    H -->|Yes| J[접근 허용]

💡 핵심 정리

최고 관리자가 할 수 있는 것

  1. 권한 그룹 관리

    • 모든 회사의 권한 그룹 조회
    • 어떤 회사에도 권한 그룹 생성 가능
    • 다른 회사 권한 그룹 수정/삭제 가능
  2. 메뉴 권한 설정

    • 모든 회사의 메뉴 조회 가능
    • 어떤 권한 그룹에도 메뉴 권한 부여 가능
    • 모든 메뉴에 대해 CRUD + Execute + Export 권한 설정
  3. 멤버 관리

    • 어떤 회사 권한 그룹에도 멤버 추가 가능
    • 다른 회사 사용자도 멤버로 추가 가능
    • ⚠️ 단, 최고 관리자는 사용자 목록에서 필터링되므로 추가 불가

회사 관리자가 할 수 있는 것

  1. 권한 그룹 관리

    • 자기 회사 권한 그룹만 조회
    • 자기 회사에만 권한 그룹 생성 가능
    • 자기 회사 권한 그룹만 수정/삭제 가능
  2. 메뉴 권한 설정

    • 자기 회사 메뉴만 조회 가능
    • 자기 회사 권한 그룹에만 메뉴 권한 부여 가능
    • 자기 회사 메뉴에 대해서만 CRUD + Execute + Export 권한 설정
  3. 멤버 관리

    • 자기 회사 권한 그룹에만 멤버 추가 가능
    • 자기 회사 사용자만 멤버로 추가 가능
    • 최고 관리자는 목록에서 절대 보이지 않음

제한 사항

  1. 회사 관리자는 절대 할 수 없는 것

    • 다른 회사 권한 그룹 조회/수정/삭제
    • 공통 메뉴(company_code = "*") 조회
    • 최고 관리자를 멤버로 추가
    • 다른 회사 사용자를 멤버로 추가
  2. 최고 관리자도 할 수 없는 것

    • 다른 최고 관리자를 권한 그룹 멤버로 추가 (사용자 목록 필터링으로 인해)

📊 권한 매트릭스

작업 최고 관리자 회사 관리자 일반 사용자
권한 그룹 조회 모든 회사 자기 회사만
권한 그룹 생성 모든 회사 자기 회사만
권한 그룹 수정 모든 회사 자기 회사만
권한 그룹 삭제 모든 회사 자기 회사만
메뉴 목록 조회 모든 회사 메뉴 자기 회사 메뉴만
메뉴 권한 설정 모든 권한 그룹 자기 회사 권한 그룹만
멤버 추가 모든 회사 사용자 자기 회사 사용자만
멤버 제거 모든 회사 멤버 자기 회사 멤버만

📝 작성자

  • 작성: AI Assistant (Claude Sonnet 4.5)
  • 검토 필요: 백엔드 개발자, 시스템 아키텍트
  • 관련 파일:
    • backend-node/src/services/roleService.ts
    • backend-node/src/controllers/roleController.ts
    • backend-node/src/utils/permissionUtils.ts
    • frontend/components/admin/RoleDetailManagement.tsx