Compare commits
18 Commits
9f4e71fc68
...
c22e38da76
| Author | SHA1 | Date |
|---|---|---|
|
|
c22e38da76 | |
|
|
786576bb76 | |
|
|
832e80cd7f | |
|
|
2e674e13d0 | |
|
|
bc826e8e49 | |
|
|
4affe623a5 | |
|
|
f53a818f2f | |
|
|
b5a83bb0f3 | |
|
|
85e1b532fa | |
|
|
4cd08c3900 | |
|
|
70dc24f7a1 | |
|
|
cd961a2162 | |
|
|
95b341df79 | |
|
|
49935189b6 | |
|
|
939a8696c8 | |
|
|
b526d8ea2c | |
|
|
7581cd1582 | |
|
|
1d87b6c3ac |
|
|
@ -0,0 +1,279 @@
|
||||||
|
# inputType 사용 가이드
|
||||||
|
|
||||||
|
## 핵심 원칙
|
||||||
|
|
||||||
|
**컬럼 타입 판단 시 반드시 `inputType`을 사용해야 합니다. `webType`은 레거시이며 더 이상 사용하지 않습니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 올바른 사용법
|
||||||
|
|
||||||
|
### ✅ inputType 사용 (권장)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 카테고리 타입 체크
|
||||||
|
if (columnMeta.inputType === "category") {
|
||||||
|
// 카테고리 처리 로직
|
||||||
|
}
|
||||||
|
|
||||||
|
// 코드 타입 체크
|
||||||
|
if (meta.inputType === "code") {
|
||||||
|
// 코드 처리 로직
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터링
|
||||||
|
const categoryColumns = Object.entries(columnMeta)
|
||||||
|
.filter(([_, meta]) => meta.inputType === "category")
|
||||||
|
.map(([columnName, _]) => columnName);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ webType 사용 (금지)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 절대 사용 금지!
|
||||||
|
if (columnMeta.webType === "category") { ... }
|
||||||
|
|
||||||
|
// ❌ 이것도 금지!
|
||||||
|
const categoryColumns = columns.filter(col => col.webType === "category");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API에서 inputType 가져오기
|
||||||
|
|
||||||
|
### Backend API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 컬럼 입력 타입 정보 가져오기
|
||||||
|
const inputTypes = await tableTypeApi.getColumnInputTypes(tableName);
|
||||||
|
|
||||||
|
// inputType 맵 생성
|
||||||
|
const inputTypeMap: Record<string, string> = {};
|
||||||
|
inputTypes.forEach((col: any) => {
|
||||||
|
inputTypeMap[col.columnName] = col.inputType;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### columnMeta 구조
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ColumnMeta {
|
||||||
|
webType?: string; // 레거시, 사용 금지
|
||||||
|
codeCategory?: string;
|
||||||
|
inputType?: string; // ✅ 반드시 이것 사용!
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnMeta: Record<string, ColumnMeta> = {
|
||||||
|
material: {
|
||||||
|
webType: "category", // 무시
|
||||||
|
codeCategory: "",
|
||||||
|
inputType: "category", // ✅ 이것만 사용
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 캐시 사용 시 주의사항
|
||||||
|
|
||||||
|
### ❌ 잘못된 캐시 처리 (inputType 누락)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cached = tableColumnCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
const meta: Record<string, ColumnMeta> = {};
|
||||||
|
|
||||||
|
cached.columns.forEach((col: any) => {
|
||||||
|
meta[col.columnName] = {
|
||||||
|
webType: col.webType,
|
||||||
|
codeCategory: col.codeCategory,
|
||||||
|
// ❌ inputType 누락!
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ 올바른 캐시 처리 (inputType 포함)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cached = tableColumnCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
const meta: Record<string, ColumnMeta> = {};
|
||||||
|
|
||||||
|
// 캐시된 inputTypes 맵 생성
|
||||||
|
const inputTypeMap: Record<string, string> = {};
|
||||||
|
if (cached.inputTypes) {
|
||||||
|
cached.inputTypes.forEach((col: any) => {
|
||||||
|
inputTypeMap[col.columnName] = col.inputType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cached.columns.forEach((col: any) => {
|
||||||
|
meta[col.columnName] = {
|
||||||
|
webType: col.webType,
|
||||||
|
codeCategory: col.codeCategory,
|
||||||
|
inputType: inputTypeMap[col.columnName], // ✅ inputType 포함!
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 inputType 종류
|
||||||
|
|
||||||
|
| inputType | 설명 | 사용 예시 |
|
||||||
|
| ---------- | ---------------- | ------------------ |
|
||||||
|
| `text` | 일반 텍스트 입력 | 이름, 설명 등 |
|
||||||
|
| `number` | 숫자 입력 | 금액, 수량 등 |
|
||||||
|
| `date` | 날짜 입력 | 생성일, 수정일 등 |
|
||||||
|
| `datetime` | 날짜+시간 입력 | 타임스탬프 등 |
|
||||||
|
| `category` | 카테고리 선택 | 분류, 상태 등 |
|
||||||
|
| `code` | 공통 코드 선택 | 코드 마스터 데이터 |
|
||||||
|
| `boolean` | 예/아니오 | 활성화 여부 등 |
|
||||||
|
| `email` | 이메일 입력 | 이메일 주소 |
|
||||||
|
| `url` | URL 입력 | 웹사이트 주소 |
|
||||||
|
| `image` | 이미지 업로드 | 프로필 사진 등 |
|
||||||
|
| `file` | 파일 업로드 | 첨부파일 등 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실제 적용 사례
|
||||||
|
|
||||||
|
### 1. TableListComponent - 카테고리 매핑 로드
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ inputType으로 카테고리 컬럼 필터링
|
||||||
|
const categoryColumns = Object.entries(columnMeta)
|
||||||
|
.filter(([_, meta]) => meta.inputType === "category")
|
||||||
|
.map(([columnName, _]) => columnName);
|
||||||
|
|
||||||
|
// 각 카테고리 컬럼의 값 목록 조회
|
||||||
|
for (const columnName of categoryColumns) {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/table-categories/${tableName}/${columnName}/values`
|
||||||
|
);
|
||||||
|
// 매핑 처리...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. InteractiveDataTable - 셀 값 렌더링
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ inputType으로 렌더링 분기
|
||||||
|
const inputType = columnMeta[column.columnName]?.inputType;
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "category":
|
||||||
|
// 카테고리 배지 렌더링
|
||||||
|
return <Badge>{categoryLabel}</Badge>;
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
// 코드명 표시
|
||||||
|
return codeName;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
// 날짜 포맷팅
|
||||||
|
return formatDate(value);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 검색 필터 생성
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ inputType에 따라 다른 검색 UI 제공
|
||||||
|
const renderSearchInput = (column: ColumnConfig) => {
|
||||||
|
const inputType = columnMeta[column.columnName]?.inputType;
|
||||||
|
|
||||||
|
switch (inputType) {
|
||||||
|
case "category":
|
||||||
|
return <CategorySelect column={column} />;
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
return <CodeSelect column={column} />;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return <DateRangePicker column={column} />;
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return <NumberRangeInput column={column} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <TextInput column={column} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 마이그레이션 체크리스트
|
||||||
|
|
||||||
|
기존 코드에서 `webType`을 `inputType`으로 전환할 때:
|
||||||
|
|
||||||
|
- [ ] `webType` 참조를 모두 `inputType`으로 변경
|
||||||
|
- [ ] API 호출 시 `getColumnInputTypes()` 포함 확인
|
||||||
|
- [ ] 캐시 사용 시 `cached.inputTypes` 매핑 확인
|
||||||
|
- [ ] 타입 정의에서 `inputType` 필드 포함
|
||||||
|
- [ ] 조건문에서 `inputType` 체크로 변경
|
||||||
|
- [ ] 테스트 실행하여 정상 동작 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 디버깅 팁
|
||||||
|
|
||||||
|
### inputType이 undefined인 경우
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 디버깅 로그 추가
|
||||||
|
console.log("columnMeta:", columnMeta);
|
||||||
|
console.log("inputType:", columnMeta[columnName]?.inputType);
|
||||||
|
|
||||||
|
// 체크 포인트:
|
||||||
|
// 1. getColumnInputTypes() 호출 확인
|
||||||
|
// 2. inputTypeMap 생성 확인
|
||||||
|
// 3. meta 객체에 inputType 할당 확인
|
||||||
|
// 4. 캐시 사용 시 cached.inputTypes 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### webType만 있고 inputType이 없는 경우
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 잘못된 데이터 구조
|
||||||
|
{
|
||||||
|
material: {
|
||||||
|
webType: "category",
|
||||||
|
codeCategory: "",
|
||||||
|
// inputType 누락!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 올바른 데이터 구조
|
||||||
|
{
|
||||||
|
material: {
|
||||||
|
webType: "category", // 레거시, 무시됨
|
||||||
|
codeCategory: "",
|
||||||
|
inputType: "category" // ✅ 필수!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- **컴포넌트**: `/frontend/lib/registry/components/table-list/TableListComponent.tsx`
|
||||||
|
- **API 클라이언트**: `/frontend/lib/api/tableType.ts`
|
||||||
|
- **타입 정의**: `/frontend/types/table.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
1. **항상 `inputType` 사용**, `webType` 사용 금지
|
||||||
|
2. **API에서 `getColumnInputTypes()` 호출** 필수
|
||||||
|
3. **캐시 사용 시 `inputTypes` 포함** 확인
|
||||||
|
4. **디버깅 시 `inputType` 값 확인**
|
||||||
|
5. **기존 코드 마이그레이션** 시 체크리스트 활용
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { useMenu } from "@/contexts/MenuContext";
|
import { useMenu } from "@/contexts/MenuContext";
|
||||||
import { useMenuManagementText, setTranslationCache } from "@/lib/utils/multilang";
|
import { useMenuManagementText, setTranslationCache, getMenuTextSync } from "@/lib/utils/multilang";
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
|
|
@ -545,9 +545,9 @@ export const MenuManagement: React.FC = () => {
|
||||||
// uiTexts에서 번역 텍스트 찾기
|
// uiTexts에서 번역 텍스트 찾기
|
||||||
let text = uiTexts[key];
|
let text = uiTexts[key];
|
||||||
|
|
||||||
// uiTexts에 없으면 fallback 또는 키 사용
|
// uiTexts에 없으면 getMenuTextSync로 기본 한글 텍스트 가져오기
|
||||||
if (!text) {
|
if (!text) {
|
||||||
text = fallback || key;
|
text = getMenuTextSync(key, userLang) || fallback || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파라미터 치환
|
// 파라미터 치환
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
|
import { MENU_MANAGEMENT_KEYS, getMenuTextSync } from "@/lib/utils/multilang";
|
||||||
|
|
||||||
interface MenuTableProps {
|
interface MenuTableProps {
|
||||||
menus: MenuItem[];
|
menus: MenuItem[];
|
||||||
|
|
@ -39,7 +39,8 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
// 다국어 텍스트 가져오기 함수
|
// 다국어 텍스트 가져오기 함수
|
||||||
const getText = (key: string, fallback?: string): string => {
|
const getText = (key: string, fallback?: string): string => {
|
||||||
return uiTexts[key] || fallback || key;
|
// uiTexts에서 먼저 찾고, 없으면 기본 한글 텍스트를 가져옴
|
||||||
|
return uiTexts[key] || getMenuTextSync(key, "KR") || fallback || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 다국어 텍스트 표시 함수 (기본값 처리)
|
// 다국어 텍스트 표시 함수 (기본값 처리)
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<ResizableDialogFooter className="gap-2 sm:gap-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<ResizableDialogFooter className="gap-2 sm:gap-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||||
|
|
||||||
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
|
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}})
|
||||||
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, string>>>({});
|
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, { label: string; color?: string }>>>({});
|
||||||
|
|
||||||
// 공통코드 옵션 가져오기
|
// 공통코드 옵션 가져오기
|
||||||
const loadCodeOptions = useCallback(
|
const loadCodeOptions = useCallback(
|
||||||
|
|
@ -208,7 +208,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
if (!categoryColumns || categoryColumns.length === 0) return;
|
if (!categoryColumns || categoryColumns.length === 0) return;
|
||||||
|
|
||||||
// 각 카테고리 컬럼의 값 목록 조회
|
// 각 카테고리 컬럼의 값 목록 조회
|
||||||
const mappings: Record<string, Record<string, string>> = {};
|
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
|
||||||
|
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -217,18 +217,23 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
// valueCode -> valueLabel 매핑 생성
|
// valueCode -> {label, color} 매핑 생성
|
||||||
const mapping: Record<string, string> = {};
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
response.data.data.forEach((item: any) => {
|
response.data.data.forEach((item: any) => {
|
||||||
mapping[item.valueCode] = item.valueLabel;
|
mapping[item.valueCode] = {
|
||||||
|
label: item.valueLabel,
|
||||||
|
color: item.color,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
mappings[col.columnName] = mapping;
|
mappings[col.columnName] = mapping;
|
||||||
|
console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 카테고리 값 로드 실패 시 무시
|
console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("📊 전체 카테고리 매핑:", mappings);
|
||||||
setCategoryMappings(mappings);
|
setCategoryMappings(mappings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("카테고리 매핑 로드 실패:", error);
|
console.error("카테고리 매핑 로드 실패:", error);
|
||||||
|
|
@ -1911,13 +1916,27 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
||||||
switch (actualWebType) {
|
switch (actualWebType) {
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 타입: 코드값 -> 라벨로 변환
|
// 카테고리 타입: 배지로 표시
|
||||||
|
if (!value) return "";
|
||||||
|
|
||||||
const mapping = categoryMappings[column.columnName];
|
const mapping = categoryMappings[column.columnName];
|
||||||
if (mapping && value) {
|
const categoryData = mapping?.[String(value)];
|
||||||
const label = mapping[String(value)];
|
|
||||||
return label || String(value);
|
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
|
||||||
}
|
const displayLabel = categoryData?.label || String(value);
|
||||||
return String(value || "");
|
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
style={{
|
||||||
|
backgroundColor: displayColor,
|
||||||
|
borderColor: displayColor
|
||||||
|
}}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
{displayLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "date":
|
case "date":
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,33 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
||||||
|
|
||||||
|
// 기본 색상 팔레트
|
||||||
|
const DEFAULT_COLORS = [
|
||||||
|
"#ef4444", // red
|
||||||
|
"#f97316", // orange
|
||||||
|
"#f59e0b", // amber
|
||||||
|
"#eab308", // yellow
|
||||||
|
"#84cc16", // lime
|
||||||
|
"#22c55e", // green
|
||||||
|
"#10b981", // emerald
|
||||||
|
"#14b8a6", // teal
|
||||||
|
"#06b6d4", // cyan
|
||||||
|
"#0ea5e9", // sky
|
||||||
|
"#3b82f6", // blue
|
||||||
|
"#6366f1", // indigo
|
||||||
|
"#8b5cf6", // violet
|
||||||
|
"#a855f7", // purple
|
||||||
|
"#d946ef", // fuchsia
|
||||||
|
"#ec4899", // pink
|
||||||
|
"#64748b", // slate
|
||||||
|
"#6b7280", // gray
|
||||||
|
];
|
||||||
|
|
||||||
interface CategoryValueAddDialogProps {
|
interface CategoryValueAddDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
|
@ -26,6 +50,7 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
||||||
const [valueLabel, setValueLabel] = useState("");
|
const [valueLabel, setValueLabel] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [color, setColor] = useState("#3b82f6");
|
||||||
|
|
||||||
// 라벨에서 코드 자동 생성
|
// 라벨에서 코드 자동 생성
|
||||||
const generateCode = (label: string): string => {
|
const generateCode = (label: string): string => {
|
||||||
|
|
@ -59,13 +84,14 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
valueCode,
|
valueCode,
|
||||||
valueLabel: valueLabel.trim(),
|
valueLabel: valueLabel.trim(),
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
color: "#3b82f6",
|
color: color,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 초기화
|
// 초기화
|
||||||
setValueLabel("");
|
setValueLabel("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setColor("#3b82f6");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -81,23 +107,56 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
<Input
|
<div>
|
||||||
id="valueLabel"
|
<Label htmlFor="valueLabel" className="text-xs sm:text-sm">
|
||||||
placeholder="이름 (예: 개발, 긴급, 진행중)"
|
이름
|
||||||
value={valueLabel}
|
</Label>
|
||||||
onChange={(e) => setValueLabel(e.target.value)}
|
<Input
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
id="valueLabel"
|
||||||
autoFocus
|
placeholder="예: 개발, 긴급, 진행중"
|
||||||
/>
|
value={valueLabel}
|
||||||
|
onChange={(e) => setValueLabel(e.target.value)}
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm mt-1.5"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Textarea
|
<div>
|
||||||
id="description"
|
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||||
placeholder="설명 (선택사항)"
|
<div className="mt-1.5 flex items-center gap-3">
|
||||||
value={description}
|
<div className="grid grid-cols-9 gap-2">
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
{DEFAULT_COLORS.map((c) => (
|
||||||
className="text-xs sm:text-sm"
|
<button
|
||||||
rows={3}
|
key={c}
|
||||||
/>
|
type="button"
|
||||||
|
onClick={() => setColor(c)}
|
||||||
|
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||||
|
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: c }}
|
||||||
|
title={c}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||||
|
미리보기
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="description" className="text-xs sm:text-sm">
|
||||||
|
설명 (선택사항)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="설명을 입력하세요"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="text-xs sm:text-sm mt-1.5"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
||||||
|
|
||||||
interface CategoryValueEditDialogProps {
|
interface CategoryValueEditDialogProps {
|
||||||
|
|
@ -22,15 +24,39 @@ interface CategoryValueEditDialogProps {
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 기본 색상 팔레트
|
||||||
|
const DEFAULT_COLORS = [
|
||||||
|
"#ef4444", // red
|
||||||
|
"#f97316", // orange
|
||||||
|
"#f59e0b", // amber
|
||||||
|
"#eab308", // yellow
|
||||||
|
"#84cc16", // lime
|
||||||
|
"#22c55e", // green
|
||||||
|
"#10b981", // emerald
|
||||||
|
"#14b8a6", // teal
|
||||||
|
"#06b6d4", // cyan
|
||||||
|
"#0ea5e9", // sky
|
||||||
|
"#3b82f6", // blue
|
||||||
|
"#6366f1", // indigo
|
||||||
|
"#8b5cf6", // violet
|
||||||
|
"#a855f7", // purple
|
||||||
|
"#d946ef", // fuchsia
|
||||||
|
"#ec4899", // pink
|
||||||
|
"#64748b", // slate
|
||||||
|
"#6b7280", // gray
|
||||||
|
];
|
||||||
|
|
||||||
export const CategoryValueEditDialog: React.FC<
|
export const CategoryValueEditDialog: React.FC<
|
||||||
CategoryValueEditDialogProps
|
CategoryValueEditDialogProps
|
||||||
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
||||||
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
||||||
const [description, setDescription] = useState(value.description || "");
|
const [description, setDescription] = useState(value.description || "");
|
||||||
|
const [color, setColor] = useState(value.color || "#3b82f6");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValueLabel(value.valueLabel);
|
setValueLabel(value.valueLabel);
|
||||||
setDescription(value.description || "");
|
setDescription(value.description || "");
|
||||||
|
setColor(value.color || "#3b82f6");
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
|
@ -41,6 +67,7 @@ export const CategoryValueEditDialog: React.FC<
|
||||||
onUpdate(value.valueId!, {
|
onUpdate(value.valueId!, {
|
||||||
valueLabel: valueLabel.trim(),
|
valueLabel: valueLabel.trim(),
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
|
color: color,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,23 +84,56 @@ export const CategoryValueEditDialog: React.FC<
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
<Input
|
<div>
|
||||||
id="valueLabel"
|
<Label htmlFor="valueLabel" className="text-xs sm:text-sm">
|
||||||
placeholder="이름 (예: 개발, 긴급, 진행중)"
|
이름
|
||||||
value={valueLabel}
|
</Label>
|
||||||
onChange={(e) => setValueLabel(e.target.value)}
|
<Input
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
id="valueLabel"
|
||||||
autoFocus
|
placeholder="예: 개발, 긴급, 진행중"
|
||||||
/>
|
value={valueLabel}
|
||||||
|
onChange={(e) => setValueLabel(e.target.value)}
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm mt-1.5"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Textarea
|
<div>
|
||||||
id="description"
|
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||||
placeholder="설명 (선택사항)"
|
<div className="mt-1.5 flex items-center gap-3">
|
||||||
value={description}
|
<div className="grid grid-cols-9 gap-2">
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
{DEFAULT_COLORS.map((c) => (
|
||||||
className="text-xs sm:text-sm"
|
<button
|
||||||
rows={3}
|
key={c}
|
||||||
/>
|
type="button"
|
||||||
|
onClick={() => setColor(c)}
|
||||||
|
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||||
|
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: c }}
|
||||||
|
title={c}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||||
|
미리보기
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="description" className="text-xs sm:text-sm">
|
||||||
|
설명 (선택사항)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="설명을 입력하세요"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="text-xs sm:text-sm mt-1.5"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
|
|
||||||
|
|
@ -49,25 +49,33 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
const [editingValue, setEditingValue] = useState<TableCategoryValue | null>(
|
const [editingValue, setEditingValue] = useState<TableCategoryValue | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [showInactive, setShowInactive] = useState(false); // 비활성 항목 표시 옵션 (기본: 숨김)
|
||||||
|
|
||||||
// 카테고리 값 로드
|
// 카테고리 값 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCategoryValues();
|
loadCategoryValues();
|
||||||
}, [tableName, columnName]);
|
}, [tableName, columnName]);
|
||||||
|
|
||||||
// 검색 필터링
|
// 검색 필터링 + 비활성 필터링
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let filtered = values;
|
||||||
|
|
||||||
|
// 비활성 항목 필터링 (기본: 활성만 표시, 체크하면 비활성도 표시)
|
||||||
|
if (!showInactive) {
|
||||||
|
filtered = filtered.filter((v) => v.isActive !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색어 필터링
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
const filtered = values.filter(
|
filtered = filtered.filter(
|
||||||
(v) =>
|
(v) =>
|
||||||
v.valueCode.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
v.valueCode.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
v.valueLabel.toLowerCase().includes(searchQuery.toLowerCase())
|
v.valueLabel.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
setFilteredValues(filtered);
|
|
||||||
} else {
|
|
||||||
setFilteredValues(values);
|
|
||||||
}
|
}
|
||||||
}, [searchQuery, values]);
|
|
||||||
|
setFilteredValues(filtered);
|
||||||
|
}, [searchQuery, values, showInactive]);
|
||||||
|
|
||||||
const loadCategoryValues = async () => {
|
const loadCategoryValues = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -264,10 +272,27 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
총 {filteredValues.length}개 항목
|
총 {filteredValues.length}개 항목
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setIsAddDialogOpen(true)} size="sm">
|
<div className="flex items-center gap-3">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
{/* 비활성 항목 표시 옵션 */}
|
||||||
새 값 추가
|
<div className="flex items-center gap-2">
|
||||||
</Button>
|
<Checkbox
|
||||||
|
id="show-inactive"
|
||||||
|
checked={showInactive}
|
||||||
|
onCheckedChange={(checked) => setShowInactive(checked as boolean)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-inactive"
|
||||||
|
className="text-sm text-muted-foreground cursor-pointer whitespace-nowrap"
|
||||||
|
>
|
||||||
|
비활성 항목 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={() => setIsAddDialogOpen(true)} size="sm">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
새 값 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 검색바 */}
|
{/* 검색바 */}
|
||||||
|
|
@ -294,73 +319,90 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filteredValues.map((value) => (
|
{filteredValues.map((value) => {
|
||||||
<div
|
const isInactive = value.isActive === false;
|
||||||
key={value.valueId}
|
|
||||||
className="flex items-center gap-3 rounded-md border bg-card p-3 transition-colors hover:bg-accent"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedValueIds.includes(value.valueId!)}
|
|
||||||
onCheckedChange={() => handleSelectValue(value.valueId!)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-1 items-center gap-2">
|
return (
|
||||||
<Badge variant="outline" className="text-xs">
|
<div
|
||||||
{value.valueCode}
|
key={value.valueId}
|
||||||
</Badge>
|
className={`flex items-center gap-3 rounded-md border bg-card p-3 transition-colors hover:bg-accent ${
|
||||||
<span className="text-sm font-medium">
|
isInactive ? "opacity-50" : ""
|
||||||
{value.valueLabel}
|
}`}
|
||||||
</span>
|
>
|
||||||
{value.description && (
|
<Checkbox
|
||||||
<span className="text-xs text-muted-foreground">
|
checked={selectedValueIds.includes(value.valueId!)}
|
||||||
- {value.description}
|
onCheckedChange={() => handleSelectValue(value.valueId!)}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{value.isDefault && (
|
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
|
||||||
기본값
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{value.color && (
|
|
||||||
<div
|
|
||||||
className="h-4 w-4 rounded-full border"
|
|
||||||
style={{ backgroundColor: value.color }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={value.isActive !== false}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
handleToggleActive(
|
|
||||||
value.valueId!,
|
|
||||||
value.isActive !== false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="data-[state=checked]:bg-emerald-500"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<div className="flex flex-1 items-center gap-2">
|
||||||
variant="ghost"
|
{/* 색상 표시 (앞쪽으로 이동) */}
|
||||||
size="icon"
|
{value.color && (
|
||||||
onClick={() => setEditingValue(value)}
|
<div
|
||||||
className="h-8 w-8"
|
className="h-4 w-4 rounded-full border flex-shrink-0"
|
||||||
>
|
style={{ backgroundColor: value.color }}
|
||||||
<Edit2 className="h-3 w-3" />
|
/>
|
||||||
</Button>
|
)}
|
||||||
|
|
||||||
<Button
|
{/* 라벨 */}
|
||||||
variant="ghost"
|
<span className={`text-sm font-medium ${isInactive ? "line-through" : ""}`}>
|
||||||
size="icon"
|
{value.valueLabel}
|
||||||
onClick={() => handleDeleteValue(value.valueId!)}
|
</span>
|
||||||
className="h-8 w-8 text-destructive"
|
|
||||||
>
|
{/* 설명 */}
|
||||||
<Trash2 className="h-3 w-3" />
|
{value.description && (
|
||||||
</Button>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
- {value.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 기본값 배지 */}
|
||||||
|
{value.isDefault && (
|
||||||
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
|
기본값
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 비활성 배지 */}
|
||||||
|
{isInactive && (
|
||||||
|
<Badge variant="outline" className="text-[10px] text-muted-foreground">
|
||||||
|
비활성
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={value.isActive !== false}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleToggleActive(
|
||||||
|
value.valueId!,
|
||||||
|
value.isActive !== false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="data-[state=checked]:bg-emerald-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setEditingValue(value)}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Edit2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDeleteValue(value.valueId!)}
|
||||||
|
className="h-8 w-8 text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
if (!stableIdRef.current) {
|
if (!stableIdRef.current) {
|
||||||
if (modalId) {
|
if (modalId) {
|
||||||
stableIdRef.current = modalId;
|
stableIdRef.current = modalId;
|
||||||
console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
// // console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
||||||
} else {
|
} else {
|
||||||
// className 기반 ID 생성
|
// className 기반 ID 생성
|
||||||
if (className) {
|
if (className) {
|
||||||
|
|
@ -95,10 +95,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||||
}, 0);
|
}, 0);
|
||||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||||
console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
|
// console.log("🔄 ResizableDialog - className 기반 ID 생성:", { className, generatedId: stableIdRef.current });
|
||||||
className,
|
|
||||||
generatedId: stableIdRef.current,
|
|
||||||
});
|
|
||||||
} else if (userStyle) {
|
} else if (userStyle) {
|
||||||
// userStyle 기반 ID 생성
|
// userStyle 기반 ID 생성
|
||||||
const styleStr = JSON.stringify(userStyle);
|
const styleStr = JSON.stringify(userStyle);
|
||||||
|
|
@ -106,14 +103,11 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||||
}, 0);
|
}, 0);
|
||||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||||
console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
|
// console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", { userStyle, generatedId: stableIdRef.current });
|
||||||
userStyle,
|
|
||||||
generatedId: stableIdRef.current,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 기본 ID
|
// 기본 ID
|
||||||
stableIdRef.current = 'modal-default';
|
stableIdRef.current = 'modal-default';
|
||||||
console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
|
// console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,22 +165,16 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
const [wasOpen, setWasOpen] = React.useState(false);
|
const [wasOpen, setWasOpen] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log("🔍 모달 상태 변화 감지:", {
|
// console.log("🔍 모달 상태 변화 감지:", { actualOpen, wasOpen, externalOpen, contextOpen: context.open, effectiveModalId });
|
||||||
actualOpen,
|
|
||||||
wasOpen,
|
|
||||||
externalOpen,
|
|
||||||
contextOpen: context.open,
|
|
||||||
effectiveModalId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actualOpen && !wasOpen) {
|
if (actualOpen && !wasOpen) {
|
||||||
// 모달이 방금 열림
|
// 모달이 방금 열림
|
||||||
console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
// console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
||||||
setIsInitialized(false);
|
setIsInitialized(false);
|
||||||
setWasOpen(true);
|
setWasOpen(true);
|
||||||
} else if (!actualOpen && wasOpen) {
|
} else if (!actualOpen && wasOpen) {
|
||||||
// 모달이 방금 닫힘
|
// 모달이 방금 닫힘
|
||||||
console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
// console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
||||||
setWasOpen(false);
|
setWasOpen(false);
|
||||||
}
|
}
|
||||||
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
|
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
|
||||||
|
|
@ -194,11 +182,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
|
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (effectiveModalId !== lastModalId) {
|
if (effectiveModalId !== lastModalId) {
|
||||||
console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
|
// console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", { 이전: lastModalId, 현재: effectiveModalId, isInitialized });
|
||||||
이전: lastModalId,
|
|
||||||
현재: effectiveModalId,
|
|
||||||
isInitialized,
|
|
||||||
});
|
|
||||||
setIsInitialized(false);
|
setIsInitialized(false);
|
||||||
setUserResized(false); // 사용자 리사이징 플래그도 리셋
|
setUserResized(false); // 사용자 리사이징 플래그도 리셋
|
||||||
setLastModalId(effectiveModalId);
|
setLastModalId(effectiveModalId);
|
||||||
|
|
@ -207,11 +191,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
|
|
||||||
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
|
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log("🔍 초기 크기 설정 useEffect 실행:", {
|
// console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
|
||||||
isInitialized,
|
|
||||||
hasContentRef: !!contentRef.current,
|
|
||||||
effectiveModalId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
|
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
|
||||||
|
|
@ -231,22 +211,9 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
contentWidth = contentRef.current.scrollWidth || defaultWidth;
|
contentWidth = contentRef.current.scrollWidth || defaultWidth;
|
||||||
contentHeight = contentRef.current.scrollHeight || defaultHeight;
|
contentHeight = contentRef.current.scrollHeight || defaultHeight;
|
||||||
|
|
||||||
console.log("📏 모달 내용 크기 측정:", {
|
// console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
|
||||||
attempt: attempts,
|
|
||||||
scrollWidth: contentRef.current.scrollWidth,
|
|
||||||
scrollHeight: contentRef.current.scrollHeight,
|
|
||||||
clientWidth: contentRef.current.clientWidth,
|
|
||||||
clientHeight: contentRef.current.clientHeight,
|
|
||||||
contentWidth,
|
|
||||||
contentHeight,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ contentRef 없음, 재시도:", {
|
// console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
|
||||||
attempt: attempts,
|
|
||||||
maxAttempts,
|
|
||||||
defaultWidth,
|
|
||||||
defaultHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
// contentRef가 아직 없으면 재시도
|
// contentRef가 아직 없으면 재시도
|
||||||
if (attempts < maxAttempts) {
|
if (attempts < maxAttempts) {
|
||||||
|
|
@ -265,7 +232,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
|
height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📐 내용 기반 크기:", contentBasedSize);
|
// console.log("📐 내용 기반 크기:", contentBasedSize);
|
||||||
|
|
||||||
// localStorage에서 저장된 크기 확인
|
// localStorage에서 저장된 크기 확인
|
||||||
let finalSize = contentBasedSize;
|
let finalSize = contentBasedSize;
|
||||||
|
|
@ -275,12 +242,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
||||||
const saved = localStorage.getItem(storageKey);
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
console.log("📦 localStorage 확인:", {
|
// console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
|
||||||
effectiveModalId,
|
|
||||||
userId,
|
|
||||||
storageKey,
|
|
||||||
saved: saved ? "있음" : "없음",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
|
|
@ -292,27 +254,22 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
|
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
|
// console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
|
||||||
|
|
||||||
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
|
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
|
||||||
// (사용자가 의도적으로 작게 만든 것을 존중)
|
// (사용자가 의도적으로 작게 만든 것을 존중)
|
||||||
finalSize = savedSize;
|
finalSize = savedSize;
|
||||||
setUserResized(true);
|
setUserResized(true);
|
||||||
|
|
||||||
console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", {
|
// console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
|
||||||
savedSize,
|
|
||||||
contentBasedSize,
|
|
||||||
finalSize,
|
|
||||||
note: "사용자가 리사이징한 크기를 그대로 사용합니다",
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
|
// console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
|
// console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 모달 크기 복원 실패:", error);
|
// console.error("❌ 모달 크기 복원 실패:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,15 +341,9 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
userResized: true, // 사용자가 직접 리사이징했음을 표시
|
userResized: true, // 사용자가 직접 리사이징했음을 표시
|
||||||
};
|
};
|
||||||
localStorage.setItem(storageKey, JSON.stringify(currentSize));
|
localStorage.setItem(storageKey, JSON.stringify(currentSize));
|
||||||
console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
|
// console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", { effectiveModalId, userId, storageKey, size: currentSize, stateSize: { width: size.width, height: size.height } });
|
||||||
effectiveModalId,
|
|
||||||
userId,
|
|
||||||
storageKey,
|
|
||||||
size: currentSize,
|
|
||||||
stateSize: { width: size.width, height: size.height },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 모달 크기 저장 실패:", error);
|
// console.error("❌ 모달 크기 저장 실패:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,14 @@ export interface TableListComponentProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
screenId?: string;
|
screenId?: string;
|
||||||
userId?: string; // 사용자 ID (컬럼 순서 저장용)
|
userId?: string; // 사용자 ID (컬럼 순서 저장용)
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
onSelectedRowsChange?: (
|
||||||
|
selectedRows: any[],
|
||||||
|
selectedRowsData: any[],
|
||||||
|
sortBy?: string,
|
||||||
|
sortOrder?: "asc" | "desc",
|
||||||
|
columnOrder?: string[],
|
||||||
|
tableDisplayData?: any[],
|
||||||
|
) => void;
|
||||||
onConfigChange?: (config: any) => void;
|
onConfigChange?: (config: any) => void;
|
||||||
refreshKey?: number;
|
refreshKey?: number;
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +252,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
||||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
||||||
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
||||||
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({});
|
const [columnMeta, setColumnMeta] = useState<
|
||||||
|
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
||||||
|
>({});
|
||||||
|
const [categoryMappings, setCategoryMappings] = useState<
|
||||||
|
Record<string, Record<string, { label: string; color?: string }>>
|
||||||
|
>({});
|
||||||
|
const [categoryMappingsKey, setCategoryMappingsKey] = useState(0); // 강제 리렌더링용
|
||||||
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||||
|
|
@ -275,7 +288,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tableConfig.selectedTable || !userId) return;
|
if (!tableConfig.selectedTable || !userId) return;
|
||||||
|
|
||||||
const userKey = userId || 'guest';
|
const userKey = userId || "guest";
|
||||||
const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`;
|
const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`;
|
||||||
const savedOrder = localStorage.getItem(storageKey);
|
const savedOrder = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
|
@ -313,9 +326,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
tableDisplayStore.setTableData(
|
tableDisplayStore.setTableData(
|
||||||
tableConfig.selectedTable,
|
tableConfig.selectedTable,
|
||||||
initialData,
|
initialData,
|
||||||
parsedOrder.filter(col => col !== '__checkbox__'),
|
parsedOrder.filter((col) => col !== "__checkbox__"),
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortDirection
|
sortDirection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,13 +358,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const cached = tableColumnCache.get(cacheKey);
|
const cached = tableColumnCache.get(cacheKey);
|
||||||
if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
|
if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
|
||||||
const labels: Record<string, string> = {};
|
const labels: Record<string, string> = {};
|
||||||
const meta: Record<string, { webType?: string; codeCategory?: string }> = {};
|
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string }> = {};
|
||||||
|
|
||||||
|
// 캐시된 inputTypes 맵 생성
|
||||||
|
const inputTypeMap: Record<string, string> = {};
|
||||||
|
if (cached.inputTypes) {
|
||||||
|
cached.inputTypes.forEach((col: any) => {
|
||||||
|
inputTypeMap[col.columnName] = col.inputType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cached.columns.forEach((col: any) => {
|
cached.columns.forEach((col: any) => {
|
||||||
labels[col.columnName] = col.displayName || col.comment || col.columnName;
|
labels[col.columnName] = col.displayName || col.comment || col.columnName;
|
||||||
meta[col.columnName] = {
|
meta[col.columnName] = {
|
||||||
webType: col.webType,
|
webType: col.webType,
|
||||||
codeCategory: col.codeCategory,
|
codeCategory: col.codeCategory,
|
||||||
|
inputType: inputTypeMap[col.columnName], // 캐시된 inputType 사용!
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -429,6 +451,135 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}, [tableConfig.selectedTable]);
|
}, [tableConfig.selectedTable]);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 카테고리 값 매핑 로드
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 카테고리 컬럼 목록 추출 (useMemo로 최적화)
|
||||||
|
const categoryColumns = useMemo(() => {
|
||||||
|
const cols = Object.entries(columnMeta)
|
||||||
|
.filter(([_, meta]) => meta.inputType === "category")
|
||||||
|
.map(([columnName, _]) => columnName);
|
||||||
|
|
||||||
|
console.log("🔍 [TableList] 카테고리 컬럼 추출:", {
|
||||||
|
columnMeta,
|
||||||
|
categoryColumns: cols,
|
||||||
|
columnMetaKeys: Object.keys(columnMeta),
|
||||||
|
});
|
||||||
|
|
||||||
|
return cols;
|
||||||
|
}, [columnMeta]);
|
||||||
|
|
||||||
|
// 카테고리 매핑 로드 (columnMeta 변경 시 즉시 실행)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCategoryMappings = async () => {
|
||||||
|
console.log("🔄 [TableList] loadCategoryMappings 트리거:", {
|
||||||
|
hasTable: !!tableConfig.selectedTable,
|
||||||
|
table: tableConfig.selectedTable,
|
||||||
|
categoryColumnsLength: categoryColumns.length,
|
||||||
|
categoryColumns,
|
||||||
|
columnMetaKeys: Object.keys(columnMeta),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tableConfig.selectedTable) {
|
||||||
|
console.log("⏭️ [TableList] 테이블 선택 안됨, 카테고리 매핑 로드 스킵");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryColumns.length === 0) {
|
||||||
|
console.log("⏭️ [TableList] 카테고리 컬럼 없음, 카테고리 매핑 로드 스킵");
|
||||||
|
setCategoryMappings({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🚀 [TableList] 카테고리 매핑 로드 시작:", {
|
||||||
|
table: tableConfig.selectedTable,
|
||||||
|
categoryColumns,
|
||||||
|
columnMetaKeys: Object.keys(columnMeta),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
|
||||||
|
|
||||||
|
for (const columnName of categoryColumns) {
|
||||||
|
try {
|
||||||
|
console.log(`📡 [TableList] API 호출 시작 [${columnName}]:`, {
|
||||||
|
url: `/table-categories/${tableConfig.selectedTable}/${columnName}/values`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiClient = (await import("@/lib/api/client")).apiClient;
|
||||||
|
const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`);
|
||||||
|
|
||||||
|
console.log(`📡 [TableList] API 응답 [${columnName}]:`, {
|
||||||
|
success: response.data.success,
|
||||||
|
dataLength: response.data.data?.length,
|
||||||
|
rawData: response.data,
|
||||||
|
items: response.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
||||||
|
response.data.data.forEach((item: any) => {
|
||||||
|
// valueCode를 문자열로 변환하여 키로 사용
|
||||||
|
const key = String(item.valueCode);
|
||||||
|
mapping[key] = {
|
||||||
|
label: item.valueLabel,
|
||||||
|
color: item.color,
|
||||||
|
};
|
||||||
|
console.log(` 🔑 [${columnName}] "${key}" => "${item.valueLabel}" (색상: ${item.color})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mapping).length > 0) {
|
||||||
|
mappings[columnName] = mapping;
|
||||||
|
console.log(`✅ [TableList] 카테고리 매핑 로드 완료 [${columnName}]:`, {
|
||||||
|
columnName,
|
||||||
|
mappingCount: Object.keys(mapping).length,
|
||||||
|
mappingKeys: Object.keys(mapping),
|
||||||
|
mapping,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ [TableList] 매핑 데이터가 비어있음 [${columnName}]`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ [TableList] 카테고리 값 없음 [${columnName}]:`, {
|
||||||
|
success: response.data.success,
|
||||||
|
hasData: !!response.data.data,
|
||||||
|
isArray: Array.isArray(response.data.data),
|
||||||
|
response: response.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ [TableList] 카테고리 값 로드 실패 [${columnName}]:`, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
response: error.response?.data,
|
||||||
|
status: error.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 [TableList] 전체 카테고리 매핑 설정:", {
|
||||||
|
mappingsCount: Object.keys(mappings).length,
|
||||||
|
mappingsKeys: Object.keys(mappings),
|
||||||
|
mappings,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mappings).length > 0) {
|
||||||
|
setCategoryMappings(mappings);
|
||||||
|
setCategoryMappingsKey((prev) => prev + 1);
|
||||||
|
console.log("✅ [TableList] setCategoryMappings 호출 완료");
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ [TableList] 매핑이 비어있어 상태 업데이트 스킵");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ [TableList] 카테고리 매핑 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCategoryMappings();
|
||||||
|
}, [tableConfig.selectedTable, categoryColumns.length, JSON.stringify(categoryColumns)]); // 더 명확한 의존성
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// 데이터 가져오기
|
// 데이터 가져오기
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
@ -585,7 +736,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const bStr = String(bVal).toLowerCase();
|
const bStr = String(bVal).toLowerCase();
|
||||||
|
|
||||||
// 자연스러운 정렬 (숫자 포함 문자열)
|
// 자연스러운 정렬 (숫자 포함 문자열)
|
||||||
const comparison = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: 'base' });
|
const comparison = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: "base" });
|
||||||
return newSortDirection === "desc" ? -comparison : comparison;
|
return newSortDirection === "desc" ? -comparison : comparison;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -614,7 +765,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
|
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
|
||||||
tableDisplayDataCount: reorderedData.length,
|
tableDisplayDataCount: reorderedData.length,
|
||||||
firstRowAfterSort: reorderedData[0]?.[newSortColumn],
|
firstRowAfterSort: reorderedData[0]?.[newSortColumn],
|
||||||
lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn]
|
lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn],
|
||||||
});
|
});
|
||||||
onSelectedRowsChange(
|
onSelectedRowsChange(
|
||||||
Array.from(selectedRows),
|
Array.from(selectedRows),
|
||||||
|
|
@ -622,18 +773,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
newSortColumn,
|
newSortColumn,
|
||||||
newSortDirection,
|
newSortDirection,
|
||||||
columnOrder.length > 0 ? columnOrder : undefined,
|
columnOrder.length > 0 ? columnOrder : undefined,
|
||||||
reorderedData
|
reorderedData,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 전역 저장소에 정렬된 데이터 저장
|
// 전역 저장소에 정렬된 데이터 저장
|
||||||
if (tableConfig.selectedTable) {
|
if (tableConfig.selectedTable) {
|
||||||
const cleanColumnOrder = (columnOrder.length > 0 ? columnOrder : visibleColumns.map(c => c.columnName)).filter(col => col !== '__checkbox__');
|
const cleanColumnOrder = (
|
||||||
|
columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName)
|
||||||
|
).filter((col) => col !== "__checkbox__");
|
||||||
tableDisplayStore.setTableData(
|
tableDisplayStore.setTableData(
|
||||||
tableConfig.selectedTable,
|
tableConfig.selectedTable,
|
||||||
reorderedData,
|
reorderedData,
|
||||||
cleanColumnOrder,
|
cleanColumnOrder,
|
||||||
newSortColumn,
|
newSortColumn,
|
||||||
newSortDirection
|
newSortDirection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -774,16 +927,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// columnOrder 상태가 있으면 그 순서대로 정렬
|
// columnOrder 상태가 있으면 그 순서대로 정렬
|
||||||
if (columnOrder.length > 0) {
|
if (columnOrder.length > 0) {
|
||||||
const orderedCols = columnOrder
|
const orderedCols = columnOrder
|
||||||
.map(colName => cols.find(c => c.columnName === colName))
|
.map((colName) => cols.find((c) => c.columnName === colName))
|
||||||
.filter(Boolean) as ColumnConfig[];
|
.filter(Boolean) as ColumnConfig[];
|
||||||
|
|
||||||
// columnOrder에 없는 새로운 컬럼들 추가
|
// columnOrder에 없는 새로운 컬럼들 추가
|
||||||
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
|
const remainingCols = cols.filter((c) => !columnOrder.includes(c.columnName));
|
||||||
|
|
||||||
console.log("🔄 columnOrder 기반 정렬:", {
|
console.log("🔄 columnOrder 기반 정렬:", {
|
||||||
columnOrder,
|
columnOrder,
|
||||||
orderedColsCount: orderedCols.length,
|
orderedColsCount: orderedCols.length,
|
||||||
remainingColsCount: remainingCols.length
|
remainingColsCount: remainingCols.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...orderedCols, ...remainingCols];
|
return [...orderedCols, ...remainingCols];
|
||||||
|
|
@ -799,7 +952,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
|
console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
|
||||||
hasCallback: !!onSelectedRowsChange,
|
hasCallback: !!onSelectedRowsChange,
|
||||||
visibleColumnsLength: visibleColumns.length,
|
visibleColumnsLength: visibleColumns.length,
|
||||||
visibleColumnsNames: visibleColumns.map(c => c.columnName),
|
visibleColumnsNames: visibleColumns.map((c) => c.columnName),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!onSelectedRowsChange) {
|
if (!onSelectedRowsChange) {
|
||||||
|
|
@ -812,9 +965,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentColumnOrder = visibleColumns
|
const currentColumnOrder = visibleColumns.map((col) => col.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 컬럼 제외
|
||||||
.map(col => col.columnName)
|
|
||||||
.filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외
|
|
||||||
|
|
||||||
console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);
|
console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);
|
||||||
|
|
||||||
|
|
@ -860,9 +1011,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
currentColumnOrder,
|
currentColumnOrder,
|
||||||
reorderedData
|
reorderedData,
|
||||||
);
|
);
|
||||||
}, [visibleColumns.length, visibleColumns.map(c => c.columnName).join(",")]); // 의존성 단순화
|
}, [visibleColumns.length, visibleColumns.map((c) => c.columnName).join(",")]); // 의존성 단순화
|
||||||
|
|
||||||
const getColumnWidth = (column: ColumnConfig) => {
|
const getColumnWidth = (column: ColumnConfig) => {
|
||||||
if (column.columnName === "__checkbox__") return 50;
|
if (column.columnName === "__checkbox__") return 50;
|
||||||
|
|
@ -936,14 +1087,51 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="이미지"
|
alt="이미지"
|
||||||
className="h-10 w-10 object-cover rounded"
|
className="h-10 w-10 rounded object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E";
|
e.currentTarget.src =
|
||||||
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 카테고리 타입: 배지로 표시
|
||||||
|
if (inputType === "category") {
|
||||||
|
if (!value) return "";
|
||||||
|
|
||||||
|
const mapping = categoryMappings[column.columnName];
|
||||||
|
const categoryData = mapping?.[String(value)];
|
||||||
|
|
||||||
|
console.log(`🎨 [카테고리 배지] ${column.columnName}:`, {
|
||||||
|
value,
|
||||||
|
stringValue: String(value),
|
||||||
|
mapping,
|
||||||
|
categoryData,
|
||||||
|
hasMapping: !!mapping,
|
||||||
|
hasCategoryData: !!categoryData,
|
||||||
|
allCategoryMappings: categoryMappings, // 전체 매핑 확인
|
||||||
|
categoryMappingsKeys: Object.keys(categoryMappings),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
|
||||||
|
const displayLabel = categoryData?.label || String(value);
|
||||||
|
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||||
|
|
||||||
|
const { Badge } = require("@/components/ui/badge");
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
style={{
|
||||||
|
backgroundColor: displayColor,
|
||||||
|
borderColor: displayColor,
|
||||||
|
}}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
{displayLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 코드 타입: 코드 값 → 코드명 변환
|
// 코드 타입: 코드 값 → 코드명 변환
|
||||||
if (inputType === "code" && meta?.codeCategory && value) {
|
if (inputType === "code" && meta?.codeCategory && value) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1000,7 +1188,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[columnMeta, optimizedConvertCode],
|
[columnMeta, optimizedConvertCode, categoryMappings],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
@ -1119,46 +1307,49 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 사용자 옵션 저장 핸들러
|
// 사용자 옵션 저장 핸들러
|
||||||
const handleTableOptionsSave = useCallback((config: {
|
const handleTableOptionsSave = useCallback(
|
||||||
columns: Array<{ columnName: string; label: string; visible: boolean; width?: number; frozen?: boolean }>;
|
(config: {
|
||||||
showGridLines: boolean;
|
columns: Array<{ columnName: string; label: string; visible: boolean; width?: number; frozen?: boolean }>;
|
||||||
viewMode: "table" | "card" | "grouped-card";
|
showGridLines: boolean;
|
||||||
}) => {
|
viewMode: "table" | "card" | "grouped-card";
|
||||||
// 컬럼 순서 업데이트
|
}) => {
|
||||||
const newColumnOrder = config.columns.map(col => col.columnName);
|
// 컬럼 순서 업데이트
|
||||||
setColumnOrder(newColumnOrder);
|
const newColumnOrder = config.columns.map((col) => col.columnName);
|
||||||
|
setColumnOrder(newColumnOrder);
|
||||||
|
|
||||||
// 컬럼 너비 업데이트
|
// 컬럼 너비 업데이트
|
||||||
const newWidths: Record<string, number> = {};
|
const newWidths: Record<string, number> = {};
|
||||||
config.columns.forEach(col => {
|
config.columns.forEach((col) => {
|
||||||
if (col.width) {
|
if (col.width) {
|
||||||
newWidths[col.columnName] = col.width;
|
newWidths[col.columnName] = col.width;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setColumnWidths(newWidths);
|
setColumnWidths(newWidths);
|
||||||
|
|
||||||
// 틀고정 컬럼 업데이트
|
// 틀고정 컬럼 업데이트
|
||||||
const newFrozenColumns = config.columns.filter(col => col.frozen).map(col => col.columnName);
|
const newFrozenColumns = config.columns.filter((col) => col.frozen).map((col) => col.columnName);
|
||||||
setFrozenColumns(newFrozenColumns);
|
setFrozenColumns(newFrozenColumns);
|
||||||
|
|
||||||
// 그리드선 표시 업데이트
|
// 그리드선 표시 업데이트
|
||||||
setShowGridLines(config.showGridLines);
|
setShowGridLines(config.showGridLines);
|
||||||
|
|
||||||
// 보기 모드 업데이트
|
// 보기 모드 업데이트
|
||||||
setViewMode(config.viewMode);
|
setViewMode(config.viewMode);
|
||||||
|
|
||||||
// 컬럼 표시/숨기기 업데이트
|
// 컬럼 표시/숨기기 업데이트
|
||||||
const newDisplayColumns = displayColumns.map(col => {
|
const newDisplayColumns = displayColumns.map((col) => {
|
||||||
const configCol = config.columns.find(c => c.columnName === col.columnName);
|
const configCol = config.columns.find((c) => c.columnName === col.columnName);
|
||||||
if (configCol) {
|
if (configCol) {
|
||||||
return { ...col, visible: configCol.visible };
|
return { ...col, visible: configCol.visible };
|
||||||
}
|
}
|
||||||
return col;
|
return col;
|
||||||
});
|
});
|
||||||
setDisplayColumns(newDisplayColumns);
|
setDisplayColumns(newDisplayColumns);
|
||||||
|
|
||||||
toast.success("테이블 옵션이 저장되었습니다");
|
toast.success("테이블 옵션이 저장되었습니다");
|
||||||
}, [displayColumns]);
|
},
|
||||||
|
[displayColumns],
|
||||||
|
);
|
||||||
|
|
||||||
// 그룹 펼치기/접기 토글
|
// 그룹 펼치기/접기 토글
|
||||||
const toggleGroupCollapse = useCallback((groupKey: string) => {
|
const toggleGroupCollapse = useCallback((groupKey: string) => {
|
||||||
|
|
@ -1307,13 +1498,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
if (!tableConfig.pagination?.enabled || isDesignMode) return null;
|
if (!tableConfig.pagination?.enabled || isDesignMode) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="border-border bg-background relative flex h-14 w-full flex-shrink-0 items-center justify-center border-t-2 px-4 sm:h-[60px] sm:px-6">
|
||||||
className="w-full h-14 flex items-center justify-center relative border-t-2 border-border bg-background px-4 flex-shrink-0 sm:h-[60px] sm:px-6"
|
|
||||||
>
|
|
||||||
{/* 중앙 페이지네이션 컨트롤 */}
|
{/* 중앙 페이지네이션 컨트롤 */}
|
||||||
<div
|
<div className="flex items-center gap-2 sm:gap-4">
|
||||||
className="flex items-center gap-2 sm:gap-4"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1333,7 +1520,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<ChevronLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
<ChevronLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span className="text-xs font-medium text-foreground min-w-[60px] text-center sm:text-sm sm:min-w-[80px]">
|
<span className="text-foreground min-w-[60px] text-center text-xs font-medium sm:min-w-[80px] sm:text-sm">
|
||||||
{currentPage} / {totalPages || 1}
|
{currentPage} / {totalPages || 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
@ -1356,7 +1543,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span className="text-[10px] text-muted-foreground ml-2 sm:text-xs sm:ml-4">
|
<span className="text-muted-foreground ml-2 text-[10px] sm:ml-4 sm:text-xs">
|
||||||
전체 {totalItems.toLocaleString()}개
|
전체 {totalItems.toLocaleString()}개
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1409,7 +1596,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return (
|
return (
|
||||||
<div {...domProps}>
|
<div {...domProps}>
|
||||||
{tableConfig.filter?.enabled && (
|
{tableConfig.filter?.enabled && (
|
||||||
<div className="px-4 py-2 border-b border-border sm:px-6 sm:py-2">
|
<div className="border-border border-b px-4 py-2 sm:px-6 sm:py-2">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<AdvancedSearchFilters
|
<AdvancedSearchFilters
|
||||||
|
|
@ -1425,7 +1612,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsTableOptionsOpen(true)}
|
onClick={() => setIsTableOptionsOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<TableIcon className="mr-2 h-4 w-4" />
|
<TableIcon className="mr-2 h-4 w-4" />
|
||||||
테이블 옵션
|
테이블 옵션
|
||||||
|
|
@ -1434,7 +1621,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFilterSettingOpen(true)}
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
필터 설정
|
필터 설정
|
||||||
|
|
@ -1443,7 +1630,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsGroupSettingOpen(true)}
|
onClick={() => setIsGroupSettingOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Layers className="mr-2 h-4 w-4" />
|
<Layers className="mr-2 h-4 w-4" />
|
||||||
그룹 설정
|
그룹 설정
|
||||||
|
|
@ -1455,7 +1642,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
{/* 그룹 표시 배지 */}
|
{/* 그룹 표시 배지 */}
|
||||||
{groupByColumns.length > 0 && (
|
{groupByColumns.length > 0 && (
|
||||||
<div className="px-4 py-1.5 border-b border-border bg-muted/30 sm:px-6">
|
<div className="border-border bg-muted/30 border-b px-4 py-1.5 sm:px-6">
|
||||||
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
||||||
<span className="text-muted-foreground">그룹:</span>
|
<span className="text-muted-foreground">그룹:</span>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
|
@ -1516,7 +1703,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<div {...domProps}>
|
<div {...domProps}>
|
||||||
{/* 필터 */}
|
{/* 필터 */}
|
||||||
{tableConfig.filter?.enabled && (
|
{tableConfig.filter?.enabled && (
|
||||||
<div className="px-4 py-3 border-b border-border flex-shrink-0 sm:px-6 sm:py-4">
|
<div className="border-border flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<AdvancedSearchFilters
|
<AdvancedSearchFilters
|
||||||
|
|
@ -1532,7 +1719,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsTableOptionsOpen(true)}
|
onClick={() => setIsTableOptionsOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<TableIcon className="mr-2 h-4 w-4" />
|
<TableIcon className="mr-2 h-4 w-4" />
|
||||||
테이블 옵션
|
테이블 옵션
|
||||||
|
|
@ -1541,7 +1728,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFilterSettingOpen(true)}
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
필터 설정
|
필터 설정
|
||||||
|
|
@ -1550,7 +1737,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsGroupSettingOpen(true)}
|
onClick={() => setIsGroupSettingOpen(true)}
|
||||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Layers className="mr-2 h-4 w-4" />
|
<Layers className="mr-2 h-4 w-4" />
|
||||||
그룹 설정
|
그룹 설정
|
||||||
|
|
@ -1562,7 +1749,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
{/* 그룹 표시 배지 */}
|
{/* 그룹 표시 배지 */}
|
||||||
{groupByColumns.length > 0 && (
|
{groupByColumns.length > 0 && (
|
||||||
<div className="px-4 py-2 border-b border-border bg-muted/30 sm:px-6">
|
<div className="border-border bg-muted/30 border-b px-4 py-2 sm:px-6">
|
||||||
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
||||||
<span className="text-muted-foreground">그룹:</span>
|
<span className="text-muted-foreground">그룹:</span>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
|
@ -1588,20 +1775,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
{/* 테이블 컨테이너 */}
|
{/* 테이블 컨테이너 */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 flex flex-col overflow-hidden w-full max-w-full"
|
className="flex w-full max-w-full flex-1 flex-col overflow-hidden"
|
||||||
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px` }}
|
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px` }}
|
||||||
>
|
>
|
||||||
{/* 스크롤 영역 */}
|
{/* 스크롤 영역 */}
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
|
className="bg-background h-[400px] w-full max-w-full overflow-x-auto overflow-y-scroll sm:h-[500px]"
|
||||||
style={{ position: "relative" }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<table
|
<table
|
||||||
className={cn(
|
className={cn("table-mobile-fixed w-full max-w-full", !showGridLines && "hide-grid")}
|
||||||
"w-full max-w-full table-mobile-fixed",
|
|
||||||
!showGridLines && "hide-grid"
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
borderCollapse: "collapse",
|
borderCollapse: "collapse",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -1619,7 +1803,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<tr
|
<tr
|
||||||
className="h-10 border-b-2 border-primary/20 bg-muted sm:h-12"
|
className="border-primary/20 bg-muted h-10 border-b-2 sm:h-12"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "hsl(var(--muted))",
|
backgroundColor: "hsl(var(--muted))",
|
||||||
}}
|
}}
|
||||||
|
|
@ -1644,19 +1828,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
ref={(el) => (columnRefs.current[column.columnName] = el)}
|
ref={(el) => (columnRefs.current[column.columnName] = el)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-8 text-xs font-bold text-foreground/90 overflow-hidden text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm",
|
"text-foreground/90 relative h-8 overflow-hidden text-xs font-bold text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm",
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
||||||
(column.sortable !== false && column.columnName !== "__checkbox__") && "cursor-pointer hover:bg-muted/70 transition-colors",
|
column.sortable !== false &&
|
||||||
isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]"
|
column.columnName !== "__checkbox__" &&
|
||||||
|
"hover:bg-muted/70 cursor-pointer transition-colors",
|
||||||
|
isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: column.columnName === "__checkbox__" ? "center" : "center",
|
textAlign: column.columnName === "__checkbox__" ? "center" : "center",
|
||||||
width: column.columnName === "__checkbox__" ? '48px' : (columnWidth ? `${columnWidth}px` : undefined),
|
width:
|
||||||
minWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
column.columnName === "__checkbox__"
|
||||||
maxWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
? "48px"
|
||||||
userSelect: 'none',
|
: columnWidth
|
||||||
|
? `${columnWidth}px`
|
||||||
|
: undefined,
|
||||||
|
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
|
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
|
userSelect: "none",
|
||||||
backgroundColor: "hsl(var(--muted))",
|
backgroundColor: "hsl(var(--muted))",
|
||||||
...(isFrozen && { left: `${leftPosition}px` })
|
...(isFrozen && { left: `${leftPosition}px` }),
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isResizing.current) return;
|
if (isResizing.current) return;
|
||||||
|
|
@ -1678,8 +1869,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 리사이즈 핸들 (체크박스 제외) */}
|
{/* 리사이즈 핸들 (체크박스 제외) */}
|
||||||
{columnIndex < visibleColumns.length - 1 && column.columnName !== "__checkbox__" && (
|
{columnIndex < visibleColumns.length - 1 && column.columnName !== "__checkbox__" && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 h-full w-2 cursor-col-resize hover:bg-blue-500 z-20"
|
className="absolute top-0 right-0 z-20 h-full w-2 cursor-col-resize hover:bg-blue-500"
|
||||||
style={{ marginRight: '-4px', paddingLeft: '4px', paddingRight: '4px' }}
|
style={{ marginRight: "-4px", paddingLeft: "4px", paddingRight: "4px" }}
|
||||||
onClick={(e) => e.stopPropagation()} // 정렬 클릭 방지
|
onClick={(e) => e.stopPropagation()} // 정렬 클릭 방지
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1694,8 +1885,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const startWidth = columnWidth || thElement.offsetWidth;
|
const startWidth = columnWidth || thElement.offsetWidth;
|
||||||
|
|
||||||
// 드래그 중 텍스트 선택 방지
|
// 드래그 중 텍스트 선택 방지
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = "none";
|
||||||
document.body.style.cursor = 'col-resize';
|
document.body.style.cursor = "col-resize";
|
||||||
|
|
||||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
moveEvent.preventDefault();
|
moveEvent.preventDefault();
|
||||||
|
|
@ -1713,24 +1904,24 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 최종 너비를 state에 저장
|
// 최종 너비를 state에 저장
|
||||||
if (thElement) {
|
if (thElement) {
|
||||||
const finalWidth = Math.max(80, thElement.offsetWidth);
|
const finalWidth = Math.max(80, thElement.offsetWidth);
|
||||||
setColumnWidths(prev => ({ ...prev, [column.columnName]: finalWidth }));
|
setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 텍스트 선택 복원
|
// 텍스트 선택 복원
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = "";
|
||||||
document.body.style.cursor = '';
|
document.body.style.cursor = "";
|
||||||
|
|
||||||
// 약간의 지연 후 리사이즈 플래그 해제 (클릭 이벤트가 먼저 처리되지 않도록)
|
// 약간의 지연 후 리사이즈 플래그 해제 (클릭 이벤트가 먼저 처리되지 않도록)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isResizing.current = false;
|
isResizing.current = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1741,13 +1932,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
{/* 바디 (스크롤) */}
|
{/* 바디 (스크롤) */}
|
||||||
<tbody style={{ position: "relative" }}>
|
<tbody key={`tbody-${categoryMappingsKey}`} style={{ position: "relative" }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
<RefreshCw className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
<div className="text-sm font-medium text-muted-foreground">로딩 중...</div>
|
<div className="text-muted-foreground text-sm font-medium">로딩 중...</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1755,8 +1946,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="text-sm font-medium text-destructive">오류 발생</div>
|
<div className="text-destructive text-sm font-medium">오류 발생</div>
|
||||||
<div className="text-xs text-muted-foreground">{error}</div>
|
<div className="text-muted-foreground text-xs">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1764,9 +1955,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<TableIcon className="h-12 w-12 text-muted-foreground/50" />
|
<TableIcon className="text-muted-foreground/50 h-12 w-12" />
|
||||||
<div className="text-sm font-medium text-muted-foreground">데이터가 없습니다</div>
|
<div className="text-muted-foreground text-sm font-medium">데이터가 없습니다</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-muted-foreground text-xs">
|
||||||
조건을 변경하거나 새로운 데이터를 추가해보세요
|
조건을 변경하거나 새로운 데이터를 추가해보세요
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1782,11 +1973,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={visibleColumns.length}
|
colSpan={visibleColumns.length}
|
||||||
className="bg-muted/50 border-b border-border sticky top-[48px] z-[5]"
|
className="bg-muted/50 border-border sticky top-[48px] z-[5] border-b"
|
||||||
style={{ top: "48px" }}
|
style={{ top: "48px" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted"
|
className="hover:bg-muted flex cursor-pointer items-center gap-3 p-3"
|
||||||
onClick={() => toggleGroupCollapse(group.groupKey)}
|
onClick={() => toggleGroupCollapse(group.groupKey)}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
|
|
@ -1794,7 +1985,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium text-sm flex-1">{group.groupKey}</span>
|
<span className="flex-1 text-sm font-medium">{group.groupKey}</span>
|
||||||
<span className="text-muted-foreground text-xs">({group.count}건)</span>
|
<span className="text-muted-foreground text-xs">({group.count}건)</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -1802,57 +1993,65 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 그룹 데이터 */}
|
{/* 그룹 데이터 */}
|
||||||
{!isCollapsed &&
|
{!isCollapsed &&
|
||||||
group.items.map((row, index) => (
|
group.items.map((row, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer sm:h-12"
|
"bg-background hover:bg-muted/50 h-10 cursor-pointer border-b transition-colors sm:h-12",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick(row, index, e)}
|
onClick={(e) => handleRowClick(row, index, e)}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, colIndex) => {
|
{visibleColumns.map((column, colIndex) => {
|
||||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||||
const cellValue = row[mappedColumnName];
|
const cellValue = row[mappedColumnName];
|
||||||
|
|
||||||
const meta = columnMeta[column.columnName];
|
const meta = columnMeta[column.columnName];
|
||||||
const inputType = meta?.inputType || column.inputType;
|
const inputType = meta?.inputType || column.inputType;
|
||||||
const isNumeric = inputType === "number" || inputType === "decimal";
|
const isNumeric = inputType === "number" || inputType === "decimal";
|
||||||
|
|
||||||
const isFrozen = frozenColumns.includes(column.columnName);
|
const isFrozen = frozenColumns.includes(column.columnName);
|
||||||
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
||||||
|
|
||||||
// 틀고정된 컬럼의 left 위치 계산
|
// 틀고정된 컬럼의 left 위치 계산
|
||||||
let leftPosition = 0;
|
let leftPosition = 0;
|
||||||
if (isFrozen && frozenIndex > 0) {
|
if (isFrozen && frozenIndex > 0) {
|
||||||
for (let i = 0; i < frozenIndex; i++) {
|
for (let i = 0; i < frozenIndex; i++) {
|
||||||
const frozenCol = frozenColumns[i];
|
const frozenCol = frozenColumns[i];
|
||||||
const frozenColWidth = columnWidths[frozenCol] || 150;
|
const frozenColWidth = columnWidths[frozenCol] || 150;
|
||||||
leftPosition += frozenColWidth;
|
leftPosition += frozenColWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 text-xs text-foreground overflow-hidden text-ellipsis whitespace-nowrap sm:h-12 sm:text-sm",
|
"text-foreground h-10 overflow-hidden text-xs text-ellipsis whitespace-nowrap sm:h-12 sm:text-sm",
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
||||||
isFrozen && "sticky z-10 bg-background shadow-[2px_0_4px_rgba(0,0,0,0.05)]"
|
isFrozen && "bg-background sticky z-10 shadow-[2px_0_4px_rgba(0,0,0,0.05)]",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: column.columnName === "__checkbox__" ? "center" : (isNumeric ? "right" : (column.align || "left")),
|
textAlign:
|
||||||
width: column.columnName === "__checkbox__" ? '48px' : `${100 / visibleColumns.length}%`,
|
column.columnName === "__checkbox__"
|
||||||
minWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
? "center"
|
||||||
maxWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
: isNumeric
|
||||||
...(isFrozen && { left: `${leftPosition}px` })
|
? "right"
|
||||||
}}
|
: column.align || "left",
|
||||||
>
|
width:
|
||||||
{column.columnName === "__checkbox__"
|
column.columnName === "__checkbox__"
|
||||||
? renderCheckboxCell(row, index)
|
? "48px"
|
||||||
: formatCellValue(cellValue, column, row)}
|
: `${100 / visibleColumns.length}%`,
|
||||||
</td>
|
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
);
|
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
})}
|
...(isFrozen && { left: `${leftPosition}px` }),
|
||||||
</tr>
|
}}
|
||||||
|
>
|
||||||
|
{column.columnName === "__checkbox__"
|
||||||
|
? renderCheckboxCell(row, index)
|
||||||
|
: formatCellValue(cellValue, column, row)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
@ -1863,7 +2062,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer sm:h-12"
|
"bg-background hover:bg-muted/50 h-10 cursor-pointer border-b transition-colors sm:h-12",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick(row, index, e)}
|
onClick={(e) => handleRowClick(row, index, e)}
|
||||||
>
|
>
|
||||||
|
|
@ -1892,16 +2091,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<td
|
<td
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 text-xs text-foreground overflow-hidden text-ellipsis whitespace-nowrap sm:h-12 sm:text-sm",
|
"text-foreground h-10 overflow-hidden text-xs text-ellipsis whitespace-nowrap sm:h-12 sm:text-sm",
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
||||||
isFrozen && "sticky z-10 bg-background shadow-[2px_0_4px_rgba(0,0,0,0.05)]"
|
isFrozen && "bg-background sticky z-10 shadow-[2px_0_4px_rgba(0,0,0,0.05)]",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: column.columnName === "__checkbox__" ? "center" : (isNumeric ? "right" : (column.align || "left")),
|
textAlign:
|
||||||
width: column.columnName === "__checkbox__" ? '48px' : `${100 / visibleColumns.length}%`,
|
column.columnName === "__checkbox__"
|
||||||
minWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
? "center"
|
||||||
maxWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
: isNumeric
|
||||||
...(isFrozen && { left: `${leftPosition}px` })
|
? "right"
|
||||||
|
: column.align || "left",
|
||||||
|
width: column.columnName === "__checkbox__" ? "48px" : `${100 / visibleColumns.length}%`,
|
||||||
|
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
|
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
|
...(isFrozen && { left: `${leftPosition}px` }),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{column.columnName === "__checkbox__"
|
{column.columnName === "__checkbox__"
|
||||||
|
|
@ -2068,7 +2272,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<TableOptionsModal
|
<TableOptionsModal
|
||||||
isOpen={isTableOptionsOpen}
|
isOpen={isTableOptionsOpen}
|
||||||
onClose={() => setIsTableOptionsOpen(false)}
|
onClose={() => setIsTableOptionsOpen(false)}
|
||||||
columns={visibleColumns.map(col => ({
|
columns={visibleColumns.map((col) => ({
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
label: columnLabels[col.columnName] || col.displayName || col.columnName,
|
label: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||||
visible: col.visible !== false,
|
visible: col.visible !== false,
|
||||||
|
|
|
||||||
|
|
@ -288,9 +288,19 @@ function getDefaultText(key: string): string {
|
||||||
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
||||||
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
|
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
|
||||||
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
|
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
|
||||||
|
[MENU_MANAGEMENT_KEYS.LIST_TITLE]: "메뉴 목록",
|
||||||
|
[MENU_MANAGEMENT_KEYS.LIST_TOTAL]: "전체",
|
||||||
|
[MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT]: "검색 결과",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY]: "회사 필터",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL]: "전체",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON]: "공통",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH]: "회사 검색...",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH]: "검색",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER]: "메뉴명 검색...",
|
||||||
|
[MENU_MANAGEMENT_KEYS.FILTER_RESET]: "초기화",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
|
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
|
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
|
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
|
||||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
|
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
|
||||||
|
|
@ -340,9 +350,48 @@ function getDefaultText(key: string): string {
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
||||||
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
|
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴를 삭제하는 중...",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 성공적으로 저장되었습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 성공적으로 삭제되었습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]: "{count}개의 메뉴가 성공적으로 삭제되었습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "{success}개 삭제됨, {failed}개 실패",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력하세요.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택하세요.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택하세요.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
|
||||||
|
[MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
|
||||||
|
[MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
|
||||||
|
[MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
|
||||||
|
[MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
|
||||||
|
// 추가 매핑: key 문자열 자체도 한글로 매핑
|
||||||
|
"menu.type.title": "메뉴 타입",
|
||||||
|
"menu.management.admin": "관리자",
|
||||||
|
"menu.management.admin.description": "시스템 관리 및 설정 메뉴",
|
||||||
|
"menu.management.user": "사용자",
|
||||||
|
"menu.management.user.description": "일반 사용자 업무 메뉴",
|
||||||
|
"menu.list.title": "메뉴 목록",
|
||||||
|
"filter.company.all": "전체",
|
||||||
|
"filter.search.placeholder": "메뉴명 검색...",
|
||||||
|
"filter.reset": "초기화",
|
||||||
|
"button.add.top.level": "최상위 메뉴 추가",
|
||||||
|
"button.delete.selected": "선택 삭제",
|
||||||
|
"table.header.menu.name": "메뉴명",
|
||||||
|
"table.header.sequence": "순서",
|
||||||
|
"table.header.company": "회사",
|
||||||
|
"table.header.menu.url": "URL",
|
||||||
|
"table.header.status": "상태",
|
||||||
|
"table.header.actions": "작업",
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaultTexts[key] || key;
|
return defaultTexts[key] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue