# 테이블 타입 관리 SQL 작성 가이드

테이블 타입 관리에서 테이블 생성 시 적용되는 컬럼, 타입, 메타데이터 등록 로직을 기반으로 한 SQL 작성 가이드입니다.

## 핵심 원칙

1. **모든 비즈니스 컬럼은 `VARCHAR(500)`로 통일**: 날짜 타입 외 모든 컬럼은 `VARCHAR(500)`
2. **날짜/시간 컬럼만 `TIMESTAMP` 사용**: `created_date`, `updated_date` 등
3. **기본 컬럼 5개 자동 포함**: 모든 테이블에 id, created_date, updated_date, writer, company_code 필수
4. **3개 메타데이터 테이블 등록 필수**: `table_labels`, `column_labels`, `table_type_columns`

---

## 1. 테이블 생성 DDL 템플릿

### 기본 구조

```sql
CREATE TABLE "테이블명" (
  -- 시스템 기본 컬럼 (자동 포함)
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),b
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),

  -- 사용자 정의 컬럼 (모두 VARCHAR(500))
  "컬럼1" varchar(500),
  "컬럼2" varchar(500),
  "컬럼3" varchar(500)
);
```

### 예시: 고객 테이블 생성

```sql
CREATE TABLE "customer_info" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),

  "customer_name" varchar(500),
  "customer_code" varchar(500),
  "phone" varchar(500),
  "email" varchar(500),
  "address" varchar(500),
  "status" varchar(500),
  "registration_date" varchar(500)
);
```

---

## 2. 메타데이터 테이블 등록

테이블 생성 시 반드시 아래 3개 테이블에 메타데이터를 등록해야 합니다.

### 2.1 table_labels (테이블 메타데이터)

```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('테이블명', '테이블 라벨', '테이블 설명', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET
  table_label = EXCLUDED.table_label,
  description = EXCLUDED.description,
  updated_date = now();
```

### 2.2 table_type_columns (컬럼 타입 정보)

**필수 컬럼**: `table_name`, `column_name`, `company_code`, `input_type`, `display_order`

```sql
-- 기본 컬럼 등록 (display_order: -5 ~ -1)
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, display_order, created_date, updated_date
) VALUES
  ('테이블명', 'id', '*', 'text', '{}', 'Y', -5, now(), now()),
  ('테이블명', 'created_date', '*', 'date', '{}', 'Y', -4, now(), now()),
  ('테이블명', 'updated_date', '*', 'date', '{}', 'Y', -3, now(), now()),
  ('테이블명', 'writer', '*', 'text', '{}', 'Y', -2, now(), now()),
  ('테이블명', 'company_code', '*', 'text', '{}', 'Y', -1, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET
  input_type = EXCLUDED.input_type,
  display_order = EXCLUDED.display_order,
  updated_date = now();

-- 사용자 정의 컬럼 등록 (display_order: 0부터 시작)
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, display_order, created_date, updated_date
) VALUES
  ('테이블명', '컬럼1', '*', 'text', '{}', 'Y', 0, now(), now()),
  ('테이블명', '컬럼2', '*', 'number', '{}', 'Y', 1, now(), now()),
  ('테이블명', '컬럼3', '*', 'code', '{"codeCategory":"카테고리코드"}', 'Y', 2, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET
  input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings,
  display_order = EXCLUDED.display_order,
  updated_date = now();
```

### 2.3 column_labels (레거시 호환용 - 필수)

```sql
-- 기본 컬럼 등록
INSERT INTO column_labels (
  table_name, column_name, column_label, input_type, detail_settings,
  description, display_order, is_visible, created_date, updated_date
) VALUES
  ('테이블명', 'id', 'ID', 'text', '{}', '기본키 (자동생성)', -5, true, now(), now()),
  ('테이블명', 'created_date', '생성일시', 'date', '{}', '레코드 생성일시', -4, true, now(), now()),
  ('테이블명', 'updated_date', '수정일시', 'date', '{}', '레코드 수정일시', -3, true, now(), now()),
  ('테이블명', 'writer', '작성자', 'text', '{}', '레코드 작성자', -2, true, now(), now()),
  ('테이블명', 'company_code', '회사코드', 'text', '{}', '회사 구분 코드', -1, true, now(), now())
ON CONFLICT (table_name, column_name)
DO UPDATE SET
  column_label = EXCLUDED.column_label,
  input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings,
  description = EXCLUDED.description,
  display_order = EXCLUDED.display_order,
  is_visible = EXCLUDED.is_visible,
  updated_date = now();

-- 사용자 정의 컬럼 등록
INSERT INTO column_labels (
  table_name, column_name, column_label, input_type, detail_settings,
  description, display_order, is_visible, created_date, updated_date
) VALUES
  ('테이블명', '컬럼1', '컬럼1 라벨', 'text', '{}', '컬럼1 설명', 0, true, now(), now()),
  ('테이블명', '컬럼2', '컬럼2 라벨', 'number', '{}', '컬럼2 설명', 1, true, now(), now())
ON CONFLICT (table_name, column_name)
DO UPDATE SET
  column_label = EXCLUDED.column_label,
  input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings,
  description = EXCLUDED.description,
  display_order = EXCLUDED.display_order,
  is_visible = EXCLUDED.is_visible,
  updated_date = now();
```

---

## 3. Input Type 정의

### 지원되는 Input Type 목록

| input_type | 설명          | DB 저장 타입 | UI 컴포넌트          |
| ---------- | ------------- | ------------ | -------------------- |
| `text`     | 텍스트 입력   | VARCHAR(500) | Input                |
| `number`   | 숫자 입력     | VARCHAR(500) | Input (type=number)  |
| `date`     | 날짜/시간     | VARCHAR(500) | DatePicker           |
| `code`     | 공통코드 선택 | VARCHAR(500) | Select (코드 목록)   |
| `entity`   | 엔티티 참조   | VARCHAR(500) | Select (테이블 참조) |
| `select`   | 선택 목록     | VARCHAR(500) | Select               |
| `checkbox` | 체크박스      | VARCHAR(500) | Checkbox             |
| `radio`    | 라디오 버튼   | VARCHAR(500) | RadioGroup           |
| `textarea` | 긴 텍스트     | VARCHAR(500) | Textarea             |
| `file`     | 파일 업로드   | VARCHAR(500) | FileUpload           |

### WebType → InputType 변환 규칙

```
text, textarea, email, tel, url, password → text
number, decimal → number
date, datetime, time → date
select, dropdown → select
checkbox, boolean → checkbox
radio → radio
code → code
entity → entity
file → text
button → text
```

---

## 4. Detail Settings 설정

### 4.1 Code 타입 (공통코드 참조)

```json
{
  "codeCategory": "코드_카테고리_ID"
}
```

```sql
INSERT INTO table_type_columns (..., input_type, detail_settings, ...)
VALUES (..., 'code', '{"codeCategory":"STATUS_CODE"}', ...);
```

### 4.2 Entity 타입 (테이블 참조)

```json
{
  "referenceTable": "참조_테이블명",
  "referenceColumn": "참조_컬럼명(보통 id)",
  "displayColumn": "표시할_컬럼명"
}
```

```sql
INSERT INTO table_type_columns (..., input_type, detail_settings, ...)
VALUES (..., 'entity', '{"referenceTable":"user_info","referenceColumn":"id","displayColumn":"user_name"}', ...);
```

### 4.3 Select 타입 (정적 옵션)

```json
{
  "options": [
    { "label": "옵션1", "value": "value1" },
    { "label": "옵션2", "value": "value2" }
  ]
}
```

---

## 5. 전체 예시: 주문 테이블 생성

### Step 1: DDL 실행

```sql
CREATE TABLE "order_info" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),

  "order_no" varchar(500),
  "order_date" varchar(500),
  "customer_id" varchar(500),
  "total_amount" varchar(500),
  "status" varchar(500),
  "notes" varchar(500)
);
```

### Step 2: table_labels 등록

```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('order_info', '주문 정보', '주문 관리 테이블', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET
  table_label = EXCLUDED.table_label,
  description = EXCLUDED.description,
  updated_date = now();
```

### Step 3: table_type_columns 등록

```sql
-- 기본 컬럼
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES
  ('order_info', 'id', '*', 'text', '{}', 'Y', -5, now(), now()),
  ('order_info', 'created_date', '*', 'date', '{}', 'Y', -4, now(), now()),
  ('order_info', 'updated_date', '*', 'date', '{}', 'Y', -3, now(), now()),
  ('order_info', 'writer', '*', 'text', '{}', 'Y', -2, now(), now()),
  ('order_info', 'company_code', '*', 'text', '{}', 'Y', -1, now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, display_order = EXCLUDED.display_order, updated_date = now();

-- 사용자 정의 컬럼
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES
  ('order_info', 'order_no', '*', 'text', '{}', 'Y', 0, now(), now()),
  ('order_info', 'order_date', '*', 'date', '{}', 'Y', 1, now(), now()),
  ('order_info', 'customer_id', '*', 'entity', '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', 'Y', 2, now(), now()),
  ('order_info', 'total_amount', '*', 'number', '{}', 'Y', 3, now(), now()),
  ('order_info', 'status', '*', 'code', '{"codeCategory":"ORDER_STATUS"}', 'Y', 4, now(), now()),
  ('order_info', 'notes', '*', 'textarea', '{}', 'Y', 5, now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, display_order = EXCLUDED.display_order, updated_date = now();
```

### Step 4: column_labels 등록 (레거시 호환)

```sql
-- 기본 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES
  ('order_info', 'id', 'ID', 'text', '{}', '기본키', -5, true, now(), now()),
  ('order_info', 'created_date', '생성일시', 'date', '{}', '레코드 생성일시', -4, true, now(), now()),
  ('order_info', 'updated_date', '수정일시', 'date', '{}', '레코드 수정일시', -3, true, now(), now()),
  ('order_info', 'writer', '작성자', 'text', '{}', '레코드 작성자', -2, true, now(), now()),
  ('order_info', 'company_code', '회사코드', 'text', '{}', '회사 구분 코드', -1, true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, updated_date = now();

-- 사용자 정의 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES
  ('order_info', 'order_no', '주문번호', 'text', '{}', '주문 식별 번호', 0, true, now(), now()),
  ('order_info', 'order_date', '주문일자', 'date', '{}', '주문 발생 일자', 1, true, now(), now()),
  ('order_info', 'customer_id', '고객', 'entity', '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', '주문 고객', 2, true, now(), now()),
  ('order_info', 'total_amount', '총금액', 'number', '{}', '주문 총 금액', 3, true, now(), now()),
  ('order_info', 'status', '상태', 'code', '{"codeCategory":"ORDER_STATUS"}', '주문 상태', 4, true, now(), now()),
  ('order_info', 'notes', '비고', 'textarea', '{}', '추가 메모', 5, true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description, display_order = EXCLUDED.display_order, updated_date = now();
```

---

## 6. 컬럼 추가 시

### DDL

```sql
ALTER TABLE "테이블명" ADD COLUMN "새컬럼명" varchar(500);
```

### 메타데이터 등록

```sql
-- table_type_columns
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date)
VALUES ('테이블명', '새컬럼명', '*', 'text', '{}', 'Y', (SELECT COALESCE(MAX(display_order), 0) + 1 FROM table_type_columns WHERE table_name = '테이블명'), now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, display_order = EXCLUDED.display_order, updated_date = now();

-- column_labels
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, created_date, updated_date)
VALUES ('테이블명', '새컬럼명', '새컬럼 라벨', 'text', '{}', '새컬럼 설명', (SELECT COALESCE(MAX(display_order), 0) + 1 FROM column_labels WHERE table_name = '테이블명'), true, now(), now())
ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, updated_date = now();
```

---

## 7. 로그 테이블 생성 (선택사항)

변경 이력 추적이 필요한 테이블에는 로그 테이블을 생성할 수 있습니다.

### 7.1 로그 테이블 DDL 템플릿

```sql
-- 로그 테이블 생성
CREATE TABLE 테이블명_log (
  log_id SERIAL PRIMARY KEY,
  operation_type VARCHAR(10) NOT NULL,       -- INSERT/UPDATE/DELETE
  original_id VARCHAR(100),                   -- 원본 테이블 PK 값
  changed_column VARCHAR(100),                -- 변경된 컬럼명
  old_value TEXT,                             -- 변경 전 값
  new_value TEXT,                             -- 변경 후 값
  changed_by VARCHAR(50),                     -- 변경자 ID
  changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 변경 시각
  ip_address VARCHAR(50),                     -- 변경 요청 IP
  user_agent TEXT,                            -- User Agent
  full_row_before JSONB,                      -- 변경 전 전체 행
  full_row_after JSONB                        -- 변경 후 전체 행
);

-- 인덱스 생성
CREATE INDEX idx_테이블명_log_original_id ON 테이블명_log(original_id);
CREATE INDEX idx_테이블명_log_changed_at ON 테이블명_log(changed_at);
CREATE INDEX idx_테이블명_log_operation ON 테이블명_log(operation_type);

-- 코멘트 추가
COMMENT ON TABLE 테이블명_log IS '테이블명 테이블 변경 이력';
```

### 7.2 트리거 함수 DDL 템플릿

```sql
CREATE OR REPLACE FUNCTION 테이블명_log_trigger_func()
RETURNS TRIGGER AS $$
DECLARE
  v_column_name TEXT;
  v_old_value TEXT;
  v_new_value TEXT;
  v_user_id VARCHAR(50);
  v_ip_address VARCHAR(50);
BEGIN
  v_user_id := current_setting('app.user_id', TRUE);
  v_ip_address := current_setting('app.ip_address', TRUE);

  IF (TG_OP = 'INSERT') THEN
    INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_after)
    VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
    RETURN NEW;

  ELSIF (TG_OP = 'UPDATE') THEN
    FOR v_column_name IN
      SELECT column_name
      FROM information_schema.columns
      WHERE table_name = '테이블명'
        AND table_schema = 'public'
    LOOP
      EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
      INTO v_old_value, v_new_value
      USING OLD, NEW;

      IF v_old_value IS DISTINCT FROM v_new_value THEN
        INSERT INTO 테이블명_log (
          operation_type, original_id, changed_column, old_value, new_value,
          changed_by, ip_address, full_row_before, full_row_after
        )
        VALUES (
          'UPDATE', NEW.id, v_column_name, v_old_value, v_new_value,
          v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb
        );
      END IF;
    END LOOP;
    RETURN NEW;

  ELSIF (TG_OP = 'DELETE') THEN
    INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_before)
    VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
    RETURN OLD;
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql;
```

### 7.3 트리거 DDL 템플릿

```sql
CREATE TRIGGER 테이블명_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON 테이블명
FOR EACH ROW EXECUTE FUNCTION 테이블명_log_trigger_func();
```

### 7.4 로그 설정 등록

```sql
INSERT INTO table_log_config (
  original_table_name, log_table_name, trigger_name,
  trigger_function_name, is_active, created_by, created_at
) VALUES (
  '테이블명', '테이블명_log', '테이블명_audit_trigger',
  '테이블명_log_trigger_func', 'Y', '생성자ID', now()
);
```

### 7.5 table_labels에 use_log_table 플래그 설정

```sql
UPDATE table_labels
SET use_log_table = 'Y', updated_date = now()
WHERE table_name = '테이블명';
```

### 7.6 전체 예시: order_info 로그 테이블 생성

```sql
-- Step 1: 로그 테이블 생성
CREATE TABLE order_info_log (
  log_id SERIAL PRIMARY KEY,
  operation_type VARCHAR(10) NOT NULL,
  original_id VARCHAR(100),
  changed_column VARCHAR(100),
  old_value TEXT,
  new_value TEXT,
  changed_by VARCHAR(50),
  changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  ip_address VARCHAR(50),
  user_agent TEXT,
  full_row_before JSONB,
  full_row_after JSONB
);

CREATE INDEX idx_order_info_log_original_id ON order_info_log(original_id);
CREATE INDEX idx_order_info_log_changed_at ON order_info_log(changed_at);
CREATE INDEX idx_order_info_log_operation ON order_info_log(operation_type);

COMMENT ON TABLE order_info_log IS 'order_info 테이블 변경 이력';

-- Step 2: 트리거 함수 생성
CREATE OR REPLACE FUNCTION order_info_log_trigger_func()
RETURNS TRIGGER AS $$
DECLARE
  v_column_name TEXT;
  v_old_value TEXT;
  v_new_value TEXT;
  v_user_id VARCHAR(50);
  v_ip_address VARCHAR(50);
BEGIN
  v_user_id := current_setting('app.user_id', TRUE);
  v_ip_address := current_setting('app.ip_address', TRUE);

  IF (TG_OP = 'INSERT') THEN
    INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_after)
    VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
    RETURN NEW;
  ELSIF (TG_OP = 'UPDATE') THEN
    FOR v_column_name IN
      SELECT column_name FROM information_schema.columns
      WHERE table_name = 'order_info' AND table_schema = 'public'
    LOOP
      EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
      INTO v_old_value, v_new_value USING OLD, NEW;
      IF v_old_value IS DISTINCT FROM v_new_value THEN
        INSERT INTO order_info_log (operation_type, original_id, changed_column, old_value, new_value, changed_by, ip_address, full_row_before, full_row_after)
        VALUES ('UPDATE', NEW.id, v_column_name, v_old_value, v_new_value, v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
      END IF;
    END LOOP;
    RETURN NEW;
  ELSIF (TG_OP = 'DELETE') THEN
    INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_before)
    VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
    RETURN OLD;
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Step 3: 트리거 생성
CREATE TRIGGER order_info_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON order_info
FOR EACH ROW EXECUTE FUNCTION order_info_log_trigger_func();

-- Step 4: 로그 설정 등록
INSERT INTO table_log_config (original_table_name, log_table_name, trigger_name, trigger_function_name, is_active, created_by, created_at)
VALUES ('order_info', 'order_info_log', 'order_info_audit_trigger', 'order_info_log_trigger_func', 'Y', 'system', now());

-- Step 5: table_labels 플래그 업데이트
UPDATE table_labels SET use_log_table = 'Y', updated_date = now() WHERE table_name = 'order_info';
```

### 7.7 로그 테이블 삭제

```sql
-- 트리거 삭제
DROP TRIGGER IF EXISTS 테이블명_audit_trigger ON 테이블명;

-- 트리거 함수 삭제
DROP FUNCTION IF EXISTS 테이블명_log_trigger_func();

-- 로그 테이블 삭제
DROP TABLE IF EXISTS 테이블명_log;

-- 로그 설정 삭제
DELETE FROM table_log_config WHERE original_table_name = '테이블명';

-- table_labels 플래그 업데이트
UPDATE table_labels SET use_log_table = 'N', updated_date = now() WHERE table_name = '테이블명';
```

---

## 8. 체크리스트

### 테이블 생성/수정 시 반드시 확인할 사항:

- [ ] DDL에 기본 5개 컬럼 포함 (id, created_date, updated_date, writer, company_code)
- [ ] 모든 비즈니스 컬럼은 `VARCHAR(500)` 타입 사용
- [ ] `table_labels`에 테이블 메타데이터 등록
- [ ] `table_type_columns`에 모든 컬럼 등록 (company_code = '\*')
- [ ] `column_labels`에 모든 컬럼 등록 (레거시 호환)
- [ ] 기본 컬럼 display_order: -5 ~ -1
- [ ] 사용자 정의 컬럼 display_order: 0부터 순차
- [ ] code/entity 타입은 detail_settings에 참조 정보 포함
- [ ] ON CONFLICT 절로 중복 시 UPDATE 처리

### 로그 테이블 생성 시 확인할 사항 (선택):

- [ ] 로그 테이블 생성 (`테이블명_log`)
- [ ] 인덱스 3개 생성 (original_id, changed_at, operation_type)
- [ ] 트리거 함수 생성 (`테이블명_log_trigger_func`)
- [ ] 트리거 생성 (`테이블명_audit_trigger`)
- [ ] `table_log_config`에 로그 설정 등록
- [ ] `table_labels.use_log_table = 'Y'` 업데이트

---

## 9. 금지 사항

1. **DB 타입 직접 지정 금지**: NUMBER, INTEGER, DATE 등 DB 타입 직접 사용 금지
2. **VARCHAR 길이 변경 금지**: 반드시 `VARCHAR(500)` 사용
3. **기본 컬럼 누락 금지**: id, created_date, updated_date, writer, company_code 필수
4. **메타데이터 미등록 금지**: 3개 테이블 모두 등록 필수
5. **web_type 사용 금지**: 레거시 컬럼이므로 `input_type` 사용

---

## 참조 파일

- `backend-node/src/services/ddlExecutionService.ts`: DDL 실행 서비스
- `backend-node/src/services/tableManagementService.ts`: 로그 테이블 생성 서비스
- `backend-node/src/types/ddl.ts`: DDL 타입 정의
- `backend-node/src/controllers/ddlController.ts`: DDL API 컨트롤러
- `backend-node/src/controllers/tableManagementController.ts`: 로그 테이블 API 컨트롤러
