diff --git a/.cursor/rules/table-type-sql-guide.mdc b/.cursor/rules/table-type-sql-guide.mdc new file mode 100644 index 00000000..3c53c537 --- /dev/null +++ b/.cursor/rules/table-type-sql-guide.mdc @@ -0,0 +1,592 @@ +# 테이블 타입 관리 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(), + "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 컨트롤러 diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 04fa1add..7c84898b 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -1973,15 +1973,21 @@ export async function multiTableSave( for (const subTableConfig of subTables || []) { const { tableName, linkColumn, items, options } = subTableConfig; - if (!tableName || !items || items.length === 0) { - logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음`); + // saveMainAsFirst가 활성화된 경우, items가 비어있어도 메인 데이터를 서브 테이블에 저장해야 함 + const hasSaveMainAsFirst = options?.saveMainAsFirst && + options?.mainFieldMappings && + options.mainFieldMappings.length > 0; + + if (!tableName || (!items?.length && !hasSaveMainAsFirst)) { + logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음 (saveMainAsFirst: ${hasSaveMainAsFirst})`); continue; } logger.info(`서브 테이블 ${tableName} 저장 시작:`, { - itemsCount: items.length, + itemsCount: items?.length || 0, linkColumn, options, + hasSaveMainAsFirst, }); // 기존 데이터 삭제 옵션 @@ -1999,7 +2005,15 @@ export async function multiTableSave( } // 메인 데이터도 서브 테이블에 저장 (옵션) - if (options?.saveMainAsFirst && options?.mainFieldMappings && linkColumn?.subColumn) { + // mainFieldMappings가 비어 있으면 건너뜀 (필수 컬럼 누락 방지) + logger.info(`saveMainAsFirst 옵션 확인:`, { + saveMainAsFirst: options?.saveMainAsFirst, + mainFieldMappings: options?.mainFieldMappings, + mainFieldMappingsLength: options?.mainFieldMappings?.length, + linkColumn, + mainDataKeys: Object.keys(mainData), + }); + if (options?.saveMainAsFirst && options?.mainFieldMappings && options.mainFieldMappings.length > 0 && linkColumn?.subColumn) { const mainSubItem: Record = { [linkColumn.subColumn]: savedPkValue, }; diff --git a/frontend/app/(main)/admin/batchmng/create/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx similarity index 100% rename from frontend/app/(main)/admin/batchmng/create/page.tsx rename to frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx diff --git a/frontend/app/(main)/admin/batchmng/edit/[id]/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx similarity index 100% rename from frontend/app/(main)/admin/batchmng/edit/[id]/page.tsx rename to frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx diff --git a/frontend/app/(main)/admin/batchmng/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx similarity index 100% rename from frontend/app/(main)/admin/batchmng/page.tsx rename to frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx similarity index 100% rename from frontend/app/(main)/admin/external-call-configs/page.tsx rename to frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/automaticMng/exconList/page.tsx similarity index 100% rename from frontend/app/(main)/admin/external-connections/page.tsx rename to frontend/app/(main)/admin/automaticMng/exconList/page.tsx diff --git a/frontend/app/(main)/admin/flow-management/[id]/page.tsx b/frontend/app/(main)/admin/automaticMng/flowMgmtList/[id]/page.tsx similarity index 100% rename from frontend/app/(main)/admin/flow-management/[id]/page.tsx rename to frontend/app/(main)/admin/automaticMng/flowMgmtList/[id]/page.tsx diff --git a/frontend/app/(main)/admin/flow-management/page.tsx b/frontend/app/(main)/admin/automaticMng/flowMgmtList/page.tsx similarity index 100% rename from frontend/app/(main)/admin/flow-management/page.tsx rename to frontend/app/(main)/admin/automaticMng/flowMgmtList/page.tsx diff --git a/frontend/app/(main)/admin/mail/accounts/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/accounts/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/accounts/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/accounts/page.tsx diff --git a/frontend/app/(main)/admin/mail/bulk-send/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/bulk-send/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/bulk-send/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/bulk-send/page.tsx diff --git a/frontend/app/(main)/admin/mail/dashboard/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/dashboardList/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/dashboard/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/dashboardList/page.tsx diff --git a/frontend/app/(main)/admin/mail/drafts/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/drafts/page.tsx similarity index 99% rename from frontend/app/(main)/admin/mail/drafts/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/drafts/page.tsx index c96129c1..e098352f 100644 --- a/frontend/app/(main)/admin/mail/drafts/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/mail/drafts/page.tsx @@ -51,7 +51,7 @@ export default function DraftsPage() { content: draft.htmlContent, accountId: draft.accountId, }); - router.push(`/admin/mail/send?${params.toString()}`); + router.push(`/admin/automaticMng/mail/send?${params.toString()}`); }; const handleDelete = async (id: string) => { diff --git a/frontend/app/(main)/admin/mail/receive/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/receive/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/receive/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/receive/page.tsx diff --git a/frontend/app/(main)/admin/mail/send/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/send/page.tsx similarity index 99% rename from frontend/app/(main)/admin/mail/send/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/send/page.tsx index 56922043..cdd8feae 100644 --- a/frontend/app/(main)/admin/mail/send/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/mail/send/page.tsx @@ -1056,7 +1056,7 @@ ${data.originalBody}`; - diff --git a/frontend/app/(main)/admin/mail/templates/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/templates/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/templates/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/templates/page.tsx diff --git a/frontend/app/(main)/admin/mail/trash/page.tsx b/frontend/app/(main)/admin/automaticMng/mail/trash/page.tsx similarity index 100% rename from frontend/app/(main)/admin/mail/trash/page.tsx rename to frontend/app/(main)/admin/automaticMng/mail/trash/page.tsx diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index f8d5d8d6..3060e0fc 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -168,7 +168,7 @@ export default function AdminPage() { - +
@@ -182,7 +182,7 @@ export default function AdminPage() {
- +
diff --git a/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx b/frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx similarity index 97% rename from frontend/app/(main)/admin/dashboard/DashboardListClient.tsx rename to frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx index 613ab16b..c50aaa51 100644 --- a/frontend/app/(main)/admin/dashboard/DashboardListClient.tsx +++ b/frontend/app/(main)/admin/screenMng/dashboardList/DashboardListClient.tsx @@ -180,7 +180,7 @@ export default function DashboardListClient() { 총 {totalCount.toLocaleString()}
-
@@ -292,7 +292,7 @@ export default function DashboardListClient() {
➕ 새 대시보드 만들기 @@ -185,7 +185,7 @@ export default function DashboardListPage() {

{!searchTerm && ( ➕ 대시보드 만들기 @@ -251,7 +251,7 @@ function DashboardCard({ dashboard }: DashboardCardProps) { 보기 편집 diff --git a/frontend/components/admin/CompanyTable.tsx b/frontend/components/admin/CompanyTable.tsx index b36a757b..9c253765 100644 --- a/frontend/components/admin/CompanyTable.tsx +++ b/frontend/components/admin/CompanyTable.tsx @@ -22,7 +22,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company // 부서 관리 페이지로 이동 const handleManageDepartments = (company: Company) => { - router.push(`/admin/company/${company.company_code}/departments`); + router.push(`/admin/userMng/companyList/${company.company_code}/departments`); }; // 디스크 사용량 포맷팅 함수 diff --git a/frontend/components/admin/RoleDetailManagement.tsx b/frontend/components/admin/RoleDetailManagement.tsx index 27a6c07d..92d03143 100644 --- a/frontend/components/admin/RoleDetailManagement.tsx +++ b/frontend/components/admin/RoleDetailManagement.tsx @@ -236,7 +236,7 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) {

오류 발생

{error || "권한 그룹을 찾을 수 없습니다."}

-
@@ -248,7 +248,7 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { {/* 페이지 헤더 */}
-
diff --git a/frontend/components/admin/RoleManagement.tsx b/frontend/components/admin/RoleManagement.tsx index fe527fe4..3834b2a4 100644 --- a/frontend/components/admin/RoleManagement.tsx +++ b/frontend/components/admin/RoleManagement.tsx @@ -141,7 +141,7 @@ export function RoleManagement() { // 상세 페이지로 이동 const handleViewDetail = useCallback( (role: RoleGroup) => { - router.push(`/admin/roles/${role.objid}`); + router.push(`/admin/userMng/rolesList/${role.objid}`); }, [router], ); diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 08296fd1..b945cb3d 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -643,7 +643,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D open={successModalOpen} onOpenChange={() => { setSuccessModalOpen(false); - router.push("/admin/dashboard"); + router.push("/admin/screenMng/dashboardList"); }} > @@ -660,7 +660,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D