457 lines
12 KiB
Markdown
457 lines
12 KiB
Markdown
|
|
# 테이블 컬럼 타입 멀티테넌시 구조적 문제 분석
|
||
|
|
|
||
|
|
> **작성일**: 2025-11-06
|
||
|
|
> **심각도**: 🔴 **치명적 (Critical)**
|
||
|
|
> **상태**: 🚨 **긴급 분석 필요**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚨 발견된 구조적 문제
|
||
|
|
|
||
|
|
### 문제 요약
|
||
|
|
|
||
|
|
**현재 `table_type_columns` 테이블에 `company_code` 컬럼이 없음!**
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 현재 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. 테이블 구조 확인
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 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. 현재 데이터 예시
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 현재 저장된 데이터
|
||
|
|
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가 `material`을 `category`로 설정하면
|
||
|
|
- 회사 B는 `material`을 `text`로 설정할 수 없음!
|
||
|
|
- **하나의 컬럼 타입 정의를 모든 회사가 공유**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 멀티테넌시 충돌 분석
|
||
|
|
|
||
|
|
### Case 1: 같은 테이블, 같은 컬럼, 다른 타입
|
||
|
|
|
||
|
|
| 요구사항 | 회사 A | 회사 B | 현재 가능? |
|
||
|
|
| ---------- | ----------- | ----------- | ------------- |
|
||
|
|
| 테이블 | `item_info` | `item_info` | ✅ 공유 |
|
||
|
|
| 컬럼 | `material` | `material` | ✅ 공유 |
|
||
|
|
| input_type | `category` | `text` | ❌ **불가능** |
|
||
|
|
|
||
|
|
**현재 동작**:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 회사 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_values`는 `company_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 추가 (권장) ⭐
|
||
|
|
|
||
|
|
**마이그레이션**:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 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: 별도 테이블 생성 (대안)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 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 필드 사용 (비추천)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- company_overrides JSON 컬럼 추가
|
||
|
|
ALTER TABLE table_type_columns
|
||
|
|
ADD COLUMN company_overrides JSONB;
|
||
|
|
|
||
|
|
-- 예시:
|
||
|
|
{
|
||
|
|
"COMPANY_A": { "input_type": "category" },
|
||
|
|
"COMPANY_B": { "input_type": "text" }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**단점**:
|
||
|
|
|
||
|
|
- ❌ 쿼리 복잡도 증가
|
||
|
|
- ❌ 인덱싱 어려움
|
||
|
|
- ❌ 데이터 무결성 보장 어려움
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 영향 받는 코드
|
||
|
|
|
||
|
|
### 백엔드 서비스
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ❌ 현재 코드 (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: 회사별 다른 타입 설정
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 회사 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: 회사별 화면 표시
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 회사 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 엔드포인트 수정
|
||
|
|
- [ ] 프론트엔드 컴포넌트 수정
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔗 관련 이슈
|
||
|
|
|
||
|
|
- [채번 규칙 멀티테넌시 버그](./채번규칙_멀티테넌시_버그_수정_완료.md) ✅ 수정 완료
|
||
|
|
- [카테고리 값 멀티테넌시 버그](./카테고리_멀티테넌시_버그_수정_완료.md) ✅ 수정 완료
|
||
|
|
- 🚨 **테이블 컬럼 타입 멀티테넌시** ← 현재 문서 (미수정)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**작성일**: 2025-11-06
|
||
|
|
**분석자**: AI Assistant (사용자 지적 기반)
|
||
|
|
**다음 단계**: 마이그레이션 작성 및 코드 수정 필요
|