Merge pull request 'feature/screen-management' (#189) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/189
This commit is contained in:
279
.cursor/rules/inputtype-usage-guide.mdc
Normal file
279
.cursor/rules/inputtype-usage-guide.mdc
Normal file
@@ -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,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
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 { apiClient } from "@/lib/api/client";
|
||||
|
||||
@@ -545,9 +545,9 @@ export const MenuManagement: React.FC = () => {
|
||||
// uiTexts에서 번역 텍스트 찾기
|
||||
let text = uiTexts[key];
|
||||
|
||||
// uiTexts에 없으면 fallback 또는 키 사용
|
||||
// uiTexts에 없으면 getMenuTextSync로 기본 한글 텍스트 가져오기
|
||||
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 { toast } from "sonner";
|
||||
|
||||
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
|
||||
import { MENU_MANAGEMENT_KEYS, getMenuTextSync } from "@/lib/utils/multilang";
|
||||
|
||||
interface MenuTableProps {
|
||||
menus: MenuItem[];
|
||||
@@ -39,7 +39,8 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
||||
}) => {
|
||||
// 다국어 텍스트 가져오기 함수
|
||||
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>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<ResizableDialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -359,7 +359,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<ResizableDialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -144,8 +144,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||
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(
|
||||
@@ -208,7 +208,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
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) {
|
||||
try {
|
||||
@@ -217,18 +217,23 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
// valueCode -> valueLabel 매핑 생성
|
||||
const mapping: Record<string, string> = {};
|
||||
// valueCode -> {label, color} 매핑 생성
|
||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
mapping[item.valueCode] = item.valueLabel;
|
||||
mapping[item.valueCode] = {
|
||||
label: item.valueLabel,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
mappings[col.columnName] = mapping;
|
||||
console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping);
|
||||
}
|
||||
} catch (error) {
|
||||
// 카테고리 값 로드 실패 시 무시
|
||||
console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📊 전체 카테고리 매핑:", mappings);
|
||||
setCategoryMappings(mappings);
|
||||
} catch (error) {
|
||||
console.error("카테고리 매핑 로드 실패:", error);
|
||||
@@ -1911,13 +1916,27 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
||||
switch (actualWebType) {
|
||||
case "category": {
|
||||
// 카테고리 타입: 코드값 -> 라벨로 변환
|
||||
// 카테고리 타입: 배지로 표시
|
||||
if (!value) return "";
|
||||
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
if (mapping && value) {
|
||||
const label = mapping[String(value)];
|
||||
return label || String(value);
|
||||
}
|
||||
return String(value || "");
|
||||
const categoryData = mapping?.[String(value)];
|
||||
|
||||
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
|
||||
const displayLabel = categoryData?.label || String(value);
|
||||
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: displayColor,
|
||||
borderColor: displayColor
|
||||
}}
|
||||
className="text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
case "date":
|
||||
|
||||
@@ -11,9 +11,33 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -26,6 +50,7 @@ export const CategoryValueAddDialog: React.FC<
|
||||
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
||||
const [valueLabel, setValueLabel] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState("#3b82f6");
|
||||
|
||||
// 라벨에서 코드 자동 생성
|
||||
const generateCode = (label: string): string => {
|
||||
@@ -59,13 +84,14 @@ export const CategoryValueAddDialog: React.FC<
|
||||
valueCode,
|
||||
valueLabel: valueLabel.trim(),
|
||||
description: description.trim(),
|
||||
color: "#3b82f6",
|
||||
color: color,
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
// 초기화
|
||||
setValueLabel("");
|
||||
setDescription("");
|
||||
setColor("#3b82f6");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -81,23 +107,56 @@ export const CategoryValueAddDialog: React.FC<
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<Input
|
||||
id="valueLabel"
|
||||
placeholder="이름 (예: 개발, 긴급, 진행중)"
|
||||
value={valueLabel}
|
||||
onChange={(e) => setValueLabel(e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="valueLabel" className="text-xs sm:text-sm">
|
||||
이름
|
||||
</Label>
|
||||
<Input
|
||||
id="valueLabel"
|
||||
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
|
||||
id="description"
|
||||
placeholder="설명 (선택사항)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="text-xs sm:text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||
<div className="mt-1.5 flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
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>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { TableCategoryValue } from "@/types/tableCategoryValue";
|
||||
|
||||
interface CategoryValueEditDialogProps {
|
||||
@@ -22,15 +24,39 @@ interface CategoryValueEditDialogProps {
|
||||
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<
|
||||
CategoryValueEditDialogProps
|
||||
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
||||
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
||||
const [description, setDescription] = useState(value.description || "");
|
||||
const [color, setColor] = useState(value.color || "#3b82f6");
|
||||
|
||||
useEffect(() => {
|
||||
setValueLabel(value.valueLabel);
|
||||
setDescription(value.description || "");
|
||||
setColor(value.color || "#3b82f6");
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -41,6 +67,7 @@ export const CategoryValueEditDialog: React.FC<
|
||||
onUpdate(value.valueId!, {
|
||||
valueLabel: valueLabel.trim(),
|
||||
description: description.trim(),
|
||||
color: color,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -57,23 +84,56 @@ export const CategoryValueEditDialog: React.FC<
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<Input
|
||||
id="valueLabel"
|
||||
placeholder="이름 (예: 개발, 긴급, 진행중)"
|
||||
value={valueLabel}
|
||||
onChange={(e) => setValueLabel(e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="valueLabel" className="text-xs sm:text-sm">
|
||||
이름
|
||||
</Label>
|
||||
<Input
|
||||
id="valueLabel"
|
||||
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
|
||||
id="description"
|
||||
placeholder="설명 (선택사항)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="text-xs sm:text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||
<div className="mt-1.5 flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
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>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
|
||||
@@ -49,25 +49,33 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
const [editingValue, setEditingValue] = useState<TableCategoryValue | null>(
|
||||
null
|
||||
);
|
||||
const [showInactive, setShowInactive] = useState(false); // 비활성 항목 표시 옵션 (기본: 숨김)
|
||||
|
||||
// 카테고리 값 로드
|
||||
useEffect(() => {
|
||||
loadCategoryValues();
|
||||
}, [tableName, columnName]);
|
||||
|
||||
// 검색 필터링
|
||||
// 검색 필터링 + 비활성 필터링
|
||||
useEffect(() => {
|
||||
let filtered = values;
|
||||
|
||||
// 비활성 항목 필터링 (기본: 활성만 표시, 체크하면 비활성도 표시)
|
||||
if (!showInactive) {
|
||||
filtered = filtered.filter((v) => v.isActive !== false);
|
||||
}
|
||||
|
||||
// 검색어 필터링
|
||||
if (searchQuery) {
|
||||
const filtered = values.filter(
|
||||
filtered = filtered.filter(
|
||||
(v) =>
|
||||
v.valueCode.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 () => {
|
||||
setIsLoading(true);
|
||||
@@ -264,10 +272,27 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
총 {filteredValues.length}개 항목
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setIsAddDialogOpen(true)} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
새 값 추가
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 비활성 항목 표시 옵션 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
|
||||
{/* 검색바 */}
|
||||
@@ -294,73 +319,90 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredValues.map((value) => (
|
||||
<div
|
||||
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">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{value.valueCode}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">
|
||||
{value.valueLabel}
|
||||
</span>
|
||||
{value.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
- {value.description}
|
||||
</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"
|
||||
{filteredValues.map((value) => {
|
||||
const isInactive = value.isActive === false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value.valueId}
|
||||
className={`flex items-center gap-3 rounded-md border bg-card p-3 transition-colors hover:bg-accent ${
|
||||
isInactive ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedValueIds.includes(value.valueId!)}
|
||||
onCheckedChange={() => handleSelectValue(value.valueId!)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setEditingValue(value)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Edit2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{/* 색상 표시 (앞쪽으로 이동) */}
|
||||
{value.color && (
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border flex-shrink-0"
|
||||
style={{ backgroundColor: value.color }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 라벨 */}
|
||||
<span className={`text-sm font-medium ${isInactive ? "line-through" : ""}`}>
|
||||
{value.valueLabel}
|
||||
</span>
|
||||
|
||||
{/* 설명 */}
|
||||
{value.description && (
|
||||
<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>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteValue(value.valueId!)}
|
||||
className="h-8 w-8 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
@@ -87,7 +87,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
if (!stableIdRef.current) {
|
||||
if (modalId) {
|
||||
stableIdRef.current = modalId;
|
||||
console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
||||
// // console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
||||
} else {
|
||||
// className 기반 ID 생성
|
||||
if (className) {
|
||||
@@ -95,10 +95,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||
}, 0);
|
||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||
console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
|
||||
className,
|
||||
generatedId: stableIdRef.current,
|
||||
});
|
||||
// console.log("🔄 ResizableDialog - className 기반 ID 생성:", { className, generatedId: stableIdRef.current });
|
||||
} else if (userStyle) {
|
||||
// userStyle 기반 ID 생성
|
||||
const styleStr = JSON.stringify(userStyle);
|
||||
@@ -106,14 +103,11 @@ const ResizableDialogContent = React.forwardRef<
|
||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||
}, 0);
|
||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||
console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
|
||||
userStyle,
|
||||
generatedId: stableIdRef.current,
|
||||
});
|
||||
// console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", { userStyle, generatedId: stableIdRef.current });
|
||||
} else {
|
||||
// 기본 ID
|
||||
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);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("🔍 모달 상태 변화 감지:", {
|
||||
actualOpen,
|
||||
wasOpen,
|
||||
externalOpen,
|
||||
contextOpen: context.open,
|
||||
effectiveModalId
|
||||
});
|
||||
// console.log("🔍 모달 상태 변화 감지:", { actualOpen, wasOpen, externalOpen, contextOpen: context.open, effectiveModalId });
|
||||
|
||||
if (actualOpen && !wasOpen) {
|
||||
// 모달이 방금 열림
|
||||
console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
||||
// console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
||||
setIsInitialized(false);
|
||||
setWasOpen(true);
|
||||
} else if (!actualOpen && wasOpen) {
|
||||
// 모달이 방금 닫힘
|
||||
console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
||||
// console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
||||
setWasOpen(false);
|
||||
}
|
||||
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
|
||||
@@ -194,11 +182,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
|
||||
React.useEffect(() => {
|
||||
if (effectiveModalId !== lastModalId) {
|
||||
console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
|
||||
이전: lastModalId,
|
||||
현재: effectiveModalId,
|
||||
isInitialized,
|
||||
});
|
||||
// console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", { 이전: lastModalId, 현재: effectiveModalId, isInitialized });
|
||||
setIsInitialized(false);
|
||||
setUserResized(false); // 사용자 리사이징 플래그도 리셋
|
||||
setLastModalId(effectiveModalId);
|
||||
@@ -207,11 +191,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
|
||||
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
|
||||
React.useEffect(() => {
|
||||
console.log("🔍 초기 크기 설정 useEffect 실행:", {
|
||||
isInitialized,
|
||||
hasContentRef: !!contentRef.current,
|
||||
effectiveModalId,
|
||||
});
|
||||
// console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
|
||||
|
||||
if (!isInitialized) {
|
||||
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
|
||||
@@ -231,22 +211,9 @@ const ResizableDialogContent = React.forwardRef<
|
||||
contentWidth = contentRef.current.scrollWidth || defaultWidth;
|
||||
contentHeight = contentRef.current.scrollHeight || defaultHeight;
|
||||
|
||||
console.log("📏 모달 내용 크기 측정:", {
|
||||
attempt: attempts,
|
||||
scrollWidth: contentRef.current.scrollWidth,
|
||||
scrollHeight: contentRef.current.scrollHeight,
|
||||
clientWidth: contentRef.current.clientWidth,
|
||||
clientHeight: contentRef.current.clientHeight,
|
||||
contentWidth,
|
||||
contentHeight,
|
||||
});
|
||||
// console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
|
||||
} else {
|
||||
console.log("⚠️ contentRef 없음, 재시도:", {
|
||||
attempt: attempts,
|
||||
maxAttempts,
|
||||
defaultWidth,
|
||||
defaultHeight
|
||||
});
|
||||
// console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
|
||||
|
||||
// contentRef가 아직 없으면 재시도
|
||||
if (attempts < maxAttempts) {
|
||||
@@ -265,7 +232,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
|
||||
};
|
||||
|
||||
console.log("📐 내용 기반 크기:", contentBasedSize);
|
||||
// console.log("📐 내용 기반 크기:", contentBasedSize);
|
||||
|
||||
// localStorage에서 저장된 크기 확인
|
||||
let finalSize = contentBasedSize;
|
||||
@@ -275,12 +242,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
|
||||
console.log("📦 localStorage 확인:", {
|
||||
effectiveModalId,
|
||||
userId,
|
||||
storageKey,
|
||||
saved: saved ? "있음" : "없음",
|
||||
});
|
||||
// console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
|
||||
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
@@ -292,27 +254,22 @@ const ResizableDialogContent = React.forwardRef<
|
||||
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
|
||||
};
|
||||
|
||||
console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
|
||||
// console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
|
||||
|
||||
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
|
||||
// (사용자가 의도적으로 작게 만든 것을 존중)
|
||||
finalSize = savedSize;
|
||||
setUserResized(true);
|
||||
|
||||
console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", {
|
||||
savedSize,
|
||||
contentBasedSize,
|
||||
finalSize,
|
||||
note: "사용자가 리사이징한 크기를 그대로 사용합니다",
|
||||
});
|
||||
// console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
|
||||
} else {
|
||||
console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
|
||||
// console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
|
||||
}
|
||||
} else {
|
||||
console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
|
||||
// console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 모달 크기 복원 실패:", error);
|
||||
// console.error("❌ 모달 크기 복원 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,15 +341,9 @@ const ResizableDialogContent = React.forwardRef<
|
||||
userResized: true, // 사용자가 직접 리사이징했음을 표시
|
||||
};
|
||||
localStorage.setItem(storageKey, JSON.stringify(currentSize));
|
||||
console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
|
||||
effectiveModalId,
|
||||
userId,
|
||||
storageKey,
|
||||
size: currentSize,
|
||||
stateSize: { width: size.width, height: size.height },
|
||||
});
|
||||
// console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", { effectiveModalId, userId, storageKey, size: currentSize, stateSize: { width: size.width, height: size.height } });
|
||||
} catch (error) {
|
||||
console.error("❌ 모달 크기 저장 실패:", error);
|
||||
// console.error("❌ 모달 크기 저장 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -288,9 +288,19 @@ function getDefaultText(key: string): string {
|
||||
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
|
||||
[MENU_MANAGEMENT_KEYS.ADMIN_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_TOP_LEVEL]: "최상위 메뉴 추가",
|
||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
|
||||
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위",
|
||||
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
|
||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
|
||||
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
|
||||
@@ -340,9 +350,48 @@ function getDefaultText(key: string): string {
|
||||
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
|
||||
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
|
||||
[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] || "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user