다국어 가이드 업데이트
This commit is contained in:
parent
49b4b6c550
commit
11f40c3fc3
|
|
@ -758,7 +758,7 @@ export const getLangText = async (
|
||||||
* 다국어 텍스트 배치 조회 API
|
* 다국어 텍스트 배치 조회 API
|
||||||
*/
|
*/
|
||||||
export const getBatchTranslations = async (
|
export const getBatchTranslations = async (
|
||||||
req: AuthenticatedRequest,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -780,7 +780,6 @@ export const getBatchTranslations = async (
|
||||||
menuCode: finalMenuCode,
|
menuCode: finalMenuCode,
|
||||||
userLang: finalUserLang,
|
userLang: finalUserLang,
|
||||||
keyCount: langKeys?.length || 0,
|
keyCount: langKeys?.length || 0,
|
||||||
user: req.user,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
|
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ import {
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 모든 다국어 관리 라우트에 인증 미들웨어 적용
|
// 다국어 배치 조회 API는 인증 없이 접근 가능
|
||||||
|
router.post("/batch", getBatchTranslations);
|
||||||
|
|
||||||
|
// 나머지 모든 다국어 관리 라우트에 인증 미들웨어 적용
|
||||||
router.use(authenticateToken);
|
router.use(authenticateToken);
|
||||||
|
|
||||||
// 언어 관리 API
|
// 언어 관리 API
|
||||||
|
|
@ -45,6 +48,5 @@ router.put("/keys/:keyId/toggle", toggleLangKey); // 다국어 키 상태 토글
|
||||||
router.post("/keys/:keyId/texts", saveLangTexts); // 다국어 텍스트 저장/수정
|
router.post("/keys/:keyId/texts", saveLangTexts); // 다국어 텍스트 저장/수정
|
||||||
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText); // 사용자별 다국어 텍스트 조회
|
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText); // 사용자별 다국어 텍스트 조회
|
||||||
router.get("/text/:companyCode/:langKey/:langCode", getLangText); // 특정 키의 다국어 텍스트 조회
|
router.get("/text/:companyCode/:langKey/:langCode", getLangText); // 특정 키의 다국어 텍스트 조회
|
||||||
router.post("/batch", getBatchTranslations); // 다국어 텍스트 배치 조회
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
### 지원 언어
|
### 지원 언어
|
||||||
|
|
||||||
- **한국어 (KR)**: 기본 언어
|
- **한국어 (KR)**: 기본 언어
|
||||||
- **영어 (EN)**: 사용자 설정 가능
|
- **영어 (US)**: 사용자 설정 가능
|
||||||
|
|
||||||
### 주요 특징
|
### 주요 특징
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE public.language_master (
|
CREATE TABLE public.language_master (
|
||||||
lang_code varchar(10) NOT NULL PRIMARY KEY, -- 언어 코드 (KR, EN)
|
lang_code varchar(10) NOT NULL PRIMARY KEY, -- 언어 코드 (KR, US)
|
||||||
lang_name varchar(50) NOT NULL, -- 언어명 (Korean, English)
|
lang_name varchar(50) NOT NULL, -- 언어명 (Korean, English)
|
||||||
lang_native varchar(50) NOT NULL, -- 원어명 (한국어, English)
|
lang_native varchar(50) NOT NULL, -- 원어명 (한국어, English)
|
||||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||||
|
|
@ -93,7 +93,7 @@ CREATE TABLE public.multi_lang_key_master (
|
||||||
lang_key varchar(100) NOT NULL, -- 다국어 키 (예: button.add)
|
lang_key varchar(100) NOT NULL, -- 다국어 키 (예: button.add)
|
||||||
description text, -- 키 설명
|
description text, -- 키 설명
|
||||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||||
menu_name varchar(50), -- 메뉴명 (사용하지 않음)
|
menu_name varchar(50), -- 메뉴명 (다국어 관리 화면에 어디에서 쓰이는지 보여지는 용도. 한국어로 어느 메뉴에 쓰이는지 작성)
|
||||||
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_by varchar(50),
|
created_by varchar(50),
|
||||||
updated_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
updated_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
@ -108,7 +108,7 @@ CREATE TABLE public.multi_lang_key_master (
|
||||||
CREATE TABLE public.multi_lang_text (
|
CREATE TABLE public.multi_lang_text (
|
||||||
text_id serial4 NOT NULL PRIMARY KEY, -- 텍스트 ID (자동 증가)
|
text_id serial4 NOT NULL PRIMARY KEY, -- 텍스트 ID (자동 증가)
|
||||||
key_id int4 NOT NULL, -- 키 마스터의 key_id
|
key_id int4 NOT NULL, -- 키 마스터의 key_id
|
||||||
lang_code varchar(10) NOT NULL, -- 언어 코드 (KR, EN)
|
lang_code varchar(10) NOT NULL, -- 언어 코드 (KR, US)
|
||||||
lang_text text NOT NULL, -- 번역된 텍스트
|
lang_text text NOT NULL, -- 번역된 텍스트
|
||||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||||
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
@ -155,7 +155,7 @@ CREATE TABLE public.multi_lang_text (
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO language_master (lang_code, lang_name, lang_native) VALUES
|
INSERT INTO language_master (lang_code, lang_name, lang_native) VALUES
|
||||||
('KR', 'Korean', '한국어'),
|
('KR', 'Korean', '한국어'),
|
||||||
('EN', 'English', 'English');
|
('US', 'English', 'English');
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. 다국어 키 등록
|
#### 2. 다국어 키 등록
|
||||||
|
|
@ -184,7 +184,7 @@ WHERE km.lang_key IN ('button.add', 'button.edit', 'menu.title')
|
||||||
|
|
||||||
-- 영어 번역
|
-- 영어 번역
|
||||||
INSERT INTO multi_lang_text (key_id, lang_code, lang_text)
|
INSERT INTO multi_lang_text (key_id, lang_code, lang_text)
|
||||||
SELECT km.key_id, 'EN',
|
SELECT km.key_id, 'US',
|
||||||
CASE km.lang_key
|
CASE km.lang_key
|
||||||
WHEN 'button.add' THEN 'Add'
|
WHEN 'button.add' THEN 'Add'
|
||||||
WHEN 'button.edit' THEN 'Edit'
|
WHEN 'button.edit' THEN 'Edit'
|
||||||
|
|
@ -209,7 +209,7 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
||||||
// 언어 변경 처리
|
// 언어 변경 처리
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userLang, // 현재 사용자 언어 (KR, EN)
|
userLang, // 현재 사용자 언어 (KR, US)
|
||||||
getText, // 다국어 텍스트 조회 함수
|
getText, // 다국어 텍스트 조회 함수
|
||||||
changeLang, // 언어 변경 함수
|
changeLang, // 언어 변경 함수
|
||||||
companyCode, // 회사 코드
|
companyCode, // 회사 코드
|
||||||
|
|
@ -229,7 +229,7 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
||||||
const { userLang, getText, changeLang } = useMultiLang({ companyCode: "*" });
|
const { userLang, getText, changeLang } = useMultiLang({ companyCode: "*" });
|
||||||
|
|
||||||
// 언어 변경
|
// 언어 변경
|
||||||
await changeLang("EN");
|
await changeLang("US");
|
||||||
|
|
||||||
// 다국어 텍스트 조회
|
// 다국어 텍스트 조회
|
||||||
const text = await getText("menu.management", "button.add");
|
const text = await getText("menu.management", "button.add");
|
||||||
|
|
@ -519,7 +519,7 @@ INSERT INTO multi_lang_text (
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
km.key_id,
|
km.key_id,
|
||||||
'EN',
|
'US',
|
||||||
CASE km.lang_key
|
CASE km.lang_key
|
||||||
WHEN 'page.title' THEN 'My Page'
|
WHEN 'page.title' THEN 'My Page'
|
||||||
WHEN 'page.description' THEN 'This is my page'
|
WHEN 'page.description' THEN 'This is my page'
|
||||||
|
|
@ -622,7 +622,7 @@ INSERT INTO multi_lang_text (
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
km.key_id,
|
km.key_id,
|
||||||
'EN',
|
'US',
|
||||||
CASE km.lang_key
|
CASE km.lang_key
|
||||||
WHEN 'menu.management.title' THEN 'Menu Management'
|
WHEN 'menu.management.title' THEN 'Menu Management'
|
||||||
WHEN 'button.add' THEN 'Add'
|
WHEN 'button.add' THEN 'Add'
|
||||||
|
|
@ -727,7 +727,7 @@ INSERT INTO multi_lang_text (
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
km.key_id,
|
km.key_id,
|
||||||
'EN',
|
'US',
|
||||||
CASE km.lang_key
|
CASE km.lang_key
|
||||||
WHEN 'new.feature.title' THEN 'New Feature'
|
WHEN 'new.feature.title' THEN 'New Feature'
|
||||||
WHEN 'new.feature.description' THEN 'This is a new feature'
|
WHEN 'new.feature.description' THEN 'This is a new feature'
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Search, Database, RefreshCw, Settings, Menu, X } from "lucide-react";
|
import { Search, Database, RefreshCw, Settings, Menu, X } from "lucide-react";
|
||||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
|
import { TABLE_MANAGEMENT_KEYS, WEB_TYPE_OPTIONS_WITH_KEYS } from "@/constants/tableManagement";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface TableInfo {
|
interface TableInfo {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
|
@ -37,6 +40,7 @@ interface ColumnTypeInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TableManagementPage() {
|
export default function TableManagementPage() {
|
||||||
|
const { userLang, getText } = useMultiLang({ companyCode: "*" });
|
||||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
const [columns, setColumns] = useState<ColumnTypeInfo[]>([]);
|
const [columns, setColumns] = useState<ColumnTypeInfo[]>([]);
|
||||||
const [selectedTable, setSelectedTable] = useState<string | null>(null);
|
const [selectedTable, setSelectedTable] = useState<string | null>(null);
|
||||||
|
|
@ -44,25 +48,60 @@ export default function TableManagementPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [columnsLoading, setColumnsLoading] = useState(false);
|
const [columnsLoading, setColumnsLoading] = useState(false);
|
||||||
const [originalColumns, setOriginalColumns] = useState<ColumnTypeInfo[]>([]); // 원본 데이터 저장
|
const [originalColumns, setOriginalColumns] = useState<ColumnTypeInfo[]>([]); // 원본 데이터 저장
|
||||||
|
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 웹 타입 옵션
|
// 다국어 텍스트 로드
|
||||||
const webTypeOptions = [
|
useEffect(() => {
|
||||||
{ value: "text", label: "text", description: "일반 텍스트 입력" },
|
const loadTexts = async () => {
|
||||||
{ value: "number", label: "number", description: "숫자 입력" },
|
if (!userLang) return;
|
||||||
{ value: "date", label: "date", description: "날짜 선택기" },
|
|
||||||
{ value: "code", label: "code", description: "코드 선택 (공통코드 지정)" },
|
try {
|
||||||
{ value: "entity", label: "entity", description: "엔티티 참조 (참조테이블 지정)" },
|
const response = await apiClient.post(
|
||||||
];
|
"/multilang/batch",
|
||||||
|
{
|
||||||
|
langKeys: Object.values(TABLE_MANAGEMENT_KEYS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
companyCode: "*",
|
||||||
|
menuCode: "TABLE_MANAGEMENT",
|
||||||
|
userLang: userLang,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
setUiTexts(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("다국어 텍스트 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTexts();
|
||||||
|
}, [userLang]);
|
||||||
|
|
||||||
|
// 텍스트 가져오기 함수
|
||||||
|
const getTextFromUI = (key: string, fallback?: string) => {
|
||||||
|
return uiTexts[key] || fallback || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹 타입 옵션 (다국어 적용)
|
||||||
|
const webTypeOptions = WEB_TYPE_OPTIONS_WITH_KEYS.map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: getTextFromUI(option.labelKey, option.value),
|
||||||
|
description: getTextFromUI(option.descriptionKey, option.value),
|
||||||
|
}));
|
||||||
|
|
||||||
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
||||||
const referenceTableOptions = [
|
const referenceTableOptions = [
|
||||||
{ value: "none", label: "테이블 선택" },
|
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") },
|
||||||
...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })),
|
...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 공통 코드 옵션 (예시 - 실제로는 API에서 가져와야 함)
|
// 공통 코드 옵션 (예시 - 실제로는 API에서 가져와야 함)
|
||||||
const commonCodeOptions = [
|
const commonCodeOptions = [
|
||||||
{ value: "none", label: "코드 선택" },
|
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_CODE_PLACEHOLDER, "코드 선택") },
|
||||||
{ value: "USER_STATUS", label: "사용자 상태" },
|
{ value: "USER_STATUS", label: "사용자 상태" },
|
||||||
{ value: "DEPT_TYPE", label: "부서 유형" },
|
{ value: "DEPT_TYPE", label: "부서 유형" },
|
||||||
{ value: "PRODUCT_CATEGORY", label: "제품 카테고리" },
|
{ value: "PRODUCT_CATEGORY", label: "제품 카테고리" },
|
||||||
|
|
@ -72,32 +111,14 @@ export default function TableManagementPage() {
|
||||||
const loadTables = async () => {
|
const loadTables = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8080/api/table-management/tables");
|
const response = await apiClient.get("/table-management/tables");
|
||||||
|
|
||||||
// 응답 상태 확인
|
// 응답 상태 확인
|
||||||
if (!response.ok) {
|
if (response.data.success) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
setTables(response.data.data);
|
||||||
}
|
|
||||||
|
|
||||||
// 응답 텍스트를 먼저 확인
|
|
||||||
const responseText = await response.text();
|
|
||||||
console.log("Raw response:", responseText);
|
|
||||||
|
|
||||||
// JSON 파싱 시도
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = JSON.parse(responseText);
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error("JSON 파싱 오류:", parseError);
|
|
||||||
console.error("응답 텍스트:", responseText);
|
|
||||||
throw new Error("JSON 파싱에 실패했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setTables(result.data);
|
|
||||||
toast.success("테이블 목록을 성공적으로 로드했습니다.");
|
toast.success("테이블 목록을 성공적으로 로드했습니다.");
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "테이블 목록 로드에 실패했습니다.");
|
toast.error(response.data.message || "테이블 목록 로드에 실패했습니다.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("테이블 목록 로드 실패:", error);
|
console.error("테이블 목록 로드 실패:", error);
|
||||||
|
|
@ -111,33 +132,15 @@ export default function TableManagementPage() {
|
||||||
const loadColumnTypes = async (tableName: string) => {
|
const loadColumnTypes = async (tableName: string) => {
|
||||||
setColumnsLoading(true);
|
setColumnsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/table-management/tables/${tableName}/columns`);
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||||
|
|
||||||
// 응답 상태 확인
|
// 응답 상태 확인
|
||||||
if (!response.ok) {
|
if (response.data.success) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
setColumns(response.data.data);
|
||||||
}
|
setOriginalColumns(response.data.data); // 원본 데이터 저장
|
||||||
|
|
||||||
// 응답 텍스트를 먼저 확인
|
|
||||||
const responseText = await response.text();
|
|
||||||
console.log("Raw column response:", responseText);
|
|
||||||
|
|
||||||
// JSON 파싱 시도
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = JSON.parse(responseText);
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error("JSON 파싱 오류:", parseError);
|
|
||||||
console.error("응답 텍스트:", responseText);
|
|
||||||
throw new Error("JSON 파싱에 실패했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setColumns(result.data);
|
|
||||||
setOriginalColumns(result.data); // 원본 데이터 저장
|
|
||||||
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
|
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "컬럼 정보 로드에 실패했습니다.");
|
toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("컬럼 타입 정보 로드 실패:", error);
|
console.error("컬럼 타입 정보 로드 실패:", error);
|
||||||
|
|
@ -234,6 +237,54 @@ export default function TableManagementPage() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 컬럼 변경 핸들러 (인덱스 기반)
|
||||||
|
const handleColumnChange = (index: number, field: keyof ColumnTypeInfo, value: any) => {
|
||||||
|
setColumns((prev) =>
|
||||||
|
prev.map((col, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 개별 컬럼 저장
|
||||||
|
const handleSaveColumn = async (column: ColumnTypeInfo) => {
|
||||||
|
if (!selectedTable) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const columnSetting = {
|
||||||
|
columnName: column.columnName,
|
||||||
|
columnLabel: column.displayName,
|
||||||
|
webType: column.webType,
|
||||||
|
detailSettings: column.detailSettings,
|
||||||
|
codeCategory: column.codeCategory,
|
||||||
|
codeValue: column.codeValue,
|
||||||
|
referenceTable: column.referenceTable,
|
||||||
|
referenceColumn: column.referenceColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [
|
||||||
|
columnSetting,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
toast.success("컬럼 설정이 성공적으로 저장되었습니다.");
|
||||||
|
// 원본 데이터 업데이트
|
||||||
|
setOriginalColumns((prev) => prev.map((col) => (col.columnName === column.columnName ? column : col)));
|
||||||
|
} else {
|
||||||
|
toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 설정 저장 실패:", error);
|
||||||
|
toast.error("컬럼 설정 저장 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 모든 컬럼 설정 저장
|
// 모든 컬럼 설정 저장
|
||||||
const saveAllColumnSettings = async () => {
|
const saveAllColumnSettings = async () => {
|
||||||
if (!selectedTable || columns.length === 0) return;
|
if (!selectedTable || columns.length === 0) return;
|
||||||
|
|
@ -252,29 +303,18 @@ export default function TableManagementPage() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 전체 테이블 설정을 한 번에 저장
|
// 전체 테이블 설정을 한 번에 저장
|
||||||
const response = await fetch(
|
const response = await apiClient.post(
|
||||||
`http://localhost:8080/api/table-management/tables/${selectedTable}/columns/settings`,
|
`/table-management/tables/${selectedTable}/columns/settings`,
|
||||||
{
|
columnSettings,
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(columnSettings),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.data.success) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
// 저장 성공 후 원본 데이터 업데이트
|
||||||
|
setOriginalColumns([...columns]);
|
||||||
|
toast.success(`${columns.length}개의 컬럼 설정이 성공적으로 저장되었습니다.`);
|
||||||
|
} else {
|
||||||
|
toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.message || "컬럼 설정 저장에 실패했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 저장 성공 후 원본 데이터 업데이트
|
|
||||||
setOriginalColumns([...columns]);
|
|
||||||
toast.success(`${columns.length}개의 컬럼 설정이 성공적으로 저장되었습니다.`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("컬럼 설정 저장 실패:", error);
|
console.error("컬럼 설정 저장 실패:", error);
|
||||||
toast.error("컬럼 설정 저장 중 오류가 발생했습니다.");
|
toast.error("컬럼 설정 저장 중 오류가 발생했습니다.");
|
||||||
|
|
@ -296,173 +336,226 @@ export default function TableManagementPage() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto space-y-4 p-4 md:space-y-6 md:p-6">
|
<div className="container mx-auto space-y-6 p-6">
|
||||||
{/* 헤더 영역 */}
|
{/* 페이지 제목 */}
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold md:text-3xl">데이터베이스 테이블 목록</h1>
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
<p className="text-muted-foreground text-sm md:text-base">데이터베이스 테이블과 컬럼 타입을 관리합니다.</p>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
|
||||||
</div>
|
</h1>
|
||||||
<div className="flex items-center gap-2 md:gap-4">
|
<p className="mt-2 text-gray-600">
|
||||||
<Button onClick={loadTables} disabled={loading} size="sm" className="md:text-base">
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, "데이터베이스 테이블과 컬럼의 타입을 관리합니다")}
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
</p>
|
||||||
<span className="hidden sm:inline">새로고침</span>
|
|
||||||
<span className="sm:hidden">새로고침</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={loadTables} disabled={loading} className="flex items-center gap-2">
|
||||||
|
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 검색 필터 */}
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
<div className="relative">
|
{/* 테이블 목록 */}
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
|
<Card className="lg:col-span-1">
|
||||||
<Input
|
<CardHeader>
|
||||||
placeholder="테이블 검색..."
|
<CardTitle className="flex items-center gap-2">
|
||||||
value={searchTerm}
|
<Database className="h-5 w-5" />
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
|
||||||
className="pl-10"
|
</CardTitle>
|
||||||
/>
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
|
{/* 검색 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 메인 컨텐츠 영역 */}
|
{/* 테이블 목록 */}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="max-h-96 space-y-2 overflow-y-auto">
|
||||||
{/* 좌측: 테이블 목록 */}
|
|
||||||
<div className="lg:col-span-1 xl:col-span-1">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
|
||||||
<Database className="h-4 w-4 md:h-5 md:w-5" />
|
|
||||||
<span className="hidden sm:inline">데이터베이스 테이블 목록</span>
|
|
||||||
<span className="sm:hidden">테이블 목록</span>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
<span className="ml-2 text-sm text-gray-500">
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : tables.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-gray-500">
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
tables
|
||||||
{filteredTables.length === 0 ? (
|
.filter(
|
||||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
(table) =>
|
||||||
{searchTerm ? "검색 결과가 없습니다." : "테이블이 없습니다."}
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
</div>
|
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
) : (
|
)
|
||||||
filteredTables.map((table) => (
|
.map((table) => (
|
||||||
<div
|
<div
|
||||||
key={table.tableName}
|
key={table.tableName}
|
||||||
onClick={() => handleTableSelect(table.tableName)}
|
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
||||||
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
selectedTable === table.tableName
|
||||||
selectedTable === table.tableName
|
? "border-blue-500 bg-blue-50"
|
||||||
? "bg-primary/10 border-primary"
|
: "border-gray-200 hover:border-gray-300"
|
||||||
: "hover:bg-muted/50 border-border"
|
}`}
|
||||||
}`}
|
onClick={() => handleTableSelect(table.tableName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="min-w-0 flex-1">
|
<div>
|
||||||
<h3 className="truncate text-sm font-medium md:text-base">{table.displayName}</h3>
|
<h3 className="font-medium text-gray-900">{table.displayName || table.tableName}</h3>
|
||||||
<p className="text-muted-foreground truncate text-xs md:text-sm">{table.tableName}</p>
|
<p className="text-sm text-gray-500">
|
||||||
<p className="text-muted-foreground mt-1 truncate text-xs">{table.description}</p>
|
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
|
||||||
</div>
|
</p>
|
||||||
<Badge variant="secondary" className="ml-2 text-xs md:text-sm">
|
|
||||||
{table.columnCount}개
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{table.columnCount} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_COLUMN_COUNT, "컬럼")}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
))
|
</div>
|
||||||
)}
|
))
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{/* 우측: 컬럼 타입 설정 */}
|
{/* 컬럼 타입 관리 */}
|
||||||
<div className="lg:col-span-2 xl:col-span-3">
|
<Card className="lg:col-span-2">
|
||||||
<Card>
|
<CardHeader>
|
||||||
<CardHeader className="pb-3">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardTitle className="text-base md:text-lg">
|
<Settings className="h-5 w-5" />
|
||||||
{selectedTable ? (
|
{selectedTable ? (
|
||||||
<div>
|
<>
|
||||||
<span className="hidden sm:inline">컬럼 타입 설정 - </span>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼")} - {selectedTable}
|
||||||
{selectedTableInfo?.displayName}
|
</>
|
||||||
<span className="text-muted-foreground ml-2 text-xs font-normal md:text-sm">
|
) : (
|
||||||
({selectedTableInfo?.tableName})
|
getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼 타입 관리")
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{!selectedTable ? (
|
||||||
|
<div className="py-12 text-center text-gray-500">
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{columnsLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<LoadingSpinner />
|
||||||
|
<span className="ml-2 text-sm text-gray-500">
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : columns.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-gray-500">
|
||||||
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"컬럼 타입 설정"
|
<div className="overflow-x-auto">
|
||||||
)}
|
<Table>
|
||||||
</CardTitle>
|
<TableHeader>
|
||||||
{selectedTable && (
|
<TableRow>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼명")}</TableHead>
|
||||||
<p className="text-muted-foreground text-xs md:text-sm">{selectedTableInfo?.description}</p>
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DISPLAY_NAME, "표시명")}</TableHead>
|
||||||
<Button onClick={saveAllColumnSettings} disabled={columnsLoading} size="sm" className="md:text-base">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DB_TYPE, "DB 타입")}</TableHead>
|
||||||
모든 설정 저장
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_WEB_TYPE, "웹 타입")}</TableHead>
|
||||||
</Button>
|
<TableHead>
|
||||||
</div>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DETAIL_SETTINGS, "상세 설정")}
|
||||||
)}
|
</TableHead>
|
||||||
</CardHeader>
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DESCRIPTION, "설명")}</TableHead>
|
||||||
<CardContent className="pt-0">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NULLABLE, "NULL 허용")}</TableHead>
|
||||||
{!selectedTable ? (
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DEFAULT_VALUE, "기본값")}</TableHead>
|
||||||
<div className="text-muted-foreground py-12 text-center text-sm md:text-base">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_MAX_LENGTH, "최대 길이")}</TableHead>
|
||||||
좌측에서 테이블을 선택하세요
|
<TableHead>
|
||||||
</div>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NUMERIC_PRECISION, "정밀도")}
|
||||||
) : columnsLoading ? (
|
</TableHead>
|
||||||
<div className="flex justify-center py-8">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NUMERIC_SCALE, "소수점")}</TableHead>
|
||||||
<LoadingSpinner />
|
<TableHead>
|
||||||
</div>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_CODE_CATEGORY, "코드 카테고리")}
|
||||||
) : (
|
</TableHead>
|
||||||
<div className="overflow-x-auto">
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_CODE_VALUE, "코드 값")}</TableHead>
|
||||||
{/* 모바일: 카드 형태 */}
|
<TableHead>
|
||||||
<div className="space-y-4 lg:hidden">
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_REFERENCE_TABLE, "참조 테이블")}
|
||||||
{columns.map((column) => (
|
</TableHead>
|
||||||
<div key={column.columnName} className="space-y-3 rounded-lg border p-4">
|
<TableHead>
|
||||||
<div className="flex items-center justify-between">
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_REFERENCE_COLUMN, "참조 컬럼")}
|
||||||
<h4 className="text-sm font-medium">{column.columnName}</h4>
|
</TableHead>
|
||||||
<Badge variant="outline" className="text-xs">
|
<TableHead>Actions</TableHead>
|
||||||
{column.dbType}
|
</TableRow>
|
||||||
</Badge>
|
</TableHeader>
|
||||||
</div>
|
<TableBody>
|
||||||
|
{columns.map((column, index) => (
|
||||||
<div className="space-y-2">
|
<TableRow key={column.columnName}>
|
||||||
<div>
|
<TableCell className="font-mono text-sm">{column.columnName}</TableCell>
|
||||||
<label className="text-muted-foreground text-xs">라벨</label>
|
<TableCell>
|
||||||
<Input
|
<Input
|
||||||
value={column.displayName}
|
value={column.displayName || ""}
|
||||||
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
|
onChange={(e) => handleColumnChange(index, "displayName", e.target.value)}
|
||||||
placeholder="라벨 입력"
|
placeholder={column.columnName}
|
||||||
className="text-sm"
|
className="w-32"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">{column.dbType}</TableCell>
|
||||||
<div>
|
<TableCell>
|
||||||
<label className="text-muted-foreground text-xs">웹 타입</label>
|
<Select
|
||||||
<Select
|
value={column.webType || "text"}
|
||||||
value={column.webType}
|
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
||||||
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
>
|
||||||
>
|
<SelectTrigger className="w-32">
|
||||||
<SelectTrigger className="text-sm">
|
<SelectValue />
|
||||||
<SelectValue />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{webTypeOptions.map((option) => (
|
||||||
{webTypeOptions.map((option) => (
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<div>
|
||||||
{option.label}
|
<div className="font-medium">{option.label}</div>
|
||||||
</SelectItem>
|
<div className="text-xs text-gray-500">{option.description}</div>
|
||||||
))}
|
</div>
|
||||||
</SelectContent>
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
</div>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
{column.webType === "code" && (
|
</TableCell>
|
||||||
<div>
|
<TableCell>
|
||||||
<label className="text-muted-foreground text-xs">공통 코드</label>
|
<Input
|
||||||
|
value={column.detailSettings || ""}
|
||||||
|
onChange={(e) => handleColumnChange(index, "detailSettings", e.target.value)}
|
||||||
|
placeholder="상세 설정"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
value={column.description || ""}
|
||||||
|
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
||||||
|
placeholder="설명"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={column.isNullable === "YES" ? "default" : "secondary"}>
|
||||||
|
{column.isNullable === "YES"
|
||||||
|
? getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_YES, "예")
|
||||||
|
: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NO, "아니오")}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">{column.defaultValue || "-"}</TableCell>
|
||||||
|
<TableCell className="text-center">{column.maxLength || "-"}</TableCell>
|
||||||
|
<TableCell className="text-center">{column.numericPrecision || "-"}</TableCell>
|
||||||
|
<TableCell className="text-center">{column.numericScale || "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
<Select
|
<Select
|
||||||
value={column.codeCategory || "none"}
|
value={column.codeCategory || "none"}
|
||||||
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
|
onValueChange={(value) => handleColumnChange(index, "codeCategory", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="text-sm">
|
<SelectTrigger className="w-32">
|
||||||
<SelectValue placeholder="코드 선택" />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{commonCodeOptions.map((option) => (
|
{commonCodeOptions.map((option) => (
|
||||||
|
|
@ -472,20 +565,22 @@ export default function TableManagementPage() {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</TableCell>
|
||||||
)}
|
<TableCell>
|
||||||
|
<Input
|
||||||
{column.webType === "entity" && (
|
value={column.codeValue || ""}
|
||||||
<div>
|
onChange={(e) => handleColumnChange(index, "codeValue", e.target.value)}
|
||||||
<label className="text-muted-foreground text-xs">참조 테이블</label>
|
placeholder="코드 값"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
<Select
|
<Select
|
||||||
value={column.referenceTable || "none"}
|
value={column.referenceTable || "none"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => handleColumnChange(index, "referenceTable", value)}
|
||||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="text-sm">
|
<SelectTrigger className="w-32">
|
||||||
<SelectValue placeholder="테이블 선택" />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{referenceTableOptions.map((option) => (
|
{referenceTableOptions.map((option) => (
|
||||||
|
|
@ -495,118 +590,36 @@ export default function TableManagementPage() {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</TableCell>
|
||||||
)}
|
<TableCell>
|
||||||
</div>
|
<Input
|
||||||
</div>
|
value={column.referenceColumn || ""}
|
||||||
))}
|
onChange={(e) => handleColumnChange(index, "referenceColumn", e.target.value)}
|
||||||
</div>
|
placeholder="참조 컬럼"
|
||||||
|
className="w-32"
|
||||||
{/* 태블릿/PC: 테이블 형태 */}
|
/>
|
||||||
<div className="hidden lg:block">
|
</TableCell>
|
||||||
<Table>
|
<TableCell>
|
||||||
<TableHeader>
|
<Button
|
||||||
<TableRow>
|
size="sm"
|
||||||
<TableHead className="text-xs md:text-sm">컬럼명</TableHead>
|
variant="outline"
|
||||||
<TableHead className="text-xs md:text-sm">라벨</TableHead>
|
onClick={() => handleSaveColumn(column)}
|
||||||
<TableHead className="text-xs md:text-sm">DB 타입</TableHead>
|
className="flex items-center gap-1"
|
||||||
<TableHead className="text-xs md:text-sm">웹 타입</TableHead>
|
>
|
||||||
<TableHead className="text-xs md:text-sm xl:table-cell">상세 설정</TableHead>
|
<Settings className="h-3 w-3" />
|
||||||
</TableRow>
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_SAVE, "저장")}
|
||||||
</TableHeader>
|
</Button>
|
||||||
<TableBody>
|
|
||||||
{columns.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="text-muted-foreground py-8 text-center text-sm">
|
|
||||||
컬럼 정보가 없습니다.
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
))}
|
||||||
columns.map((column) => (
|
|
||||||
<TableRow key={column.columnName}>
|
|
||||||
<TableCell className="text-xs font-medium md:text-sm">{column.columnName}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
value={column.displayName}
|
|
||||||
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
|
|
||||||
placeholder="라벨 입력"
|
|
||||||
className="w-24 text-xs md:w-32 md:text-sm"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{column.dbType}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Select
|
|
||||||
value={column.webType}
|
|
||||||
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-20 text-xs md:w-32 md:text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{webTypeOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-xs md:text-sm xl:table-cell">
|
|
||||||
{column.webType === "code" ? (
|
|
||||||
<Select
|
|
||||||
value={column.codeCategory || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleDetailSettingsChange(column.columnName, "code", value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32 text-xs md:w-40 md:text-sm">
|
|
||||||
<SelectValue placeholder="코드 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCodeOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : column.webType === "entity" ? (
|
|
||||||
<Select
|
|
||||||
value={column.referenceTable || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32 text-xs md:w-40 md:text-sm">
|
|
||||||
<SelectValue placeholder="테이블 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{referenceTableOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs md:text-sm">{column.detailSettings}</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 테이블 타입 관리 다국어 키 상수
|
||||||
|
export const TABLE_MANAGEMENT_KEYS = {
|
||||||
|
// 페이지 제목 및 설명
|
||||||
|
PAGE_TITLE: "table.management.page.title",
|
||||||
|
PAGE_DESCRIPTION: "table.management.page.description",
|
||||||
|
|
||||||
|
// 테이블 관련
|
||||||
|
TABLE_NAME: "table.management.table.name",
|
||||||
|
TABLE_DISPLAY_NAME: "table.management.table.display.name",
|
||||||
|
TABLE_DESCRIPTION: "table.management.table.description",
|
||||||
|
TABLE_COLUMN_COUNT: "table.management.table.column.count",
|
||||||
|
|
||||||
|
// 컬럼 관련
|
||||||
|
COLUMN_NAME: "table.management.column.name",
|
||||||
|
COLUMN_DISPLAY_NAME: "table.management.column.display.name",
|
||||||
|
COLUMN_DB_TYPE: "table.management.column.db.type",
|
||||||
|
COLUMN_WEB_TYPE: "table.management.column.web.type",
|
||||||
|
COLUMN_DETAIL_SETTINGS: "table.management.column.detail.settings",
|
||||||
|
COLUMN_DESCRIPTION: "table.management.column.description",
|
||||||
|
COLUMN_NULLABLE: "table.management.column.nullable",
|
||||||
|
COLUMN_DEFAULT_VALUE: "table.management.column.default.value",
|
||||||
|
COLUMN_MAX_LENGTH: "table.management.column.max.length",
|
||||||
|
COLUMN_NUMERIC_PRECISION: "table.management.column.numeric.precision",
|
||||||
|
COLUMN_NUMERIC_SCALE: "table.management.column.numeric.scale",
|
||||||
|
COLUMN_CODE_CATEGORY: "table.management.column.code.category",
|
||||||
|
COLUMN_CODE_VALUE: "table.management.column.code.value",
|
||||||
|
COLUMN_REFERENCE_TABLE: "table.management.column.reference.table",
|
||||||
|
COLUMN_REFERENCE_COLUMN: "table.management.column.reference.column",
|
||||||
|
|
||||||
|
// 웹 타입 옵션
|
||||||
|
WEB_TYPE_TEXT: "table.management.web.type.text",
|
||||||
|
WEB_TYPE_TEXT_DESC: "table.management.web.type.text.description",
|
||||||
|
WEB_TYPE_NUMBER: "table.management.web.type.number",
|
||||||
|
WEB_TYPE_NUMBER_DESC: "table.management.web.type.number.description",
|
||||||
|
WEB_TYPE_DATE: "table.management.web.type.date",
|
||||||
|
WEB_TYPE_DATE_DESC: "table.management.web.type.date.description",
|
||||||
|
WEB_TYPE_CODE: "table.management.web.type.code",
|
||||||
|
WEB_TYPE_CODE_DESC: "table.management.web.type.code.description",
|
||||||
|
WEB_TYPE_ENTITY: "table.management.web.type.entity",
|
||||||
|
WEB_TYPE_ENTITY_DESC: "table.management.web.type.entity.description",
|
||||||
|
|
||||||
|
// 공통 UI 요소
|
||||||
|
BUTTON_REFRESH: "table.management.button.refresh",
|
||||||
|
BUTTON_SAVE: "table.management.button.save",
|
||||||
|
BUTTON_CANCEL: "table.management.button.cancel",
|
||||||
|
BUTTON_EDIT: "table.management.button.edit",
|
||||||
|
|
||||||
|
// 검색 및 필터
|
||||||
|
SEARCH_PLACEHOLDER: "table.management.search.placeholder",
|
||||||
|
SELECT_TABLE_PLACEHOLDER: "table.management.select.table.placeholder",
|
||||||
|
SELECT_CODE_PLACEHOLDER: "table.management.select.code.placeholder",
|
||||||
|
|
||||||
|
// 메시지
|
||||||
|
MESSAGE_LOADING_TABLES: "table.management.message.loading.tables",
|
||||||
|
MESSAGE_LOADING_COLUMNS: "table.management.message.loading.columns",
|
||||||
|
MESSAGE_TABLES_LOADED: "table.management.message.tables.loaded",
|
||||||
|
MESSAGE_COLUMNS_LOADED: "table.management.message.columns.loaded",
|
||||||
|
MESSAGE_SAVE_SUCCESS: "table.management.message.save.success",
|
||||||
|
MESSAGE_SAVE_ERROR: "table.management.message.save.error",
|
||||||
|
MESSAGE_NO_TABLES: "table.management.message.no.tables",
|
||||||
|
MESSAGE_NO_COLUMNS: "table.management.message.no.columns",
|
||||||
|
|
||||||
|
// 상태 및 라벨
|
||||||
|
STATUS_ACTIVE: "table.management.status.active",
|
||||||
|
STATUS_INACTIVE: "table.management.status.inactive",
|
||||||
|
LABEL_YES: "table.management.label.yes",
|
||||||
|
LABEL_NO: "table.management.label.no",
|
||||||
|
LABEL_NONE: "table.management.label.none",
|
||||||
|
|
||||||
|
// 도움말 및 설명
|
||||||
|
HELP_WEB_TYPE: "table.management.help.web.type",
|
||||||
|
HELP_CODE_CATEGORY: "table.management.help.code.category",
|
||||||
|
HELP_REFERENCE_TABLE: "table.management.help.reference.table",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 웹 타입 옵션을 다국어 키와 매핑
|
||||||
|
export const WEB_TYPE_OPTIONS_WITH_KEYS = [
|
||||||
|
{
|
||||||
|
value: "text",
|
||||||
|
labelKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_TEXT,
|
||||||
|
descriptionKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_TEXT_DESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "number",
|
||||||
|
labelKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_NUMBER,
|
||||||
|
descriptionKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_NUMBER_DESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "date",
|
||||||
|
labelKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_DATE,
|
||||||
|
descriptionKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_DATE_DESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "code",
|
||||||
|
labelKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_CODE,
|
||||||
|
descriptionKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_CODE_DESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "entity",
|
||||||
|
labelKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_ENTITY,
|
||||||
|
descriptionKey: TABLE_MANAGEMENT_KEYS.WEB_TYPE_ENTITY_DESC,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
Loading…
Reference in New Issue