diff --git a/backend-node/scripts/migrate-input-type-to-web-type.ts b/backend-node/scripts/migrate-input-type-to-web-type.ts new file mode 100644 index 00000000..65c64b14 --- /dev/null +++ b/backend-node/scripts/migrate-input-type-to-web-type.ts @@ -0,0 +1,168 @@ +import { query } from "../src/database/db"; +import { logger } from "../src/utils/logger"; + +/** + * input_type을 web_type으로 마이그레이션하는 스크립트 + * + * 목적: + * - column_labels 테이블의 input_type 값을 읽어서 + * - 해당하는 기본 web_type 값으로 변환 + * - web_type이 null인 경우에만 업데이트 + */ + +// input_type → 기본 web_type 매핑 +const INPUT_TYPE_TO_WEB_TYPE: Record = { + text: "text", // 일반 텍스트 + number: "number", // 정수 + date: "date", // 날짜 + code: "code", // 코드 선택박스 + entity: "entity", // 엔티티 참조 + select: "select", // 선택박스 + checkbox: "checkbox", // 체크박스 + radio: "radio", // 라디오버튼 + direct: "text", // direct는 text로 매핑 +}; + +async function migrateInputTypeToWebType() { + try { + logger.info("=".repeat(60)); + logger.info("input_type → web_type 마이그레이션 시작"); + logger.info("=".repeat(60)); + + // 1. 현재 상태 확인 + const stats = await query<{ + total: string; + has_input_type: string; + has_web_type: string; + needs_migration: string; + }>( + `SELECT + COUNT(*) as total, + COUNT(input_type) FILTER (WHERE input_type IS NOT NULL) as has_input_type, + COUNT(web_type) FILTER (WHERE web_type IS NOT NULL) as has_web_type, + COUNT(*) FILTER (WHERE input_type IS NOT NULL AND web_type IS NULL) as needs_migration + FROM column_labels` + ); + + const stat = stats[0]; + logger.info("\n📊 현재 상태:"); + logger.info(` - 전체 컬럼: ${stat.total}개`); + logger.info(` - input_type 있음: ${stat.has_input_type}개`); + logger.info(` - web_type 있음: ${stat.has_web_type}개`); + logger.info(` - 마이그레이션 필요: ${stat.needs_migration}개`); + + if (parseInt(stat.needs_migration) === 0) { + logger.info("\n✅ 마이그레이션이 필요한 데이터가 없습니다."); + return; + } + + // 2. input_type별 분포 확인 + const distribution = await query<{ + input_type: string; + count: string; + }>( + `SELECT + input_type, + COUNT(*) as count + FROM column_labels + WHERE input_type IS NOT NULL AND web_type IS NULL + GROUP BY input_type + ORDER BY input_type` + ); + + logger.info("\n📋 input_type별 분포:"); + distribution.forEach((item) => { + const webType = + INPUT_TYPE_TO_WEB_TYPE[item.input_type] || item.input_type; + logger.info(` - ${item.input_type} → ${webType}: ${item.count}개`); + }); + + // 3. 마이그레이션 실행 + logger.info("\n🔄 마이그레이션 실행 중..."); + + let totalUpdated = 0; + + for (const [inputType, webType] of Object.entries(INPUT_TYPE_TO_WEB_TYPE)) { + const result = await query( + `UPDATE column_labels + SET + web_type = $1, + updated_date = NOW() + WHERE input_type = $2 + AND web_type IS NULL + RETURNING id, table_name, column_name`, + [webType, inputType] + ); + + if (result.length > 0) { + logger.info( + ` ✓ ${inputType} → ${webType}: ${result.length}개 업데이트` + ); + totalUpdated += result.length; + + // 처음 5개만 출력 + result.slice(0, 5).forEach((row: any) => { + logger.info(` - ${row.table_name}.${row.column_name}`); + }); + if (result.length > 5) { + logger.info(` ... 외 ${result.length - 5}개`); + } + } + } + + // 4. 결과 확인 + const afterStats = await query<{ + total: string; + has_web_type: string; + }>( + `SELECT + COUNT(*) as total, + COUNT(web_type) FILTER (WHERE web_type IS NOT NULL) as has_web_type + FROM column_labels` + ); + + const afterStat = afterStats[0]; + + logger.info("\n" + "=".repeat(60)); + logger.info("✅ 마이그레이션 완료!"); + logger.info("=".repeat(60)); + logger.info(`📊 최종 통계:`); + logger.info(` - 전체 컬럼: ${afterStat.total}개`); + logger.info(` - web_type 설정됨: ${afterStat.has_web_type}개`); + logger.info(` - 업데이트된 컬럼: ${totalUpdated}개`); + logger.info("=".repeat(60)); + + // 5. 샘플 데이터 출력 + logger.info("\n📝 샘플 데이터 (check_report_mng 테이블):"); + const samples = await query<{ + column_name: string; + input_type: string; + web_type: string; + detail_settings: string; + }>( + `SELECT + column_name, + input_type, + web_type, + detail_settings + FROM column_labels + WHERE table_name = 'check_report_mng' + ORDER BY column_name + LIMIT 10` + ); + + samples.forEach((sample) => { + logger.info( + ` ${sample.column_name}: ${sample.input_type} → ${sample.web_type}` + ); + }); + + process.exit(0); + } catch (error) { + logger.error("❌ 마이그레이션 실패:", error); + process.exit(1); + } +} + +// 스크립트 실행 +migrateInputTypeToWebType(); diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 7c32bda6..6da8d16a 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1018,14 +1018,14 @@ export class ScreenManagementService { [tableName] ); - // column_labels 테이블에서 웹타입 정보 조회 (있는 경우) + // column_labels 테이블에서 입력타입 정보 조회 (있는 경우) const webTypeInfo = await query<{ column_name: string; - web_type: string | null; + input_type: string | null; column_label: string | null; detail_settings: any; }>( - `SELECT column_name, web_type, column_label, detail_settings + `SELECT column_name, input_type, column_label, detail_settings FROM column_labels WHERE table_name = $1`, [tableName] @@ -1045,7 +1045,7 @@ export class ScreenManagementService { this.getColumnLabel(column.column_name), dataType: column.data_type, webType: - (webTypeData?.web_type as WebType) || + (webTypeData?.input_type as WebType) || this.inferWebType(column.data_type), isNullable: column.is_nullable, columnDefault: column.column_default || undefined, @@ -1522,7 +1522,7 @@ export class ScreenManagementService { c.column_name, COALESCE(cl.column_label, c.column_name) as column_label, c.data_type, - COALESCE(cl.web_type, 'text') as web_type, + COALESCE(cl.input_type, 'text') as web_type, c.is_nullable, c.column_default, c.character_maximum_length, @@ -1548,7 +1548,7 @@ export class ScreenManagementService { } /** - * 웹 타입 설정 (✅ Raw Query 전환 완료) + * 입력 타입 설정 (✅ Raw Query 전환 완료) */ async setColumnWebType( tableName: string, @@ -1556,16 +1556,16 @@ export class ScreenManagementService { webType: WebType, additionalSettings?: Partial ): Promise { - // UPSERT를 INSERT ... ON CONFLICT로 변환 + // UPSERT를 INSERT ... ON CONFLICT로 변환 (input_type 사용) await query( `INSERT INTO column_labels ( - table_name, column_name, column_label, web_type, detail_settings, + table_name, column_name, column_label, input_type, detail_settings, code_category, reference_table, reference_column, display_column, is_visible, display_order, description, created_date, updated_date ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (table_name, column_name) DO UPDATE SET - web_type = $4, + input_type = $4, column_label = $3, detail_settings = $5, code_category = $6, diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index dd8cb1cc..83f3a696 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -27,13 +27,13 @@ export class TableManagementService { columnName: string ): Promise<{ isCodeType: boolean; codeCategory?: string }> { try { - // column_labels 테이블에서 해당 컬럼의 web_type이 'code'인지 확인 + // column_labels 테이블에서 해당 컬럼의 input_type이 'code'인지 확인 const result = await query( - `SELECT web_type, code_category + `SELECT input_type, code_category FROM column_labels WHERE table_name = $1 AND column_name = $2 - AND web_type = 'code'`, + AND input_type = 'code'`, [tableName, columnName] ); @@ -167,7 +167,7 @@ export class TableManagementService { COALESCE(cl.column_label, c.column_name) as "displayName", c.data_type as "dataType", c.data_type as "dbType", - COALESCE(cl.web_type, 'text') as "webType", + COALESCE(cl.input_type, 'text') as "webType", COALESCE(cl.input_type, 'direct') as "inputType", COALESCE(cl.detail_settings, '') as "detailSettings", COALESCE(cl.description, '') as "description", @@ -483,7 +483,7 @@ export class TableManagementService { table_name: string; column_name: string; column_label: string | null; - web_type: string | null; + input_type: string | null; detail_settings: any; description: string | null; display_order: number | null; @@ -495,7 +495,7 @@ export class TableManagementService { created_date: Date | null; updated_date: Date | null; }>( - `SELECT id, table_name, column_name, column_label, web_type, detail_settings, + `SELECT id, table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, code_category, code_value, reference_table, reference_column, created_date, updated_date FROM column_labels @@ -512,7 +512,7 @@ export class TableManagementService { tableName: columnLabel.table_name || "", columnName: columnLabel.column_name || "", columnLabel: columnLabel.column_label || undefined, - webType: columnLabel.web_type || undefined, + webType: columnLabel.input_type || undefined, detailSettings: columnLabel.detail_settings || undefined, description: columnLabel.description || undefined, displayOrder: columnLabel.display_order || undefined, @@ -539,7 +539,7 @@ export class TableManagementService { } /** - * 컬럼 웹 타입 설정 + * 컬럼 입력 타입 설정 (web_type → input_type 통합) */ async updateColumnWebType( tableName: string, @@ -550,7 +550,7 @@ export class TableManagementService { ): Promise { try { logger.info( - `컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType}` + `컬럼 입력 타입 설정 시작: ${tableName}.${columnName} = ${webType}` ); // 웹 타입별 기본 상세 설정 생성 @@ -562,35 +562,28 @@ export class TableManagementService { ...detailSettings, }; - // column_labels UPSERT로 업데이트 또는 생성 + // column_labels UPSERT로 업데이트 또는 생성 (input_type만 사용) await query( `INSERT INTO column_labels ( - table_name, column_name, web_type, detail_settings, input_type, created_date, updated_date - ) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) + table_name, column_name, input_type, detail_settings, created_date, updated_date + ) VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (table_name, column_name) DO UPDATE SET - web_type = EXCLUDED.web_type, + input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, - input_type = COALESCE(EXCLUDED.input_type, column_labels.input_type), updated_date = NOW()`, - [ - tableName, - columnName, - webType, - JSON.stringify(finalDetailSettings), - inputType || null, - ] + [tableName, columnName, webType, JSON.stringify(finalDetailSettings)] ); logger.info( - `컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}` + `컬럼 입력 타입 설정 완료: ${tableName}.${columnName} = ${webType}` ); } catch (error) { logger.error( - `컬럼 웹 타입 설정 중 오류 발생: ${tableName}.${columnName}`, + `컬럼 입력 타입 설정 중 오류 발생: ${tableName}.${columnName}`, error ); throw new Error( - `컬럼 웹 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}` + `컬럼 입력 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } diff --git a/docs/input-type-detail-type-system.md b/docs/input-type-detail-type-system.md new file mode 100644 index 00000000..e214a256 --- /dev/null +++ b/docs/input-type-detail-type-system.md @@ -0,0 +1,304 @@ +# 입력 타입과 세부 타입 시스템 가이드 + +## 📋 개요 + +화면 관리 시스템에서 사용자가 **입력 타입**과 **세부 타입**을 2단계로 선택할 수 있는 시스템입니다. + +### 구조 + +1. **입력 타입 (Input Type)**: 테이블 타입 관리에서 정의한 8개 핵심 타입 +2. **세부 타입 (Detail Type)**: 입력 타입의 구체적인 구현 방식 (웹타입) + +``` +입력 타입 (PropertiesPanel에서 선택) + ↓ +세부 타입 (DetailSettingsPanel에서 선택) + ↓ +세부 설정 (DetailSettingsPanel에서 설정) +``` + +--- + +## 🎯 8개 핵심 입력 타입과 세부 타입 + +### 1. **텍스트 (text)** + +사용 가능한 세부 타입: + +- `text` - 일반 텍스트 입력 +- `email` - 이메일 주소 입력 +- `tel` - 전화번호 입력 +- `url` - 웹사이트 주소 입력 +- `textarea` - 여러 줄 텍스트 입력 +- `password` - 비밀번호 입력 (마스킹) + +### 2. **숫자 (number)** + +사용 가능한 세부 타입: + +- `number` - 정수 숫자 입력 +- `decimal` - 소수점 포함 숫자 입력 + +### 3. **날짜 (date)** + +사용 가능한 세부 타입: + +- `date` - 날짜 선택 (YYYY-MM-DD) +- `datetime` - 날짜와 시간 선택 +- `time` - 시간 선택 (HH:mm) + +### 4. **코드 (code)** + +세부 타입: + +- `code` - 공통 코드 선택 (세부 타입 고정) +- 코드 카테고리는 상세 설정에서 선택 + +### 5. **엔티티 (entity)** + +세부 타입: + +- `entity` - 다른 테이블 참조 (세부 타입 고정) +- 참조 테이블은 상세 설정에서 선택 + +### 6. **선택박스 (select)** + +사용 가능한 세부 타입: + +- `select` - 기본 드롭다운 선택 +- `dropdown` - 검색 기능이 있는 드롭다운 + +### 7. **체크박스 (checkbox)** + +사용 가능한 세부 타입: + +- `checkbox` - 단일 체크박스 +- `boolean` - On/Off 스위치 + +### 8. **라디오버튼 (radio)** + +세부 타입: + +- `radio` - 라디오 버튼 그룹 (세부 타입 고정) + +--- + +## 🔧 사용 방법 + +### 1. PropertiesPanel - 입력 타입 선택 + +위젯 컴포넌트를 선택하면 **속성 편집** 패널에서 입력 타입을 선택할 수 있습니다. + +```typescript +// 입력 타입 선택 + +``` + +**동작:** + +- 입력 타입을 선택하면 해당 타입의 **기본 세부 타입**이 자동으로 설정됩니다 +- 예: `text` 입력 타입 선택 → `text` 세부 타입 자동 설정 + +### 2. DetailSettingsPanel - 세부 타입 선택 + +**상세 설정** 패널에서 선택한 입력 타입의 세부 타입을 선택할 수 있습니다. + +```typescript +// 세부 타입 선택 + +``` + +**동작:** + +- 입력 타입에 해당하는 세부 타입만 표시됩니다 +- 세부 타입을 변경하면 `widgetType` 속성이 업데이트됩니다 + +### 3. DetailSettingsPanel - 세부 설정 + +세부 타입을 선택한 후, 해당 타입의 상세 설정을 할 수 있습니다. + +예: + +- **날짜 (date)**: 날짜 형식, 최소/최대 날짜 등 +- **숫자 (number)**: 최소/최대값, 소수점 자리수 등 +- **코드 (code)**: 코드 카테고리 선택 +- **엔티티 (entity)**: 참조 테이블, 표시 컬럼 선택 + +--- + +## 📁 파일 구조 + +### 새로 추가된 파일 + +#### `frontend/types/input-type-mapping.ts` + +입력 타입과 세부 타입 매핑 정의 + +```typescript +// 8개 핵심 입력 타입 +export type BaseInputType = "text" | "number" | "date" | ...; + +// 입력 타입별 세부 타입 매핑 +export const INPUT_TYPE_DETAIL_TYPES: Record; + +// 유틸리티 함수들 +export function getBaseInputType(webType: WebType): BaseInputType; +export function getDetailTypes(baseInputType: BaseInputType): DetailTypeOption[]; +export function getDefaultDetailType(baseInputType: BaseInputType): WebType; +``` + +### 수정된 파일 + +#### `frontend/components/screen/panels/PropertiesPanel.tsx` + +- 입력 타입 선택 UI 추가 +- 웹타입 선택 → 입력 타입 선택으로 변경 + +#### `frontend/components/screen/panels/DetailSettingsPanel.tsx` + +- 세부 타입 선택 UI 추가 +- 입력 타입 표시 +- 세부 타입 목록 동적 생성 + +--- + +## 🎨 UI 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 속성 편집 (PropertiesPanel) │ +├─────────────────────────────────────────────────────────────┤ +│ 입력 타입: [텍스트 ▼] ← 8개 중 선택 │ +│ 세부 타입은 "상세 설정"에서 선택하세요 │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ 상세 설정 (DetailSettingsPanel) │ +├─────────────────────────────────────────────────────────────┤ +│ 입력 타입: [text] │ +├─────────────────────────────────────────────────────────────┤ +│ 세부 타입 선택: │ +│ [일반 텍스트 ▼] ← 입력 타입에 따라 동적으로 변경 │ +│ - 일반 텍스트 │ +│ - 이메일 │ +│ - 전화번호 │ +│ - URL │ +│ - 여러 줄 텍스트 │ +│ - 비밀번호 │ +├─────────────────────────────────────────────────────────────┤ +│ [세부 설정 영역] │ +│ (선택한 세부 타입에 맞는 설정 패널) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 🔄 데이터 흐름 + +### 1. 새 컴포넌트 생성 시 + +``` +테이블 컬럼 드래그 + → 컬럼의 dataType 분석 + → 입력 타입 자동 선택 (text, number, date 등) + → 기본 세부 타입 자동 설정 (text, number, date 등) +``` + +### 2. 입력 타입 변경 시 + +``` +PropertiesPanel에서 입력 타입 선택 + → 해당 입력 타입의 기본 세부 타입 설정 + → DetailSettingsPanel 세부 타입 목록 업데이트 +``` + +### 3. 세부 타입 변경 시 + +``` +DetailSettingsPanel에서 세부 타입 선택 + → widgetType 업데이트 + → 해당 세부 타입의 설정 패널 표시 +``` + +--- + +## 🚀 확장 가능성 + +### 세부 타입 추가 + +새로운 세부 타입을 추가하려면: + +1. `frontend/types/input-type-mapping.ts`의 `INPUT_TYPE_DETAIL_TYPES`에 추가 +2. 해당 세부 타입의 설정 패널 구현 +3. DB의 `web_types` 테이블에 레코드 추가 + +### 입력 타입 추가 + +새로운 입력 타입을 추가하려면: + +1. `BaseInputType` 타입에 추가 +2. `BASE_INPUT_TYPE_OPTIONS`에 옵션 추가 +3. `INPUT_TYPE_DETAIL_TYPES`에 세부 타입 목록 정의 +4. 테이블 타입 관리 시스템 업데이트 + +--- + +## ✅ 체크리스트 + +- [x] 8개 핵심 입력 타입 정의 +- [x] 입력 타입별 세부 타입 매핑 +- [x] PropertiesPanel에 입력 타입 선택 UI 추가 +- [x] DetailSettingsPanel에 세부 타입 선택 UI 추가 +- [x] 입력 타입 변경 시 기본 세부 타입 자동 설정 +- [x] 세부 타입 변경 시 widgetType 업데이트 +- [x] 타입 안전성 보장 (TypeScript) + +--- + +## 📝 사용 예시 + +### 텍스트 입력 필드 생성 + +1. **PropertiesPanel**에서 입력 타입을 "텍스트"로 선택 +2. **DetailSettingsPanel**로 이동 +3. 세부 타입에서 "이메일" 선택 +4. 이메일 형식 검증 등 세부 설정 입력 + +### 날짜 입력 필드 생성 + +1. **PropertiesPanel**에서 입력 타입을 "날짜"로 선택 +2. **DetailSettingsPanel**로 이동 +3. 세부 타입에서 "날짜+시간" 선택 +4. 날짜 형식, 최소/최대 날짜 등 설정 + +--- + +## 🐛 문제 해결 + +### 세부 타입이 표시되지 않음 + +- 입력 타입이 올바르게 설정되었는지 확인 +- `getDetailTypes()` 함수가 올바른 값을 반환하는지 확인 + +### 입력 타입 변경 시 세부 타입이 초기화되지 않음 + +- `getDefaultDetailType()` 함수 확인 +- `onUpdateProperty("widgetType", ...)` 호출 확인 + +--- + +## 📚 참고 자료 + +- [테이블 타입 관리 개선 계획서](../테이블_타입_관리_개선_계획서.md) +- [테이블 타입 관리 개선 사용 가이드](../테이블_타입_관리_개선_사용_가이드.md) +- [화면관리 시스템 개요](./screen-management-system.md) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index f2b3027e..7d92dc1a 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -122,9 +122,9 @@ export default function ScreenViewPage() { if (loading) { return (
-
+
-

화면을 불러오는 중...

+

화면을 불러오는 중...

); @@ -133,12 +133,12 @@ export default function ScreenViewPage() { if (error || !screen) { return (
-
+
⚠️

화면을 찾을 수 없습니다

-

{error || "요청하신 화면이 존재하지 않습니다."}

+

{error || "요청하신 화면이 존재하지 않습니다."}

@@ -156,7 +156,7 @@ export default function ScreenViewPage() { {layout && layout.components.length > 0 ? ( // 캔버스 컴포넌트들을 정확한 해상도로 표시
{/* 그룹 제목 */} {(component as any).title && ( -
{(component as any).title}
+
+ {(component as any).title} +
)} {/* 그룹 내 자식 컴포넌트들 렌더링 */} @@ -295,7 +297,13 @@ export default function ScreenViewPage() { {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} {component.type !== "widget" ? ( { @@ -335,7 +343,7 @@ export default function ScreenViewPage() { console.log(`🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"`, { componentId: component.id, componentType: component.type, - originalWebType: component.webType + originalWebType: component.webType, }); return "file"; } @@ -382,7 +390,7 @@ export default function ScreenViewPage() { ) : ( // 빈 화면일 때도 깔끔하게 표시
= ( }; + // 상위에서 라벨을 표시한 경우, 컴포넌트 내부에서는 라벨을 숨김 + const componentForRendering = shouldShowLabel + ? { + ...component, + style: { + ...component.style, + labelDisplay: false, // 상위에서 라벨을 표시했으므로 컴포넌트 내부에서는 숨김 + }, + } + : component; + return ( <>
@@ -1763,8 +1774,8 @@ export const InteractiveScreenViewer: React.FC = (
)} - {/* 실제 위젯 */} -
{renderInteractiveWidget(component)}
+ {/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */} +
{renderInteractiveWidget(componentForRendering)}
{/* 개선된 검증 패널 (선택적 표시) */} diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index cbca5ee2..6a46d7ce 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState, useEffect } from "react"; import { Settings } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; @@ -16,6 +16,7 @@ import { // 레거시 ButtonConfigPanel 제거됨 import { FileComponentConfigPanel } from "./FileComponentConfigPanel"; import { isFileComponent } from "@/lib/utils/componentTypeUtils"; +import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping"; // 새로운 컴포넌트 설정 패널들 import import { ButtonConfigPanel as NewButtonConfigPanel } from "../config-panels/ButtonConfigPanel"; @@ -47,11 +48,11 @@ export const DetailSettingsPanel: React.FC = ({ const { webTypes } = useWebTypes({ active: "Y" }); // console.log(`🔍 DetailSettingsPanel props:`, { - // selectedComponent: selectedComponent?.id, - // componentType: selectedComponent?.type, - // currentTableName, - // currentTable: currentTable?.tableName, - // selectedComponentTableName: selectedComponent?.tableName, + // selectedComponent: selectedComponent?.id, + // componentType: selectedComponent?.type, + // currentTableName, + // currentTable: currentTable?.tableName, + // selectedComponentTableName: selectedComponent?.tableName, // }); // console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개"); // console.log(`🔍 webTypes:`, webTypes); @@ -59,6 +60,19 @@ export const DetailSettingsPanel: React.FC = ({ // console.log(`🔍 DetailSettingsPanel selectedComponent.widgetType:`, selectedComponent?.widgetType); const inputableWebTypes = webTypes.map((wt) => wt.web_type); + // 새로운 컴포넌트 시스템용 로컬 상태 + const [localComponentDetailType, setLocalComponentDetailType] = useState(""); + + // 새로운 컴포넌트 시스템의 webType 동기화 + useEffect(() => { + if (selectedComponent?.type === "component") { + const webType = selectedComponent.componentConfig?.webType; + if (webType) { + setLocalComponentDetailType(webType); + } + } + }, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]); + // 레이아웃 컴포넌트 설정 렌더링 함수 const renderLayoutConfig = (layoutComponent: LayoutComponent) => { return ( @@ -66,11 +80,11 @@ export const DetailSettingsPanel: React.FC = ({ {/* 헤더 */}
- +

레이아웃 설정

- 타입: + 타입: {layoutComponent.layoutType} @@ -87,7 +101,7 @@ export const DetailSettingsPanel: React.FC = ({ type="text" value={layoutComponent.label || ""} onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)} - className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-2 focus:ring-blue-500" + className="focus:border-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500" placeholder="레이아웃 이름을 입력하세요" />
@@ -218,9 +232,9 @@ export const DetailSettingsPanel: React.FC = ({ })); // console.log("🔄 존 크기 자동 조정:", { - // direction: newDirection, - // zoneCount, - // updatedZones: updatedZones.map((z) => ({ id: z.id, size: z.size })), + // direction: newDirection, + // zoneCount, + // updatedZones: updatedZones.map((z) => ({ id: z.id, size: z.size })), // }); onUpdateProperty(layoutComponent.id, "zones", updatedZones); @@ -334,7 +348,7 @@ export const DetailSettingsPanel: React.FC = ({
테이블 컬럼 매핑
{currentTable && ( - + 테이블: {currentTable.table_name} )} @@ -354,7 +368,7 @@ export const DetailSettingsPanel: React.FC = ({ {currentTable && ( <>
- + @@ -398,7 +412,7 @@ export const DetailSettingsPanel: React.FC = ({
- + @@ -444,7 +458,7 @@ export const DetailSettingsPanel: React.FC = ({ {/* 동적 표시 컬럼 추가 */}
- + @@ -502,7 +516,7 @@ export const DetailSettingsPanel: React.FC = ({ currentColumns, ); }} - className="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground hover:bg-destructive/90" + className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs" > 삭제 @@ -528,7 +542,7 @@ export const DetailSettingsPanel: React.FC = ({
- + = ({
- + = ({ } className="rounded border-gray-300" /> -
@@ -586,7 +600,7 @@ export const DetailSettingsPanel: React.FC = ({ } className="rounded border-gray-300" /> -
@@ -605,7 +619,7 @@ export const DetailSettingsPanel: React.FC = ({ } className="rounded border-gray-300" /> -
@@ -620,14 +634,14 @@ export const DetailSettingsPanel: React.FC = ({ } className="rounded border-gray-300" /> -
- + = ({
- + = ({ />
- + = ({ const currentConfig = widget.webTypeConfig || {}; // console.log("🎨 DetailSettingsPanel renderWebTypeConfig 호출:", { - // componentId: widget.id, - // widgetType: widget.widgetType, - // currentConfig, - // configExists: !!currentConfig, - // configKeys: Object.keys(currentConfig), - // configStringified: JSON.stringify(currentConfig), - // widgetWebTypeConfig: widget.webTypeConfig, - // widgetWebTypeConfigExists: !!widget.webTypeConfig, - // timestamp: new Date().toISOString(), + // componentId: widget.id, + // widgetType: widget.widgetType, + // currentConfig, + // configExists: !!currentConfig, + // configKeys: Object.keys(currentConfig), + // configStringified: JSON.stringify(currentConfig), + // widgetWebTypeConfig: widget.webTypeConfig, + // widgetWebTypeConfigExists: !!widget.webTypeConfig, + // timestamp: new Date().toISOString(), // }); // console.log("🎨 selectedComponent 전체:", selectedComponent); const handleConfigChange = (newConfig: WebTypeConfig) => { // console.log("🔧 WebTypeConfig 업데이트:", { - // widgetType: widget.widgetType, - // oldConfig: currentConfig, - // newConfig, - // componentId: widget.id, - // isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig), + // widgetType: widget.widgetType, + // oldConfig: currentConfig, + // newConfig, + // componentId: widget.id, + // isEqual: JSON.stringify(currentConfig) === JSON.stringify(newConfig), // }); // 강제 새 객체 생성으로 React 변경 감지 보장 @@ -761,17 +775,17 @@ export const DetailSettingsPanel: React.FC = ({ if (!selectedComponent) { return ( -
+
{/* 헤더 */}
-

상세 설정

+

상세 설정

컴포넌트를 선택하여 상세 설정을 편집하세요

- + {/* 빈 상태 */} -
+

컴포넌트를 선택하세요

위젯 컴포넌트를 선택하면 상세 설정을 편집할 수 있습니다.

@@ -847,9 +861,9 @@ export const DetailSettingsPanel: React.FC = ({ // 새로운 컴포넌트 타입들에 대한 설정 패널 확인 const componentType = selectedComponent?.componentConfig?.type || selectedComponent?.type; // console.log("🔍 DetailSettingsPanel componentType 확인:", { - // selectedComponentType: selectedComponent?.type, - // componentConfigType: selectedComponent?.componentConfig?.type, - // finalComponentType: componentType, + // selectedComponentType: selectedComponent?.type, + // componentConfigType: selectedComponent?.componentConfig?.type, + // finalComponentType: componentType, // }); const hasNewConfigPanel = @@ -880,11 +894,11 @@ export const DetailSettingsPanel: React.FC = ({ {/* 헤더 */}
- +

컴포넌트 설정

- 타입: + 타입: {componentType}
@@ -928,15 +942,17 @@ export const DetailSettingsPanel: React.FC = ({ {/* 헤더 */}
- +

파일 컴포넌트 설정

- 타입: + 타입: 파일 업로드
- {selectedComponent.type === "widget" ? `위젯타입: ${selectedComponent.widgetType}` : `문서 타입: ${fileComponent.fileConfig?.docTypeName || "일반 문서"}`} + {selectedComponent.type === "widget" + ? `위젯타입: ${selectedComponent.widgetType}` + : `문서 타입: ${fileComponent.fileConfig?.docTypeName || "일반 문서"}`}
@@ -993,36 +1009,77 @@ export const DetailSettingsPanel: React.FC = ({ ); } + // 현재 웹타입의 기본 입력 타입 추출 + const currentBaseInputType = webType ? getBaseInputType(webType as any) : null; + + // 선택 가능한 세부 타입 목록 + const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : []; + + // 세부 타입 변경 핸들러 + const handleDetailTypeChange = (newDetailType: string) => { + setLocalComponentDetailType(newDetailType); + onUpdateProperty(selectedComponent.id, "componentConfig.webType", newDetailType); + }; + return ( -
+
{/* 헤더 */}
-

상세 설정

+

상세 설정

선택한 컴포넌트의 속성을 편집하세요

{/* 컴포넌트 정보 */} -
+
- 컴포넌트: + 컴포넌트: {componentId}
- {webType && ( + {webType && currentBaseInputType && (
- 웹타입: - {webType} + 입력 타입: + + {currentBaseInputType} +
)} {selectedComponent.columnName && (
- 컬럼: + 컬럼: {selectedComponent.columnName}
)}
+ {/* 세부 타입 선택 영역 */} + {webType && availableDetailTypes.length > 1 && ( +
+
+ + +

+ 입력 타입 "{currentBaseInputType}"에 사용할 구체적인 형태를 선택하세요 +

+
+
+ )} + {/* 컴포넌트 설정 패널 */}
= ({ screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} tableColumns={(() => { // console.log("🔍 DetailSettingsPanel tableColumns 전달:", { - // currentTable, - // columns: currentTable?.columns, - // columnsLength: currentTable?.columns?.length, - // sampleColumn: currentTable?.columns?.[0], - // deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"), + // currentTable, + // columns: currentTable?.columns, + // columnsLength: currentTable?.columns?.length, + // sampleColumn: currentTable?.columns?.[0], + // deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"), // }); return currentTable?.columns || []; })()} @@ -1060,21 +1117,71 @@ export const DetailSettingsPanel: React.FC = ({ // 기존 위젯 시스템 처리 (type: "widget") const widget = selectedComponent as WidgetComponent; + // 현재 웹타입의 기본 입력 타입 추출 + const currentBaseInputType = getBaseInputType(widget.widgetType); + + // 선택 가능한 세부 타입 목록 + const availableDetailTypes = getDetailTypes(currentBaseInputType); + + // 로컬 상태: 세부 타입 선택 + const [localDetailType, setLocalDetailType] = useState(widget.widgetType); + + // 컴포넌트 변경 시 로컬 상태 동기화 + useEffect(() => { + setLocalDetailType(widget.widgetType); + }, [widget.widgetType, widget.id]); + + // 세부 타입 변경 핸들러 + const handleDetailTypeChange = (newDetailType: string) => { + setLocalDetailType(newDetailType); + onUpdateProperty(widget.id, "widgetType", newDetailType); + + // 웹타입 변경 시 기존 설정 초기화 (선택적) + // onUpdateProperty(widget.id, "webTypeConfig", {}); + }; + return (
{/* 헤더 */}
- +

상세 설정

- 웹타입: - {widget.widgetType} + 입력 타입: + + {currentBaseInputType} +
컬럼: {widget.columnName}
+ {/* 세부 타입 선택 영역 */} +
+
+ + +

+ 입력 타입 "{currentBaseInputType}"에 사용할 구체적인 형태를 선택하세요 +

+
+
+ {/* 상세 설정 영역 */}
{renderWebTypeConfig(widget)}
diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 657ba6c7..1589b1c7 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -25,6 +25,12 @@ import { ColumnSpanPreset, COLUMN_SPAN_PRESETS, COLUMN_SPAN_VALUES } from "@/lib import { cn } from "@/lib/utils"; import DataTableConfigPanel from "./DataTableConfigPanel"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; +import { + BaseInputType, + BASE_INPUT_TYPE_OPTIONS, + getBaseInputType, + getDefaultDetailType, +} from "@/types/input-type-mapping"; // DataTableConfigPanel을 위한 안정화된 래퍼 컴포넌트 const DataTableConfigPanelWrapper: React.FC<{ @@ -518,24 +524,27 @@ const PropertiesPanelComponent: React.FC = ({
-
diff --git a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx index e2480e0d..076fa8de 100644 --- a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx +++ b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx @@ -1,8 +1,10 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { ComponentRendererProps } from "@/types/component"; import { CheckboxBasicConfig } from "./types"; +import { cn } from "@/lib/registry/components/common/inputStyles"; +import { filterDOMProps } from "@/lib/utils/domPropsFilter"; export interface CheckboxBasicComponentProps extends ComponentRendererProps { config?: CheckboxBasicConfig; @@ -33,6 +35,13 @@ export const CheckboxBasicComponent: React.FC = ({ ...component.config, } as CheckboxBasicConfig; + // webType에 따른 세부 타입 결정 (TextInputComponent와 동일한 방식) + const webType = component.componentConfig?.webType || "checkbox"; + + // 상태 관리 + const [isChecked, setIsChecked] = useState(component.value === true || component.value === "true"); + const [checkedValues, setCheckedValues] = useState([]); + // 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외) const componentStyle: React.CSSProperties = { width: "100%", @@ -53,116 +62,122 @@ export const CheckboxBasicComponent: React.FC = ({ onClick?.(); }; - // DOM에 전달하면 안 되는 React-specific props 필터링 - const { - selectedScreen, - onZoneComponentDrop, - onZoneClick, - componentConfig: _componentConfig, - component: _component, - isSelected: _isSelected, - onClick: _onClick, - onDragStart: _onDragStart, - onDragEnd: _onDragEnd, - size: _size, - position: _position, - style: _style, - screenId: _screenId, - tableName: _tableName, - onRefresh: _onRefresh, - onClose: _onClose, - ...domProps - } = props; + const handleCheckboxChange = (checked: boolean) => { + setIsChecked(checked); + if (component.onChange) { + component.onChange(checked); + } + if (isInteractive && onFormDataChange && component.columnName) { + onFormDataChange(component.columnName, checked); + } + }; - return ( -
- {/* 라벨 렌더링 */} - {component.label && (component.style?.labelDisplay ?? true) && ( -