diff --git a/docs/그리드_컬럼수_옵션_통합.md b/docs/그리드_컬럼수_옵션_통합.md new file mode 100644 index 00000000..34b1ba41 --- /dev/null +++ b/docs/그리드_컬럼수_옵션_통합.md @@ -0,0 +1,223 @@ +# 그리드 컬럼 수 옵션 통합 + +## 개요 + +"그리드 컬럼 수" 옵션과 "컴포넌트 너비" 옵션이 중복된 기능을 제공하여 혼란을 야기했습니다. +사용자 편의성을 위해 **"컴포넌트 너비" 옵션만 사용**하도록 통합하고, 내부적으로 `gridColumns` 값을 자동 계산하도록 변경했습니다. + +## 문제점 + +### 기존 상황 + +1. **그리드 컬럼 수 옵션**: 1-12 숫자 입력 +2. **컴포넌트 너비 옵션**: 1/12 ~ 12/12 선택 (퍼센트로 변환) + +→ 같은 기능을 두 가지 방식으로 제공하여 사용자 혼란 발생 + +### 예시 + +- 사용자가 "그리드 컬럼 수"를 6으로 설정 +- 하지만 "컴포넌트 너비"가 1/4 (3컬럼)로 설정되어 있음 +- 두 설정이 충돌하여 예상과 다른 결과 발생 + +## 해결 방법 + +### 1. UI 단순화 + +**제거된 옵션**: + +- ❌ PropertiesPanel의 "그리드 컬럼 수 (1-12)" 입력 필드 +- ❌ DataTableConfigPanel의 "그리드 컬럼 수" 선택 상자 + +**유지된 옵션**: + +- ✅ PropertiesPanel의 "컴포넌트 너비" 선택 상자 (1/12 ~ 12/12) + +### 2. 자동 계산 로직 + +컴포넌트 너비 선택 시 `gridColumns` 자동 계산: + +```typescript +// PropertiesPanel.tsx (764-788줄) +const columnsMap: Record = { + twelfth: 1, // 1/12 + small: 2, // 2/12 + quarter: 3, // 3/12 (1/4) + third: 4, // 4/12 (1/3) + "five-twelfths": 5, // 5/12 + half: 6, // 6/12 (절반) + "seven-twelfths": 7, // 7/12 + twoThirds: 8, // 8/12 (2/3) + threeQuarters: 9, // 9/12 (3/4) + "five-sixths": 10, // 10/12 + "eleven-twelfths": 11, // 11/12 + full: 12, // 12/12 (전체) +}; + +// 컴포넌트 너비 변경 시 +onUpdateProperty("style.width", newWidth); // 퍼센트 값 저장 +const gridColumns = columnsMap[value] || 6; +onUpdateProperty("gridColumns", gridColumns); // 컬럼 수 자동 계산 +``` + +### 3. 컴포넌트 생성 시 동작 + +```typescript +// ScreenDesigner.tsx (1756-1772줄) +// 일반 컴포넌트: defaultSize.width를 기준으로 그리드 컬럼 수 계산 +if (layout.gridSettings?.snapToGrid && gridInfo) { + const columnWidth = gridInfo.columnWidth + gridInfo.gap; + const estimatedColumns = Math.round( + component.defaultSize.width / columnWidth + ); + gridColumns = Math.max(1, Math.min(12, estimatedColumns)); // 1-12 범위 +} +``` + +## 변경 사항 + +### 파일 수정 + +#### 1. PropertiesPanel.tsx + +- ❌ 삭제: "그리드 컬럼 수" 입력 필드 (916-940줄) +- ❌ 삭제: `localInputs.gridColumns` 상태 (206-213줄) +- ✅ 추가: 컴포넌트 너비 변경 시 `gridColumns` 자동 계산 (764-788줄) + +#### 2. DataTableConfigPanel.tsx + +- ❌ 삭제: "그리드 컬럼 수" 선택 상자 (1437-1456줄) +- ❌ 삭제: `localValues.gridColumns` 초기화 (72줄, 182줄) + +#### 3. ScreenDesigner.tsx + +- ✅ 개선: 컴포넌트 드롭 시 `defaultSize.width` 기반으로 `gridColumns` 자동 계산 (1756-1772줄) + +## 사용 방법 + +### 컴포넌트 너비 조정 + +#### 방법 1: 드롭다운 선택 + +1. 컴포넌트 선택 +2. 속성 패널 > "컴포넌트 너비" 드롭다운 +3. 원하는 너비 선택 (예: "절반 (6/12)") +4. 자동으로 `style.width`와 `gridColumns` 모두 업데이트됨 + +#### 방법 2: 컴포넌트 생성 시 + +1. 컴포넌트 팔레트에서 드래그 +2. 캔버스에 드롭 +3. `defaultSize.width`를 기준으로 적절한 `gridColumns` 자동 설정 + +### 너비 옵션 설명 + +| 옵션 | 컬럼 수 | 퍼센트 | 설명 | +| ------------ | ------- | ------ | ----------- | +| 1/12 | 1 | 8.33% | 최소 | +| 작게 (2/12) | 2 | 16.67% | 매우 작음 | +| 1/4 (3/12) | 3 | 25% | 4등분의 1 | +| 1/3 (4/12) | 4 | 33.33% | 3등분의 1 | +| 5/12 | 5 | 41.67% | | +| 절반 (6/12) | 6 | 50% | 정확히 절반 | +| 7/12 | 7 | 58.33% | | +| 2/3 (8/12) | 8 | 66.67% | 3등분의 2 | +| 3/4 (9/12) | 9 | 75% | 4등분의 3 | +| 10/12 | 10 | 83.33% | | +| 11/12 | 11 | 91.67% | | +| 전체 (12/12) | 12 | 100% | 최대 | + +## 적용 효과 + +### 1. 사용자 경험 개선 + +- ✅ 단일 옵션으로 간소화 +- ✅ 직관적인 분수 표현 (1/4, 절반, 2/3 등) +- ✅ 설정 충돌 제거 + +### 2. 일관성 보장 + +- ✅ 컴포넌트 너비와 gridColumns 항상 동기화 +- ✅ 그리드 시스템과 자연스러운 통합 + +### 3. 개발자 편의 + +- ✅ 내부적으로 gridColumns는 여전히 사용 가능 +- ✅ 기존 데이터 호환성 유지 (gridColumns 필드 존재) + +## 내부 동작 + +### gridColumns 사용처 + +`gridColumns` 값은 사용자에게 직접 노출되지 않지만, 내부적으로 여전히 중요한 역할을 합니다: + +1. **그리드 레이아웃 계산**: 컴포넌트가 차지할 그리드 셀 수 결정 +2. **자동 배치**: 컴포넌트 자동 정렬 시 참조 +3. **반응형 조정**: 화면 크기 변경 시 비율 유지 + +### 값 동기화 흐름 + +``` +사용자 선택: "절반 (6/12)" + ↓ +1. style.width = "50%" 저장 + ↓ +2. gridColumns = 6 자동 계산 + ↓ +3. 그리드 시스템에서 6컬럼 너비로 렌더링 + ↓ +4. 실제 픽셀 너비 계산 및 적용 +``` + +## 마이그레이션 가이드 + +### 기존 화면 데이터 + +- **영향 없음**: 기존에 저장된 `gridColumns` 값은 그대로 유지 +- **자동 변환**: 컴포넌트 편집 시 `style.width`로부터 재계산 + +### 사용자 교육 + +1. "그리드 컬럼 수" 설정이 제거되었음을 안내 +2. "컴포넌트 너비"로 동일한 기능 사용 가능 +3. 더 직관적인 분수 표현 (1/4, 1/2 등) 강조 + +## 테스트 체크리스트 + +### UI 확인 + +- [ ] PropertiesPanel에 "그리드 컬럼 수" 입력 필드가 없는지 확인 +- [ ] DataTableConfigPanel에 "그리드 컬럼 수" 선택 상자가 없는지 확인 +- [ ] "컴포넌트 너비" 드롭다운이 정상 작동하는지 확인 + +### 기능 확인 + +- [ ] 컴포넌트 너비 변경 시 시각적으로 제대로 반영되는지 확인 +- [ ] 새 컴포넌트 생성 시 적절한 초기 너비로 생성되는지 확인 +- [ ] 그리드 ON/OFF 시 너비가 올바르게 적용되는지 확인 + +### 데이터 확인 + +- [ ] 컴포넌트 너비 변경 후 저장/불러오기 테스트 +- [ ] 기존 화면 데이터가 정상적으로 로드되는지 확인 +- [ ] `gridColumns` 값이 자동으로 계산되는지 확인 + +## 관련 파일 + +### 수정된 파일 + +- `/frontend/components/screen/panels/PropertiesPanel.tsx` +- `/frontend/components/screen/panels/DataTableConfigPanel.tsx` +- `/frontend/components/screen/ScreenDesigner.tsx` + +### 관련 문서 + +- [컴포넌트*기본*너비*설정*가이드.md](./컴포넌트_기본_너비_설정_가이드.md) + +## 버전 히스토리 + +### v1.0.0 (2025-10-14) + +- "그리드 컬럼 수" 옵션 제거 +- "컴포넌트 너비" 옵션으로 통합 +- `gridColumns` 자동 계산 로직 추가 diff --git a/docs/컴포넌트_기본_너비_설정_가이드.md b/docs/컴포넌트_기본_너비_설정_가이드.md new file mode 100644 index 00000000..94c36736 --- /dev/null +++ b/docs/컴포넌트_기본_너비_설정_가이드.md @@ -0,0 +1,225 @@ +# 컴포넌트 기본 너비 설정 가이드 + +## 개요 + +화면 관리에서 각 컴포넌트 타입별로 적절한 기본 너비를 설정하고, 컴포넌트가 지정된 너비를 벗어나지 않도록 스타일을 적용했습니다. + +## 변경 사항 + +### 1. 인풋 컴포넌트 기본 너비 조정 + +각 인풋 타입별로 적절한 기본 크기를 설정했습니다: + +#### 텍스트 입력 계열 + +- **텍스트 입력** (`text-input`): 300px × 40px +- **숫자 입력** (`number-input`): 200px × 40px +- **텍스트 영역** (`textarea-basic`): 400px × 100px + +#### 선택 입력 계열 + +- **선택상자** (`select-basic`): 250px × 40px +- **날짜 선택** (`date-input`): 220px × 40px +- **체크박스** (`checkbox-basic`): 150px × 32px +- **라디오 버튼** (`radio-basic`): 150px × 32px +- **슬라이더** (`slider-basic`): 250px × 40px +- **토글 스위치** (`toggle-switch`): 180px × 40px + +#### 파일 및 기타 + +- **파일 업로드** (`file-upload`): 350px × 40px + +#### 표시 컴포넌트 + +- **기본 버튼** (`button-primary`): 120px × 40px +- **텍스트 표시** (`text-display`): 150px × 24px +- **이미지 표시** (`image-display`): 200px × 200px +- **구분선** (`divider-line`): 400px × 2px +- **아코디언** (`accordion-basic`): 400px × 200px + +#### 데이터 컴포넌트 + +- **테이블 리스트** (`table-list`): 120px × 600px +- **카드 표시** (`card-display`): 기존 유지 + +### 2. 공통 스타일 적용 + +`/frontend/lib/registry/components/common/inputStyles.ts` 파일의 모든 스타일 클래스에 다음을 추가: + +- `max-w-full`: 최대 너비를 부모 컨테이너로 제한 +- `overflow-hidden`: 내용이 넘칠 경우 숨김 처리 + +적용된 클래스: + +- `INPUT_CLASSES.base` +- `INPUT_CLASSES.container` +- `INPUT_CLASSES.textarea` +- `INPUT_CLASSES.select` +- `INPUT_CLASSES.flexContainer` + +### 3. 개별 컴포넌트 스타일 적용 + +#### TextInputComponent + +- 컨테이너 div: `max-w-full overflow-hidden` 추가 +- input 요소: `max-w-full` 추가 +- textarea 요소: `max-w-full` 추가 + +#### RealtimePreviewDynamic + +- 컴포넌트 렌더링 컨테이너: `max-w-full overflow-hidden` 추가 + +## 적용 효과 + +### 1. 일관된 초기 크기 + +- 컴포넌트 드래그 앤 드롭 시 각 타입별로 적절한 기본 크기로 생성됨 +- 사용자가 별도로 크기를 조정할 필요 없이 바로 사용 가능 + +### 2. 그리드 시스템과의 통합 + +- **그리드 활성화 시**: `defaultSize.width`를 기준으로 적절한 그리드 컬럼 수 자동 계산 + - 예: 300px 너비 → 약 3-4 컬럼 (그리드 설정에 따라 다름) + - 계산된 컬럼 수에 맞춰 정확한 너비로 재조정 +- **그리드 비활성화 시**: `defaultSize`의 픽셀 값을 그대로 사용 +- 일관된 사용자 경험 제공 + +### 3. 너비 제한 + +- 컴포넌트가 설정된 너비를 벗어나지 않음 +- 부모 컨테이너 크기에 맞춰 자동으로 조정됨 +- 레이아웃 깨짐 방지 + +### 4. 반응형 대응 + +- `max-w-full` 속성으로 부모 컨테이너에 맞춰 자동 축소 +- `overflow-hidden`으로 내용 넘침 방지 + +## 사용 방법 + +### 새 컴포넌트 생성 시 + +1. 컴포넌트 팔레트에서 원하는 타입 선택 +2. 캔버스에 드래그 앤 드롭 +3. 자동으로 적절한 기본 크기로 생성됨 + +### 크기 조정 + +1. 컴포넌트 선택 +2. 속성 패널에서 "컴포넌트 너비" 선택 +3. 드롭다운에서 원하는 너비 선택 (1/12 ~ 12/12) +4. 또는 직접 픽셀 값 입력 + +## 주의 사항 + +### 기존 화면에 미치는 영향 + +- 이미 생성된 컴포넌트는 영향 받지 않음 +- 새로 생성되는 컴포넌트만 새로운 기본값 적용 + +### 스타일 우선순위 + +1. 인라인 style 속성 +2. componentConfig에서 설정한 크기 +3. defaultSize (새로 적용된 기본값) + +### 커스터마이징 + +- 각 컴포넌트의 `index.ts` 파일에서 `defaultSize` 수정 가능 +- 프로젝트 요구사항에 맞춰 조정 가능 + +## 테스트 방법 + +### 기본 크기 테스트 + +``` +1. 화면 디자이너 열기 +2. 각 인풋 타입 컴포넌트를 캔버스에 드롭 +3. 기본 크기가 적절한지 확인 +4. 여러 컴포넌트를 나란히 배치하여 일관성 확인 +``` + +### 너비 제한 테스트 + +``` +1. 컴포넌트 생성 후 선택 +2. 속성 패널에서 너비를 작은 값으로 설정 (예: 100px) +3. 컴포넌트 내부의 input이 너비를 벗어나지 않는지 확인 +4. 긴 텍스트 입력 시 overflow 처리 확인 +``` + +### 반응형 테스트 + +``` +1. 레이아웃 컨테이너 내부에 컴포넌트 배치 +2. 레이아웃 크기를 조정하여 컴포넌트가 적절히 축소되는지 확인 +3. 다양한 화면 해상도에서 테스트 +``` + +## 관련 파일 + +### 컴포넌트 정의 파일 + +- `/frontend/lib/registry/components/text-input/index.ts` +- `/frontend/lib/registry/components/number-input/index.ts` +- `/frontend/lib/registry/components/select-basic/index.ts` +- `/frontend/lib/registry/components/date-input/index.ts` +- `/frontend/lib/registry/components/textarea-basic/index.ts` +- `/frontend/lib/registry/components/checkbox-basic/index.ts` +- `/frontend/lib/registry/components/radio-basic/index.ts` +- `/frontend/lib/registry/components/file-upload/index.ts` +- `/frontend/lib/registry/components/slider-basic/index.ts` +- `/frontend/lib/registry/components/toggle-switch/index.ts` +- `/frontend/lib/registry/components/button-primary/index.ts` +- `/frontend/lib/registry/components/text-display/index.ts` +- `/frontend/lib/registry/components/image-display/index.ts` +- `/frontend/lib/registry/components/divider-line/index.ts` +- `/frontend/lib/registry/components/accordion-basic/index.ts` +- `/frontend/lib/registry/components/table-list/index.ts` + +### 공통 스타일 파일 + +- `/frontend/lib/registry/components/common/inputStyles.ts` + +### 렌더링 관련 파일 + +- `/frontend/components/screen/RealtimePreviewDynamic.tsx` +- `/frontend/lib/registry/components/text-input/TextInputComponent.tsx` + +### 화면 디자이너 + +- `/frontend/components/screen/ScreenDesigner.tsx` + - `handleComponentDrop` 함수 (1751-1800줄): 컴포넌트 드롭 시 그리드 컬럼 수 자동 계산 + - 그리드 활성화 시: `defaultSize.width` 기반으로 gridColumns 계산 후 너비 재조정 + - 그리드 비활성화 시: `defaultSize` 그대로 사용 + +## 향후 개선 사항 + +### 1. 반응형 기본값 + +- 화면 크기에 따라 다른 기본값 적용 +- 모바일, 태블릿, 데스크톱 각각 최적화 + +### 2. 사용자 정의 기본값 + +- 사용자가 자주 사용하는 크기를 기본값으로 저장 +- 프로젝트별 기본값 설정 기능 + +### 3. 스마트 크기 조정 + +- 주변 컴포넌트에 맞춰 자동으로 크기 조정 +- 레이블 길이에 따른 동적 너비 계산 + +### 4. 프리셋 제공 + +- 폼 레이아웃 프리셋 (라벨-입력 쌍) +- 검색 바 프리셋 +- 로그인 폼 프리셋 + +## 버전 히스토리 + +### v1.0.0 (2025-10-14) + +- 초기 기본 너비 설정 적용 +- 공통 스타일에 max-w-full, overflow-hidden 추가 +- 모든 인풋 컴포넌트 기본 크기 조정 diff --git a/docs/테이블_패널_컴포넌트_기본_너비_설정.md b/docs/테이블_패널_컴포넌트_기본_너비_설정.md new file mode 100644 index 00000000..2b3da3dd --- /dev/null +++ b/docs/테이블_패널_컴포넌트_기본_너비_설정.md @@ -0,0 +1,322 @@ +# 테이블 패널 컴포넌트 기본 너비 설정 + +## 개요 + +테이블 패널에서 컬럼과 필터를 드래그 드롭으로 추가할 때, 각 웹타입별로 적절한 기본 너비(gridColumns)가 자동으로 설정되도록 개선했습니다. + +## 문제점 + +### 기존 방식 + +- **모든 컬럼**: `gridColumns: 2` (2/12, 16.67%) 고정 +- **모든 필터**: `gridColumns: 3` (3/12, 25%) 고정 +- 웹타입에 관계없이 동일한 너비 적용 +- 긴 텍스트 입력이나 짧은 숫자 입력 모두 같은 크기 + +### 문제 사례 + +``` +❌ text (긴 텍스트) → 2컬럼 (너무 좁음) +❌ textarea (여러 줄) → 2컬럼 (너무 좁음) +❌ checkbox (체크박스) → 2컬럼 (너무 넓음) +``` + +## 해결 방법 + +### 웹타입별 기본 너비 함수 추가 + +```typescript +// DataTableConfigPanel.tsx (891-929줄) +const getDefaultGridColumns = (webType: WebType): number => { + const widthMap: Record = { + // 텍스트 입력 계열 (넓게) + text: 4, // 1/3 (33%) + email: 4, // 1/3 (33%) + tel: 3, // 1/4 (25%) + url: 4, // 1/3 (33%) + textarea: 6, // 절반 (50%) + + // 숫자/날짜 입력 (중간) + number: 2, // 2/12 (16.67%) + decimal: 2, // 2/12 (16.67%) + date: 3, // 1/4 (25%) + datetime: 3, // 1/4 (25%) + time: 2, // 2/12 (16.67%) + + // 선택 입력 (중간) + select: 3, // 1/4 (25%) + radio: 3, // 1/4 (25%) + checkbox: 2, // 2/12 (16.67%) + boolean: 2, // 2/12 (16.67%) + + // 코드/참조 (넓게) + code: 3, // 1/4 (25%) + entity: 4, // 1/3 (33%) + + // 파일/이미지 (넓게) + file: 4, // 1/3 (33%) + image: 3, // 1/4 (25%) + + // 기타 + button: 2, // 2/12 (16.67%) + label: 2, // 2/12 (16.67%) + }; + + return widthMap[webType] || 3; // 기본값 3 (1/4, 25%) +}; +``` + +## 적용된 함수 + +### 1. addColumn (컬럼 추가) + +```typescript +// Before +const newColumn: DataTableColumn = { + // ... + gridColumns: 2, // ❌ 모든 타입에 2 고정 + // ... +}; + +// After +const newColumn: DataTableColumn = { + // ... + gridColumns: getDefaultGridColumns(widgetType), // ✅ 웹타입별 자동 계산 + // ... +}; +``` + +### 2. addFilter (필터 추가) + +```typescript +// Before +const newFilter: DataTableFilter = { + // ... + gridColumns: 3, // ❌ 모든 타입에 3 고정 + // ... +}; + +// After +const newFilter: DataTableFilter = { + // ... + gridColumns: getDefaultGridColumns(widgetType), // ✅ 웹타입별 자동 계산 + // ... +}; +``` + +### 3. addVirtualFileColumn (가상 파일 컬럼 추가) + +```typescript +// Before +const newColumn: DataTableColumn = { + // ... + widgetType: "file", + gridColumns: 2, // ❌ 파일 타입에 2 고정 + // ... +}; + +// After +const newColumn: DataTableColumn = { + // ... + widgetType: "file", + gridColumns: getDefaultGridColumns("file"), // ✅ 파일 타입 기본값 (4컬럼, 33%) + // ... +}; +``` + +## 웹타입별 기본 너비 상세 + +### 텍스트 입력 계열 (넓게 설정) + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| -------- | ------- | ------ | -------------------------- | +| text | 4 | 33% | 일반 텍스트 입력 | +| email | 4 | 33% | 이메일 주소 (길이 필요) | +| tel | 3 | 25% | 전화번호 (중간 길이) | +| url | 4 | 33% | URL 주소 (길이 필요) | +| textarea | 6 | 50% | 여러 줄 텍스트 (가장 넓게) | + +### 숫자/날짜 입력 (중간 설정) + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| -------- | ------- | ------ | -------------- | +| number | 2 | 16.67% | 정수 입력 | +| decimal | 2 | 16.67% | 소수 입력 | +| date | 3 | 25% | 날짜 선택 | +| datetime | 3 | 25% | 날짜+시간 선택 | +| time | 2 | 16.67% | 시간 선택 | + +### 선택 입력 (중간 설정) + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| -------- | ------- | ------ | --------------- | +| select | 3 | 25% | 드롭다운 선택 | +| radio | 3 | 25% | 라디오 버튼 | +| checkbox | 2 | 16.67% | 체크박스 (작게) | +| boolean | 2 | 16.67% | 참/거짓 (작게) | + +### 코드/참조 (넓게 설정) + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| ------ | ------- | ------ | ----------------------- | +| code | 3 | 25% | 코드 선택 | +| entity | 4 | 33% | 엔티티 참조 (길이 필요) | + +### 파일/이미지 (넓게 설정) + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| ------ | ------- | ------ | ------------- | +| file | 4 | 33% | 파일 업로드 | +| image | 3 | 25% | 이미지 업로드 | + +### 기타 + +| 웹타입 | 컬럼 수 | 퍼센트 | 설명 | +| ---------- | ------- | ------ | ------------------ | +| button | 2 | 16.67% | 버튼 | +| label | 2 | 16.67% | 라벨 | +| **기본값** | 3 | 25% | 정의되지 않은 타입 | + +## 적용 효과 + +### Before (기존) + +``` +[컬럼 추가] +- 이름 (text) → 2컬럼 → 너무 좁음 😞 +- 설명 (textarea) → 2컬럼 → 너무 좁음 😞 +- 나이 (number) → 2컬럼 → 적절함 😐 +- 활성화 (checkbox) → 2컬럼 → 너무 넓음 😞 + +[필터 추가] +- 검색어 (text) → 3컬럼 → 약간 좁음 😐 +- 날짜 (date) → 3컬럼 → 적절함 😐 +- 승인 (boolean) → 3컬럼 → 너무 넓음 😞 +``` + +### After (개선) + +``` +[컬럼 추가] +- 이름 (text) → 4컬럼 (33%) → 적절함 ✅ +- 설명 (textarea) → 6컬럼 (50%) → 충분함 ✅ +- 나이 (number) → 2컬럼 (16.67%) → 적절함 ✅ +- 활성화 (checkbox) → 2컬럼 (16.67%) → 적절함 ✅ + +[필터 추가] +- 검색어 (text) → 4컬럼 (33%) → 충분함 ✅ +- 날짜 (date) → 3컬럼 (25%) → 적절함 ✅ +- 승인 (boolean) → 2컬럼 (16.67%) → 적절함 ✅ +``` + +## 사용 방법 + +### 1. 컬럼 추가 + +1. 테이블 선택 +2. "컬럼 추가" 버튼 클릭 또는 드롭다운에서 컬럼 선택 +3. 웹타입에 맞는 기본 너비로 자동 생성됨 +4. 필요시 속성 패널에서 너비 조정 가능 + +### 2. 필터 추가 + +1. 테이블 선택 +2. "필터 추가" 버튼 클릭 +3. 웹타입에 맞는 기본 너비로 자동 생성됨 +4. 필요시 컬럼별 너비 조정 가능 + +### 3. 가상 파일 컬럼 추가 + +1. "파일 컬럼" 버튼 클릭 +2. 파일 타입에 맞는 기본 너비(4컬럼, 33%)로 생성됨 + +### 4. 너비 조정 (수동) + +**컬럼 너비 조정**: + +- 컬럼 설정 탭에서 각 컬럼별 "컬럼 너비" 드롭다운 선택 +- 1/12 (8.33%)부터 12/12 (100%)까지 선택 가능 +- 기본값은 웹타입에 따라 자동 설정됨 + +**필터 너비 조정**: + +- 필터 설정 탭에서 각 필터별 "필터 너비" 드롭다운 선택 +- 1/12 (8.33%)부터 12/12 (100%)까지 선택 가능 +- 기본값은 웹타입에 따라 자동 설정됨 + +## 주의 사항 + +### 기존 데이터 + +- **영향 없음**: 이미 생성된 컬럼/필터는 변경되지 않음 +- **새로 추가되는 항목만** 새로운 기본값 적용 + +### 커스터마이징 + +- 기본값이 맞지 않으면 수동으로 조정 가능 +- 자주 사용하는 너비가 있다면 `getDefaultGridColumns` 함수 수정 가능 + +### 레이아웃 고려 + +- 한 행에 총 12컬럼까지 배치 가능 +- 예: 4컬럼 + 4컬럼 + 4컬럼 = 12컬럼 (딱 맞음) +- 예: 4컬럼 + 4컬럼 + 6컬럼 = 14컬럼 (넘침 → 다음 줄로 이동) + +## 테스트 체크리스트 + +### 컬럼 추가 테스트 + +- [ ] text 타입 컬럼 추가 → 4컬럼(33%) 확인 +- [ ] number 타입 컬럼 추가 → 2컬럼(16.67%) 확인 +- [ ] textarea 타입 컬럼 추가 → 6컬럼(50%) 확인 +- [ ] select 타입 컬럼 추가 → 3컬럼(25%) 확인 +- [ ] checkbox 타입 컬럼 추가 → 2컬럼(16.67%) 확인 + +### 필터 추가 테스트 + +- [ ] text 타입 필터 추가 → 4컬럼(33%) 확인 +- [ ] date 타입 필터 추가 → 3컬럼(25%) 확인 +- [ ] boolean 타입 필터 추가 → 2컬럼(16.67%) 확인 + +### 가상 파일 컬럼 테스트 + +- [ ] 파일 컬럼 추가 → 4컬럼(33%) 확인 + +### 수동 조정 테스트 + +- [ ] 생성 후 너비 수동 변경 가능한지 확인 +- [ ] 변경된 너비가 저장/로드 시 유지되는지 확인 + +## 관련 파일 + +### 수정된 파일 + +#### 1. `/frontend/components/screen/panels/DataTableConfigPanel.tsx` + +- `getDefaultGridColumns` 함수 추가 (891-929줄) +- `addColumn` 함수 수정 (954줄) - 웹타입별 기본 너비 자동 계산 +- `addFilter` 함수 수정 (781줄) - 웹타입별 기본 너비 자동 계산 +- `addVirtualFileColumn` 함수 수정 (1055줄) - 파일 타입 기본 너비 적용 +- 컬럼 설정 UI 개선 (1652줄) - "그리드 컬럼" → "컬럼 너비" (1/12 ~ 12/12) +- 필터 설정 UI 개선 (2131줄) - "그리드 컬럼" → "필터 너비" (1/12 ~ 12/12) + +#### 2. `/frontend/components/screen/ScreenDesigner.tsx` + +- `getDefaultGridColumns` 함수 추가 (1946-1984줄) - 드래그 드롭 컴포넌트용 +- `getDefaultGridColumnsForTemplate` 함수 추가 (1429-1438줄) - 템플릿 컴포넌트용 +- 템플릿 컴포넌트 생성 시 기본 너비 적용 (1514줄) +- 폼 컨테이너 내 컴포넌트 생성 시 기본 너비 적용 (2151줄) +- 드래그 드롭 컴포넌트 생성 시 기본 너비 적용 (2194줄) + +### 관련 문서 + +- [컴포넌트*기본*너비*설정*가이드.md](./컴포넌트_기본_너비_설정_가이드.md) +- [그리드*컬럼수*옵션\_통합.md](./그리드_컬럼수_옵션_통합.md) + +## 버전 히스토리 + +### v1.0.0 (2025-10-14) + +- 웹타입별 기본 너비 자동 설정 기능 추가 +- `getDefaultGridColumns` 함수 구현 +- `addColumn`, `addFilter`, `addVirtualFileColumn` 함수에 적용 diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 666570e0..02d14f05 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -160,7 +160,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 동적 컴포넌트 렌더링 */} -
+
{ + const widthMap: Record = { + text: 4, + email: 4, + tel: 3, + url: 4, + textarea: 6, + number: 2, + decimal: 2, + date: 3, + datetime: 3, + time: 2, + select: 3, + radio: 3, + checkbox: 2, + boolean: 2, + code: 3, + entity: 4, + file: 4, + image: 3, + button: 2, + label: 2, + }; + return widthMap[wType] || 3; + }; + // 웹타입별 기본 설정 생성 const getDefaultWebTypeConfig = (wType: string) => { switch (wType) { @@ -1499,7 +1526,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD size: widgetSize, required: templateComp.required || false, readonly: templateComp.readonly || false, - gridColumns: 1, + gridColumns: getDefaultGridColumnsForTemplate(widgetType), webTypeConfig: getDefaultWebTypeConfig(widgetType), style: { labelDisplay: true, @@ -1753,10 +1780,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const isCardDisplay = component.id === "card-display"; const isTableList = component.id === "table-list"; - // 컴포넌트별 기본 그리드 컬럼 수 설정 - const gridColumns = isCardDisplay ? 8 : isTableList ? 1 : 1; + // 컴포넌트 타입별 기본 그리드 컬럼 수 설정 + let gridColumns = 1; // 기본값 - if ((isCardDisplay || isTableList) && layout.gridSettings?.snapToGrid && gridInfo) { + // 특수 컴포넌트 + if (isCardDisplay) { + gridColumns = 8; + } else if (isTableList) { + gridColumns = 1; + } else { + // 일반 컴포넌트: defaultSize.width를 기준으로 그리드 컬럼 수 계산 + // 그리드가 활성화된 경우에만 + if (layout.gridSettings?.snapToGrid && gridInfo) { + const columnWidth = gridInfo.columnWidth + gridInfo.gap; + const estimatedColumns = Math.round(component.defaultSize.width / columnWidth); + gridColumns = Math.max(1, Math.min(12, estimatedColumns)); // 1-12 범위로 제한 + } + } + + // 그리드 시스템이 활성화된 경우 gridColumns에 맞춰 너비 재계산 + if (layout.gridSettings?.snapToGrid && gridInfo) { // gridColumns에 맞는 정확한 너비 계산 const calculatedWidth = calculateWidthFromColumns( gridColumns, @@ -1765,7 +1808,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ); // 컴포넌트별 최소 크기 보장 - const minWidth = isTableList ? 120 : isCardDisplay ? 400 : 100; + const minWidth = isTableList ? 120 : isCardDisplay ? 400 : component.defaultSize.width; componentSize = { ...component.defaultSize, @@ -1777,6 +1820,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridColumns, defaultWidth: component.defaultSize.width, calculatedWidth, + finalWidth: componentSize.width, gridInfo, gridSettings: layout.gridSettings, }); @@ -1925,6 +1969,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD snapToGrid: layout.gridSettings?.snapToGrid, }); + // 웹타입별 기본 그리드 컬럼 수 계산 + const getDefaultGridColumns = (widgetType: string): number => { + const widthMap: Record = { + // 텍스트 입력 계열 (넓게) + text: 4, // 1/3 (33%) + email: 4, // 1/3 (33%) + tel: 3, // 1/4 (25%) + url: 4, // 1/3 (33%) + textarea: 6, // 절반 (50%) + + // 숫자/날짜 입력 (중간) + number: 2, // 2/12 (16.67%) + decimal: 2, // 2/12 (16.67%) + date: 3, // 1/4 (25%) + datetime: 3, // 1/4 (25%) + time: 2, // 2/12 (16.67%) + + // 선택 입력 (중간) + select: 3, // 1/4 (25%) + radio: 3, // 1/4 (25%) + checkbox: 2, // 2/12 (16.67%) + boolean: 2, // 2/12 (16.67%) + + // 코드/참조 (넓게) + code: 3, // 1/4 (25%) + entity: 4, // 1/3 (33%) + + // 파일/이미지 (넓게) + file: 4, // 1/3 (33%) + image: 3, // 1/4 (25%) + + // 기타 + button: 2, // 2/12 (16.67%) + label: 2, // 2/12 (16.67%) + }; + + const defaultColumns = widthMap[widgetType] || 3; // 기본값 3 (1/4, 25%) + console.log("🎯 [ScreenDesigner] getDefaultGridColumns:", { widgetType, defaultColumns }); + return defaultColumns; + }; + // 웹타입별 기본 설정 생성 const getDefaultWebTypeConfig = (widgetType: string) => { switch (widgetType) { @@ -2078,6 +2163,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const componentId = getComponentIdFromWebType(column.widgetType); // console.log(`🔄 폼 컨테이너 드롭: ${column.widgetType} → ${componentId}`); + // 웹타입별 적절한 gridColumns 계산 + const calculatedGridColumns = getDefaultGridColumns(column.widgetType); + + // gridColumns에 맞는 실제 너비 계산 + const componentWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns( + calculatedGridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ) + : defaultWidth; + + console.log("🎯 폼 컨테이너 컴포넌트 생성:", { + widgetType: column.widgetType, + calculatedGridColumns, + componentWidth, + defaultWidth, + }); + newComponent = { id: generateComponentId(), type: "component", // ✅ 새로운 컴포넌트 시스템 사용 @@ -2089,8 +2194,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD parentId: formContainerId, // 폼 컨테이너의 자식으로 설정 componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입 position: { x: relativeX, y: relativeY, z: 1 } as Position, - size: { width: defaultWidth, height: 40 }, - gridColumns: 1, + size: { width: componentWidth, height: 40 }, + gridColumns: calculatedGridColumns, // 코드 타입인 경우 코드 카테고리 정보 추가 ...(column.widgetType === "code" && column.codeCategory && { @@ -2122,6 +2227,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const componentId = getComponentIdFromWebType(column.widgetType); // console.log(`🔄 캔버스 드롭: ${column.widgetType} → ${componentId}`); + // 웹타입별 적절한 gridColumns 계산 + const calculatedGridColumns = getDefaultGridColumns(column.widgetType); + + // gridColumns에 맞는 실제 너비 계산 + const componentWidth = + currentGridInfo && layout.gridSettings?.snapToGrid + ? calculateWidthFromColumns( + calculatedGridColumns, + currentGridInfo, + layout.gridSettings as GridUtilSettings, + ) + : defaultWidth; + + console.log("🎯 캔버스 컴포넌트 생성:", { + widgetType: column.widgetType, + calculatedGridColumns, + componentWidth, + defaultWidth, + }); + newComponent = { id: generateComponentId(), type: "component", // ✅ 새로운 컴포넌트 시스템 사용 @@ -2132,8 +2257,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD readonly: false, componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입 position: { x, y, z: 1 } as Position, - size: { width: defaultWidth, height: 40 }, - gridColumns: 1, + size: { width: componentWidth, height: 40 }, + gridColumns: calculatedGridColumns, // 코드 타입인 경우 코드 카테고리 정보 추가 ...(column.widgetType === "code" && column.codeCategory && { diff --git a/frontend/components/screen/panels/DataTableConfigPanel.tsx b/frontend/components/screen/panels/DataTableConfigPanel.tsx index 666d071e..163a446d 100644 --- a/frontend/components/screen/panels/DataTableConfigPanel.tsx +++ b/frontend/components/screen/panels/DataTableConfigPanel.tsx @@ -69,7 +69,6 @@ const DataTableConfigPanelComponent: React.FC = ({ showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true, showPageInfo: component.pagination?.showPageInfo ?? true, showFirstLast: component.pagination?.showFirstLast ?? true, - gridColumns: component.gridColumns || 6, }); // 컬럼별 로컬 입력 상태 @@ -110,45 +109,45 @@ const DataTableConfigPanelComponent: React.FC = ({ // 컴포넌트 변경 시 로컬 값 동기화 useEffect(() => { // console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", { - // componentId: component.id, - // title: component.title, - // searchButtonText: component.searchButtonText, - // columnsCount: component.columns.length, - // filtersCount: component.filters.length, - // columnIds: component.columns.map((col) => col.id), - // filterColumnNames: component.filters.map((filter) => filter.columnName), - // timestamp: new Date().toISOString(), + // componentId: component.id, + // title: component.title, + // searchButtonText: component.searchButtonText, + // columnsCount: component.columns.length, + // filtersCount: component.filters.length, + // columnIds: component.columns.map((col) => col.id), + // filterColumnNames: component.filters.map((filter) => filter.columnName), + // timestamp: new Date().toISOString(), // }); // 컬럼과 필터 상세 정보 로그 if (component.columns.length > 0) { // console.log( - // "📋 현재 컬럼 목록:", - // component.columns.map((col) => ({ - // id: col.id, - // columnName: col.columnName, - // label: col.label, - // visible: col.visible, - // gridColumns: col.gridColumns, - // })), + // "📋 현재 컬럼 목록:", + // component.columns.map((col) => ({ + // id: col.id, + // columnName: col.columnName, + // label: col.label, + // visible: col.visible, + // gridColumns: col.gridColumns, + // })), // ); } // 로컬 상태 정보 로그 // console.log("🔧 로컬 상태 정보:", { - // localColumnInputsCount: Object.keys(localColumnInputs).length, - // localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length, - // localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length, + // localColumnInputsCount: Object.keys(localColumnInputs).length, + // localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length, + // localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length, // }); if (component.filters.length > 0) { // console.log( - // "🔍 현재 필터 목록:", - // component.filters.map((filter) => ({ - // columnName: filter.columnName, - // widgetType: filter.widgetType, - // label: filter.label, - // })), + // "🔍 현재 필터 목록:", + // component.filters.map((filter) => ({ + // columnName: filter.columnName, + // widgetType: filter.widgetType, + // label: filter.label, + // })), // ); } @@ -179,7 +178,6 @@ const DataTableConfigPanelComponent: React.FC = ({ showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true, showPageInfo: component.pagination?.showPageInfo ?? true, showFirstLast: component.pagination?.showFirstLast ?? true, - gridColumns: component.gridColumns || 6, // 테이블명 동기화 tableName: component.tableName || "", }); @@ -259,9 +257,9 @@ const DataTableConfigPanelComponent: React.FC = ({ if (!(filterKey in newFilterInputs)) { newFilterInputs[filterKey] = filter.label || filter.columnName; // console.log("🆕 새 필터 로컬 상태 추가:", { - // filterKey, - // label: filter.label, - // columnName: filter.columnName, + // filterKey, + // label: filter.label, + // columnName: filter.columnName, // }); } }); @@ -278,9 +276,9 @@ const DataTableConfigPanelComponent: React.FC = ({ }); // console.log("📝 필터 로컬 상태 동기화 완료:", { - // prevCount: Object.keys(prev).length, - // newCount: Object.keys(newFilterInputs).length, - // newKeys: Object.keys(newFilterInputs), + // prevCount: Object.keys(prev).length, + // newCount: Object.keys(newFilterInputs).length, + // newKeys: Object.keys(newFilterInputs), // }); return newFilterInputs; @@ -320,18 +318,18 @@ const DataTableConfigPanelComponent: React.FC = ({ if (!table) return; // console.log("🔄 테이블 변경:", { - // tableName, - // currentTableName: localValues.tableName, - // table, - // columnsCount: table.columns.length, + // tableName, + // currentTableName: localValues.tableName, + // table, + // columnsCount: table.columns.length, // }); // 테이블 변경 시 컬럼을 자동으로 추가하지 않음 (사용자가 수동으로 추가해야 함) const defaultColumns: DataTableColumn[] = []; // console.log("✅ 생성된 컬럼 설정:", { - // defaultColumnsCount: defaultColumns.length, - // visibleColumns: defaultColumns.filter((col) => col.visible).length, + // defaultColumnsCount: defaultColumns.length, + // visibleColumns: defaultColumns.filter((col) => col.visible).length, // }); // 상태 업데이트를 한 번에 처리 @@ -378,10 +376,10 @@ const DataTableConfigPanelComponent: React.FC = ({ try { // TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트 // console.log("📡 테이블 타입 관리 업데이트 필요:", { - // tableName: component.tableName, - // columnName: targetColumn.columnName, - // webType: "radio", - // detailSettings: JSON.stringify(webTypeConfig), + // tableName: component.tableName, + // columnName: targetColumn.columnName, + // webType: "radio", + // detailSettings: JSON.stringify(webTypeConfig), // }); } catch (error) { // console.error("테이블 타입 관리 업데이트 실패:", error); @@ -738,9 +736,9 @@ const DataTableConfigPanelComponent: React.FC = ({ }); // console.log("🗑️ 컬럼 삭제:", { - // columnId, - // columnName: columnToRemove?.columnName, - // remainingColumns: updatedColumns.length, + // columnId, + // columnName: columnToRemove?.columnName, + // remainingColumns: updatedColumns.length, // }); onUpdateComponent({ @@ -780,7 +778,7 @@ const DataTableConfigPanelComponent: React.FC = ({ columnName: targetColumn.columnName, widgetType, label: targetColumn.columnLabel || targetColumn.columnName, - gridColumns: 3, + gridColumns: getDefaultGridColumns(widgetType), // 웹타입별 추가 정보 설정 codeCategory: targetColumn.codeCategory, referenceTable: targetColumn.referenceTable, @@ -789,28 +787,28 @@ const DataTableConfigPanelComponent: React.FC = ({ }; // console.log("➕ 필터 추가 시작:", { - // targetColumnName: targetColumn.columnName, - // targetColumnLabel: targetColumn.columnLabel, - // inferredWidgetType: widgetType, - // currentFiltersCount: component.filters.length, + // targetColumnName: targetColumn.columnName, + // targetColumnLabel: targetColumn.columnLabel, + // inferredWidgetType: widgetType, + // currentFiltersCount: component.filters.length, // }); // console.log("➕ 생성된 새 필터:", { - // columnName: newFilter.columnName, - // widgetType: newFilter.widgetType, - // label: newFilter.label, - // gridColumns: newFilter.gridColumns, + // columnName: newFilter.columnName, + // widgetType: newFilter.widgetType, + // label: newFilter.label, + // gridColumns: newFilter.gridColumns, // }); const updatedFilters = [...component.filters, newFilter]; // console.log("🔄 필터 업데이트 호출:", { - // filtersToAdd: 1, - // totalFiltersAfter: updatedFilters.length, - // updatedFilters: updatedFilters.map((filter) => ({ - // columnName: filter.columnName, - // widgetType: filter.widgetType, - // label: filter.label, - // })), + // filtersToAdd: 1, + // totalFiltersAfter: updatedFilters.length, + // updatedFilters: updatedFilters.map((filter) => ({ + // columnName: filter.columnName, + // widgetType: filter.widgetType, + // label: filter.label, + // })), // }); // 먼저 로컬 상태를 업데이트하고 @@ -821,10 +819,10 @@ const DataTableConfigPanelComponent: React.FC = ({ [filterKey]: newFilter.label, }; // console.log("📝 필터 로컬 상태 업데이트:", { - // filterKey, - // newLabel: newFilter.label, - // prevState: prev, - // newState, + // filterKey, + // newLabel: newFilter.label, + // prevState: prev, + // newState, // }); return newState; }); @@ -836,8 +834,8 @@ const DataTableConfigPanelComponent: React.FC = ({ setActiveTab("filters"); // console.log("🔍 필터 추가 후 탭 이동:", { - // activeTab: "filters", - // isExternalControl: !!onTabChange, + // activeTab: "filters", + // isExternalControl: !!onTabChange, // }); // 강제로 리렌더링을 트리거하기 위해 여러 방법 사용 @@ -859,9 +857,9 @@ const DataTableConfigPanelComponent: React.FC = ({ }, 100); // console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", { - // filterKey, - // newFilterLabel: newFilter.label, - // switchedToTab: "filters", + // filterKey, + // newFilterLabel: newFilter.label, + // switchedToTab: "filters", // }); }, [selectedTable, component.filters, onUpdateComponent]); @@ -890,6 +888,48 @@ const DataTableConfigPanelComponent: React.FC = ({ return !nonFilterableTypes.includes(webType); }; + // 웹타입별 기본 컬럼 수 계산 (컴포넌트 너비 기반) + const getDefaultGridColumns = (webType: WebType): number => { + // 각 웹타입별 적절한 기본 너비 설정 + const widthMap: Record = { + // 텍스트 입력 계열 (넓게) + text: 4, // 1/3 (33%) + email: 4, // 1/3 (33%) + tel: 3, // 1/4 (25%) + url: 4, // 1/3 (33%) + textarea: 6, // 절반 (50%) + + // 숫자/날짜 입력 (중간) + number: 2, // 2/12 (16.67%) + decimal: 2, // 2/12 (16.67%) + date: 3, // 1/4 (25%) + datetime: 3, // 1/4 (25%) + time: 2, // 2/12 (16.67%) + + // 선택 입력 (중간) + select: 3, // 1/4 (25%) + radio: 3, // 1/4 (25%) + checkbox: 2, // 2/12 (16.67%) + boolean: 2, // 2/12 (16.67%) + + // 코드/참조 (넓게) + code: 3, // 1/4 (25%) + entity: 4, // 1/3 (33%) + + // 파일/이미지 (넓게) + file: 4, // 1/3 (33%) + image: 3, // 1/4 (25%) + + // 기타 + button: 2, // 2/12 (16.67%) + label: 2, // 2/12 (16.67%) + }; + + const defaultColumns = widthMap[webType] || 3; // 기본값 3 (1/4, 25%) + console.log("🎯 getDefaultGridColumns 호출:", { webType, defaultColumns, widthMap: widthMap[webType] }); + return defaultColumns; + }; + // 컬럼 추가 (테이블에서 선택) const addColumn = useCallback( (columnName?: string) => { @@ -907,38 +947,47 @@ const DataTableConfigPanelComponent: React.FC = ({ : availableColumns[0]; const widgetType = getWidgetTypeFromColumn(targetColumn); + const calculatedGridColumns = getDefaultGridColumns(widgetType); + + console.log("➕ addColumn 호출:", { + columnName: targetColumn.columnName, + widgetType, + calculatedGridColumns, + }); const newColumn: DataTableColumn = { id: generateComponentId(), columnName: targetColumn.columnName, label: targetColumn.columnLabel || targetColumn.columnName, widgetType, - gridColumns: 2, + gridColumns: calculatedGridColumns, visible: true, filterable: isFilterableWebType(widgetType), sortable: true, searchable: ["text", "email", "tel"].includes(widgetType), }; + console.log("✅ 생성된 newColumn:", newColumn); + // 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가) // console.log("➕ 컬럼 추가 시작:", { - // targetColumnName: targetColumn.columnName, - // targetColumnLabel: targetColumn.columnLabel, - // inferredWidgetType: widgetType, - // currentColumnsCount: component.columns.length, - // currentFiltersCount: component.filters.length, + // targetColumnName: targetColumn.columnName, + // targetColumnLabel: targetColumn.columnLabel, + // inferredWidgetType: widgetType, + // currentColumnsCount: component.columns.length, + // currentFiltersCount: component.filters.length, // }); // console.log("➕ 생성된 새 컬럼:", { - // id: newColumn.id, - // columnName: newColumn.columnName, - // label: newColumn.label, - // widgetType: newColumn.widgetType, - // filterable: newColumn.filterable, - // visible: newColumn.visible, - // sortable: newColumn.sortable, - // searchable: newColumn.searchable, + // id: newColumn.id, + // columnName: newColumn.columnName, + // label: newColumn.label, + // widgetType: newColumn.widgetType, + // filterable: newColumn.filterable, + // visible: newColumn.visible, + // sortable: newColumn.sortable, + // searchable: newColumn.searchable, // }); // 필터는 수동으로만 추가 @@ -950,9 +999,9 @@ const DataTableConfigPanelComponent: React.FC = ({ [newColumn.id]: newColumn.label, }; // console.log("🔄 로컬 컬럼 상태 업데이트:", { - // newColumnId: newColumn.id, - // newLabel: newColumn.label, - // totalLocalInputs: Object.keys(newInputs).length, + // newColumnId: newColumn.id, + // newLabel: newColumn.label, + // totalLocalInputs: Object.keys(newInputs).length, // }); return newInputs; }); @@ -979,14 +1028,14 @@ const DataTableConfigPanelComponent: React.FC = ({ }; // console.log("🔄 컴포넌트 업데이트 호출:", { - // columnsToAdd: 1, - // totalColumnsAfter: updates.columns?.length, - // hasColumns: !!updates.columns, - // updateKeys: Object.keys(updates), + // columnsToAdd: 1, + // totalColumnsAfter: updates.columns?.length, + // hasColumns: !!updates.columns, + // updateKeys: Object.keys(updates), // }); // console.log("🔄 업데이트 상세 내용:", { - // columns: updates.columns?.map((col) => ({ id: col.id, columnName: col.columnName, label: col.label })), + // columns: updates.columns?.map((col) => ({ id: col.id, columnName: col.columnName, label: col.label })), // }); onUpdateComponent(updates); @@ -995,8 +1044,8 @@ const DataTableConfigPanelComponent: React.FC = ({ setActiveTab("columns"); // console.log("📋 컬럼 추가 후 탭 이동:", { - // activeTab: "columns", - // isExternalControl: !!onTabChange, + // activeTab: "columns", + // isExternalControl: !!onTabChange, // }); // console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨"); @@ -1014,7 +1063,7 @@ const DataTableConfigPanelComponent: React.FC = ({ columnName: newColumnName, label: `파일 컬럼 ${fileColumnCount + 1}`, widgetType: "file", - gridColumns: 2, + gridColumns: getDefaultGridColumns("file"), visible: true, filterable: false, // 파일 컬럼은 필터링 불가 sortable: false, // 파일 컬럼은 정렬 불가 @@ -1029,9 +1078,9 @@ const DataTableConfigPanelComponent: React.FC = ({ }; // console.log("📁 가상 파일 컬럼 추가:", { - // columnName: newColumn.columnName, - // label: newColumn.label, - // isVirtualFileColumn: newColumn.isVirtualFileColumn, + // columnName: newColumn.columnName, + // label: newColumn.label, + // isVirtualFileColumn: newColumn.isVirtualFileColumn, // }); // 로컬 상태에 새 컬럼 입력값 추가 @@ -1092,7 +1141,7 @@ const DataTableConfigPanelComponent: React.FC = ({
{ - const gridColumns = parseInt(e.target.value, 10); - // console.log("🔄 테이블 그리드 컬럼 수 변경:", gridColumns); - setLocalValues((prev) => ({ ...prev, gridColumns })); - onUpdateComponent({ gridColumns }); - }} - > - - {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((num) => ( - - ))} - -
-
= ({
- +
@@ -2023,11 +2058,11 @@ const DataTableConfigPanelComponent: React.FC = ({ const localValue = localFilterInputs[filterKey]; const finalValue = localValue !== undefined ? localValue : filter.label; // console.log("🎯 필터 입력 값 결정:", { - // filterKey, - // localValue, - // filterLabel: filter.label, - // finalValue, - // allLocalInputs: Object.keys(localFilterInputs), + // filterKey, + // localValue, + // filterLabel: filter.label, + // finalValue, + // allLocalInputs: Object.keys(localFilterInputs), // }); return finalValue; })()} @@ -2104,7 +2139,7 @@ const DataTableConfigPanelComponent: React.FC = ({
- +
@@ -2180,7 +2222,7 @@ const DataTableConfigPanelComponent: React.FC = ({
{ - const newValue = e.target.value; - const numValue = Number(newValue); - if (numValue >= 1 && numValue <= 12) { - setLocalInputs((prev) => ({ ...prev, gridColumns: newValue })); - onUpdateProperty("gridColumns", numValue); - } - }} - placeholder="1" - className="mt-1" - /> -
- 이 컴포넌트가 차지할 그리드 컬럼 수를 설정합니다 (기본: 1) -
-
diff --git a/frontend/components/screen/panels/WebTypeConfigPanel.tsx b/frontend/components/screen/panels/WebTypeConfigPanel.tsx new file mode 100644 index 00000000..9227c269 --- /dev/null +++ b/frontend/components/screen/panels/WebTypeConfigPanel.tsx @@ -0,0 +1,419 @@ +"use client"; + +import React from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Separator } from "@/components/ui/separator"; +import { Plus, Trash2 } from "lucide-react"; +import { WebType } from "@/types/screen"; + +interface WebTypeConfigPanelProps { + webType: WebType | string; + config: any; + onUpdateConfig: (config: any) => void; +} + +/** + * 웹타입별 특화 설정 패널 + */ +export const WebTypeConfigPanel: React.FC = ({ webType, config, onUpdateConfig }) => { + // webType을 소문자로 변환하고 기본 타입 추출 + const normalizedWebType = String(webType || "").toLowerCase(); + + // 기본 타입 추출 (예: "radio-horizontal" -> "radio", "checkbox-basic" -> "checkbox") + const getBaseType = (type: string): string => { + if (type.startsWith("radio")) return "radio"; + if (type.startsWith("checkbox")) return "checkbox"; + if (type.startsWith("select")) return "select"; + if (type.startsWith("number")) return "number"; + if (type.startsWith("text")) return "text"; + if (type.startsWith("date")) return "date"; + if (type.startsWith("file")) return "file"; + if (type.startsWith("image")) return "image"; + return type; + }; + + const baseType = getBaseType(normalizedWebType); + + console.log("🎨 WebTypeConfigPanel:", { webType, normalizedWebType, baseType }); + + // 선택형 입력 (select, radio, checkbox) - 옵션 관리 + if (baseType === "select" || baseType === "radio" || baseType === "checkbox") { + const options = config?.options || []; + const multiple = config?.multiple || false; + const searchable = config?.searchable || false; + + return ( +
+
+ + +
+ + {/* 옵션 리스트 */} +
+ {options.map((option: any, index: number) => ( +
+ { + const newOptions = [...options]; + newOptions[index] = { ...option, label: e.target.value }; + onUpdateConfig({ ...config, options: newOptions }); + }} + className="flex-1" + /> + { + const newOptions = [...options]; + newOptions[index] = { ...option, value: e.target.value }; + onUpdateConfig({ ...config, options: newOptions }); + }} + className="flex-1" + /> + +
+ ))} +
+ + {/* Select 전용 설정 */} + {baseType === "select" && ( + <> + +
+ + { + onUpdateConfig({ ...config, multiple: checked }); + }} + /> +
+
+ + { + onUpdateConfig({ ...config, searchable: checked }); + }} + /> +
+ + )} +
+ ); + } + + // 숫자 입력 (number, decimal) - 범위 설정 + if (baseType === "number" || baseType === "decimal") { + const min = config?.min; + const max = config?.max; + const step = config?.step || (baseType === "decimal" ? 0.1 : 1); + + return ( +
+ + +
+
+ + { + onUpdateConfig({ + ...config, + min: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> +
+
+ + { + onUpdateConfig({ + ...config, + max: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> +
+
+ +
+ + { + onUpdateConfig({ + ...config, + step: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> +
+
+ ); + } + + // 날짜/시간 입력 - 형식 설정 + if (baseType === "date" || baseType === "datetime" || baseType === "time") { + const format = + config?.format || (baseType === "date" ? "YYYY-MM-DD" : baseType === "datetime" ? "YYYY-MM-DD HH:mm" : "HH:mm"); + const showTime = config?.showTime || false; + + return ( +
+ + +
+ + +
+ + {baseType === "date" && ( +
+ + { + onUpdateConfig({ ...config, showTime: checked }); + }} + /> +
+ )} +
+ ); + } + + // 텍스트 입력 - 검증 규칙 + if ( + baseType === "text" || + baseType === "email" || + baseType === "tel" || + baseType === "url" || + baseType === "textarea" + ) { + const minLength = config?.minLength; + const maxLength = config?.maxLength; + const pattern = config?.pattern; + + return ( +
+ + +
+
+ + { + onUpdateConfig({ + ...config, + minLength: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> +
+
+ + { + onUpdateConfig({ + ...config, + maxLength: e.target.value ? Number(e.target.value) : undefined, + }); + }} + /> +
+
+ + {(webType === "text" || webType === "textarea") && ( +
+ + { + onUpdateConfig({ + ...config, + pattern: e.target.value || undefined, + }); + }} + /> +

입력값 검증에 사용할 정규식 패턴

+
+ )} +
+ ); + } + + // 파일 업로드 - 파일 타입 제한 + if (baseType === "file" || baseType === "image") { + const accept = config?.accept || (baseType === "image" ? "image/*" : "*/*"); + const maxSize = config?.maxSize || 10; + const multiple = config?.multiple || false; + + return ( +
+ + +
+ + { + onUpdateConfig({ ...config, accept: e.target.value }); + }} + /> +

쉼표로 구분된 파일 확장자 또는 MIME 타입

+
+ +
+ + { + onUpdateConfig({ + ...config, + maxSize: e.target.value ? Number(e.target.value) : 10, + }); + }} + /> +
+ +
+ + { + onUpdateConfig({ ...config, multiple: checked }); + }} + /> +
+
+ ); + } + + // 기본 메시지 (설정 불필요) + return ( +
+

이 웹타입은 추가 설정이 필요하지 않습니다.

+
+ ); +}; diff --git a/frontend/lib/registry/components/accordion-basic/index.ts b/frontend/lib/registry/components/accordion-basic/index.ts index a509395e..ce740bc1 100644 --- a/frontend/lib/registry/components/accordion-basic/index.ts +++ b/frontend/lib/registry/components/accordion-basic/index.ts @@ -50,7 +50,7 @@ export const AccordionBasicDefinition = createComponentDefinition({ collapsible: true, defaultValue: "item-1", }, - defaultSize: { width: 300, height: 200 }, + defaultSize: { width: 400, height: 200 }, configPanel: AccordionBasicConfigPanel, icon: "ChevronDown", tags: ["아코디언", "접기", "펼치기", "콘텐츠", "섹션"], diff --git a/frontend/lib/registry/components/button-primary/index.ts b/frontend/lib/registry/components/button-primary/index.ts index d98339de..f9e19a14 100644 --- a/frontend/lib/registry/components/button-primary/index.ts +++ b/frontend/lib/registry/components/button-primary/index.ts @@ -30,7 +30,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({ errorMessage: "저장 중 오류가 발생했습니다.", }, }, - defaultSize: { width: 120, height: 36 }, + defaultSize: { width: 120, height: 40 }, configPanel: ButtonPrimaryConfigPanel, icon: "MousePointer", tags: ["버튼", "액션", "클릭"], diff --git a/frontend/lib/registry/components/checkbox-basic/index.ts b/frontend/lib/registry/components/checkbox-basic/index.ts index 30caa613..3360224d 100644 --- a/frontend/lib/registry/components/checkbox-basic/index.ts +++ b/frontend/lib/registry/components/checkbox-basic/index.ts @@ -23,7 +23,7 @@ export const CheckboxBasicDefinition = createComponentDefinition({ defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 120, height: 24 }, + defaultSize: { width: 150, height: 32 }, configPanel: CheckboxBasicConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/common/inputStyles.ts b/frontend/lib/registry/components/common/inputStyles.ts index 991219f6..8fff94ce 100644 --- a/frontend/lib/registry/components/common/inputStyles.ts +++ b/frontend/lib/registry/components/common/inputStyles.ts @@ -12,6 +12,7 @@ export const INPUT_CLASSES = { focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed placeholder:text-gray-400 + max-w-full overflow-hidden `, // 선택된 상태 @@ -31,7 +32,7 @@ export const INPUT_CLASSES = { // 컨테이너 container: ` - relative w-full + relative w-full max-w-full overflow-hidden `, // textarea @@ -43,6 +44,7 @@ export const INPUT_CLASSES = { focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed resize-vertical + max-w-full overflow-hidden `, // select @@ -54,11 +56,12 @@ export const INPUT_CLASSES = { focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed cursor-pointer + max-w-full overflow-hidden `, // flex 컨테이너 (email, tel, url 등) flexContainer: ` - flex items-center gap-2 w-full h-10 + flex items-center gap-2 w-full h-10 max-w-full overflow-hidden `, // 구분자 (@ , ~ 등) diff --git a/frontend/lib/registry/components/date-input/index.ts b/frontend/lib/registry/components/date-input/index.ts index 639e28cc..593ffc6f 100644 --- a/frontend/lib/registry/components/date-input/index.ts +++ b/frontend/lib/registry/components/date-input/index.ts @@ -23,7 +23,7 @@ export const DateInputDefinition = createComponentDefinition({ defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 180, height: 36 }, + defaultSize: { width: 220, height: 40 }, configPanel: DateInputConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/divider-line/index.ts b/frontend/lib/registry/components/divider-line/index.ts index 68f5da58..61f02c36 100644 --- a/frontend/lib/registry/components/divider-line/index.ts +++ b/frontend/lib/registry/components/divider-line/index.ts @@ -24,7 +24,7 @@ export const DividerLineDefinition = createComponentDefinition({ placeholder: "텍스트를 입력하세요", maxLength: 255, }, - defaultSize: { width: 200, height: 36 }, + defaultSize: { width: 400, height: 2 }, configPanel: DividerLineConfigPanel, icon: "Layout", tags: [], diff --git a/frontend/lib/registry/components/file-upload/index.ts b/frontend/lib/registry/components/file-upload/index.ts index 6ea5acc0..89dbffb5 100644 --- a/frontend/lib/registry/components/file-upload/index.ts +++ b/frontend/lib/registry/components/file-upload/index.ts @@ -23,7 +23,7 @@ export const FileUploadDefinition = createComponentDefinition({ defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 250, height: 36 }, + defaultSize: { width: 350, height: 40 }, configPanel: FileUploadConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/image-display/index.ts b/frontend/lib/registry/components/image-display/index.ts index ec2f5e10..ddb38f95 100644 --- a/frontend/lib/registry/components/image-display/index.ts +++ b/frontend/lib/registry/components/image-display/index.ts @@ -23,7 +23,7 @@ export const ImageDisplayDefinition = createComponentDefinition({ defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 200, height: 36 }, + defaultSize: { width: 200, height: 200 }, configPanel: ImageDisplayConfigPanel, icon: "Eye", tags: [], diff --git a/frontend/lib/registry/components/number-input/index.ts b/frontend/lib/registry/components/number-input/index.ts index a5b31080..138ae488 100644 --- a/frontend/lib/registry/components/number-input/index.ts +++ b/frontend/lib/registry/components/number-input/index.ts @@ -25,7 +25,7 @@ export const NumberInputDefinition = createComponentDefinition({ max: 999999, step: 1, }, - defaultSize: { width: 150, height: 36 }, + defaultSize: { width: 200, height: 40 }, configPanel: NumberInputConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/radio-basic/index.ts b/frontend/lib/registry/components/radio-basic/index.ts index 61054d06..9f7f068e 100644 --- a/frontend/lib/registry/components/radio-basic/index.ts +++ b/frontend/lib/registry/components/radio-basic/index.ts @@ -23,7 +23,7 @@ export const RadioBasicDefinition = createComponentDefinition({ defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 120, height: 24 }, + defaultSize: { width: 150, height: 32 }, configPanel: RadioBasicConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/select-basic/index.ts b/frontend/lib/registry/components/select-basic/index.ts index d4e9266d..f391dda9 100644 --- a/frontend/lib/registry/components/select-basic/index.ts +++ b/frontend/lib/registry/components/select-basic/index.ts @@ -24,7 +24,7 @@ export const SelectBasicDefinition = createComponentDefinition({ options: [], placeholder: "선택하세요", }, - defaultSize: { width: 200, height: 36 }, + defaultSize: { width: 250, height: 40 }, configPanel: SelectBasicConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/slider-basic/index.ts b/frontend/lib/registry/components/slider-basic/index.ts index 6aa8c8b3..dfbb2ccb 100644 --- a/frontend/lib/registry/components/slider-basic/index.ts +++ b/frontend/lib/registry/components/slider-basic/index.ts @@ -25,7 +25,7 @@ export const SliderBasicDefinition = createComponentDefinition({ max: 999999, step: 1, }, - defaultSize: { width: 200, height: 36 }, + defaultSize: { width: 250, height: 40 }, configPanel: SliderBasicConfigPanel, icon: "Edit", tags: [], diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 10d8d877..675c0047 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -7,6 +7,9 @@ import { TextInputConfig } from "./types"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; import { INPUT_CLASSES, cn, getInputClasses } from "../common/inputStyles"; +import { ChevronDown, Check, ChevronsUpDown } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; export interface TextInputComponentProps extends ComponentRendererProps { config?: TextInputConfig; @@ -234,7 +237,7 @@ export const TextInputComponent: React.FC = ({ // 이메일 입력 상태 (username@domain 분리) const [emailUsername, setEmailUsername] = React.useState(""); const [emailDomain, setEmailDomain] = React.useState("gmail.com"); - const [isCustomDomain, setIsCustomDomain] = React.useState(false); + const [emailDomainOpen, setEmailDomainOpen] = React.useState(false); // 전화번호 입력 상태 (3개 부분으로 분리) const [telPart1, setTelPart1] = React.useState(""); @@ -257,13 +260,7 @@ export const TextInputComponent: React.FC = ({ if (currentValue && typeof currentValue === "string" && currentValue.includes("@")) { const [username, domain] = currentValue.split("@"); setEmailUsername(username || ""); - if (domain && emailDomains.includes(domain)) { - setEmailDomain(domain); - setIsCustomDomain(false); - } else { - setEmailDomain(domain || ""); - setIsCustomDomain(true); - } + setEmailDomain(domain || "gmail.com"); } } }, [webType, component.value, formData, component.columnName, isInteractive]); @@ -341,58 +338,74 @@ export const TextInputComponent: React.FC = ({ {/* @ 구분자 */} @ - {/* 도메인 선택/입력 */} - {isCustomDomain ? ( - { - const newDomain = e.target.value; - setEmailDomain(newDomain); - const fullEmail = `${emailUsername}@${newDomain}`; + {/* 도메인 선택/입력 (Combobox) */} + + + + + + + { + setEmailDomain(value); + const fullEmail = `${emailUsername}@${value}`; - if (isInteractive && formData && onFormDataChange && component.columnName) { - onFormDataChange({ - ...formData, - [component.columnName]: fullEmail, - }); - } - }} - className={`h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300"} ${componentConfig.disabled ? "bg-gray-100 text-gray-400" : "bg-white text-gray-900"} focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:cursor-not-allowed`} - /> - ) : ( - - )} + if (isInteractive && formData && onFormDataChange && component.columnName) { + onFormDataChange({ + ...formData, + [component.columnName]: fullEmail, + }); + } + setEmailDomainOpen(false); + }} + > + + {domain} + + ))} + + + + +
); @@ -589,14 +602,14 @@ export const TextInputComponent: React.FC = ({ }); } }} - className={`min-h-[80px] w-full resize-y rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300"} ${componentConfig.disabled ? "bg-gray-100 text-gray-400" : "bg-white text-gray-900"} placeholder:text-gray-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:cursor-not-allowed`} + className={`min-h-[80px] w-full max-w-full resize-y rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300"} ${componentConfig.disabled ? "bg-gray-100 text-gray-400" : "bg-white text-gray-900"} placeholder:text-gray-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:cursor-not-allowed`} /> ); } return ( -
+
{/* 라벨 렌더링 */} {component.label && component.style?.labelDisplay !== false && (