Compare commits

...

19 Commits

Author SHA1 Message Date
kjs ff2a069b79 fix: 테이블 리스트 불필요한 스크롤 및 하단 공간 문제 해결
문제:
- 고정 높이 (h-[400px] sm:h-[500px])로 인해 데이터가 적어도 큰 공간 차지
- 하단에 빈 공간이 남는데도 스크롤이 생기는 비효율적인 UX
- overflow-y-scroll이 항상 스크롤바를 표시함

해결:
- 고정 높이 제거 → flex-1 (부모의 남은 공간 차지)
- overflow-y-scroll → overflow-y-auto (필요할 때만 스크롤)
- 데이터 양에 따라 자동으로 높이 조정

개선 사항:
 데이터가 적을 때: 불필요한 공간 없이 컴팩트하게 표시
 데이터가 많을 때: 자동으로 스크롤 생성
 반응형 레이아웃에 자연스럽게 적응
 스크롤바가 필요할 때만 표시되어 깔끔한 UI
2025-11-06 11:53:59 +09:00
kjs 310f43e1bd fix: 테이블 그룹 헤더 스크롤 시 배경 비침 현상 수정
문제:
- 그룹 헤더의 bg-muted/50 (반투명 배경)으로 인해 스크롤 시 뒤 내용이 비쳐 보임
- sticky 위치에서 가독성 저하

해결:
- bg-muted/50 → bg-muted (불투명 배경)
- hover 효과도 hover:bg-muted → hover:bg-muted/80으로 조정
- 스크롤 시 깔끔한 가림 효과 제공

개선 사항:
- sticky 그룹 헤더의 완전한 배경 덮기
- 스크롤 시 가독성 향상
- shadcn 가이드라인 준수 (단색 배경)
2025-11-06 11:52:43 +09:00
kjs 4f02f0bad1 refactor: TableList 컴포넌트 그라데이션 제거 (shadcn 가이드라인 준수)
- 테이블 헤더의 그라데이션 제거 (bg-gradient-to-b from-muted/50 to-muted → bg-muted)
- CardModeRenderer 빈 상태 아이콘의 그라데이션 제거
- 하드코딩된 slate 색상을 shadcn 토큰으로 변경 (bg-muted, text-muted-foreground)
- 일관된 단색 배경으로 심플하고 깔끔한 디자인 유지

shadcn/ui 가이드라인:
- 테이블 헤더는 단색 bg-muted 사용
- 색상 토큰 사용으로 다크모드 자동 대응
- 불필요한 그라데이션 제거
2025-11-06 11:51:11 +09:00
kjs 2b2c096a99 refactor: ButtonPrimaryComponent를 shadcn 가이드라인에 맞게 수정
- 그라데이션 배경 제거하고 단색 배경 적용
- 동적 색상 기반 그림자 제거하고 표준 shadcn 그림자 적용
- hover:opacity-90 효과 추가 (부드러운 어두워짐)
- active:scale-95 효과 추가 (클릭 피드백)
- transition-colors duration-150으로 빠른 색상 전환 적용
- disabled 상태를 단색 회색으로 개선

shadcn/ui 가이드라인 준수:
- 심플하고 깔끔한 단색 디자인
- 일관된 인터랙션 패턴
- 표준화된 그림자 및 전환 효과
2025-11-06 11:49:24 +09:00
kjs fe306aed26 feat: 카테고리 위젯에 드래그 가능한 리사이저 추가
- 좌우 영역을 드래그로 조절 가능
- 리사이저: GripVertical 아이콘으로 시각적 표시
- 좌측 영역: 최소 10%, 최대 40%로 제한
- 호버 시 배경색 변경으로 피드백 제공
- 드래그 중 커서 및 텍스트 선택 방지
2025-11-06 11:40:59 +09:00
kjs 4b568f86b1 style: 카테고리 위젯 좌측 영역 더 축소
- 좌측 영역: 20% → 15%
- 우측 영역: 80% → 85%
- 최소한의 공간으로 컬럼 목록 표시
2025-11-06 11:40:19 +09:00
kjs 107ca3b0b8 style: 카테고리 위젯 좌우 비율 조정
- 좌측 영역: 30% → 20%
- 우측 영역: 70% → 80%
- 좌측은 컬럼 목록만 표시하므로 좁게 조정
- 우측 값 관리 영역에 더 많은 공간 확보
2025-11-06 11:39:21 +09:00
kjs 7efb31a367 feat: 카테고리 컬럼 카드에 항목 개수 표시
- 컬럼명(column_name) 제거
- 우측에 해당 카테고리의 항목 개수 표시
- getCategoryValues API로 각 컬럼의 값 개수 조회
- 'N개' 형식으로 깔끔하게 표시
- 로딩 중에는 '...' 표시
2025-11-06 11:36:45 +09:00
kjs 9f9e9ecd82 style: 카테고리 컬럼 카드 상하 패딩 8px로 조정
- CategoryColumnList 카드: p-4 → px-4 py-2
- 상하 여백 16px → 8px
- 좌우 여백은 16px 유지
- 채번규칙과 일관된 레이아웃
2025-11-06 11:34:08 +09:00
kjs ec2f544a3e style: 채번규칙 규칙명과 미리보기를 한 줄로 배치
- 규칙명과 미리보기를 flex로 나란히 배치
- 각각 flex-1로 동일한 너비 (50:50)
- gap-3로 간격 설정
- 공간 효율성 향상
2025-11-06 11:26:38 +09:00
kjs e964c04523 style: 채번규칙 미리보기 UI 간소화
- '미리보기' 제목 및 Card 컴포넌트 제거
- '코드 미리보기' 라벨 제거
- 한 줄로 간결하게 표현 (px-3 py-2)
- 불필요한 여백 제거로 깔끔한 레이아웃
2025-11-06 11:25:59 +09:00
kjs fc18523bb6 feat: 채번규칙 적용 범위 UI 제거 및 기본값 '메뉴 적용'으로 변경
- 적용 범위 선택 섹션 제거 (UI 간소화)
- 새 규칙 생성 시 scopeType 기본값: 'global' → 'menu'
- 모든 규칙이 자동으로 메뉴별 적용으로 생성됨
2025-11-06 11:23:27 +09:00
kjs 8fa068222e style: 채번규칙 카드에서 코드 미리보기 제거
- NumberingRulePreview 컴포넌트 제거
- CardContent 섹션 제거
- 규칙 이름과 삭제 버튼만 표시하는 심플한 레이아웃
2025-11-06 11:22:22 +09:00
kjs 654cc4575b style: 채번규칙 카드 상하 패딩 8px로 조정
- py-0 → py-2 (8px)
- 적절한 여백 유지하면서 컴팩트한 레이아웃
2025-11-06 11:21:02 +09:00
kjs 1ee2d8f365 style: 채번규칙 카드 자체의 상하 패딩 제거
- Card 컴포넌트에 py-0 추가
- 카드 내부 여백 최소화
2025-11-06 11:20:13 +09:00
kjs f7f410dbbe style: 채번규칙 카드 내부 상하 여백 완전 제거
- CardHeader, CardContent의 py를 0으로 설정
- 좌우 여백(px-3)만 유지
- 최대한 컴팩트한 카드 레이아웃
2025-11-06 10:44:08 +09:00
kjs 7132f4a90f style: 채번규칙 카드 내부 여백 축소
- CardHeader: py-3 → py-2 (12px → 8px)
- CardContent: py-3 → pb-2 (하단만 8px)
- 더 컴팩트한 카드 레이아웃
2025-11-06 10:42:55 +09:00
kjs 38734079e8 style: 채번규칙 카드 UI 개선
- '규칙 N개' 텍스트 제거 (불필요한 정보)
- 카드 내부 상하 여백 명시적으로 12px(py-3)로 설정
2025-11-06 10:41:01 +09:00
kjs 44def0979c fix: 화면 편집기 높이 입력 필드 1px 단위 조절 가능하도록 수정
- 문제: 높이 입력 시 10 단위로만 입력 가능 (예: 1080 입력 불가)
- 원인: 격자 스냅 로직이 onChange마다 높이를 10/20 단위로 강제 반올림
- 해결:
  1. 모든 number input 필드에 step="1" 추가
  2. ScreenDesigner.tsx의 격자 스냅 로직 수정 (높이 스냅 제거)
  3. UnifiedPropertiesPanel.tsx에 로컬 상태 추가하여 입력 중 스냅 방지
  4. onBlur/Enter 시에만 실제 값 업데이트

수정 파일:
- frontend/components/screen/ScreenDesigner.tsx
- frontend/components/screen/panels/UnifiedPropertiesPanel.tsx
- frontend/components/screen/panels/PropertiesPanel.tsx
- frontend/components/screen/panels/ResolutionPanel.tsx
- frontend/components/screen/panels/RowSettingsPanel.tsx
- frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx
- frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx
2025-11-06 10:37:20 +09:00
20 changed files with 215 additions and 267 deletions

View File

@ -203,7 +203,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
separator: "-",
resetPeriod: "none",
currentSequence: 1,
scopeType: "global",
scopeType: "menu",
};
setSelectedRuleId(newRule.ruleId);
@ -251,16 +251,15 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
savedRules.map((rule) => (
<Card
key={rule.ruleId}
className={`border-border hover:bg-accent cursor-pointer transition-colors ${
className={`py-2 border-border hover:bg-accent cursor-pointer transition-colors ${
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
}`}
onClick={() => handleSelectRule(rule)}
>
<CardHeader className="p-3">
<CardHeader className="px-3 py-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
<p className="text-muted-foreground mt-1 text-xs"> {rule.parts.length}</p>
</div>
<Button
variant="ghost"
@ -275,9 +274,6 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
</Button>
</div>
</CardHeader>
<CardContent className="p-3 pt-0">
<NumberingRulePreview config={rule} compact />
</CardContent>
</Card>
))
)}
@ -316,46 +312,21 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
</Button>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium"></Label>
<Input
value={currentRule.ruleName}
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
className="h-9"
placeholder="예: 프로젝트 코드"
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium"> </Label>
<Select
value={currentRule.scopeType || "global"}
onValueChange={(value: "global" | "menu") => setCurrentRule((prev) => ({ ...prev!, scopeType: value }))}
disabled={isPreview}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="global"> </SelectItem>
<SelectItem value="menu"></SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
{currentRule.scopeType === "menu"
? "⚠️ 현재 화면이 속한 2레벨 메뉴와 그 하위 메뉴(3레벨 이상)에서만 사용됩니다. 형제 메뉴와 구분하여 채번 규칙을 관리할 때 유용합니다."
: "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}
</p>
</div>
<Card className="border-border bg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<div className="flex-1 space-y-2">
<Label className="text-sm font-medium"></Label>
<Input
value={currentRule.ruleName}
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
className="h-9"
placeholder="예: 프로젝트 코드"
/>
</div>
<div className="flex-1 space-y-2">
<Label className="text-sm font-medium"></Label>
<NumberingRulePreview config={currentRule} />
</CardContent>
</Card>
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div className="mb-3 flex items-center justify-between">

View File

@ -81,11 +81,8 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
}
return (
<div className="space-y-2">
<p className="text-xs text-muted-foreground sm:text-sm"> </p>
<div className="rounded-md bg-muted p-3 sm:p-4">
<code className="text-sm font-mono text-foreground sm:text-base">{generatedCode}</code>
</div>
<div className="rounded-md bg-muted px-3 py-2">
<code className="text-sm font-mono text-foreground">{generatedCode}</code>
</div>
);
};

View File

@ -181,7 +181,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
// 🆕 전역 테이블 새로고침 이벤트 리스너
useEffect(() => {
const handleRefreshTable = () => {
console.log("🔄 InteractiveDataTable: 전역 새로고침 이벤트 수신");
if (component.tableName) {
loadData(currentPage, searchValues);
}
@ -206,15 +205,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
return webType === "category";
});
console.log(`🔍 InteractiveDataTable 카테고리 컬럼 확인:`, {
tableName: component.tableName,
totalColumns: component.columns?.length,
categoryColumns: categoryColumns?.map(c => ({
name: c.columnName,
webType: getColumnWebType(c.columnName)
}))
});
if (!categoryColumns || categoryColumns.length === 0) return;
// 각 카테고리 컬럼의 값 목록 조회
@ -239,12 +229,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}
}
console.log(`✅ InteractiveDataTable 카테고리 매핑 완료:`, {
tableName: component.tableName,
mappedColumns: Object.keys(mappings),
mappings
});
setCategoryMappings(mappings);
} catch (error) {
console.error("카테고리 매핑 로드 실패:", error);
@ -403,7 +387,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
// 대체 URL 생성 (직접 파일 경로 사용)
if (previewImage.path) {
const altUrl = getDirectFileUrl(previewImage.path);
// console.log("대체 URL 시도:", altUrl);
setAlternativeImageUrl(altUrl);
} else {
toast.error("이미지를 불러올 수 없습니다.");
@ -469,7 +452,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
try {
return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {};
} catch {
// console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings);
return {};
}
},
@ -672,15 +654,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const handleRefreshFileStatus = async (event: CustomEvent) => {
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
// console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
// tableName,
// recordId,
// columnName,
// targetObjid,
// fileCount,
// currentTableName: component.tableName
// });
// 현재 테이블과 일치하는지 확인
if (tableName === component.tableName) {
// 해당 행의 파일 상태 업데이트
@ -690,13 +663,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
[recordId]: { hasFiles: fileCount > 0, fileCount },
[columnKey]: { hasFiles: fileCount > 0, fileCount },
}));
// console.log("✅ 파일 상태 업데이트 완료:", {
// recordId,
// columnKey,
// hasFiles: fileCount > 0,
// fileCount
// });
}
};
@ -1104,7 +1070,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
setIsAdding(true);
// 실제 API 호출로 데이터 추가
// console.log("🔥 추가할 데이터:", addFormData);
await tableTypeApi.addTableData(component.tableName, addFormData);
// 모달 닫기 및 폼 초기화
@ -1127,9 +1092,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
setIsEditing(true);
// 실제 API 호출로 데이터 수정
// console.log("🔥 수정할 데이터:", editFormData);
// console.log("🔥 원본 데이터:", editingRowData);
if (editingRowData) {
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
@ -1200,7 +1162,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const selectedData = Array.from(selectedRows).map((index) => data[index]);
// 실제 삭제 API 호출
// console.log("🗑️ 삭제할 데이터:", selectedData);
await tableTypeApi.deleteTableData(component.tableName, selectedData);
// 선택 해제 및 다이얼로그 닫기
@ -1488,12 +1449,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
case "category": {
// 카테고리 셀렉트 (동적 import)
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
console.log("🎯 카테고리 렌더링 (편집 폼):", {
tableName: component.tableName,
columnName: column.columnName,
columnLabel: column.label,
value,
});
return (
<div>
<CategorySelectComponent
@ -1775,12 +1730,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
case "category": {
// 카테고리 셀렉트 (동적 import)
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
console.log("🎯 카테고리 렌더링 (추가 폼):", {
tableName: component.tableName,
columnName: column.columnName,
columnLabel: column.label,
value,
});
return (
<div>
<CategorySelectComponent
@ -1868,8 +1817,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const handleDeleteLinkedFile = useCallback(
async (fileId: string, fileName: string) => {
try {
// console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
// 삭제 확인 다이얼로그
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
return;
@ -1884,7 +1831,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
});
const result = response.data;
// console.log("📡 파일 삭제 API 응답:", result);
if (!result.success) {
throw new Error(result.message || "파일 삭제 실패");
@ -1901,15 +1847,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
try {
const response = await getLinkedFiles(component.tableName, recordId);
setLinkedFiles(response.files || []);
// console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
} catch (error) {
// console.error("파일 목록 새로고침 실패:", error);
// 파일 목록 새로고침 실패 시 무시
}
}
// console.log("✅ 파일 삭제 완료:", fileName);
} catch (error) {
// console.error("❌ 파일 삭제 실패:", error);
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
}
},

View File

@ -619,7 +619,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
const snappedHeight = Math.max(10, Math.round(newComp.size.height / 10) * 10);
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
const snappedHeight = Math.max(10, newComp.size.height);
newComp.position = {
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
@ -3028,7 +3029,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth));
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
const snappedHeight = Math.max(40, Math.round(comp.size.height / 20) * 20);
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
const snappedHeight = Math.max(40, comp.size.height);
newPosition = {
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보

View File

@ -695,6 +695,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
id="positionX"
type="number"
step="1"
value={(() => {
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
if (isDragging) {
@ -725,6 +726,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
id="positionY"
type="number"
step="1"
value={(() => {
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
if (isDragging) {
@ -762,6 +764,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
type="number"
min={1}
max={gridSettings?.columns || 12}
step="1"
value={(selectedComponent as any)?.gridColumns || 1}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
@ -961,27 +964,27 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<div className="col-span-2">
<Label htmlFor="height" className="text-sm font-medium">
(10px )
</Label>
<div className="mt-1 flex items-center space-x-2">
<Input
id="height"
type="number"
min="1"
max="100"
value={Math.round((localInputs.height || 10) / 10)}
min="10"
max="2000"
step="1"
value={localInputs.height || 40}
onChange={(e) => {
const units = Math.max(1, Math.min(100, Number(e.target.value)));
const newHeight = units * 10;
const newHeight = Math.max(10, Number(e.target.value));
setLocalInputs((prev) => ({ ...prev, height: newHeight.toString() }));
onUpdateProperty("size.height", newHeight);
}}
className="flex-1"
/>
<span className="text-sm text-gray-500"> = {localInputs.height || 10}px</span>
<span className="text-sm text-gray-500">{localInputs.height || 40}px</span>
</div>
<p className="mt-1 text-xs text-gray-500">
1 = 10px ( {Math.round((localInputs.height || 10) / 10)}) -
(10px ~ 2000px, 1px )
</p>
</div>
</>
@ -996,11 +999,12 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Label htmlFor="zIndex" className="text-sm font-medium">
Z-Index ( )
</Label>
<Input
<Input
id="zIndex"
type="number"
min="0"
max="9999"
step="1"
value={localInputs.positionZ}
onChange={(e) => {
const newValue = e.target.value;
@ -1266,6 +1270,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
type="number"
min="1"
max="12"
step="1"
value={(selectedComponent as AreaComponent).layoutConfig?.gridColumns || 3}
onChange={(e) => {
const value = Number(e.target.value);
@ -1279,6 +1284,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
type="number"
min="0"
step="1"
value={(selectedComponent as AreaComponent).layoutConfig?.gridGap || 16}
onChange={(e) => {
const value = Number(e.target.value);
@ -1315,6 +1321,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
type="number"
min="0"
step="1"
value={(selectedComponent as AreaComponent).layoutConfig?.gap || 16}
onChange={(e) => {
const value = Number(e.target.value);
@ -1345,6 +1352,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
type="number"
min="100"
step="1"
value={(selectedComponent as AreaComponent).layoutConfig?.sidebarWidth || 200}
onChange={(e) => {
const value = Number(e.target.value);

View File

@ -146,6 +146,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
onChange={(e) => setCustomWidth(e.target.value)}
placeholder="1920"
min="1"
step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
@ -158,6 +159,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
onChange={(e) => setCustomHeight(e.target.value)}
placeholder="1080"
min="1"
step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>

View File

@ -57,6 +57,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
placeholder="100"
min={50}
max={1000}
step="1"
/>
</div>
)}
@ -73,6 +74,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
placeholder="50"
min={0}
max={1000}
step="1"
/>
</div>
)}
@ -89,6 +91,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
placeholder="500"
min={0}
max={2000}
step="1"
/>
</div>
)}

View File

@ -104,6 +104,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
}) => {
const { webTypes } = useWebTypes({ active: "Y" });
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
// 높이 입력 로컬 상태 (격자 스냅 방지)
const [localHeight, setLocalHeight] = useState<string>("");
// 새로운 컴포넌트 시스템의 webType 동기화
useEffect(() => {
@ -114,6 +117,13 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
}
}
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
// 높이 값 동기화
useEffect(() => {
if (selectedComponent?.size?.height !== undefined) {
setLocalHeight(String(selectedComponent.size.height));
}
}, [selectedComponent?.size?.height, selectedComponent?.id]);
// 격자 설정 업데이트 함수 (early return 이전에 정의)
const updateGridSetting = (key: string, value: any) => {
@ -180,6 +190,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
id="columns"
type="number"
min={1}
step="1"
value={gridSettings.columns}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
@ -361,11 +372,27 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Label className="text-xs"></Label>
<Input
type="number"
value={selectedComponent.size?.height || 0}
value={localHeight}
onChange={(e) => {
// 입력 중에는 로컬 상태만 업데이트 (격자 스냅 방지)
setLocalHeight(e.target.value);
}}
onBlur={(e) => {
// 포커스를 잃을 때만 실제로 업데이트
const value = parseInt(e.target.value) || 0;
// 최소값 제한 없이, 1px 단위로 조절 가능
handleUpdate("size.height", Math.max(1, value));
if (value >= 1) {
handleUpdate("size.height", value);
}
}}
onKeyDown={(e) => {
// Enter 키를 누르면 즉시 적용
if (e.key === "Enter") {
const value = parseInt(e.currentTarget.value) || 0;
if (value >= 1) {
handleUpdate("size.height", value);
}
e.currentTarget.blur(); // 포커스 제거
}
}}
step={1}
placeholder="10"
@ -430,6 +457,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
type="number"
min={1}
max={gridSettings?.columns || 12}
step="1"
value={(selectedComponent as any).gridColumns || 1}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
@ -455,6 +483,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Label className="text-xs">Z-Index</Label>
<Input
type="number"
step="1"
value={currentPosition.z || 1}
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
className="h-6 w-full px-2 py-0 text-xs"

View File

@ -132,6 +132,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
<Input
id="min"
type="number"
step="1"
value={localValues.min}
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"
@ -146,6 +147,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
<Input
id="max"
type="number"
step="1"
value={localValues.max}
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"
@ -181,6 +183,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
type="number"
min="0"
max="10"
step="1"
value={localValues.decimalPlaces}
onChange={(e) => updateConfig("decimalPlaces", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"

View File

@ -168,6 +168,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
id="minLength"
type="number"
min="0"
step="1"
value={localValues.minLength}
onChange={(e) => updateConfig("minLength", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"
@ -183,6 +184,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
id="maxLength"
type="number"
min="0"
step="1"
value={localValues.maxLength}
onChange={(e) => updateConfig("maxLength", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"

View File

@ -75,3 +75,4 @@ export const numberingRuleTemplate = {
],
};

View File

@ -1,8 +1,9 @@
"use client";
import React, { useState } from "react";
import React, { useState, useRef, useCallback } from "react";
import { CategoryColumnList } from "@/components/table-category/CategoryColumnList";
import { CategoryValueManager } from "@/components/table-category/CategoryValueManager";
import { GripVertical } from "lucide-react";
interface CategoryWidgetProps {
widgetId: string;
@ -19,11 +20,49 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
columnName: string;
columnLabel: string;
} | null>(null);
const [leftWidth, setLeftWidth] = useState(15); // 초기값 15%
const containerRef = useRef<HTMLDivElement>(null);
const isDraggingRef = useRef(false);
const handleMouseDown = useCallback(() => {
isDraggingRef.current = true;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}, []);
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!isDraggingRef.current || !containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
// 최소 10%, 최대 40%로 제한
if (newLeftWidth >= 10 && newLeftWidth <= 40) {
setLeftWidth(newLeftWidth);
}
}, []);
const handleMouseUp = useCallback(() => {
isDraggingRef.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}, []);
React.useEffect(() => {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
return (
<div className="flex h-full min-h-[10px] gap-6">
{/* 좌측: 카테고리 컬럼 리스트 (30%) */}
<div className="w-[30%] border-r pr-6">
<div ref={containerRef} className="flex h-full min-h-[10px] gap-0">
{/* 좌측: 카테고리 컬럼 리스트 */}
<div style={{ width: `${leftWidth}%` }} className="pr-3">
<CategoryColumnList
tableName={tableName}
selectedColumn={selectedColumn?.columnName || null}
@ -33,8 +72,16 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
/>
</div>
{/* 우측: 카테고리 값 관리 (70%) */}
<div className="w-[70%]">
{/* 리사이저 */}
<div
onMouseDown={handleMouseDown}
className="group relative flex w-3 cursor-col-resize items-center justify-center border-r hover:bg-accent/50 transition-colors"
>
<GripVertical className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
{/* 우측: 카테고리 값 관리 */}
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
{selectedColumn ? (
<CategoryValueManager
tableName={tableName}

View File

@ -66,6 +66,35 @@ export function FlowWidget({
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { user } = useAuth(); // 사용자 정보 가져오기
// 🆕 전역 상태 관리
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
const resetFlow = useFlowStepStore((state) => state.resetFlow);
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
const [steps, setSteps] = useState<FlowStep[]>([]);
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [connections, setConnections] = useState<any[]>([]); // 플로우 연결 정보
// 선택된 스텝의 데이터 리스트 상태
const [selectedStepId, setSelectedStepId] = useState<number | null>(null);
const [stepData, setStepData] = useState<any[]>([]);
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
const [stepDataLoading, setStepDataLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
// 🆕 검색 필터 관련 상태
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, string>>>({});
// 값 포맷팅 함수 (숫자, 카테고리 등)
const formatValue = useCallback((value: any, columnName?: string): string => {
if (value === null || value === undefined || value === "") {
@ -97,35 +126,6 @@ export function FlowWidget({
return String(value);
}, [categoryMappings]);
// 🆕 전역 상태 관리
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
const resetFlow = useFlowStepStore((state) => state.resetFlow);
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
const [steps, setSteps] = useState<FlowStep[]>([]);
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [connections, setConnections] = useState<any[]>([]); // 플로우 연결 정보
// 선택된 스텝의 데이터 리스트 상태
const [selectedStepId, setSelectedStepId] = useState<number | null>(null);
const [stepData, setStepData] = useState<any[]>([]);
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
const [stepDataLoading, setStepDataLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
// 🆕 검색 필터 관련 상태
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, string>>>({});
// 🆕 그룹 설정 관련 상태
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
@ -382,12 +382,6 @@ export function FlowWidget({
});
setFilteredData(filtered);
console.log("🔍 검색 실행:", {
totalRows: stepData.length,
filteredRows: filtered.length,
searchValues,
hasSearchValue,
});
}, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행
// 선택된 스텝의 데이터를 다시 로드하는 함수
@ -471,7 +465,6 @@ export function FlowWidget({
// 프리뷰 모드에서는 샘플 데이터만 표시
if (isPreviewMode) {
console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시");
setFlowData({
id: flowId || 0,
flowName: flowName || "샘플 플로우",
@ -648,16 +641,9 @@ export function FlowWidget({
try {
// 컬럼 라벨 조회
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
console.log("🏷️ 컬럼 라벨 조회 결과:", {
stepId,
success: labelsResponse.success,
labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0,
labels: labelsResponse.data,
});
if (labelsResponse.success && labelsResponse.data) {
setColumnLabels(labelsResponse.data);
} else {
console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse);
setColumnLabels({});
}
@ -761,13 +747,6 @@ export function FlowWidget({
// 선택된 데이터를 상위로 전달
const selectedData = Array.from(newSelected).map((index) => stepData[index]);
console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", {
rowIndex,
newSelectedSize: newSelected.size,
selectedData,
selectedStepId,
hasCallback: !!onSelectedDataChange,
});
onSelectedDataChange?.(selectedData, selectedStepId);
};

View File

@ -2,12 +2,14 @@
import React, { useState, useEffect } from "react";
import { apiClient } from "@/lib/api/client";
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { FolderTree, Loader2 } from "lucide-react";
interface CategoryColumn {
columnName: string;
columnLabel: string;
inputType: string;
valueCount?: number;
}
interface CategoryColumnListProps {
@ -79,20 +81,37 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
})),
});
setColumns(
categoryColumns.map((col: any) => ({
columnName: col.columnName || col.column_name,
columnLabel: col.columnLabel || col.column_label || col.displayName || col.columnName || col.column_name,
inputType: col.inputType || col.input_type,
})),
const columnsWithCount = await Promise.all(
categoryColumns.map(async (col: any) => {
const colName = col.columnName || col.column_name;
const colLabel = col.columnLabel || col.column_label || col.displayName || colName;
// 각 컬럼의 값 개수 가져오기
let valueCount = 0;
try {
const valuesResult = await getCategoryValues(tableName, colName, false);
if (valuesResult.success && valuesResult.data) {
valueCount = valuesResult.data.length;
}
} catch (error) {
console.error(`항목 개수 조회 실패 (${colName}):`, error);
}
return {
columnName: colName,
columnLabel: colLabel,
inputType: col.inputType || col.input_type,
valueCount,
};
}),
);
setColumns(columnsWithCount);
// 첫 번째 컬럼 자동 선택
if (categoryColumns.length > 0 && !selectedColumn) {
const firstCol = categoryColumns[0];
const colName = firstCol.columnName || firstCol.column_name;
const colLabel = firstCol.columnLabel || firstCol.column_label || firstCol.displayName || colName;
onColumnSelect(colName, colLabel);
if (columnsWithCount.length > 0 && !selectedColumn) {
const firstCol = columnsWithCount[0];
onColumnSelect(firstCol.columnName, firstCol.columnLabel);
}
} catch (error) {
console.error("❌ 카테고리 컬럼 조회 실패:", error);
@ -137,7 +156,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
<div
key={column.columnName}
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName)}
className={`cursor-pointer rounded-lg border p-4 transition-all ${
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
}`}
>
@ -147,8 +166,10 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
/>
<div className="flex-1">
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
<p className="text-muted-foreground text-xs">{column.columnName}</p>
</div>
<span className="text-muted-foreground text-xs font-medium">
{column.valueCount !== undefined ? `${column.valueCount}` : "..."}
</span>
</div>
</div>
))}

View File

@ -72,7 +72,8 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
const loadCategoryValues = async () => {
setIsLoading(true);
try {
const response = await getCategoryValues(tableName, columnName);
// includeInactive: true로 비활성 값도 포함
const response = await getCategoryValues(tableName, columnName, true);
if (response.success && response.data) {
setValues(response.data);
setFilteredValues(response.data);

View File

@ -133,3 +133,4 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
}
}

View File

@ -195,17 +195,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const buttonColor = getLabelColor();
// 그라데이션용 어두운 색상 계산
const getDarkColor = (baseColor: string) => {
const hex = baseColor.replace("#", "");
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40);
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40);
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40);
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
};
const buttonDarkColor = getDarkColor(buttonColor);
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
const processedConfig = { ...componentConfig };
if (componentConfig.action && typeof componentConfig.action === "string") {
@ -545,16 +534,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
<button
type={componentConfig.actionType || "button"}
disabled={componentConfig.disabled || false}
className="transition-all duration-200"
className="transition-colors duration-150 hover:opacity-90 active:scale-95 transition-transform"
style={{
width: "100%",
height: "100%",
minHeight: "40px",
border: "none",
borderRadius: "0.5rem",
background: componentConfig.disabled
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
background: componentConfig.disabled ? "#e5e7eb" : buttonColor,
color: componentConfig.disabled ? "#9ca3af" : "white",
// 🔧 크기 설정 적용 (sm/md/lg)
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
@ -570,7 +557,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
margin: "0",
lineHeight: "1.25",
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 1px 3px 0 ${buttonColor}40`,
boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
// isInteractive 모드에서는 사용자 스타일 우선 적용 (width/height 제외)
...(isInteractive && component.style ? Object.fromEntries(
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')

View File

@ -113,11 +113,11 @@ export const CardModeRenderer: React.FC<CardModeRendererProps> = ({
if (!data || data.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-slate-100 to-slate-200 rounded-2xl flex items-center justify-center mb-4">
<div className="w-8 h-8 bg-slate-300 rounded-lg"></div>
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mb-4">
<div className="w-8 h-8 bg-muted-foreground/20 rounded-lg"></div>
</div>
<div className="text-sm font-medium text-slate-600 mb-1"> </div>
<div className="text-xs text-slate-400"> </div>
<div className="text-sm font-medium text-muted-foreground mb-1"> </div>
<div className="text-xs text-muted-foreground/60"> </div>
</div>
);
}

View File

@ -186,9 +186,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 객체인 경우 tableName 속성 추출 시도
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable);
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable);
}
tableConfig.selectedTable = finalSelectedTable;
@ -282,13 +280,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder);
console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder });
setColumnOrder(parsedOrder);
// 부모 컴포넌트에 초기 컬럼 순서 전달
if (onSelectedRowsChange && parsedOrder.length > 0) {
console.log("✅ 초기 컬럼 순서 전달:", parsedOrder);
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
const initialData = data.map((row: any) => {
const reordered: any = {};
@ -306,8 +301,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return reordered;
});
console.log("📊 초기 화면 표시 데이터 전달:", { count: initialData.length, firstRow: initialData[0] });
// 전역 저장소에 데이터 저장
if (tableConfig.selectedTable) {
tableDisplayStore.setTableData(
@ -584,8 +577,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
const handleSort = (column: string) => {
console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection });
let newSortColumn = column;
let newSortDirection: "asc" | "desc" = "asc";
@ -599,9 +590,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
newSortDirection = "asc";
}
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
if (onSelectedRowsChange) {
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
@ -651,16 +639,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return reordered;
});
console.log("✅ 정렬 정보 전달:", {
selectedRowsCount: selectedRows.size,
selectedRowsDataCount: selectedRowsData.length,
sortBy: newSortColumn,
sortOrder: newSortDirection,
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
tableDisplayDataCount: reorderedData.length,
firstRowAfterSort: reorderedData[0]?.[newSortColumn],
lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn]
});
onSelectedRowsChange(
Array.from(selectedRows),
selectedRowsData,
@ -681,8 +659,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
newSortDirection
);
}
} else {
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
}
};
@ -774,8 +750,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const isCurrentlySelected = selectedRows.has(rowKey);
handleRowSelection(rowKey, !isCurrentlySelected);
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
};
// 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능)
@ -820,12 +794,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// columnOrder에 없는 새로운 컬럼들 추가
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
console.log("🔄 columnOrder 기반 정렬:", {
columnOrder,
orderedColsCount: orderedCols.length,
remainingColsCount: remainingCols.length
});
return [...orderedCols, ...remainingCols];
}
@ -836,19 +804,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const lastColumnOrderRef = useRef<string>("");
useEffect(() => {
console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
hasCallback: !!onSelectedRowsChange,
visibleColumnsLength: visibleColumns.length,
visibleColumnsNames: visibleColumns.map(c => c.columnName),
});
if (!onSelectedRowsChange) {
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
return;
}
if (visibleColumns.length === 0) {
console.warn("⚠️ visibleColumns가 비어있습니다!");
return;
}
@ -856,23 +816,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
.map(col => col.columnName)
.filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외
console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);
// 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지)
const columnOrderString = currentColumnOrder.join(",");
console.log("🔍 [컬럼 순서] 비교:", {
current: columnOrderString,
last: lastColumnOrderRef.current,
isDifferent: columnOrderString !== lastColumnOrderRef.current,
});
if (columnOrderString === lastColumnOrderRef.current) {
console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵");
return;
}
lastColumnOrderRef.current = columnOrderString;
console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder);
// 선택된 행 데이터 가져오기
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
@ -1628,7 +1579,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
>
{/* 스크롤 영역 */}
<div
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
className="w-full max-w-full flex-1 overflow-y-auto overflow-x-auto bg-background"
>
{/* 테이블 */}
<table
@ -1646,7 +1597,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<thead
className="sticky top-0 z-10"
>
<tr className="h-10 border-b-2 border-primary/20 bg-gradient-to-b from-muted/50 to-muted sm:h-12">
<tr className="h-10 border-b-2 border-primary/20 bg-muted sm:h-12">
{visibleColumns.map((column, columnIndex) => {
const columnWidth = columnWidths[column.columnName];
const isFrozen = frozenColumns.includes(column.columnName);
@ -1804,11 +1755,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<tr>
<td
colSpan={visibleColumns.length}
className="bg-muted/50 border-b border-border sticky top-[48px] z-[5]"
className="bg-muted border-b border-border sticky top-[48px] z-[5]"
style={{ top: "48px" }}
>
<div
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted"
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/80"
onClick={() => toggleGroupCollapse(group.groupKey)}
>
{isCollapsed ? (

View File

@ -375,3 +375,4 @@ interface TablePermission {
**이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!**