세부타입설정
This commit is contained in:
parent
8bc8df4eb8
commit
a2c3737f7a
|
|
@ -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<string, number> = {
|
||||||
|
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` 자동 계산 로직 추가
|
||||||
|
|
@ -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 추가
|
||||||
|
- 모든 인풋 컴포넌트 기본 크기 조정
|
||||||
|
|
@ -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<WebType, number> = {
|
||||||
|
// 텍스트 입력 계열 (넓게)
|
||||||
|
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` 함수에 적용
|
||||||
|
|
@ -160,7 +160,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* 동적 컴포넌트 렌더링 */}
|
{/* 동적 컴포넌트 렌더링 */}
|
||||||
<div className={`h-full w-full ${component.componentConfig?.type === "table-list" ? "overflow-hidden" : ""}`}>
|
<div
|
||||||
|
className={`h-full w-full max-w-full ${component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-hidden"}`}
|
||||||
|
>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const isLayoutComponent = targetComponent?.type === "layout";
|
const isLayoutComponent = targetComponent?.type === "layout";
|
||||||
|
|
||||||
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
||||||
let positionDelta = { x: 0, y: 0 };
|
const positionDelta = { x: 0, y: 0 };
|
||||||
if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) {
|
if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) {
|
||||||
const oldPosition = targetComponent.position;
|
const oldPosition = targetComponent.position;
|
||||||
let newPosition = { ...oldPosition };
|
let newPosition = { ...oldPosition };
|
||||||
|
|
@ -1425,6 +1425,33 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 위젯 컴포넌트
|
// 위젯 컴포넌트
|
||||||
const widgetType = templateComp.widgetType || "text";
|
const widgetType = templateComp.widgetType || "text";
|
||||||
|
|
||||||
|
// 웹타입별 기본 그리드 컬럼 수 계산
|
||||||
|
const getDefaultGridColumnsForTemplate = (wType: string): number => {
|
||||||
|
const widthMap: Record<string, number> = {
|
||||||
|
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) => {
|
const getDefaultWebTypeConfig = (wType: string) => {
|
||||||
switch (wType) {
|
switch (wType) {
|
||||||
|
|
@ -1499,7 +1526,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
size: widgetSize,
|
size: widgetSize,
|
||||||
required: templateComp.required || false,
|
required: templateComp.required || false,
|
||||||
readonly: templateComp.readonly || false,
|
readonly: templateComp.readonly || false,
|
||||||
gridColumns: 1,
|
gridColumns: getDefaultGridColumnsForTemplate(widgetType),
|
||||||
webTypeConfig: getDefaultWebTypeConfig(widgetType),
|
webTypeConfig: getDefaultWebTypeConfig(widgetType),
|
||||||
style: {
|
style: {
|
||||||
labelDisplay: true,
|
labelDisplay: true,
|
||||||
|
|
@ -1753,10 +1780,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const isCardDisplay = component.id === "card-display";
|
const isCardDisplay = component.id === "card-display";
|
||||||
const isTableList = component.id === "table-list";
|
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에 맞는 정확한 너비 계산
|
// gridColumns에 맞는 정확한 너비 계산
|
||||||
const calculatedWidth = calculateWidthFromColumns(
|
const calculatedWidth = calculateWidthFromColumns(
|
||||||
gridColumns,
|
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 = {
|
componentSize = {
|
||||||
...component.defaultSize,
|
...component.defaultSize,
|
||||||
|
|
@ -1777,6 +1820,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
gridColumns,
|
gridColumns,
|
||||||
defaultWidth: component.defaultSize.width,
|
defaultWidth: component.defaultSize.width,
|
||||||
calculatedWidth,
|
calculatedWidth,
|
||||||
|
finalWidth: componentSize.width,
|
||||||
gridInfo,
|
gridInfo,
|
||||||
gridSettings: layout.gridSettings,
|
gridSettings: layout.gridSettings,
|
||||||
});
|
});
|
||||||
|
|
@ -1925,6 +1969,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
snapToGrid: layout.gridSettings?.snapToGrid,
|
snapToGrid: layout.gridSettings?.snapToGrid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 웹타입별 기본 그리드 컬럼 수 계산
|
||||||
|
const getDefaultGridColumns = (widgetType: string): number => {
|
||||||
|
const widthMap: Record<string, number> = {
|
||||||
|
// 텍스트 입력 계열 (넓게)
|
||||||
|
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) => {
|
const getDefaultWebTypeConfig = (widgetType: string) => {
|
||||||
switch (widgetType) {
|
switch (widgetType) {
|
||||||
|
|
@ -2078,6 +2163,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const componentId = getComponentIdFromWebType(column.widgetType);
|
const componentId = getComponentIdFromWebType(column.widgetType);
|
||||||
// console.log(`🔄 폼 컨테이너 드롭: ${column.widgetType} → ${componentId}`);
|
// 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 = {
|
newComponent = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
||||||
|
|
@ -2089,8 +2194,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
||||||
componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입
|
componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입
|
||||||
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
||||||
size: { width: defaultWidth, height: 40 },
|
size: { width: componentWidth, height: 40 },
|
||||||
gridColumns: 1,
|
gridColumns: calculatedGridColumns,
|
||||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||||
...(column.widgetType === "code" &&
|
...(column.widgetType === "code" &&
|
||||||
column.codeCategory && {
|
column.codeCategory && {
|
||||||
|
|
@ -2122,6 +2227,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const componentId = getComponentIdFromWebType(column.widgetType);
|
const componentId = getComponentIdFromWebType(column.widgetType);
|
||||||
// console.log(`🔄 캔버스 드롭: ${column.widgetType} → ${componentId}`);
|
// 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 = {
|
newComponent = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
||||||
|
|
@ -2132,8 +2257,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
readonly: false,
|
readonly: false,
|
||||||
componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입
|
componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입
|
||||||
position: { x, y, z: 1 } as Position,
|
position: { x, y, z: 1 } as Position,
|
||||||
size: { width: defaultWidth, height: 40 },
|
size: { width: componentWidth, height: 40 },
|
||||||
gridColumns: 1,
|
gridColumns: calculatedGridColumns,
|
||||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||||
...(column.widgetType === "code" &&
|
...(column.widgetType === "code" &&
|
||||||
column.codeCategory && {
|
column.codeCategory && {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
||||||
showPageInfo: component.pagination?.showPageInfo ?? true,
|
showPageInfo: component.pagination?.showPageInfo ?? true,
|
||||||
showFirstLast: component.pagination?.showFirstLast ?? true,
|
showFirstLast: component.pagination?.showFirstLast ?? true,
|
||||||
gridColumns: component.gridColumns || 6,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 컬럼별 로컬 입력 상태
|
// 컬럼별 로컬 입력 상태
|
||||||
|
|
@ -110,45 +109,45 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
// 컴포넌트 변경 시 로컬 값 동기화
|
// 컴포넌트 변경 시 로컬 값 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", {
|
// console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", {
|
||||||
// componentId: component.id,
|
// componentId: component.id,
|
||||||
// title: component.title,
|
// title: component.title,
|
||||||
// searchButtonText: component.searchButtonText,
|
// searchButtonText: component.searchButtonText,
|
||||||
// columnsCount: component.columns.length,
|
// columnsCount: component.columns.length,
|
||||||
// filtersCount: component.filters.length,
|
// filtersCount: component.filters.length,
|
||||||
// columnIds: component.columns.map((col) => col.id),
|
// columnIds: component.columns.map((col) => col.id),
|
||||||
// filterColumnNames: component.filters.map((filter) => filter.columnName),
|
// filterColumnNames: component.filters.map((filter) => filter.columnName),
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 컬럼과 필터 상세 정보 로그
|
// 컬럼과 필터 상세 정보 로그
|
||||||
if (component.columns.length > 0) {
|
if (component.columns.length > 0) {
|
||||||
// console.log(
|
// console.log(
|
||||||
// "📋 현재 컬럼 목록:",
|
// "📋 현재 컬럼 목록:",
|
||||||
// component.columns.map((col) => ({
|
// component.columns.map((col) => ({
|
||||||
// id: col.id,
|
// id: col.id,
|
||||||
// columnName: col.columnName,
|
// columnName: col.columnName,
|
||||||
// label: col.label,
|
// label: col.label,
|
||||||
// visible: col.visible,
|
// visible: col.visible,
|
||||||
// gridColumns: col.gridColumns,
|
// gridColumns: col.gridColumns,
|
||||||
// })),
|
// })),
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로컬 상태 정보 로그
|
// 로컬 상태 정보 로그
|
||||||
// console.log("🔧 로컬 상태 정보:", {
|
// console.log("🔧 로컬 상태 정보:", {
|
||||||
// localColumnInputsCount: Object.keys(localColumnInputs).length,
|
// localColumnInputsCount: Object.keys(localColumnInputs).length,
|
||||||
// localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length,
|
// localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length,
|
||||||
// localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length,
|
// localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
if (component.filters.length > 0) {
|
if (component.filters.length > 0) {
|
||||||
// console.log(
|
// console.log(
|
||||||
// "🔍 현재 필터 목록:",
|
// "🔍 현재 필터 목록:",
|
||||||
// component.filters.map((filter) => ({
|
// component.filters.map((filter) => ({
|
||||||
// columnName: filter.columnName,
|
// columnName: filter.columnName,
|
||||||
// widgetType: filter.widgetType,
|
// widgetType: filter.widgetType,
|
||||||
// label: filter.label,
|
// label: filter.label,
|
||||||
// })),
|
// })),
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +178,6 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
||||||
showPageInfo: component.pagination?.showPageInfo ?? true,
|
showPageInfo: component.pagination?.showPageInfo ?? true,
|
||||||
showFirstLast: component.pagination?.showFirstLast ?? true,
|
showFirstLast: component.pagination?.showFirstLast ?? true,
|
||||||
gridColumns: component.gridColumns || 6,
|
|
||||||
// 테이블명 동기화
|
// 테이블명 동기화
|
||||||
tableName: component.tableName || "",
|
tableName: component.tableName || "",
|
||||||
});
|
});
|
||||||
|
|
@ -259,9 +257,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
if (!(filterKey in newFilterInputs)) {
|
if (!(filterKey in newFilterInputs)) {
|
||||||
newFilterInputs[filterKey] = filter.label || filter.columnName;
|
newFilterInputs[filterKey] = filter.label || filter.columnName;
|
||||||
// console.log("🆕 새 필터 로컬 상태 추가:", {
|
// console.log("🆕 새 필터 로컬 상태 추가:", {
|
||||||
// filterKey,
|
// filterKey,
|
||||||
// label: filter.label,
|
// label: filter.label,
|
||||||
// columnName: filter.columnName,
|
// columnName: filter.columnName,
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -278,9 +276,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log("📝 필터 로컬 상태 동기화 완료:", {
|
// console.log("📝 필터 로컬 상태 동기화 완료:", {
|
||||||
// prevCount: Object.keys(prev).length,
|
// prevCount: Object.keys(prev).length,
|
||||||
// newCount: Object.keys(newFilterInputs).length,
|
// newCount: Object.keys(newFilterInputs).length,
|
||||||
// newKeys: Object.keys(newFilterInputs),
|
// newKeys: Object.keys(newFilterInputs),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
return newFilterInputs;
|
return newFilterInputs;
|
||||||
|
|
@ -320,18 +318,18 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
|
||||||
// console.log("🔄 테이블 변경:", {
|
// console.log("🔄 테이블 변경:", {
|
||||||
// tableName,
|
// tableName,
|
||||||
// currentTableName: localValues.tableName,
|
// currentTableName: localValues.tableName,
|
||||||
// table,
|
// table,
|
||||||
// columnsCount: table.columns.length,
|
// columnsCount: table.columns.length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 테이블 변경 시 컬럼을 자동으로 추가하지 않음 (사용자가 수동으로 추가해야 함)
|
// 테이블 변경 시 컬럼을 자동으로 추가하지 않음 (사용자가 수동으로 추가해야 함)
|
||||||
const defaultColumns: DataTableColumn[] = [];
|
const defaultColumns: DataTableColumn[] = [];
|
||||||
|
|
||||||
// console.log("✅ 생성된 컬럼 설정:", {
|
// console.log("✅ 생성된 컬럼 설정:", {
|
||||||
// defaultColumnsCount: defaultColumns.length,
|
// defaultColumnsCount: defaultColumns.length,
|
||||||
// visibleColumns: defaultColumns.filter((col) => col.visible).length,
|
// visibleColumns: defaultColumns.filter((col) => col.visible).length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 상태 업데이트를 한 번에 처리
|
// 상태 업데이트를 한 번에 처리
|
||||||
|
|
@ -378,10 +376,10 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
try {
|
try {
|
||||||
// TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트
|
// TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트
|
||||||
// console.log("📡 테이블 타입 관리 업데이트 필요:", {
|
// console.log("📡 테이블 타입 관리 업데이트 필요:", {
|
||||||
// tableName: component.tableName,
|
// tableName: component.tableName,
|
||||||
// columnName: targetColumn.columnName,
|
// columnName: targetColumn.columnName,
|
||||||
// webType: "radio",
|
// webType: "radio",
|
||||||
// detailSettings: JSON.stringify(webTypeConfig),
|
// detailSettings: JSON.stringify(webTypeConfig),
|
||||||
// });
|
// });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("테이블 타입 관리 업데이트 실패:", error);
|
// console.error("테이블 타입 관리 업데이트 실패:", error);
|
||||||
|
|
@ -738,9 +736,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log("🗑️ 컬럼 삭제:", {
|
// console.log("🗑️ 컬럼 삭제:", {
|
||||||
// columnId,
|
// columnId,
|
||||||
// columnName: columnToRemove?.columnName,
|
// columnName: columnToRemove?.columnName,
|
||||||
// remainingColumns: updatedColumns.length,
|
// remainingColumns: updatedColumns.length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
onUpdateComponent({
|
onUpdateComponent({
|
||||||
|
|
@ -780,7 +778,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
columnName: targetColumn.columnName,
|
columnName: targetColumn.columnName,
|
||||||
widgetType,
|
widgetType,
|
||||||
label: targetColumn.columnLabel || targetColumn.columnName,
|
label: targetColumn.columnLabel || targetColumn.columnName,
|
||||||
gridColumns: 3,
|
gridColumns: getDefaultGridColumns(widgetType),
|
||||||
// 웹타입별 추가 정보 설정
|
// 웹타입별 추가 정보 설정
|
||||||
codeCategory: targetColumn.codeCategory,
|
codeCategory: targetColumn.codeCategory,
|
||||||
referenceTable: targetColumn.referenceTable,
|
referenceTable: targetColumn.referenceTable,
|
||||||
|
|
@ -789,28 +787,28 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("➕ 필터 추가 시작:", {
|
// console.log("➕ 필터 추가 시작:", {
|
||||||
// targetColumnName: targetColumn.columnName,
|
// targetColumnName: targetColumn.columnName,
|
||||||
// targetColumnLabel: targetColumn.columnLabel,
|
// targetColumnLabel: targetColumn.columnLabel,
|
||||||
// inferredWidgetType: widgetType,
|
// inferredWidgetType: widgetType,
|
||||||
// currentFiltersCount: component.filters.length,
|
// currentFiltersCount: component.filters.length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log("➕ 생성된 새 필터:", {
|
// console.log("➕ 생성된 새 필터:", {
|
||||||
// columnName: newFilter.columnName,
|
// columnName: newFilter.columnName,
|
||||||
// widgetType: newFilter.widgetType,
|
// widgetType: newFilter.widgetType,
|
||||||
// label: newFilter.label,
|
// label: newFilter.label,
|
||||||
// gridColumns: newFilter.gridColumns,
|
// gridColumns: newFilter.gridColumns,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const updatedFilters = [...component.filters, newFilter];
|
const updatedFilters = [...component.filters, newFilter];
|
||||||
// console.log("🔄 필터 업데이트 호출:", {
|
// console.log("🔄 필터 업데이트 호출:", {
|
||||||
// filtersToAdd: 1,
|
// filtersToAdd: 1,
|
||||||
// totalFiltersAfter: updatedFilters.length,
|
// totalFiltersAfter: updatedFilters.length,
|
||||||
// updatedFilters: updatedFilters.map((filter) => ({
|
// updatedFilters: updatedFilters.map((filter) => ({
|
||||||
// columnName: filter.columnName,
|
// columnName: filter.columnName,
|
||||||
// widgetType: filter.widgetType,
|
// widgetType: filter.widgetType,
|
||||||
// label: filter.label,
|
// label: filter.label,
|
||||||
// })),
|
// })),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 먼저 로컬 상태를 업데이트하고
|
// 먼저 로컬 상태를 업데이트하고
|
||||||
|
|
@ -821,10 +819,10 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
[filterKey]: newFilter.label,
|
[filterKey]: newFilter.label,
|
||||||
};
|
};
|
||||||
// console.log("📝 필터 로컬 상태 업데이트:", {
|
// console.log("📝 필터 로컬 상태 업데이트:", {
|
||||||
// filterKey,
|
// filterKey,
|
||||||
// newLabel: newFilter.label,
|
// newLabel: newFilter.label,
|
||||||
// prevState: prev,
|
// prevState: prev,
|
||||||
// newState,
|
// newState,
|
||||||
// });
|
// });
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
|
|
@ -836,8 +834,8 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
setActiveTab("filters");
|
setActiveTab("filters");
|
||||||
|
|
||||||
// console.log("🔍 필터 추가 후 탭 이동:", {
|
// console.log("🔍 필터 추가 후 탭 이동:", {
|
||||||
// activeTab: "filters",
|
// activeTab: "filters",
|
||||||
// isExternalControl: !!onTabChange,
|
// isExternalControl: !!onTabChange,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 강제로 리렌더링을 트리거하기 위해 여러 방법 사용
|
// 강제로 리렌더링을 트리거하기 위해 여러 방법 사용
|
||||||
|
|
@ -859,9 +857,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", {
|
// console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", {
|
||||||
// filterKey,
|
// filterKey,
|
||||||
// newFilterLabel: newFilter.label,
|
// newFilterLabel: newFilter.label,
|
||||||
// switchedToTab: "filters",
|
// switchedToTab: "filters",
|
||||||
// });
|
// });
|
||||||
}, [selectedTable, component.filters, onUpdateComponent]);
|
}, [selectedTable, component.filters, onUpdateComponent]);
|
||||||
|
|
||||||
|
|
@ -890,6 +888,48 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
return !nonFilterableTypes.includes(webType);
|
return !nonFilterableTypes.includes(webType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 웹타입별 기본 컬럼 수 계산 (컴포넌트 너비 기반)
|
||||||
|
const getDefaultGridColumns = (webType: WebType): number => {
|
||||||
|
// 각 웹타입별 적절한 기본 너비 설정
|
||||||
|
const widthMap: Record<WebType, number> = {
|
||||||
|
// 텍스트 입력 계열 (넓게)
|
||||||
|
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(
|
const addColumn = useCallback(
|
||||||
(columnName?: string) => {
|
(columnName?: string) => {
|
||||||
|
|
@ -907,38 +947,47 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
: availableColumns[0];
|
: availableColumns[0];
|
||||||
|
|
||||||
const widgetType = getWidgetTypeFromColumn(targetColumn);
|
const widgetType = getWidgetTypeFromColumn(targetColumn);
|
||||||
|
const calculatedGridColumns = getDefaultGridColumns(widgetType);
|
||||||
|
|
||||||
|
console.log("➕ addColumn 호출:", {
|
||||||
|
columnName: targetColumn.columnName,
|
||||||
|
widgetType,
|
||||||
|
calculatedGridColumns,
|
||||||
|
});
|
||||||
|
|
||||||
const newColumn: DataTableColumn = {
|
const newColumn: DataTableColumn = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
columnName: targetColumn.columnName,
|
columnName: targetColumn.columnName,
|
||||||
label: targetColumn.columnLabel || targetColumn.columnName,
|
label: targetColumn.columnLabel || targetColumn.columnName,
|
||||||
widgetType,
|
widgetType,
|
||||||
gridColumns: 2,
|
gridColumns: calculatedGridColumns,
|
||||||
visible: true,
|
visible: true,
|
||||||
filterable: isFilterableWebType(widgetType),
|
filterable: isFilterableWebType(widgetType),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
searchable: ["text", "email", "tel"].includes(widgetType),
|
searchable: ["text", "email", "tel"].includes(widgetType),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("✅ 생성된 newColumn:", newColumn);
|
||||||
|
|
||||||
// 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가)
|
// 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가)
|
||||||
|
|
||||||
// console.log("➕ 컬럼 추가 시작:", {
|
// console.log("➕ 컬럼 추가 시작:", {
|
||||||
// targetColumnName: targetColumn.columnName,
|
// targetColumnName: targetColumn.columnName,
|
||||||
// targetColumnLabel: targetColumn.columnLabel,
|
// targetColumnLabel: targetColumn.columnLabel,
|
||||||
// inferredWidgetType: widgetType,
|
// inferredWidgetType: widgetType,
|
||||||
// currentColumnsCount: component.columns.length,
|
// currentColumnsCount: component.columns.length,
|
||||||
// currentFiltersCount: component.filters.length,
|
// currentFiltersCount: component.filters.length,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log("➕ 생성된 새 컬럼:", {
|
// console.log("➕ 생성된 새 컬럼:", {
|
||||||
// id: newColumn.id,
|
// id: newColumn.id,
|
||||||
// columnName: newColumn.columnName,
|
// columnName: newColumn.columnName,
|
||||||
// label: newColumn.label,
|
// label: newColumn.label,
|
||||||
// widgetType: newColumn.widgetType,
|
// widgetType: newColumn.widgetType,
|
||||||
// filterable: newColumn.filterable,
|
// filterable: newColumn.filterable,
|
||||||
// visible: newColumn.visible,
|
// visible: newColumn.visible,
|
||||||
// sortable: newColumn.sortable,
|
// sortable: newColumn.sortable,
|
||||||
// searchable: newColumn.searchable,
|
// searchable: newColumn.searchable,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 필터는 수동으로만 추가
|
// 필터는 수동으로만 추가
|
||||||
|
|
@ -950,9 +999,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
[newColumn.id]: newColumn.label,
|
[newColumn.id]: newColumn.label,
|
||||||
};
|
};
|
||||||
// console.log("🔄 로컬 컬럼 상태 업데이트:", {
|
// console.log("🔄 로컬 컬럼 상태 업데이트:", {
|
||||||
// newColumnId: newColumn.id,
|
// newColumnId: newColumn.id,
|
||||||
// newLabel: newColumn.label,
|
// newLabel: newColumn.label,
|
||||||
// totalLocalInputs: Object.keys(newInputs).length,
|
// totalLocalInputs: Object.keys(newInputs).length,
|
||||||
// });
|
// });
|
||||||
return newInputs;
|
return newInputs;
|
||||||
});
|
});
|
||||||
|
|
@ -979,14 +1028,14 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("🔄 컴포넌트 업데이트 호출:", {
|
// console.log("🔄 컴포넌트 업데이트 호출:", {
|
||||||
// columnsToAdd: 1,
|
// columnsToAdd: 1,
|
||||||
// totalColumnsAfter: updates.columns?.length,
|
// totalColumnsAfter: updates.columns?.length,
|
||||||
// hasColumns: !!updates.columns,
|
// hasColumns: !!updates.columns,
|
||||||
// updateKeys: Object.keys(updates),
|
// updateKeys: Object.keys(updates),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log("🔄 업데이트 상세 내용:", {
|
// 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);
|
onUpdateComponent(updates);
|
||||||
|
|
@ -995,8 +1044,8 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
setActiveTab("columns");
|
setActiveTab("columns");
|
||||||
|
|
||||||
// console.log("📋 컬럼 추가 후 탭 이동:", {
|
// console.log("📋 컬럼 추가 후 탭 이동:", {
|
||||||
// activeTab: "columns",
|
// activeTab: "columns",
|
||||||
// isExternalControl: !!onTabChange,
|
// isExternalControl: !!onTabChange,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
|
// console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
|
||||||
|
|
@ -1014,7 +1063,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
columnName: newColumnName,
|
columnName: newColumnName,
|
||||||
label: `파일 컬럼 ${fileColumnCount + 1}`,
|
label: `파일 컬럼 ${fileColumnCount + 1}`,
|
||||||
widgetType: "file",
|
widgetType: "file",
|
||||||
gridColumns: 2,
|
gridColumns: getDefaultGridColumns("file"),
|
||||||
visible: true,
|
visible: true,
|
||||||
filterable: false, // 파일 컬럼은 필터링 불가
|
filterable: false, // 파일 컬럼은 필터링 불가
|
||||||
sortable: false, // 파일 컬럼은 정렬 불가
|
sortable: false, // 파일 컬럼은 정렬 불가
|
||||||
|
|
@ -1029,9 +1078,9 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("📁 가상 파일 컬럼 추가:", {
|
// console.log("📁 가상 파일 컬럼 추가:", {
|
||||||
// columnName: newColumn.columnName,
|
// columnName: newColumn.columnName,
|
||||||
// label: newColumn.label,
|
// label: newColumn.label,
|
||||||
// isVirtualFileColumn: newColumn.isVirtualFileColumn,
|
// isVirtualFileColumn: newColumn.isVirtualFileColumn,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 로컬 상태에 새 컬럼 입력값 추가
|
// 로컬 상태에 새 컬럼 입력값 추가
|
||||||
|
|
@ -1092,7 +1141,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="table-select">연결 테이블</Label>
|
<Label htmlFor="table-select">연결 테이블</Label>
|
||||||
<select
|
<select
|
||||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
className="focus:border-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||||
value={localValues.tableName}
|
value={localValues.tableName}
|
||||||
onChange={(e) => handleTableChange(e.target.value)}
|
onChange={(e) => handleTableChange(e.target.value)}
|
||||||
>
|
>
|
||||||
|
|
@ -1434,27 +1483,6 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="grid-columns">그리드 컬럼 수</Label>
|
|
||||||
<select
|
|
||||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
||||||
value={localValues.gridColumns.toString()}
|
|
||||||
onChange={(e) => {
|
|
||||||
const gridColumns = parseInt(e.target.value, 10);
|
|
||||||
// console.log("🔄 테이블 그리드 컬럼 수 변경:", gridColumns);
|
|
||||||
setLocalValues((prev) => ({ ...prev, gridColumns }));
|
|
||||||
onUpdateComponent({ gridColumns });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">그리드 컬럼 수 선택</option>
|
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((num) => (
|
|
||||||
<option key={num} value={num.toString()}>
|
|
||||||
{num}컬럼 ({Math.round((num / 12) * 100)}%)
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -1632,12 +1660,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">그리드 컬럼</Label>
|
<Label className="text-xs">컬럼 너비</Label>
|
||||||
<Select
|
<Select
|
||||||
value={(localColumnGridColumns[column.id] ?? column.gridColumns).toString()}
|
value={(localColumnGridColumns[column.id] ?? column.gridColumns).toString()}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const newGridColumns = parseInt(value);
|
const newGridColumns = parseInt(value);
|
||||||
// console.log("🔄 컬럼 그리드 컬럼 변경:", { columnId: column.id, newGridColumns });
|
// console.log("🔄 컬럼 너비 변경:", { columnId: column.id, newGridColumns });
|
||||||
setLocalColumnGridColumns((prev) => ({
|
setLocalColumnGridColumns((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[column.id]: newGridColumns,
|
[column.id]: newGridColumns,
|
||||||
|
|
@ -1649,11 +1677,18 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{[1, 2, 3, 4, 6, 12].map((num) => (
|
<SelectItem value="1">1/12 (8.33%)</SelectItem>
|
||||||
<SelectItem key={num} value={num.toString()}>
|
<SelectItem value="2">2/12 (16.67%)</SelectItem>
|
||||||
{num}칸
|
<SelectItem value="3">3/12 (25%)</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="4">4/12 (33%)</SelectItem>
|
||||||
))}
|
<SelectItem value="5">5/12 (41.67%)</SelectItem>
|
||||||
|
<SelectItem value="6">6/12 (50%)</SelectItem>
|
||||||
|
<SelectItem value="7">7/12 (58.33%)</SelectItem>
|
||||||
|
<SelectItem value="8">8/12 (66.67%)</SelectItem>
|
||||||
|
<SelectItem value="9">9/12 (75%)</SelectItem>
|
||||||
|
<SelectItem value="10">10/12 (83.33%)</SelectItem>
|
||||||
|
<SelectItem value="11">11/12 (91.67%)</SelectItem>
|
||||||
|
<SelectItem value="12">12/12 (100%)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2023,11 +2058,11 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
const localValue = localFilterInputs[filterKey];
|
const localValue = localFilterInputs[filterKey];
|
||||||
const finalValue = localValue !== undefined ? localValue : filter.label;
|
const finalValue = localValue !== undefined ? localValue : filter.label;
|
||||||
// console.log("🎯 필터 입력 값 결정:", {
|
// console.log("🎯 필터 입력 값 결정:", {
|
||||||
// filterKey,
|
// filterKey,
|
||||||
// localValue,
|
// localValue,
|
||||||
// filterLabel: filter.label,
|
// filterLabel: filter.label,
|
||||||
// finalValue,
|
// finalValue,
|
||||||
// allLocalInputs: Object.keys(localFilterInputs),
|
// allLocalInputs: Object.keys(localFilterInputs),
|
||||||
// });
|
// });
|
||||||
return finalValue;
|
return finalValue;
|
||||||
})()}
|
})()}
|
||||||
|
|
@ -2104,7 +2139,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">그리드 컬럼</Label>
|
<Label className="text-xs">필터 너비</Label>
|
||||||
<Select
|
<Select
|
||||||
value={filter.gridColumns.toString()}
|
value={filter.gridColumns.toString()}
|
||||||
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
|
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
|
||||||
|
|
@ -2113,11 +2148,18 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{[1, 2, 3, 4, 6, 12].map((num) => (
|
<SelectItem value="1">1/12 (8.33%)</SelectItem>
|
||||||
<SelectItem key={num} value={num.toString()}>
|
<SelectItem value="2">2/12 (16.67%)</SelectItem>
|
||||||
{num}칸
|
<SelectItem value="3">3/12 (25%)</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="4">4/12 (33%)</SelectItem>
|
||||||
))}
|
<SelectItem value="5">5/12 (41.67%)</SelectItem>
|
||||||
|
<SelectItem value="6">6/12 (50%)</SelectItem>
|
||||||
|
<SelectItem value="7">7/12 (58.33%)</SelectItem>
|
||||||
|
<SelectItem value="8">8/12 (66.67%)</SelectItem>
|
||||||
|
<SelectItem value="9">9/12 (75%)</SelectItem>
|
||||||
|
<SelectItem value="10">10/12 (83.33%)</SelectItem>
|
||||||
|
<SelectItem value="11">11/12 (91.67%)</SelectItem>
|
||||||
|
<SelectItem value="12">12/12 (100%)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2180,7 +2222,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>페이지당 행 수</Label>
|
<Label>페이지당 행 수</Label>
|
||||||
<select
|
<select
|
||||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
className="focus:border-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||||
value={component.pagination.pageSize.toString()}
|
value={component.pagination.pageSize.toString()}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdateComponent({
|
onUpdateComponent({
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from "@/types/screen";
|
} from "@/types/screen";
|
||||||
// 레거시 ButtonConfigPanel 제거됨
|
// 레거시 ButtonConfigPanel 제거됨
|
||||||
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
||||||
|
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
|
||||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||||
import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping";
|
import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping";
|
||||||
|
|
||||||
|
|
@ -752,23 +753,23 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
// console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
|
// console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
|
||||||
return <ConfigPanelComponent config={currentConfig} onConfigChange={handleConfigChange} />;
|
return <ConfigPanelComponent config={currentConfig} onConfigChange={handleConfigChange} />;
|
||||||
} else {
|
} else {
|
||||||
// console.log(`🎨 ❌ ConfigPanelComponent가 null - 기본 설정 표시`);
|
// console.log(`🎨 ❌ ConfigPanelComponent가 null - WebTypeConfigPanel 사용`);
|
||||||
return (
|
return (
|
||||||
<div className="py-8 text-center text-gray-500">
|
<WebTypeConfigPanel
|
||||||
⚙️ 기본 설정
|
webType={widget.widgetType as any}
|
||||||
<br />
|
config={currentConfig}
|
||||||
웹타입 "{widget.widgetType}"은 추가 설정이 없습니다.
|
onUpdateConfig={handleConfigChange}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log(`🎨 config_panel이 없음 - 기본 설정 표시`);
|
// console.log(`🎨 config_panel이 없음 - WebTypeConfigPanel 사용`);
|
||||||
return (
|
return (
|
||||||
<div className="py-8 text-center text-gray-500">
|
<WebTypeConfigPanel
|
||||||
⚙️ 기본 설정
|
webType={widget.widgetType as any}
|
||||||
<br />
|
config={currentConfig}
|
||||||
웹타입 "{widget.widgetType}"은 추가 설정이 없습니다.
|
onUpdateConfig={handleConfigChange}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1082,33 +1083,53 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
|
|
||||||
{/* 컴포넌트 설정 패널 */}
|
{/* 컴포넌트 설정 패널 */}
|
||||||
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||||
<DynamicComponentConfigPanel
|
<div className="space-y-6">
|
||||||
componentId={componentId}
|
{/* DynamicComponentConfigPanel */}
|
||||||
config={(() => {
|
<DynamicComponentConfigPanel
|
||||||
const config = selectedComponent.componentConfig || {};
|
componentId={componentId}
|
||||||
// console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
|
config={(() => {
|
||||||
// console.log("🔍 selectedComponent 전체:", selectedComponent);
|
const config = selectedComponent.componentConfig || {};
|
||||||
return config;
|
// console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
|
||||||
})()}
|
// console.log("🔍 selectedComponent 전체:", selectedComponent);
|
||||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
return config;
|
||||||
tableColumns={(() => {
|
})()}
|
||||||
// console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||||
// currentTable,
|
tableColumns={(() => {
|
||||||
// columns: currentTable?.columns,
|
// console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
|
||||||
// columnsLength: currentTable?.columns?.length,
|
// currentTable,
|
||||||
// sampleColumn: currentTable?.columns?.[0],
|
// columns: currentTable?.columns,
|
||||||
// deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
|
// columnsLength: currentTable?.columns?.length,
|
||||||
// });
|
// sampleColumn: currentTable?.columns?.[0],
|
||||||
return currentTable?.columns || [];
|
// deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
|
||||||
})()}
|
// });
|
||||||
onChange={(newConfig) => {
|
return currentTable?.columns || [];
|
||||||
// console.log("🔧 컴포넌트 설정 변경:", newConfig);
|
})()}
|
||||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
onChange={(newConfig) => {
|
||||||
Object.entries(newConfig).forEach(([key, value]) => {
|
// console.log("🔧 컴포넌트 설정 변경:", newConfig);
|
||||||
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
});
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
}}
|
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
||||||
/>
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 웹타입별 특화 설정 */}
|
||||||
|
{webType && (
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<h4 className="mb-4 text-sm font-semibold text-gray-900">웹타입 설정</h4>
|
||||||
|
<WebTypeConfigPanel
|
||||||
|
webType={webType as any}
|
||||||
|
config={selectedComponent.componentConfig || {}}
|
||||||
|
onUpdateConfig={(newConfig) => {
|
||||||
|
// 기존 설정과 병합하여 업데이트
|
||||||
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
|
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -203,14 +203,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
positionZ: selectedComponent?.position.z?.toString() || "1",
|
positionZ: selectedComponent?.position.z?.toString() || "1",
|
||||||
width: selectedComponent?.size?.width?.toString() || "0",
|
width: selectedComponent?.size?.width?.toString() || "0",
|
||||||
height: selectedComponent?.size?.height?.toString() || "0",
|
height: selectedComponent?.size?.height?.toString() || "0",
|
||||||
gridColumns:
|
|
||||||
selectedComponent?.gridColumns?.toString() ||
|
|
||||||
(selectedComponent?.type === "layout" && (selectedComponent as any)?.layoutType === "card-layout"
|
|
||||||
? "8"
|
|
||||||
: selectedComponent?.type === "component" &&
|
|
||||||
(selectedComponent as any)?.componentConfig?.type === "card-display"
|
|
||||||
? "8"
|
|
||||||
: "1"),
|
|
||||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||||
labelColor: selectedComponent?.style?.labelColor || "#212121",
|
labelColor: selectedComponent?.style?.labelColor || "#212121",
|
||||||
|
|
@ -224,7 +216,32 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// 너비 드롭다운 로컬 상태 - 실시간 업데이트를 위한 별도 관리
|
// 너비 드롭다운 로컬 상태 - 실시간 업데이트를 위한 별도 관리
|
||||||
const calculateWidthSpan = (width: string | number | undefined): string => {
|
const calculateWidthSpan = (width: string | number | undefined, gridColumns?: number): string => {
|
||||||
|
// gridColumns 값이 있으면 우선 사용
|
||||||
|
if (gridColumns) {
|
||||||
|
const columnsToSpan: Record<number, string> = {
|
||||||
|
1: "twelfth", // 1/12
|
||||||
|
2: "small", // 2/12
|
||||||
|
3: "quarter", // 3/12
|
||||||
|
4: "third", // 4/12
|
||||||
|
5: "five-twelfths", // 5/12
|
||||||
|
6: "half", // 6/12
|
||||||
|
7: "seven-twelfths", // 7/12
|
||||||
|
8: "twoThirds", // 8/12
|
||||||
|
9: "threeQuarters", // 9/12
|
||||||
|
10: "five-sixths", // 10/12
|
||||||
|
11: "eleven-twelfths", // 11/12
|
||||||
|
12: "full", // 12/12
|
||||||
|
};
|
||||||
|
|
||||||
|
const span = columnsToSpan[gridColumns];
|
||||||
|
if (span) {
|
||||||
|
console.log("🎯 calculateWidthSpan - gridColumns 사용:", { gridColumns, span });
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gridColumns가 없으면 style.width에서 계산
|
||||||
if (!width) return "half";
|
if (!width) return "half";
|
||||||
|
|
||||||
if (typeof width === "string" && width.includes("%")) {
|
if (typeof width === "string" && width.includes("%")) {
|
||||||
|
|
@ -260,6 +277,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("🎯 calculateWidthSpan - width% 사용:", { width, percent, closestSpan });
|
||||||
return closestSpan;
|
return closestSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,14 +285,19 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const [localWidthSpan, setLocalWidthSpan] = useState<string>(() =>
|
const [localWidthSpan, setLocalWidthSpan] = useState<string>(() =>
|
||||||
calculateWidthSpan(selectedComponent?.style?.width),
|
calculateWidthSpan(selectedComponent?.style?.width, (selectedComponent as any)?.gridColumns),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컴포넌트 또는 style.width가 변경될 때 로컬 상태 업데이트
|
// 컴포넌트 또는 style.width, gridColumns가 변경될 때 로컬 상태 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newSpan = calculateWidthSpan(selectedComponent?.style?.width);
|
const newSpan = calculateWidthSpan(selectedComponent?.style?.width, (selectedComponent as any)?.gridColumns);
|
||||||
setLocalWidthSpan(newSpan);
|
setLocalWidthSpan(newSpan);
|
||||||
}, [selectedComponent?.id, selectedComponent?.style?.width]);
|
console.log("🔄 localWidthSpan 업데이트:", {
|
||||||
|
gridColumns: (selectedComponent as any)?.gridColumns,
|
||||||
|
width: selectedComponent?.style?.width,
|
||||||
|
newSpan,
|
||||||
|
});
|
||||||
|
}, [selectedComponent?.id, selectedComponent?.style?.width, (selectedComponent as any)?.gridColumns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedComponentRef.current = selectedComponent;
|
selectedComponentRef.current = selectedComponent;
|
||||||
|
|
@ -768,6 +791,32 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 컴포넌트 속성 업데이트
|
// 컴포넌트 속성 업데이트
|
||||||
onUpdateProperty("style.width", newWidth);
|
onUpdateProperty("style.width", newWidth);
|
||||||
|
|
||||||
|
// gridColumns도 자동 계산하여 업데이트 (1/12 = 1컬럼, 2/12 = 2컬럼, ...)
|
||||||
|
const columnsMap: Record<string, number> = {
|
||||||
|
twelfth: 1,
|
||||||
|
small: 2,
|
||||||
|
quarter: 3,
|
||||||
|
third: 4,
|
||||||
|
"five-twelfths": 5,
|
||||||
|
half: 6,
|
||||||
|
"seven-twelfths": 7,
|
||||||
|
twoThirds: 8,
|
||||||
|
threeQuarters: 9,
|
||||||
|
"five-sixths": 10,
|
||||||
|
"eleven-twelfths": 11,
|
||||||
|
full: 12,
|
||||||
|
// 레거시 호환
|
||||||
|
sixth: 2,
|
||||||
|
label: 3,
|
||||||
|
medium: 4,
|
||||||
|
large: 8,
|
||||||
|
input: 9,
|
||||||
|
"two-thirds": 8,
|
||||||
|
"three-quarters": 9,
|
||||||
|
};
|
||||||
|
const gridColumns = columnsMap[value] || 6;
|
||||||
|
onUpdateProperty("gridColumns", gridColumns);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1">
|
||||||
|
|
@ -912,32 +961,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="gridColumns" className="text-sm font-medium">
|
|
||||||
그리드 컬럼 수 (1-12)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="gridColumns"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="12"
|
|
||||||
value={localInputs.gridColumns}
|
|
||||||
onChange={(e) => {
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
|
||||||
이 컴포넌트가 차지할 그리드 컬럼 수를 설정합니다 (기본: 1)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<WebTypeConfigPanelProps> = ({ 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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-medium">옵션 관리</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = [
|
||||||
|
...options,
|
||||||
|
{ label: `옵션 ${options.length + 1}`, value: `option${options.length + 1}` },
|
||||||
|
];
|
||||||
|
onUpdateConfig({ ...config, options: newOptions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
옵션 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 옵션 리스트 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{options.map((option: any, index: number) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="라벨"
|
||||||
|
value={option.label || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newOptions = [...options];
|
||||||
|
newOptions[index] = { ...option, label: e.target.value };
|
||||||
|
onUpdateConfig({ ...config, options: newOptions });
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="값"
|
||||||
|
value={option.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newOptions = [...options];
|
||||||
|
newOptions[index] = { ...option, value: e.target.value };
|
||||||
|
onUpdateConfig({ ...config, options: newOptions });
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = options.filter((_: any, i: number) => i !== index);
|
||||||
|
onUpdateConfig({ ...config, options: newOptions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select 전용 설정 */}
|
||||||
|
{baseType === "select" && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="multiple" className="text-sm">
|
||||||
|
다중 선택
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="multiple"
|
||||||
|
checked={multiple}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateConfig({ ...config, multiple: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="searchable" className="text-sm">
|
||||||
|
검색 가능
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="searchable"
|
||||||
|
checked={searchable}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateConfig({ ...config, searchable: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 숫자 입력 (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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-sm font-medium">숫자 범위 설정</Label>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="min" className="text-xs text-gray-500">
|
||||||
|
최소값
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="min"
|
||||||
|
type="number"
|
||||||
|
placeholder="최소값"
|
||||||
|
value={min ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
min: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="max" className="text-xs text-gray-500">
|
||||||
|
최대값
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="max"
|
||||||
|
type="number"
|
||||||
|
placeholder="최대값"
|
||||||
|
value={max ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
max: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="step" className="text-xs text-gray-500">
|
||||||
|
증감 단위
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="step"
|
||||||
|
type="number"
|
||||||
|
step={webType === "decimal" ? 0.01 : 1}
|
||||||
|
placeholder="증감 단위"
|
||||||
|
value={step}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
step: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜/시간 입력 - 형식 설정
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-sm font-medium">날짜/시간 형식 설정</Label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="format" className="text-xs text-gray-500">
|
||||||
|
표시 형식
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
onUpdateConfig({ ...config, format: value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="format">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{baseType === "date" && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="YYYY-MM-DD">YYYY-MM-DD (2024-01-15)</SelectItem>
|
||||||
|
<SelectItem value="YYYY/MM/DD">YYYY/MM/DD (2024/01/15)</SelectItem>
|
||||||
|
<SelectItem value="YYYY.MM.DD">YYYY.MM.DD (2024.01.15)</SelectItem>
|
||||||
|
<SelectItem value="DD-MM-YYYY">DD-MM-YYYY (15-01-2024)</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{baseType === "datetime" && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="YYYY-MM-DD HH:mm">YYYY-MM-DD HH:mm</SelectItem>
|
||||||
|
<SelectItem value="YYYY-MM-DD HH:mm:ss">YYYY-MM-DD HH:mm:ss</SelectItem>
|
||||||
|
<SelectItem value="YYYY/MM/DD HH:mm">YYYY/MM/DD HH:mm</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{baseType === "time" && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="HH:mm">HH:mm (24시간)</SelectItem>
|
||||||
|
<SelectItem value="HH:mm:ss">HH:mm:ss (24시간)</SelectItem>
|
||||||
|
<SelectItem value="hh:mm A">hh:mm A (12시간)</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{baseType === "date" && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="showTime" className="text-sm">
|
||||||
|
시간 입력 포함
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="showTime"
|
||||||
|
checked={showTime}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateConfig({ ...config, showTime: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 텍스트 입력 - 검증 규칙
|
||||||
|
if (
|
||||||
|
baseType === "text" ||
|
||||||
|
baseType === "email" ||
|
||||||
|
baseType === "tel" ||
|
||||||
|
baseType === "url" ||
|
||||||
|
baseType === "textarea"
|
||||||
|
) {
|
||||||
|
const minLength = config?.minLength;
|
||||||
|
const maxLength = config?.maxLength;
|
||||||
|
const pattern = config?.pattern;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-sm font-medium">텍스트 검증 설정</Label>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="minLength" className="text-xs text-gray-500">
|
||||||
|
최소 길이
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="minLength"
|
||||||
|
type="number"
|
||||||
|
placeholder="최소 길이"
|
||||||
|
value={minLength ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
minLength: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="maxLength" className="text-xs text-gray-500">
|
||||||
|
최대 길이
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="maxLength"
|
||||||
|
type="number"
|
||||||
|
placeholder="최대 길이"
|
||||||
|
value={maxLength ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
maxLength: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(webType === "text" || webType === "textarea") && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="pattern" className="text-xs text-gray-500">
|
||||||
|
정규식 패턴 (선택)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="pattern"
|
||||||
|
placeholder="예: ^[A-Za-z]+$"
|
||||||
|
value={pattern ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
pattern: e.target.value || undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-400">입력값 검증에 사용할 정규식 패턴</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 업로드 - 파일 타입 제한
|
||||||
|
if (baseType === "file" || baseType === "image") {
|
||||||
|
const accept = config?.accept || (baseType === "image" ? "image/*" : "*/*");
|
||||||
|
const maxSize = config?.maxSize || 10;
|
||||||
|
const multiple = config?.multiple || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-sm font-medium">파일 업로드 설정</Label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="accept" className="text-xs text-gray-500">
|
||||||
|
허용 파일 타입
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="accept"
|
||||||
|
placeholder="예: .jpg,.png,.pdf"
|
||||||
|
value={accept}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({ ...config, accept: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-400">쉼표로 구분된 파일 확장자 또는 MIME 타입</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="maxSize" className="text-xs text-gray-500">
|
||||||
|
최대 파일 크기 (MB)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="maxSize"
|
||||||
|
type="number"
|
||||||
|
placeholder="최대 크기"
|
||||||
|
value={maxSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
onUpdateConfig({
|
||||||
|
...config,
|
||||||
|
maxSize: e.target.value ? Number(e.target.value) : 10,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="fileMultiple" className="text-sm">
|
||||||
|
다중 파일 선택
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="fileMultiple"
|
||||||
|
checked={multiple}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateConfig({ ...config, multiple: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 메시지 (설정 불필요)
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 text-sm text-gray-500">
|
||||||
|
<p>이 웹타입은 추가 설정이 필요하지 않습니다.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -50,7 +50,7 @@ export const AccordionBasicDefinition = createComponentDefinition({
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
defaultValue: "item-1",
|
defaultValue: "item-1",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 300, height: 200 },
|
defaultSize: { width: 400, height: 200 },
|
||||||
configPanel: AccordionBasicConfigPanel,
|
configPanel: AccordionBasicConfigPanel,
|
||||||
icon: "ChevronDown",
|
icon: "ChevronDown",
|
||||||
tags: ["아코디언", "접기", "펼치기", "콘텐츠", "섹션"],
|
tags: ["아코디언", "접기", "펼치기", "콘텐츠", "섹션"],
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({
|
||||||
errorMessage: "저장 중 오류가 발생했습니다.",
|
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultSize: { width: 120, height: 36 },
|
defaultSize: { width: 120, height: 40 },
|
||||||
configPanel: ButtonPrimaryConfigPanel,
|
configPanel: ButtonPrimaryConfigPanel,
|
||||||
icon: "MousePointer",
|
icon: "MousePointer",
|
||||||
tags: ["버튼", "액션", "클릭"],
|
tags: ["버튼", "액션", "클릭"],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const CheckboxBasicDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 120, height: 24 },
|
defaultSize: { width: 150, height: 32 },
|
||||||
configPanel: CheckboxBasicConfigPanel,
|
configPanel: CheckboxBasicConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export const INPUT_CLASSES = {
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||||
placeholder:text-gray-400
|
placeholder:text-gray-400
|
||||||
|
max-w-full overflow-hidden
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 선택된 상태
|
// 선택된 상태
|
||||||
|
|
@ -31,7 +32,7 @@ export const INPUT_CLASSES = {
|
||||||
|
|
||||||
// 컨테이너
|
// 컨테이너
|
||||||
container: `
|
container: `
|
||||||
relative w-full
|
relative w-full max-w-full overflow-hidden
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// textarea
|
// textarea
|
||||||
|
|
@ -43,6 +44,7 @@ export const INPUT_CLASSES = {
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||||
resize-vertical
|
resize-vertical
|
||||||
|
max-w-full overflow-hidden
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// select
|
// select
|
||||||
|
|
@ -54,11 +56,12 @@ export const INPUT_CLASSES = {
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
|
max-w-full overflow-hidden
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// flex 컨테이너 (email, tel, url 등)
|
// flex 컨테이너 (email, tel, url 등)
|
||||||
flexContainer: `
|
flexContainer: `
|
||||||
flex items-center gap-2 w-full h-10
|
flex items-center gap-2 w-full h-10 max-w-full overflow-hidden
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 구분자 (@ , ~ 등)
|
// 구분자 (@ , ~ 등)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const DateInputDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 180, height: 36 },
|
defaultSize: { width: 220, height: 40 },
|
||||||
configPanel: DateInputConfigPanel,
|
configPanel: DateInputConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const DividerLineDefinition = createComponentDefinition({
|
||||||
placeholder: "텍스트를 입력하세요",
|
placeholder: "텍스트를 입력하세요",
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 400, height: 2 },
|
||||||
configPanel: DividerLineConfigPanel,
|
configPanel: DividerLineConfigPanel,
|
||||||
icon: "Layout",
|
icon: "Layout",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const FileUploadDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 250, height: 36 },
|
defaultSize: { width: 350, height: 40 },
|
||||||
configPanel: FileUploadConfigPanel,
|
configPanel: FileUploadConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const ImageDisplayDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 200, height: 200 },
|
||||||
configPanel: ImageDisplayConfigPanel,
|
configPanel: ImageDisplayConfigPanel,
|
||||||
icon: "Eye",
|
icon: "Eye",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const NumberInputDefinition = createComponentDefinition({
|
||||||
max: 999999,
|
max: 999999,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
defaultSize: { width: 150, height: 36 },
|
defaultSize: { width: 200, height: 40 },
|
||||||
configPanel: NumberInputConfigPanel,
|
configPanel: NumberInputConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const RadioBasicDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 120, height: 24 },
|
defaultSize: { width: 150, height: 32 },
|
||||||
configPanel: RadioBasicConfigPanel,
|
configPanel: RadioBasicConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const SelectBasicDefinition = createComponentDefinition({
|
||||||
options: [],
|
options: [],
|
||||||
placeholder: "선택하세요",
|
placeholder: "선택하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 250, height: 40 },
|
||||||
configPanel: SelectBasicConfigPanel,
|
configPanel: SelectBasicConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const SliderBasicDefinition = createComponentDefinition({
|
||||||
max: 999999,
|
max: 999999,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 250, height: 40 },
|
||||||
configPanel: SliderBasicConfigPanel,
|
configPanel: SliderBasicConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import { TextInputConfig } from "./types";
|
||||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||||
import { INPUT_CLASSES, cn, getInputClasses } from "../common/inputStyles";
|
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 {
|
export interface TextInputComponentProps extends ComponentRendererProps {
|
||||||
config?: TextInputConfig;
|
config?: TextInputConfig;
|
||||||
|
|
@ -234,7 +237,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// 이메일 입력 상태 (username@domain 분리)
|
// 이메일 입력 상태 (username@domain 분리)
|
||||||
const [emailUsername, setEmailUsername] = React.useState("");
|
const [emailUsername, setEmailUsername] = React.useState("");
|
||||||
const [emailDomain, setEmailDomain] = React.useState("gmail.com");
|
const [emailDomain, setEmailDomain] = React.useState("gmail.com");
|
||||||
const [isCustomDomain, setIsCustomDomain] = React.useState(false);
|
const [emailDomainOpen, setEmailDomainOpen] = React.useState(false);
|
||||||
|
|
||||||
// 전화번호 입력 상태 (3개 부분으로 분리)
|
// 전화번호 입력 상태 (3개 부분으로 분리)
|
||||||
const [telPart1, setTelPart1] = React.useState("");
|
const [telPart1, setTelPart1] = React.useState("");
|
||||||
|
|
@ -257,13 +260,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
if (currentValue && typeof currentValue === "string" && currentValue.includes("@")) {
|
if (currentValue && typeof currentValue === "string" && currentValue.includes("@")) {
|
||||||
const [username, domain] = currentValue.split("@");
|
const [username, domain] = currentValue.split("@");
|
||||||
setEmailUsername(username || "");
|
setEmailUsername(username || "");
|
||||||
if (domain && emailDomains.includes(domain)) {
|
setEmailDomain(domain || "gmail.com");
|
||||||
setEmailDomain(domain);
|
|
||||||
setIsCustomDomain(false);
|
|
||||||
} else {
|
|
||||||
setEmailDomain(domain || "");
|
|
||||||
setIsCustomDomain(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [webType, component.value, formData, component.columnName, isInteractive]);
|
}, [webType, component.value, formData, component.columnName, isInteractive]);
|
||||||
|
|
@ -341,58 +338,74 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
{/* @ 구분자 */}
|
{/* @ 구분자 */}
|
||||||
<span className="text-base font-medium text-gray-500">@</span>
|
<span className="text-base font-medium text-gray-500">@</span>
|
||||||
|
|
||||||
{/* 도메인 선택/입력 */}
|
{/* 도메인 선택/입력 (Combobox) */}
|
||||||
{isCustomDomain ? (
|
<Popover open={emailDomainOpen} onOpenChange={setEmailDomainOpen}>
|
||||||
<input
|
<PopoverTrigger asChild>
|
||||||
type="text"
|
<button
|
||||||
value={emailDomain}
|
type="button"
|
||||||
placeholder="도메인"
|
role="combobox"
|
||||||
disabled={componentConfig.disabled || false}
|
aria-expanded={emailDomainOpen}
|
||||||
readOnly={componentConfig.readonly || false}
|
disabled={componentConfig.disabled || false}
|
||||||
onChange={(e) => {
|
className={cn(
|
||||||
const newDomain = e.target.value;
|
"flex h-full flex-1 items-center justify-between rounded-md border px-3 py-2 text-sm transition-all duration-200",
|
||||||
setEmailDomain(newDomain);
|
isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300",
|
||||||
const fullEmail = `${emailUsername}@${newDomain}`;
|
componentConfig.disabled ? "cursor-not-allowed bg-gray-100 text-gray-400" : "bg-white text-gray-900",
|
||||||
|
"hover:border-orange-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-100 focus:outline-none",
|
||||||
|
emailDomainOpen && "border-orange-500 ring-2 ring-orange-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn("truncate", !emailDomain && "text-gray-400")}>{emailDomain || "도메인 선택"}</span>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="도메인 검색 또는 입력..."
|
||||||
|
value={emailDomain}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setEmailDomain(value);
|
||||||
|
const fullEmail = `${emailUsername}@${value}`;
|
||||||
|
|
||||||
if (isInteractive && formData && onFormDataChange && component.columnName) {
|
if (isInteractive && formData && onFormDataChange && component.columnName) {
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
...formData,
|
...formData,
|
||||||
[component.columnName]: fullEmail,
|
[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`}
|
/>
|
||||||
/>
|
<CommandList>
|
||||||
) : (
|
<CommandEmpty>직접 입력한 도메인: {emailDomain}</CommandEmpty>
|
||||||
<select
|
<CommandGroup>
|
||||||
value={emailDomain}
|
{emailDomains
|
||||||
disabled={componentConfig.disabled || false}
|
.filter((d) => d !== "직접입력")
|
||||||
onChange={(e) => {
|
.map((domain) => (
|
||||||
const newDomain = e.target.value;
|
<CommandItem
|
||||||
if (newDomain === "직접입력") {
|
key={domain}
|
||||||
setIsCustomDomain(true);
|
value={domain}
|
||||||
setEmailDomain("");
|
onSelect={(currentValue) => {
|
||||||
} else {
|
setEmailDomain(currentValue);
|
||||||
setEmailDomain(newDomain);
|
const fullEmail = `${emailUsername}@${currentValue}`;
|
||||||
const fullEmail = `${emailUsername}@${newDomain}`;
|
|
||||||
|
|
||||||
if (isInteractive && formData && onFormDataChange && component.columnName) {
|
if (isInteractive && formData && onFormDataChange && component.columnName) {
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
...formData,
|
...formData,
|
||||||
[component.columnName]: fullEmail,
|
[component.columnName]: fullEmail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
setEmailDomainOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`h-full flex-1 cursor-pointer 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`}
|
>
|
||||||
>
|
<Check className={cn("mr-2 h-4 w-4", emailDomain === domain ? "opacity-100" : "opacity-0")} />
|
||||||
{emailDomains.map((domain) => (
|
{domain}
|
||||||
<option key={domain} value={domain}>
|
</CommandItem>
|
||||||
{domain}
|
))}
|
||||||
</option>
|
</CommandGroup>
|
||||||
))}
|
</CommandList>
|
||||||
</select>
|
</Command>
|
||||||
)}
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -589,14 +602,14 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full max-w-full overflow-hidden ${className || ""}`} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -640,7 +653,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
||||||
className={`h-10 w-full rounded-md border px-3 py-2 text-sm shadow-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={`h-10 w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-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`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const TextInputDefinition = createComponentDefinition({
|
||||||
placeholder: "텍스트를 입력하세요",
|
placeholder: "텍스트를 입력하세요",
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 300, height: 40 },
|
||||||
configPanel: TextInputConfigPanel,
|
configPanel: TextInputConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: ["텍스트", "입력", "폼"],
|
tags: ["텍스트", "입력", "폼"],
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const TextareaBasicDefinition = createComponentDefinition({
|
||||||
rows: 3,
|
rows: 3,
|
||||||
maxLength: 1000,
|
maxLength: 1000,
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 80 },
|
defaultSize: { width: 400, height: 100 },
|
||||||
configPanel: TextareaBasicConfigPanel,
|
configPanel: TextareaBasicConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const ToggleSwitchDefinition = createComponentDefinition({
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: "입력하세요",
|
placeholder: "입력하세요",
|
||||||
},
|
},
|
||||||
defaultSize: { width: 200, height: 36 },
|
defaultSize: { width: 180, height: 40 },
|
||||||
configPanel: ToggleSwitchConfigPanel,
|
configPanel: ToggleSwitchConfigPanel,
|
||||||
icon: "Edit",
|
icon: "Edit",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue