ERP-node/docs/테이블_컬럼_타입_멀티테넌시_구조적_문제_분석.md

12 KiB

테이블 컬럼 타입 멀티테넌시 구조적 문제 분석

작성일: 2025-11-06
심각도: 🔴 치명적 (Critical)
상태: 🚨 긴급 분석 필요


🚨 발견된 구조적 문제

문제 요약

현재 table_type_columns 테이블에 company_code 컬럼이 없음!

-- 현재 table_type_columns 구조
CREATE TABLE table_type_columns (
  id SERIAL PRIMARY KEY,
  table_name VARCHAR NOT NULL,
  column_name VARCHAR NOT NULL,
  input_type VARCHAR NOT NULL,       -- 🔴 문제: 회사별로 다르게 설정 불가!
  detail_settings TEXT,
  is_nullable VARCHAR,
  display_order INTEGER,
  created_date TIMESTAMP,
  updated_date TIMESTAMP
  -- ❌ company_code 컬럼 없음!
);

🎯 사용자가 지적한 시나리오

시나리오: "재질" 컬럼의 충돌

회사 A: item_info.material 컬럼을 "카테고리" 타입으로 사용
  → 드롭다운 선택 (철, 알루미늄, 플라스틱)

회사 B: item_info.material 컬럼을 "텍스트" 타입으로 사용
  → 자유 입력 (SUS304, AL6061, PVC 등)

현재 구조:
  ❌ table_type_columns에 company_code가 없음
  ❌ 둘 중 하나만 선택 가능
  ❌ 회사별로 다른 input_type 설정 불가능!

📊 현재 구조의 문제점

1. 테이블 구조 확인

-- table_type_columns 실제 컬럼 확인
SELECT column_name FROM information_schema.columns
WHERE table_name = 'table_type_columns';

-- 결과:
id
table_name
column_name
input_type           🔴 회사별 구분 없음!
detail_settings
is_nullable
display_order
created_date
updated_date
-- ❌ company_code 없음!

2. 현재 데이터 예시

-- 현재 저장된 데이터
SELECT * FROM table_type_columns
WHERE table_name = 'item_info' AND column_name = 'material';

-- 가능한 결과:
id | table_name | column_name | input_type | company_code
---|------------|-------------|------------|-------------
1  | item_info  | material    | category   |  없음

문제:

  • 회사 A가 materialcategory로 설정하면
  • 회사 B는 materialtext로 설정할 수 없음!
  • 하나의 컬럼 타입 정의를 모든 회사가 공유

🔍 멀티테넌시 충돌 분석

Case 1: 같은 테이블, 같은 컬럼, 다른 타입

요구사항 회사 A 회사 B 현재 가능?
테이블 item_info item_info 공유
컬럼 material material 공유
input_type category text 불가능

현재 동작:

// 회사 A가 설정
await updateColumnType("item_info", "material", "category");
// → table_type_columns에 저장 (company_code 없음)

// 회사 B가 설정 시도
await updateColumnType("item_info", "material", "text");
// → ❌ 기존 레코드 덮어쓰기 또는 충돌!

Case 2: 카테고리 값 충돌

요구사항 회사 A 회사 B 현재 상태
카테고리 값 철, 알루미늄, 플라스틱 SUS304, AL6061, PVC 🟡 company_code로 분리됨

이미 수정 완료:

  • table_column_category_valuescompany_code 컬럼이 있음
  • 카테고리 은 회사별로 다르게 저장 가능
  • 하지만 카테고리 타입 자체는 공유됨

🏗️ 현재 아키텍처 vs 필요한 아키텍처

현재 (잘못된) 아키텍처

┌─────────────────────────────┐
│   table_type_columns        │
│  (컬럼 타입 정의 - 전역)    │
├─────────────────────────────┤
│ id  | table | column | type │
│ 1   | item  | material | ❓ │ ← 🔴 충돌!
└─────────────────────────────┘
        ↓
회사 A: material = category?
회사 B: material = text?
→ ❌ 둘 중 하나만 가능

필요한 (올바른) 아키텍처

┌────────────────────────────────────────┐
│   table_type_columns                   │
│  (컬럼 타입 정의 - 회사별 분리)        │
├────────────────────────────────────────┤
│ id | table | column | type | company   │
│ 1  | item  | material | category | A   │ ✅ 회사 A
│ 2  | item  | material | text | B       │ ✅ 회사 B
└────────────────────────────────────────┘

💥 실제 발생 가능한 시나리오

시나리오 1: 프로젝트 타입

회사 A (IT 회사):
  - projects.project_type → category
  - 카테고리 값: 개발, 유지보수, 컨설팅

회사 B (건설 회사):
  - projects.project_type → text
  - 자유 입력: 아파트 신축, 도로 보수 공사, 리모델링 등

현재: ❌ 둘 중 하나만 선택 가능
필요: ✅ 회사별로 다른 input_type 설정

시나리오 2: 담당자 필드

회사 A (소규모):
  - tasks.assignee → text
  - 자유 입력: 이름 직접 입력

회사 B (대규모):
  - tasks.assignee → reference
  - 참조: user_info 테이블에서 선택

현재: ❌ 하나의 타입만 설정 가능
필요: ✅ 회사별로 다른 방식

시나리오 3: 금액 필드

회사 A:
  - contracts.amount → number
  - 숫자 입력 (10,000,000)

회사 B:
  - contracts.amount → text
  - 특수 형식 입력 (₩10M, $100K, negotiable)

현재: ❌ 하나의 타입만
필요: ✅ 회사별 다른 타입

🔧 해결 방안

방안 1: company_code 추가 (권장)

마이그레이션:

-- 1. company_code 컬럼 추가
ALTER TABLE table_type_columns
ADD COLUMN company_code VARCHAR(20);

-- 2. 기존 데이터 마이그레이션 (모든 회사에 복제)
INSERT INTO table_type_columns (
  table_name, column_name, input_type, detail_settings,
  is_nullable, display_order, company_code, created_date
)
SELECT
  table_name, column_name, input_type, detail_settings,
  is_nullable, display_order,
  ci.company_code,  -- 각 회사별로 복제
  created_date
FROM table_type_columns ttc
CROSS JOIN company_info ci
WHERE ttc.company_code IS NULL;  -- 기존 데이터만

-- 3. NOT NULL 제약조건 추가
ALTER TABLE table_type_columns
ALTER COLUMN company_code SET NOT NULL;

-- 4. 복합 유니크 인덱스 생성
CREATE UNIQUE INDEX idx_table_column_type_company
ON table_type_columns(table_name, column_name, company_code);

-- 5. company_code 인덱스 생성
CREATE INDEX idx_table_type_columns_company
ON table_type_columns(company_code);

-- 6. 외래키 제약조건 추가
ALTER TABLE table_type_columns
ADD CONSTRAINT fk_table_type_columns_company
FOREIGN KEY (company_code) REFERENCES company_info(company_code);

장점:

  • 회사별로 완전히 독립적인 컬럼 타입 정의
  • 멀티테넌시 원칙 준수
  • 다른 테이블과 일관된 구조

단점:

  • 🟡 기존 데이터 마이그레이션 필요
  • 🟡 모든 회사에 동일한 타입 정의가 복제됨

방안 2: 별도 테이블 생성 (대안)

-- company_specific_column_types 테이블 생성
CREATE TABLE company_specific_column_types (
  id SERIAL PRIMARY KEY,
  company_code VARCHAR(20) NOT NULL,
  table_name VARCHAR NOT NULL,
  column_name VARCHAR NOT NULL,
  input_type VARCHAR NOT NULL,
  detail_settings TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  FOREIGN KEY (company_code) REFERENCES company_info(company_code),
  UNIQUE(company_code, table_name, column_name)
);

-- 조회 시 우선순위
-- 1순위: company_specific_column_types (회사별 설정)
-- 2순위: table_type_columns (전역 기본값)

장점:

  • 기존 table_type_columns는 기본값으로 유지
  • 회사별 커스터마이징은 별도 관리

단점:

  • 복잡한 조회 로직 (2개 테이블 조인)
  • 일관성 없는 구조

방안 3: JSON 필드 사용 (비추천)

-- company_overrides JSON 컬럼 추가
ALTER TABLE table_type_columns
ADD COLUMN company_overrides JSONB;

-- 예시:
{
  "COMPANY_A": { "input_type": "category" },
  "COMPANY_B": { "input_type": "text" }
}

단점:

  • 쿼리 복잡도 증가
  • 인덱싱 어려움
  • 데이터 무결성 보장 어려움

📋 영향 받는 코드

백엔드 서비스

// ❌ 현재 코드 (company_code 없음)
async getColumnType(tableName: string, columnName: string) {
  const query = `
    SELECT input_type FROM table_type_columns
    WHERE table_name = $1 AND column_name = $2
  `;
  return await pool.query(query, [tableName, columnName]);
}

// ✅ 수정 필요 (company_code 추가)
async getColumnType(tableName: string, columnName: string, companyCode: string) {
  const query = `
    SELECT input_type FROM table_type_columns
    WHERE table_name = $1
      AND column_name = $2
      AND company_code = $3
  `;
  return await pool.query(query, [tableName, columnName, companyCode]);
}

영향받는 파일 (예상)

  • backend-node/src/services/tableService.ts
  • backend-node/src/services/dataService.ts
  • backend-node/src/controllers/tableController.ts
  • frontend/components/table-category/CategoryColumnList.tsx
  • 기타 table_type_columns를 참조하는 모든 코드

🧪 테스트 시나리오

테스트 1: 회사별 다른 타입 설정

-- 회사 A: material을 카테고리로
INSERT INTO table_type_columns (table_name, column_name, input_type, company_code)
VALUES ('item_info', 'material', 'category', 'COMPANY_A');

-- 회사 B: material을 텍스트로
INSERT INTO table_type_columns (table_name, column_name, input_type, company_code)
VALUES ('item_info', 'material', 'text', 'COMPANY_B');

-- 조회 확인
SELECT * FROM table_type_columns
WHERE table_name = 'item_info' AND column_name = 'material';

-- 예상 결과:
-- id | table_name | column_name | input_type | company_code
-- 1  | item_info  | material    | category   | COMPANY_A
-- 2  | item_info  | material    | text       | COMPANY_B

테스트 2: 회사별 화면 표시

// 회사 A 사용자가 item_info 테이블 열람
GET /api/tables/item_info/columns
Authorization: Bearer {token_company_a}

// 예상 결과:
{
  "material": {
    "inputType": "category",  // 드롭다운
    "categoryValues": ["철", "알루미늄", "플라스틱"]
  }
}

// 회사 B 사용자가 item_info 테이블 열람
GET /api/tables/item_info/columns
Authorization: Bearer {token_company_b}

// 예상 결과:
{
  "material": {
    "inputType": "text",  // 텍스트 입력
    "placeholder": "재질을 입력하세요"
  }
}

🚨 긴급도 평가

항목 평가 설명
심각도 🔴 높음 회사별 독립적인 테이블 설정 불가능
영향 범위 🔴 전체 시스템 모든 동적 테이블 기능에 영향
수정 난이도 🟡 중간 마이그레이션 + 코드 수정 필요
긴급도 🔴 높음 멀티테넌시 핵심 기능

📝 권장 조치

우선순위 1: 즉시 확인

  • 현재 table_type_columns 사용 현황 파악
  • 실제로 충돌이 발생하고 있는지 확인
  • 회사별로 다른 타입 설정이 필요한 케이스 수집

우선순위 2: 마이그레이션 준비

  • company_code 추가 마이그레이션 작성
  • 기존 데이터 백업 계획 수립
  • 롤백 방안 준비

우선순위 3: 코드 수정

  • 백엔드 서비스 수정 (company_code 추가)
  • API 엔드포인트 수정
  • 프론트엔드 컴포넌트 수정

🔗 관련 이슈


작성일: 2025-11-06
분석자: AI Assistant (사용자 지적 기반)
다음 단계: 마이그레이션 작성 및 코드 수정 필요